diff --git a/MANACORE-TODOS.md b/MANACORE-TODOS.md index 923685db5..c715754e8 100644 --- a/MANACORE-TODOS.md +++ b/MANACORE-TODOS.md @@ -93,111 +93,92 @@ --- -### 2. App-Config aktualisieren +### 2. ✅ App-Config aktualisieren (ERLEDIGT) -**Problem:** `apps.ts` enthält veraltete Apps und fehlt neue +**Status:** Bereits vollständig implementiert -**Betroffene Datei:** `apps/manacore/apps/web/src/lib/config/apps.ts` +**Datei:** `apps/manacore/apps/web/src/lib/config/apps.ts` -**Aktuell konfiguriert:** +**Alle Apps konfiguriert:** -- memoro (archiviert!) -- manadeck ✅ -- storyteller (archiviert!) -- manacore ✅ +| Kategorie | Apps | +| ------------ | ------------------------------------------- | +| Core | manacore | +| AI-Powered | chat, picture, presi, mail | +| Productivity | manadeck, todo, calendar, contacts, finance | +| Utility | clock, zitare, storage, moodlit | -**Fehlende Apps:** -| App | Typ | Priorität | -|-----|-----|-----------| -| chat | AI-Chat | Hoch | -| picture | AI-Bilder | Hoch | -| zitare | Zitate | Hoch | -| calendar | Kalender | Hoch | -| todo | Aufgaben | Hoch | -| contacts | Kontakte | Mittel | -| clock | Uhren | Mittel | -| presi | Präsentationen | Mittel | -| finance | Finanzen | Mittel | -| mail | E-Mail | Niedrig | -| storage | Cloud-Speicher | Niedrig | -| moodlit | Ambient Lighting | Niedrig | - -**Aufgaben:** - -- [ ] Archivierte Apps entfernen (memoro, storyteller) -- [ ] Alle aktiven Apps hinzufügen -- [ ] Features pro App definieren -- [ ] Icons/Emojis festlegen -- [ ] Farben pro App definieren - -**Geschätzter Aufwand:** 2-4 Stunden +Archivierte Apps (memoro, storyteller) wurden bereits entfernt. --- -### 3. Dashboard-Widgets erweitern +### 3. ✅ Dashboard-Widgets erweitern (GRÖSSTENTEILS ERLEDIGT) -**Problem:** Nur 6 Widget-Typen, neue Apps fehlen +**Status:** 10 von 13 Widgets implementiert -**Betroffene Dateien:** +**Existierende Widgets (13 Typen):** -- `lib/components/dashboard/widgets/` -- `lib/types/dashboard.ts` -- `lib/config/default-dashboard.ts` +| Widget | App | Status | +| ----------------------- | -------------- | ------ | +| CreditsWidget | mana-core-auth | ✅ | +| TransactionsWidget | mana-core-auth | ✅ | +| ReferralWidget | mana-core-auth | ✅ | +| QuickActionsWidget | core | ✅ | +| TasksTodayWidget | todo | ✅ | +| TasksUpcomingWidget | todo | ✅ | +| CalendarEventsWidget | calendar | ✅ | +| ChatRecentWidget | chat | ✅ | +| ContactsFavoritesWidget | contacts | ✅ | +| ZitareQuoteWidget | zitare | ✅ | +| PictureRecentWidget | picture | ✅ | +| ManadeckProgressWidget | manadeck | ✅ | +| ClockTimersWidget | clock | ✅ | -**Neue Widgets erstellen:** +**Noch offen (Backend fehlt noch):** -| Widget | App | Beschreibung | -| ---------------------- | -------- | ------------------------------- | -| PictureRecentWidget | picture | Letzte AI-Generierungen | -| ManadeckProgressWidget | manadeck | Lernfortschritt, fällige Karten | -| FinanceBalanceWidget | finance | Kontostand, Budget-Status | -| ZitareQuoteWidget | zitare | Tägliches Zitat | -| ClockAlarmsWidget | clock | Nächste Wecker/Timer | -| MailInboxWidget | mail | Ungelesene E-Mails | -| StorageUsageWidget | storage | Speicherplatz-Übersicht | - -**Aufgaben:** - -- [ ] Widget-Komponenten erstellen -- [ ] API-Services erweitern -- [ ] Widget-Registry aktualisieren -- [ ] Default-Dashboard anpassen - -**Geschätzter Aufwand:** 1-2 Tage +- [ ] FinanceBalanceWidget (finance Backend nötig) +- [ ] MailInboxWidget (mail Backend nötig) +- [ ] StorageUsageWidget (storage Backend nötig) --- -### 4. Profil-Features vervollständigen +### 4. ✅ Profil-Features vervollständigen (Backend ERLEDIGT) -**Problem:** Mehrere Profil-Aktionen sind nicht implementiert +**Status:** Backend implementiert am 2026-02-13 -**Betroffene Datei:** `apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte` +**Implementierte Backend-Endpoints:** -```typescript -// Zeile 20-22: Nur Alert -onDeleteAccount: () => { - alert('Konto löschen ist noch nicht implementiert.'); -}, -``` +| Endpoint | Methode | Beschreibung | +| ----------------------- | ------- | ---------------------------------------------------- | +| `/auth/profile` | GET | Profil-Daten abrufen | +| `/auth/profile` | POST | Profil aktualisieren (Name, Bild) | +| `/auth/change-password` | POST | Passwort ändern (mit aktuellem Passwort) | +| `/auth/account` | DELETE | Konto löschen (Soft-Delete mit Passwort-Bestätigung) | -**Fehlende Features:** +**Feature-Status:** -| Feature | Status | Priorität | -| ----------------- | ------ | --------- | -| Profil bearbeiten | ❌ | Hoch | -| Passwort ändern | ❌ | Hoch | -| Konto löschen | ❌ | Mittel | -| Avatar hochladen | ❌ | Niedrig | -| 2FA aktivieren | ❌ | Niedrig | +| Feature | Backend | Frontend | Priorität | +| ----------------- | ------- | -------- | --------- | +| Profil bearbeiten | ✅ | ❌ | Hoch | +| Passwort ändern | ✅ | ❌ | Hoch | +| Konto löschen | ✅ | ❌ | Mittel | +| Avatar hochladen | ✅ | ❌ | Niedrig | +| 2FA aktivieren | ❌ | ❌ | Niedrig | -**Aufgaben:** +**Dateien:** + +- `services/mana-core-auth/src/auth/auth.controller.ts` - Endpoints +- `services/mana-core-auth/src/auth/services/better-auth.service.ts` - Service-Methoden +- `services/mana-core-auth/src/auth/dto/update-profile.dto.ts` - Profil-Update DTO +- `services/mana-core-auth/src/auth/dto/change-password.dto.ts` - Passwort-Ändern DTO +- `services/mana-core-auth/src/auth/dto/delete-account.dto.ts` - Konto-Löschen DTO + +**Noch offen (Frontend):** - [ ] Profil-Edit Modal/Seite erstellen - [ ] Passwort-Ändern Dialog - [ ] Konto-Löschung mit Bestätigung -- [ ] Backend-Endpoints prüfen/erstellen - -**Geschätzter Aufwand:** 1-2 Tage +- [ ] Avatar-Upload mit S3/MinIO Integration --- @@ -414,4 +395,4 @@ Diese Tasks können schnell erledigt werden: --- -_Zuletzt aktualisiert: 2026-02-13_ +_Zuletzt aktualisiert: 2026-02-13 (Profile-Features Backend)_ diff --git a/services/mana-core-auth/src/auth/auth.controller.ts b/services/mana-core-auth/src/auth/auth.controller.ts index 7214ca547..a50c011da 100644 --- a/services/mana-core-auth/src/auth/auth.controller.ts +++ b/services/mana-core-auth/src/auth/auth.controller.ts @@ -26,7 +26,12 @@ import { SetActiveOrganizationDto } from './dto/set-active-organization.dto'; import { ForgotPasswordDto } from './dto/forgot-password.dto'; import { ResetPasswordDto } from './dto/reset-password.dto'; import { ResendVerificationDto } from './dto/resend-verification.dto'; +import { UpdateProfileDto } from './dto/update-profile.dto'; +import { ChangePasswordDto } from './dto/change-password.dto'; +import { DeleteAccountDto } from './dto/delete-account.dto'; import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { CurrentUserData } from '../common/decorators/current-user.decorator'; /** * Auth Controller @@ -294,6 +299,101 @@ export class AuthController { ); } + // ========================================================================= + // Profile Management Endpoints + // ========================================================================= + + /** + * Get current user profile + * + * Returns the authenticated user's profile data. + */ + @Get('profile') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Get current user profile' }) + @ApiResponse({ + status: 200, + description: 'Returns user profile', + schema: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + email: { type: 'string' }, + emailVerified: { type: 'boolean' }, + image: { type: 'string' }, + role: { type: 'string' }, + createdAt: { type: 'string', format: 'date-time' }, + }, + }, + }) + @ApiResponse({ status: 401, description: 'Not authenticated' }) + async getProfile(@CurrentUser() user: CurrentUserData) { + return this.betterAuthService.getProfile(user.userId); + } + + /** + * Update user profile + * + * Updates the user's name and/or profile image. + */ + @Post('profile') + @UseGuards(JwtAuthGuard) + @HttpCode(HttpStatus.OK) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Update user profile' }) + @ApiBody({ type: UpdateProfileDto }) + @ApiResponse({ status: 200, description: 'Profile updated successfully' }) + @ApiResponse({ status: 401, description: 'Not authenticated' }) + async updateProfile(@CurrentUser() user: CurrentUserData, @Body() updateDto: UpdateProfileDto) { + return this.betterAuthService.updateProfile(user.userId, { + name: updateDto.name, + image: updateDto.image, + }); + } + + /** + * Change password + * + * Changes the user's password. Requires current password for verification. + * Rate limited to 5 requests per minute. + */ + @Post('change-password') + @UseGuards(JwtAuthGuard) + @Throttle({ default: { ttl: 60000, limit: 5 } }) + @HttpCode(HttpStatus.OK) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Change password' }) + @ApiBody({ type: ChangePasswordDto }) + @ApiResponse({ status: 200, description: 'Password changed successfully' }) + @ApiResponse({ status: 401, description: 'Current password is incorrect' }) + async changePassword(@CurrentUser() user: CurrentUserData, @Body() changeDto: ChangePasswordDto) { + return this.betterAuthService.changePassword( + user.userId, + changeDto.currentPassword, + changeDto.newPassword + ); + } + + /** + * Delete account + * + * Soft-deletes the user's account. Requires password confirmation. + * Rate limited to 3 requests per minute. + */ + @Delete('account') + @UseGuards(JwtAuthGuard) + @Throttle({ default: { ttl: 60000, limit: 3 } }) + @ApiBearerAuth('JWT-auth') + @ApiOperation({ summary: 'Delete user account' }) + @ApiBody({ type: DeleteAccountDto }) + @ApiResponse({ status: 200, description: 'Account deleted' }) + @ApiResponse({ status: 401, description: 'Password is incorrect' }) + async deleteAccount(@CurrentUser() user: CurrentUserData, @Body() deleteDto: DeleteAccountDto) { + return this.betterAuthService.deleteAccount(user.userId, deleteDto.password, deleteDto.reason); + } + // ========================================================================= // B2B Registration // ========================================================================= diff --git a/services/mana-core-auth/src/auth/dto/change-password.dto.ts b/services/mana-core-auth/src/auth/dto/change-password.dto.ts new file mode 100644 index 000000000..23d034c15 --- /dev/null +++ b/services/mana-core-auth/src/auth/dto/change-password.dto.ts @@ -0,0 +1,15 @@ +import { IsString, MinLength, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ChangePasswordDto { + @ApiProperty({ description: 'Current password', example: 'currentPassword123' }) + @IsString() + @MinLength(1) + currentPassword: string; + + @ApiProperty({ description: 'New password (min 8 characters)', example: 'newSecurePassword456' }) + @IsString() + @MinLength(8) + @MaxLength(128) + newPassword: string; +} diff --git a/services/mana-core-auth/src/auth/dto/delete-account.dto.ts b/services/mana-core-auth/src/auth/dto/delete-account.dto.ts new file mode 100644 index 000000000..01eb60f58 --- /dev/null +++ b/services/mana-core-auth/src/auth/dto/delete-account.dto.ts @@ -0,0 +1,20 @@ +import { IsString, IsOptional, MinLength } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class DeleteAccountDto { + @ApiProperty({ + description: 'Current password to confirm account deletion', + example: 'myPassword123', + }) + @IsString() + @MinLength(1) + password: string; + + @ApiPropertyOptional({ + description: 'Optional reason for leaving', + example: 'I found a better service', + }) + @IsOptional() + @IsString() + reason?: string; +} diff --git a/services/mana-core-auth/src/auth/dto/index.ts b/services/mana-core-auth/src/auth/dto/index.ts index c9b8b7227..3340d4132 100644 --- a/services/mana-core-auth/src/auth/dto/index.ts +++ b/services/mana-core-auth/src/auth/dto/index.ts @@ -14,3 +14,13 @@ export { RegisterB2BDto } from './register-b2b.dto'; export { InviteEmployeeDto } from './invite-employee.dto'; export { AcceptInvitationDto } from './accept-invitation.dto'; export { SetActiveOrganizationDto } from './set-active-organization.dto'; + +// Password management DTOs +export { ForgotPasswordDto } from './forgot-password.dto'; +export { ResetPasswordDto } from './reset-password.dto'; +export { ResendVerificationDto } from './resend-verification.dto'; + +// Profile management DTOs +export { UpdateProfileDto } from './update-profile.dto'; +export { ChangePasswordDto } from './change-password.dto'; +export { DeleteAccountDto } from './delete-account.dto'; diff --git a/services/mana-core-auth/src/auth/dto/update-profile.dto.ts b/services/mana-core-auth/src/auth/dto/update-profile.dto.ts new file mode 100644 index 000000000..39532ae0a --- /dev/null +++ b/services/mana-core-auth/src/auth/dto/update-profile.dto.ts @@ -0,0 +1,19 @@ +import { IsString, IsOptional, IsEmail, MinLength, MaxLength, IsUrl } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class UpdateProfileDto { + @ApiPropertyOptional({ description: 'New display name', example: 'Max Mustermann' }) + @IsOptional() + @IsString() + @MinLength(2) + @MaxLength(100) + name?: string; + + @ApiPropertyOptional({ + description: 'Profile image URL', + example: 'https://example.com/avatar.jpg', + }) + @IsOptional() + @IsUrl() + image?: string; +} diff --git a/services/mana-core-auth/src/auth/services/better-auth.service.ts b/services/mana-core-auth/src/auth/services/better-auth.service.ts index cc5b94fc5..772cba5d6 100644 --- a/services/mana-core-auth/src/auth/services/better-auth.service.ts +++ b/services/mana-core-auth/src/auth/services/better-auth.service.ts @@ -1126,6 +1126,250 @@ export class BetterAuthService { return sourceAppStore.getAndDelete(email); } + // ========================================================================= + // Profile Management Methods + // ========================================================================= + + /** + * Update user profile + * + * Updates the user's name and/or image. + * + * @param userId - User ID + * @param updates - Fields to update (name, image) + * @returns Updated user data + */ + async updateProfile( + userId: string, + updates: { name?: string; image?: string } + ): Promise<{ + success: boolean; + user: { id: string; name: string; email: string; image?: string }; + }> { + const db = getDb(this.databaseUrl); + const { users } = await import('../../db/schema/auth.schema'); + const { eq } = await import('drizzle-orm'); + + // Get current user + const [currentUser] = await db.select().from(users).where(eq(users.id, userId)).limit(1); + + if (!currentUser || currentUser.deletedAt) { + throw new NotFoundException('User not found'); + } + + // Build update object + const updateData: Partial<{ name: string; image: string; updatedAt: Date }> = { + updatedAt: new Date(), + }; + + if (updates.name !== undefined) { + updateData.name = updates.name; + } + + if (updates.image !== undefined) { + updateData.image = updates.image; + } + + // Update user + const [updatedUser] = await db + .update(users) + .set(updateData) + .where(eq(users.id, userId)) + .returning(); + + this.logger.log('Profile updated', { userId }); + + return { + success: true, + user: { + id: updatedUser.id, + name: updatedUser.name, + email: updatedUser.email, + image: updatedUser.image || undefined, + }, + }; + } + + /** + * Change user password + * + * Verifies the current password and updates to the new one. + * Requires the user to be authenticated. + * + * @param userId - User ID + * @param currentPassword - Current password for verification + * @param newPassword - New password to set + * @returns Success status + * @throws UnauthorizedException if current password is incorrect + */ + async changePassword( + userId: string, + currentPassword: string, + newPassword: string + ): Promise<{ success: boolean; message: string }> { + const db = getDb(this.databaseUrl); + const { accounts } = await import('../../db/schema/auth.schema'); + const { eq, and } = await import('drizzle-orm'); + const bcrypt = await import('bcrypt'); + + // Get credential account (where password is stored) + const [account] = await db + .select() + .from(accounts) + .where(and(eq(accounts.userId, userId), eq(accounts.providerId, 'credential'))) + .limit(1); + + if (!account || !account.password) { + throw new NotFoundException('No password credential found for this account'); + } + + // Verify current password + const isValid = await bcrypt.compare(currentPassword, account.password); + + if (!isValid) { + throw new UnauthorizedException('Current password is incorrect'); + } + + // Hash new password + const hashedPassword = await bcrypt.hash(newPassword, 10); + + // Update password + await db + .update(accounts) + .set({ + password: hashedPassword, + updatedAt: new Date(), + }) + .where(eq(accounts.id, account.id)); + + this.logger.log('Password changed', { userId }); + + // Log security event + try { + const { securityEvents } = await import('../../db/schema/auth.schema'); + await db.insert(securityEvents).values({ + userId, + eventType: 'password_changed', + metadata: { changedAt: new Date().toISOString() }, + }); + } catch { + // Non-critical - just log + this.logger.warn('Failed to log security event for password change'); + } + + return { + success: true, + message: 'Password changed successfully', + }; + } + + /** + * Delete user account + * + * Soft-deletes the user account after password verification. + * Sets deletedAt timestamp instead of hard delete for data retention. + * + * @param userId - User ID + * @param password - Password for verification + * @param reason - Optional reason for deletion + * @returns Success status + * @throws UnauthorizedException if password is incorrect + */ + async deleteAccount( + userId: string, + password: string, + reason?: string + ): Promise<{ success: boolean; message: string }> { + const db = getDb(this.databaseUrl); + const { accounts, users, sessions } = await import('../../db/schema/auth.schema'); + const { eq, and } = await import('drizzle-orm'); + const bcrypt = await import('bcrypt'); + + // Get credential account + const [account] = await db + .select() + .from(accounts) + .where(and(eq(accounts.userId, userId), eq(accounts.providerId, 'credential'))) + .limit(1); + + if (!account || !account.password) { + throw new NotFoundException('No password credential found for this account'); + } + + // Verify password + const isValid = await bcrypt.compare(password, account.password); + + if (!isValid) { + throw new UnauthorizedException('Password is incorrect'); + } + + const now = new Date(); + + // Soft delete user + await db.update(users).set({ deletedAt: now, updatedAt: now }).where(eq(users.id, userId)); + + // Revoke all sessions + await db.update(sessions).set({ revokedAt: now }).where(eq(sessions.userId, userId)); + + this.logger.log('Account deleted', { userId, reason }); + + // Log security event + try { + const { securityEvents } = await import('../../db/schema/auth.schema'); + await db.insert(securityEvents).values({ + userId, + eventType: 'account_deleted', + metadata: { reason, deletedAt: now.toISOString() }, + }); + } catch { + // Non-critical + this.logger.warn('Failed to log security event for account deletion'); + } + + return { + success: true, + message: 'Account has been deleted', + }; + } + + /** + * Get user profile + * + * Returns the full user profile data. + * + * @param userId - User ID + * @returns User profile data + */ + async getProfile(userId: string): Promise<{ + id: string; + name: string; + email: string; + emailVerified: boolean; + image?: string; + role: string; + createdAt: Date; + }> { + const db = getDb(this.databaseUrl); + const { users } = await import('../../db/schema/auth.schema'); + const { eq } = await import('drizzle-orm'); + + const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1); + + if (!user || user.deletedAt) { + throw new NotFoundException('User not found'); + } + + return { + id: user.id, + name: user.name, + email: user.email, + emailVerified: user.emailVerified, + image: user.image || undefined, + role: user.role, + createdAt: user.createdAt, + }; + } + // ========================================================================= // Private Helper Methods // =========================================================================