Remove unused stub backend folder and add web frontend plan

Cleanup project structure by removing duplicate maternal-app-backend/ folder at root level. The working backend is located at maternal-app/maternal-app-backend/. Also added web frontend implementation plan documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andupetcu
2025-09-30 20:48:57 +03:00
parent 98e01ebe80
commit 1de21044d6
13 changed files with 1733 additions and 537 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
export const getDatabaseConfig = (
configService: ConfigService,
): TypeOrmModuleOptions => ({
type: 'postgres',
host: configService.get<string>('DATABASE_HOST', 'localhost'),
port: configService.get<number>('DATABASE_PORT', 5555),
username: configService.get<string>('DATABASE_USER', 'maternal_user'),
password: configService.get<string>('DATABASE_PASSWORD'),
database: configService.get<string>('DATABASE_NAME', 'maternal_app'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
synchronize: false, // Always use migrations in production
logging: configService.get<string>('NODE_ENV') === 'development',
ssl: configService.get<string>('NODE_ENV') === 'production',
});

View File

@@ -1,15 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { getDatabaseConfig } from '../config/database.config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: getDatabaseConfig,
inject: [ConfigService],
}),
],
})
export class DatabaseModule {}

View File

@@ -1,60 +0,0 @@
import {
Entity,
Column,
PrimaryColumn,
ManyToOne,
JoinColumn,
CreateDateColumn,
BeforeInsert,
} from 'typeorm';
import { Family } from './family.entity';
@Entity('children')
export class Child {
@PrimaryColumn({ length: 20 })
id: string;
@Column({ name: 'family_id', length: 20 })
familyId: string;
@Column({ length: 100 })
name: string;
@Column({ name: 'birth_date', type: 'date' })
birthDate: Date;
@Column({ length: 20, nullable: true })
gender?: string;
@Column({ name: 'photo_url', type: 'text', nullable: true })
photoUrl?: string;
@Column({ name: 'medical_info', type: 'jsonb', default: {} })
medicalInfo: Record<string, any>;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@Column({ name: 'deleted_at', type: 'timestamp', nullable: true })
deletedAt?: Date;
@ManyToOne(() => Family, (family) => family.children, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'family_id' })
family: Family;
@BeforeInsert()
generateId() {
if (!this.id) {
this.id = `chd_${this.generateNanoId()}`;
}
}
private generateNanoId(): string {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 12; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
}

View File

@@ -1,53 +0,0 @@
import {
Entity,
Column,
PrimaryColumn,
ManyToOne,
JoinColumn,
CreateDateColumn,
BeforeInsert,
Index,
} from 'typeorm';
import { User } from './user.entity';
@Entity('device_registry')
@Index(['userId', 'deviceFingerprint'], { unique: true })
export class DeviceRegistry {
@PrimaryColumn({ length: 20 })
id: string;
@Column({ name: 'user_id', length: 20 })
userId: string;
@Column({ name: 'device_fingerprint', length: 255 })
deviceFingerprint: string;
@Column({ length: 20 })
platform: string;
@Column({ default: false })
trusted: boolean;
@Column({ name: 'last_seen', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
lastSeen: Date;
@ManyToOne(() => User, (user) => user.devices, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: User;
@BeforeInsert()
generateId() {
if (!this.id) {
this.id = `dev_${this.generateNanoId()}`;
}
}
private generateNanoId(): string {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 12; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
}

View File

@@ -1,61 +0,0 @@
import {
Entity,
Column,
PrimaryColumn,
ManyToOne,
JoinColumn,
CreateDateColumn,
} from 'typeorm';
import { User } from './user.entity';
import { Family } from './family.entity';
export enum FamilyRole {
PARENT = 'parent',
CAREGIVER = 'caregiver',
VIEWER = 'viewer',
}
export interface FamilyPermissions {
canAddChildren: boolean;
canEditChildren: boolean;
canLogActivities: boolean;
canViewReports: boolean;
}
@Entity('family_members')
export class FamilyMember {
@PrimaryColumn({ name: 'user_id', length: 20 })
userId: string;
@PrimaryColumn({ name: 'family_id', length: 20 })
familyId: string;
@Column({
type: 'varchar',
length: 20,
enum: FamilyRole,
})
role: FamilyRole;
@Column({
type: 'jsonb',
default: {
canAddChildren: false,
canEditChildren: false,
canLogActivities: true,
canViewReports: true,
},
})
permissions: FamilyPermissions;
@CreateDateColumn({ name: 'joined_at' })
joinedAt: Date;
@ManyToOne(() => User, (user) => user.familyMemberships, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: User;
@ManyToOne(() => Family, (family) => family.members, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'family_id' })
family: Family;
}

View File

@@ -1,72 +0,0 @@
import {
Entity,
Column,
PrimaryColumn,
ManyToOne,
OneToMany,
JoinColumn,
CreateDateColumn,
BeforeInsert,
} from 'typeorm';
import { User } from './user.entity';
import { FamilyMember } from './family-member.entity';
import { Child } from './child.entity';
@Entity('families')
export class Family {
@PrimaryColumn({ length: 20 })
id: string;
@Column({ length: 100, nullable: true })
name?: string;
@Column({ name: 'share_code', length: 10, unique: true })
shareCode: string;
@Column({ name: 'created_by', length: 20 })
createdBy: string;
@Column({ name: 'subscription_tier', length: 20, default: 'free' })
subscriptionTier: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
creator: User;
@OneToMany(() => FamilyMember, (member) => member.family)
members: FamilyMember[];
@OneToMany(() => Child, (child) => child.family)
children: Child[];
@BeforeInsert()
generateId() {
if (!this.id) {
this.id = `fam_${this.generateNanoId()}`;
}
if (!this.shareCode) {
this.shareCode = this.generateShareCode();
}
}
private generateNanoId(): string {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 12; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
private generateShareCode(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
}

View File

@@ -1,5 +0,0 @@
export { User } from './user.entity';
export { DeviceRegistry } from './device-registry.entity';
export { Family } from './family.entity';
export { FamilyMember, FamilyRole, FamilyPermissions } from './family-member.entity';
export { Child } from './child.entity';

View File

@@ -1,66 +0,0 @@
import {
Entity,
Column,
PrimaryColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
BeforeInsert,
} from 'typeorm';
import { DeviceRegistry } from './device-registry.entity';
import { FamilyMember } from './family-member.entity';
@Entity('users')
export class User {
@PrimaryColumn({ length: 20 })
id: string;
@Column({ length: 255, unique: true })
email: string;
@Column({ length: 20, nullable: true })
phone?: string;
@Column({ name: 'password_hash', length: 255 })
passwordHash: string;
@Column({ length: 100 })
name: string;
@Column({ length: 10, default: 'en-US' })
locale: string;
@Column({ length: 50, default: 'UTC' })
timezone: string;
@Column({ name: 'email_verified', default: false })
emailVerified: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
@OneToMany(() => DeviceRegistry, (device) => device.user)
devices: DeviceRegistry[];
@OneToMany(() => FamilyMember, (familyMember) => familyMember.user)
familyMemberships: FamilyMember[];
@BeforeInsert()
generateId() {
if (!this.id) {
this.id = `usr_${this.generateNanoId()}`;
}
}
private generateNanoId(): string {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 12; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
}

View File

@@ -1,47 +0,0 @@
-- V001_20240110120000_create_core_auth.sql
-- Migration V001: Core Authentication Tables
-- Create extension for generating random IDs
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Users table
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(20) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20),
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
locale VARCHAR(10) DEFAULT 'en-US',
timezone VARCHAR(50) DEFAULT 'UTC',
email_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Device registry table
CREATE TABLE IF NOT EXISTS device_registry (
id VARCHAR(20) PRIMARY KEY,
user_id VARCHAR(20) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
device_fingerprint VARCHAR(255) NOT NULL,
platform VARCHAR(20) NOT NULL,
trusted BOOLEAN DEFAULT FALSE,
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, device_fingerprint)
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_devices_user ON device_registry(user_id);
-- Update timestamp trigger function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply trigger to users table
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

View File

@@ -1,41 +0,0 @@
-- V002_20240110130000_create_family_structure.sql
-- Migration V002: Family Structure
-- Families table
CREATE TABLE IF NOT EXISTS families (
id VARCHAR(20) PRIMARY KEY,
name VARCHAR(100),
share_code VARCHAR(10) UNIQUE,
created_by VARCHAR(20) REFERENCES users(id),
subscription_tier VARCHAR(20) DEFAULT 'free',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Family members table (junction table with additional data)
CREATE TABLE IF NOT EXISTS family_members (
user_id VARCHAR(20) REFERENCES users(id) ON DELETE CASCADE,
family_id VARCHAR(20) REFERENCES families(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL CHECK (role IN ('parent', 'caregiver', 'viewer')),
permissions JSONB DEFAULT '{"canAddChildren": false, "canEditChildren": false, "canLogActivities": true, "canViewReports": true}'::jsonb,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, family_id)
);
-- Children table
CREATE TABLE IF NOT EXISTS children (
id VARCHAR(20) PRIMARY KEY,
family_id VARCHAR(20) NOT NULL REFERENCES families(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
birth_date DATE NOT NULL,
gender VARCHAR(20),
photo_url TEXT,
medical_info JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_families_share_code ON families(share_code);
CREATE INDEX IF NOT EXISTS idx_family_members_family ON family_members(family_id);
CREATE INDEX IF NOT EXISTS idx_children_family ON children(family_id);
CREATE INDEX IF NOT EXISTS idx_children_active ON children(deleted_at) WHERE deleted_at IS NULL;

View File

@@ -1,20 +0,0 @@
-- V003_20240110140000_create_refresh_tokens.sql
-- Migration V003: Refresh Tokens Table
-- Refresh tokens table for JWT authentication
CREATE TABLE IF NOT EXISTS refresh_tokens (
id VARCHAR(20) PRIMARY KEY,
user_id VARCHAR(20) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
device_id VARCHAR(20) REFERENCES device_registry(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked BOOLEAN DEFAULT FALSE,
revoked_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for refresh token lookups
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_device ON refresh_tokens(device_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_active ON refresh_tokens(expires_at, revoked) WHERE revoked = FALSE;

View File

@@ -1,79 +0,0 @@
import { Client } from 'pg';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config();
const client = new Client({
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT || '5555'),
user: process.env.DATABASE_USER || 'maternal_user',
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME || 'maternal_app',
});
const MIGRATIONS_DIR = __dirname;
async function runMigrations() {
try {
await client.connect();
console.log('Connected to database');
// Create migrations tracking table
await client.query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(50) PRIMARY KEY,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
// Get list of migration files
const files = fs
.readdirSync(MIGRATIONS_DIR)
.filter((file) => file.startsWith('V') && file.endsWith('.sql'))
.sort();
console.log(`Found ${files.length} migration files`);
for (const file of files) {
const version = file.split('_')[0]; // Extract V001, V002, etc.
// Check if migration already executed
const result = await client.query(
'SELECT version FROM schema_migrations WHERE version = $1',
[version],
);
if (result.rows.length > 0) {
console.log(`✓ Migration ${version} already executed`);
continue;
}
// Read and execute migration
const migrationPath = path.join(MIGRATIONS_DIR, file);
const sql = fs.readFileSync(migrationPath, 'utf-8');
console.log(`Running migration ${version}...`);
await client.query(sql);
// Record migration
await client.query(
'INSERT INTO schema_migrations (version) VALUES ($1)',
[version],
);
console.log(`✓ Migration ${version} completed`);
}
console.log('All migrations completed successfully');
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
} finally {
await client.end();
}
}
runMigrations();