feat(auth): add profile management endpoints

Add backend endpoints for user profile management:
- GET /auth/profile - retrieve user profile data
- POST /auth/profile - update name and profile image
- POST /auth/change-password - change password (requires current)
- DELETE /auth/account - soft-delete account (requires password)

Security features:
- Password verification before sensitive actions
- Soft-delete preserves data for retention
- Security events logged for audit trail
- Rate limiting on sensitive endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-13 22:29:32 +01:00
parent ae30ce3323
commit ce4e982651
7 changed files with 469 additions and 80 deletions

View file

@ -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)_

View file

@ -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
// =========================================================================

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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
// =========================================================================