diff --git a/maternal-app/maternal-app-backend/src/database/entities/index.ts b/maternal-app/maternal-app-backend/src/database/entities/index.ts index afeab8c..458d80c 100644 --- a/maternal-app/maternal-app-backend/src/database/entities/index.ts +++ b/maternal-app/maternal-app-backend/src/database/entities/index.ts @@ -31,3 +31,4 @@ export { DeletionRequestStatus, DataType, } from './data-deletion-request.entity'; +export { Settings } from './settings.entity'; diff --git a/maternal-app/maternal-app-backend/src/database/entities/settings.entity.ts b/maternal-app/maternal-app-backend/src/database/entities/settings.entity.ts new file mode 100644 index 0000000..3d939df --- /dev/null +++ b/maternal-app/maternal-app-backend/src/database/entities/settings.entity.ts @@ -0,0 +1,25 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('settings') +export class Settings { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true, length: 100 }) + key: string; + + @Column({ type: 'text' }) + value: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ default: 'string', length: 50 }) + type: string; // 'string', 'boolean', 'number', 'json' + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/maternal-app/maternal-app-backend/src/database/migrations/1735000000000-CreateSettingsTable.ts b/maternal-app/maternal-app-backend/src/database/migrations/1735000000000-CreateSettingsTable.ts new file mode 100644 index 0000000..e1b079e --- /dev/null +++ b/maternal-app/maternal-app-backend/src/database/migrations/1735000000000-CreateSettingsTable.ts @@ -0,0 +1,82 @@ +import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; + +export class CreateSettingsTable1735000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Create settings table + await queryRunner.createTable( + new Table({ + name: 'settings', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'uuid_generate_v4()', + }, + { + name: 'key', + type: 'varchar', + length: '100', + isUnique: true, + }, + { + name: 'value', + type: 'text', + }, + { + name: 'description', + type: 'text', + isNullable: true, + }, + { + name: 'type', + type: 'varchar', + length: '50', + default: "'string'", + }, + { + name: 'created_at', + type: 'timestamptz', + default: 'CURRENT_TIMESTAMP', + }, + { + name: 'updated_at', + type: 'timestamptz', + default: 'CURRENT_TIMESTAMP', + }, + ], + }), + true, + ); + + // Create index on key for faster lookups + await queryRunner.createIndex( + 'settings', + new TableIndex({ + name: 'IDX_SETTINGS_KEY', + columnNames: ['key'], + }), + ); + + // Insert default settings + await queryRunner.query(` + INSERT INTO settings (key, value, description, type) VALUES + ('registration_mode', 'public', 'Registration mode: public or invite_only', 'string'), + ('require_invite_code', 'false', 'Whether invite codes are required for registration', 'boolean'), + ('max_family_size', '10', 'Maximum number of members allowed in a family', 'number'), + ('max_children_per_family', '10', 'Maximum number of children allowed per family', 'number'), + ('enable_ai_features', 'true', 'Enable AI assistant features', 'boolean'), + ('enable_voice_input', 'true', 'Enable voice input for activity tracking', 'boolean') + ON CONFLICT (key) DO NOTHING; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop index + await queryRunner.dropIndex('settings', 'IDX_SETTINGS_KEY'); + + // Drop table + await queryRunner.dropTable('settings'); + } +} diff --git a/maternal-app/maternal-app-backend/src/modules/admin/dashboard/dashboard.module.ts b/maternal-app/maternal-app-backend/src/modules/admin/dashboard/dashboard.module.ts index 720b845..9bd5f8a 100644 --- a/maternal-app/maternal-app-backend/src/modules/admin/dashboard/dashboard.module.ts +++ b/maternal-app/maternal-app-backend/src/modules/admin/dashboard/dashboard.module.ts @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { DashboardController } from './dashboard.controller'; import { DashboardService } from './dashboard.service'; import { User } from '../../../database/entities/user.entity'; +import { Settings } from '../../../database/entities/settings.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [TypeOrmModule.forFeature([User, Settings])], controllers: [DashboardController], providers: [DashboardService], exports: [DashboardService], diff --git a/maternal-app/maternal-app-backend/src/modules/admin/dashboard/dashboard.service.ts b/maternal-app/maternal-app-backend/src/modules/admin/dashboard/dashboard.service.ts index 446447c..8178152 100644 --- a/maternal-app/maternal-app-backend/src/modules/admin/dashboard/dashboard.service.ts +++ b/maternal-app/maternal-app-backend/src/modules/admin/dashboard/dashboard.service.ts @@ -2,12 +2,15 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from '../../../database/entities/user.entity'; +import { Settings } from '../../../database/entities/settings.entity'; @Injectable() export class DashboardService { constructor( @InjectRepository(User) private readonly userRepository: Repository, + @InjectRepository(Settings) + private readonly settingsRepository: Repository, ) {} async getStats() { @@ -346,8 +349,34 @@ export class DashboardService { }; } + // Helper method to get a setting value from database with fallback to env var + private async getSetting(key: string, defaultValue: any = null): Promise { + const setting = await this.settingsRepository.findOne({ where: { key } }); + if (setting) { + // Parse value based on type + switch (setting.type) { + case 'boolean': + return setting.value === 'true'; + case 'number': + return parseFloat(setting.value); + case 'json': + return JSON.parse(setting.value); + default: + return setting.value; + } + } + return defaultValue; + } + async getSettings() { - // Return current system settings (from env vars and database) + // Get settings from database with fallbacks + const registrationMode = await this.getSetting('registration_mode', process.env.REGISTRATION_MODE || 'public'); + const requireInviteCode = await this.getSetting('require_invite_code', process.env.REQUIRE_INVITE_CODE === 'true'); + const maxFamilySize = await this.getSetting('max_family_size', 10); + const maxChildrenPerFamily = await this.getSetting('max_children_per_family', 10); + const enableAiFeatures = await this.getSetting('enable_ai_features', true); + const enableVoiceInput = await this.getSetting('enable_voice_input', true); + return { // General Settings siteName: process.env.APP_NAME || 'ParentFlow', @@ -356,9 +385,15 @@ export class DashboardService { timezone: process.env.TZ || 'UTC', language: 'en', - // Registration Settings - registrationMode: process.env.REGISTRATION_MODE || 'invite_only', // 'public' or 'invite_only' - requireInviteCode: process.env.REQUIRE_INVITE_CODE === 'true' || true, + // Registration Settings (from database) + registrationMode, + requireInviteCode, + + // Feature Settings (from database) + maxFamilySize, + maxChildrenPerFamily, + enableAiFeatures, + enableVoiceInput, // Security Settings enforcePasswordPolicy: true, @@ -403,16 +438,52 @@ export class DashboardService { } async updateSettings(settings: any) { - // In a real implementation, you would: - // 1. Validate the settings - // 2. Update environment variables or configuration file - // 3. Update database records if needed - // 4. Restart services if required + // Define which settings can be stored in database + const dbSettingsMap = { + registrationMode: { key: 'registration_mode', type: 'string' }, + requireInviteCode: { key: 'require_invite_code', type: 'boolean' }, + maxFamilySize: { key: 'max_family_size', type: 'number' }, + maxChildrenPerFamily: { key: 'max_children_per_family', type: 'number' }, + enableAiFeatures: { key: 'enable_ai_features', type: 'boolean' }, + enableVoiceInput: { key: 'enable_voice_input', type: 'boolean' }, + }; + + const updatedSettings = []; + + // Update database settings + for (const [settingName, config] of Object.entries(dbSettingsMap)) { + if (settings[settingName] !== undefined) { + const { key, type } = config; + let value = settings[settingName]; + + // Convert value to string for storage + if (type === 'boolean') { + value = value ? 'true' : 'false'; + } else if (type === 'number') { + value = value.toString(); + } else if (type === 'json') { + value = JSON.stringify(value); + } + + // Update or create setting + let setting = await this.settingsRepository.findOne({ where: { key } }); + if (setting) { + setting.value = value; + setting.type = type; + await this.settingsRepository.save(setting); + } else { + setting = this.settingsRepository.create({ key, value, type }); + await this.settingsRepository.save(setting); + } + + updatedSettings.push(key); + } + } - // For now, return success message return { success: true, - message: 'Settings updated successfully. Some changes may require a server restart.', + message: `Settings updated successfully: ${updatedSettings.join(', ')}`, + updatedSettings, }; } } diff --git a/maternal-app/maternal-app-backend/src/modules/auth/auth.controller.ts b/maternal-app/maternal-app-backend/src/modules/auth/auth.controller.ts index 5aec27f..be9da39 100644 --- a/maternal-app/maternal-app-backend/src/modules/auth/auth.controller.ts +++ b/maternal-app/maternal-app-backend/src/modules/auth/auth.controller.ts @@ -15,12 +15,15 @@ import { Req, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { AuthService } from './auth.service'; import { PasswordResetService } from './password-reset.service'; import { MFAService } from './mfa.service'; import { SessionService } from './session.service'; import { DeviceTrustService } from './device-trust.service'; import { BiometricAuthService } from './biometric-auth.service'; +import { Settings } from '../../database/entities/settings.entity'; import { RegisterDto } from './dto/register.dto'; import { LoginDto } from './dto/login.dto'; import { RefreshTokenDto } from './dto/refresh-token.dto'; @@ -46,13 +49,25 @@ export class AuthController { private readonly deviceTrustService: DeviceTrustService, private readonly biometricAuthService: BiometricAuthService, private readonly configService: ConfigService, + @InjectRepository(Settings) + private readonly settingsRepository: Repository, ) {} @Public() @Get('registration/config') @HttpCode(HttpStatus.OK) async getRegistrationConfig() { - const registrationMode = this.configService.get('REGISTRATION_MODE', 'public'); + // Try to get from database first, fall back to env vars + let registrationMode = 'public'; + const registrationModeSetting = await this.settingsRepository.findOne({ + where: { key: 'registration_mode' } + }); + if (registrationModeSetting) { + registrationMode = registrationModeSetting.value; + } else { + registrationMode = this.configService.get('REGISTRATION_MODE', 'public'); + } + const requireInviteCode = registrationMode === 'invite_only'; return { diff --git a/maternal-app/maternal-app-backend/src/modules/auth/auth.module.ts b/maternal-app/maternal-app-backend/src/modules/auth/auth.module.ts index f5345a6..f5441dd 100644 --- a/maternal-app/maternal-app-backend/src/modules/auth/auth.module.ts +++ b/maternal-app/maternal-app-backend/src/modules/auth/auth.module.ts @@ -21,6 +21,7 @@ import { PasswordResetToken, Family, FamilyMember, + Settings, } from '../../database/entities'; import { InviteCode, InviteCodeUse } from '../invite-codes/invite-codes.entity'; @@ -36,6 +37,7 @@ import { InviteCode, InviteCodeUse } from '../invite-codes/invite-codes.entity'; WebAuthnCredential, InviteCode, InviteCodeUse, + Settings, ]), PassportModule, CommonModule, diff --git a/maternal-app/maternal-app-backend/src/modules/auth/auth.service.ts b/maternal-app/maternal-app-backend/src/modules/auth/auth.service.ts index 6fd2b6a..0829205 100644 --- a/maternal-app/maternal-app-backend/src/modules/auth/auth.service.ts +++ b/maternal-app/maternal-app-backend/src/modules/auth/auth.service.ts @@ -19,6 +19,7 @@ import { FamilyMember, AuditAction, EntityType, + Settings, } from '../../database/entities'; import { InviteCode, InviteCodeUse } from '../invite-codes/invite-codes.entity'; import { RegisterDto } from './dto/register.dto'; @@ -48,14 +49,25 @@ export class AuthService { private inviteCodeRepository: Repository, @InjectRepository(InviteCodeUse) private inviteCodeUseRepository: Repository, + @InjectRepository(Settings) + private settingsRepository: Repository, private jwtService: JwtService, private configService: ConfigService, private auditService: AuditService, ) {} async register(registerDto: RegisterDto): Promise { - // Check registration mode and validate invite code if required - const registrationMode = this.configService.get('REGISTRATION_MODE', 'public'); + // Check registration mode from database first, fall back to env vars + let registrationMode = 'public'; + const registrationModeSetting = await this.settingsRepository.findOne({ + where: { key: 'registration_mode' } + }); + if (registrationModeSetting) { + registrationMode = registrationModeSetting.value; + } else { + registrationMode = this.configService.get('REGISTRATION_MODE', 'public'); + } + const requireInviteCode = registrationMode === 'invite_only'; let validatedInviteCode: InviteCode | null = null;