mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
✨ 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:
parent
ae30ce3323
commit
ce4e982651
7 changed files with 469 additions and 80 deletions
|
|
@ -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)_
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =========================================================================
|
||||
|
|
|
|||
15
services/mana-core-auth/src/auth/dto/change-password.dto.ts
Normal file
15
services/mana-core-auth/src/auth/dto/change-password.dto.ts
Normal 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;
|
||||
}
|
||||
20
services/mana-core-auth/src/auth/dto/delete-account.dto.ts
Normal file
20
services/mana-core-auth/src/auth/dto/delete-account.dto.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
19
services/mana-core-auth/src/auth/dto/update-profile.dto.ts
Normal file
19
services/mana-core-auth/src/auth/dto/update-profile.dto.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
// =========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue