diff --git a/packages/core-service/src/core/auth/auth.service.ts b/packages/core-service/src/core/auth/auth.service.ts index 9d1b2ba..6d1323e 100644 --- a/packages/core-service/src/core/auth/auth.service.ts +++ b/packages/core-service/src/core/auth/auth.service.ts @@ -65,13 +65,13 @@ export class AuthService { if (!user || !user.isActive) { // Generische Fehlermeldung (kein Hinweis ob User existiert) - throw new UnauthorizedException('Ungueltige Anmeldedaten'); + throw new UnauthorizedException('Ungültige Anmeldedaten'); } // Passwort pruefen (nur fuer lokale Auth) const localAuth = user.authProvider.find((ap) => ap.provider === 'LOCAL'); if (!localAuth?.passwordHash) { - throw new UnauthorizedException('Ungueltige Anmeldedaten'); + throw new UnauthorizedException('Ungültige Anmeldedaten'); } const passwordValid = await bcrypt.compare( @@ -87,7 +87,7 @@ export class AuthService { lastFailedLogin: new Date(), }, }); - throw new UnauthorizedException('Ungueltige Anmeldedaten'); + throw new UnauthorizedException('Ungültige Anmeldedaten'); } // Account-Sperre pruefen (nach 5 Fehlversuchen) @@ -125,11 +125,11 @@ export class AuthService { localAuth.totpSecret ?? '', ); if (!totpValid) { - throw new UnauthorizedException('Ungueltiger 2FA-Code'); + throw new UnauthorizedException('Ungültiger 2FA-Code'); } } - // Erfolgreicher Login: Counter zuruecksetzen + // Erfolgreicher Login: Counter zurücksetzen await this.prisma.user.update({ where: { id: user.id }, data: { @@ -179,12 +179,12 @@ export class AuthService { payload.jti, ); if (!isValid) { - // Moeglicherweise Refresh-Token-Diebstahl: alle invalidieren + // Möglicherweise Refresh-Token-Diebstahl: alle invalidieren this.logger.warn( - `Verdaechtiger Refresh-Token Wiederverwendung fuer User ${payload.sub}`, + `Verdächtiger Refresh-Token Wiederverwendung für User ${payload.sub}`, ); await this.redis.invalidateAllRefreshTokens(payload.sub); - throw new UnauthorizedException('Refresh Token ungueltig'); + throw new UnauthorizedException('Refresh Token ungültig'); } // Alten Refresh-Token invalidieren @@ -200,7 +200,7 @@ export class AuthService { }); } catch (error) { if (error instanceof UnauthorizedException) throw error; - throw new UnauthorizedException('Refresh Token ungueltig oder abgelaufen'); + throw new UnauthorizedException('Refresh Token ungültig oder abgelaufen'); } } @@ -272,10 +272,10 @@ export class AuthService { ); } - // TOTP-Code pruefen + // TOTP-Code prüfen const isValid = this.totp.verify(totpCode, secret); if (!isValid) { - throw new UnauthorizedException('Ungueltiger 2FA-Code'); + throw new UnauthorizedException('Ungültiger 2FA-Code'); } // Secret permanent in AuthProvider speichern + 2FA aktivieren @@ -290,10 +290,10 @@ export class AuthService { }), ]); - // Temporaeres Secret aus Redis loeschen + // Temporäres Secret aus Redis löschen await this.redis.del(`2fa_setup:${userId}`); - this.logger.log(`2FA aktiviert fuer User ${userId}`); + this.logger.log(`2FA aktiviert für User ${userId}`); } /** @@ -313,7 +313,7 @@ export class AuthService { throw new ForbiddenException('2FA ist nicht aktiviert'); } - // Passwort pruefen + // Passwort prüfen const localAuth = user.authProvider.find((ap) => ap.provider === 'LOCAL'); if (!localAuth?.passwordHash) { throw new UnauthorizedException('Kein lokaler Auth-Provider gefunden'); @@ -321,10 +321,10 @@ export class AuthService { const passwordValid = await bcrypt.compare(password, localAuth.passwordHash); if (!passwordValid) { - throw new UnauthorizedException('Ungueltiges Passwort'); + throw new UnauthorizedException('Ungültiges Passwort'); } - // 2FA deaktivieren + Secret loeschen + // 2FA deaktivieren + Secret löschen await this.prisma.$transaction([ this.prisma.authProvider.updateMany({ where: { userId, provider: 'LOCAL' }, @@ -336,7 +336,7 @@ export class AuthService { }), ]); - this.logger.log(`2FA deaktiviert fuer User ${userId}`); + this.logger.log(`2FA deaktiviert für User ${userId}`); } /** diff --git a/packages/core-service/src/core/auth/dto/login.dto.ts b/packages/core-service/src/core/auth/dto/login.dto.ts index bceaba4..1b6a89c 100644 --- a/packages/core-service/src/core/auth/dto/login.dto.ts +++ b/packages/core-service/src/core/auth/dto/login.dto.ts @@ -6,7 +6,7 @@ export class LoginDto { example: 'admin@xinion.de', description: 'E-Mail-Adresse des Benutzers', }) - @IsEmail({}, { message: 'Bitte gueltige E-Mail-Adresse angeben' }) + @IsEmail({}, { message: 'Bitte gültige E-Mail-Adresse angeben' }) @IsNotEmpty({ message: 'E-Mail darf nicht leer sein' }) email!: string; diff --git a/packages/core-service/src/core/users/dto/create-user.dto.ts b/packages/core-service/src/core/users/dto/create-user.dto.ts index f355ec0..a8027cf 100644 --- a/packages/core-service/src/core/users/dto/create-user.dto.ts +++ b/packages/core-service/src/core/users/dto/create-user.dto.ts @@ -11,7 +11,7 @@ import { ApiProperty } from '@nestjs/swagger'; export class CreateUserDto { @ApiProperty({ example: 'max.mustermann@xinion.de' }) - @IsEmail({}, { message: 'Bitte gueltige E-Mail-Adresse angeben' }) + @IsEmail({}, { message: 'Bitte gültige E-Mail-Adresse angeben' }) @IsNotEmpty() email!: string; diff --git a/packages/core-service/src/core/users/dto/update-user.dto.ts b/packages/core-service/src/core/users/dto/update-user.dto.ts index 0c2ff34..111397a 100644 --- a/packages/core-service/src/core/users/dto/update-user.dto.ts +++ b/packages/core-service/src/core/users/dto/update-user.dto.ts @@ -35,9 +35,9 @@ export class UpdateUserDto { @IsOptional() @ValidateIf((o: UpdateUserDto) => o.avatar !== null) @IsString() - @MaxLength(400000, { message: 'Profilbild darf maximal 400KB gross sein' }) + @MaxLength(400000, { message: 'Profilbild darf maximal 400KB groß sein' }) @Matches(/^data:image\/(jpeg|png|gif|webp);base64,[A-Za-z0-9+/=]+$/, { - message: 'Profilbild muss ein gueltiges Base64-Bild sein (JPEG, PNG, GIF oder WebP)', + message: 'Profilbild muss ein gültiges Base64-Bild sein (JPEG, PNG, GIF oder WebP)', }) avatar?: string | null; } diff --git a/packages/core-service/src/core/users/users.controller.ts b/packages/core-service/src/core/users/users.controller.ts index d8b5990..32599bc 100644 --- a/packages/core-service/src/core/users/users.controller.ts +++ b/packages/core-service/src/core/users/users.controller.ts @@ -51,11 +51,11 @@ export class UsersController { /** * POST /api/v1/users/me/change-password - * Eigenes Passwort aendern. + * Eigenes Passwort ändern. */ @Post('me/change-password') @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Eigenes Passwort aendern' }) + @ApiOperation({ summary: 'Eigenes Passwort ändern' }) async changePassword( @CurrentUser('sub') userId: string, @Body() dto: ChangePasswordDto, @@ -65,7 +65,7 @@ export class UsersController { dto.currentPassword, dto.newPassword, ); - return { message: 'Passwort erfolgreich geaendert' }; + return { message: 'Passwort erfolgreich geändert' }; } /** diff --git a/packages/core-service/src/core/users/users.service.ts b/packages/core-service/src/core/users/users.service.ts index 18d785a..e9f38b2 100644 --- a/packages/core-service/src/core/users/users.service.ts +++ b/packages/core-service/src/core/users/users.service.ts @@ -60,7 +60,7 @@ export class UsersService { this.logger.log(`User erstellt: ${user.email} (${user.role})`); - // Passwort-Hash nicht zurueckgeben + // Passwort-Hash nicht zurückgeben return { id: user.id, email: user.email, @@ -171,7 +171,7 @@ export class UsersService { } /** - * Eigenes Passwort aendern (mit Verifikation des aktuellen Passworts). + * Eigenes Passwort ändern (mit Verifikation des aktuellen Passworts). */ async changePassword( userId: string, @@ -211,11 +211,11 @@ export class UsersService { data: { passwordHash: newHash }, }); - this.logger.log(`Passwort geaendert fuer User ${user.email}`); + this.logger.log(`Passwort geändert für User ${user.email}`); } /** - * Alle User auflisten (fuer Admin). + * Alle User auflisten (für Admin). */ async findAll(page = 1, limit = 20) { const skip = (page - 1) * limit; diff --git a/packages/frontend/src/auth/AuthContext.tsx b/packages/frontend/src/auth/AuthContext.tsx index 96fc15b..ce5a802 100644 --- a/packages/frontend/src/auth/AuthContext.tsx +++ b/packages/frontend/src/auth/AuthContext.tsx @@ -103,7 +103,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const { data } = await api.get('/users/me'); setUser(data); } catch { - // Fehler ignorieren - User bleibt unveraendert + // Fehler ignorieren - User bleibt unverändert } }, []); diff --git a/packages/frontend/src/profile/ProfilePage.module.css b/packages/frontend/src/profile/ProfilePage.module.css index 31c1727..a9cedc2 100644 --- a/packages/frontend/src/profile/ProfilePage.module.css +++ b/packages/frontend/src/profile/ProfilePage.module.css @@ -1,3 +1,42 @@ +/* === Tabs === */ +.tabs { + display: flex; + gap: 0; + border-bottom: 2px solid var(--color-border); + margin-bottom: 1.5rem; +} + +.tab { + padding: 0.75rem 1.5rem; + font-size: 0.9375rem; + font-weight: 500; + color: var(--color-text-secondary); + background: none; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + cursor: pointer; + transition: all 0.15s; +} + +.tab:hover { + color: var(--color-text); +} + +.tabActive { + color: var(--color-primary); + border-bottom-color: var(--color-primary); + font-weight: 600; +} + +/* === Platzhalter === */ +.placeholder { + color: var(--color-text-muted); + font-size: 0.9375rem; + padding: 2rem 0; + text-align: center; +} + .section { background: var(--color-bg-card); border-radius: var(--radius-md); diff --git a/packages/frontend/src/profile/ProfilePage.tsx b/packages/frontend/src/profile/ProfilePage.tsx index 8207a47..7b94f84 100644 --- a/packages/frontend/src/profile/ProfilePage.tsx +++ b/packages/frontend/src/profile/ProfilePage.tsx @@ -5,10 +5,12 @@ import { UserAvatar } from '../components/UserAvatar'; import { resizeImageToBase64 } from '../utils/imageUtils'; import styles from './ProfilePage.module.css'; +type ProfileTab = 'personal' | 'expert' | 'password'; + export function ProfilePage() { const { user, refreshUser } = useAuth(); - // --- Persoenliche Informationen --- + // --- Persönliche Informationen --- const [firstName, setFirstName] = useState(user?.firstName ?? ''); const [lastName, setLastName] = useState(user?.lastName ?? ''); const [profileMsg, setProfileMsg] = useState(''); @@ -22,7 +24,7 @@ export function ProfilePage() { const [avatarLoading, setAvatarLoading] = useState(false); const fileInputRef = useRef(null); - // --- Passwort aendern --- + // --- Passwort ändern --- const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); @@ -65,12 +67,12 @@ export function ProfilePage() { if (!file) return; if (!file.type.startsWith('image/')) { - setAvatarError('Bitte waehlen Sie eine Bilddatei aus'); + setAvatarError('Bitte wählen Sie eine Bilddatei aus'); return; } if (file.size > 5 * 1024 * 1024) { - setAvatarError('Bild darf maximal 5MB gross sein'); + setAvatarError('Bild darf maximal 5MB groß sein'); return; } @@ -139,14 +141,14 @@ export function ProfilePage() { } }; - // === Handler: Passwort aendern === + // === Handler: Passwort ändern === const handlePasswordChange = async (e: FormEvent) => { e.preventDefault(); setPasswordMsg(''); setPasswordError(''); if (newPassword !== confirmPassword) { - setPasswordError('Passwoerter stimmen nicht ueberein'); + setPasswordError('Passwörter stimmen nicht überein'); return; } @@ -162,14 +164,14 @@ export function ProfilePage() { currentPassword, newPassword, }); - setPasswordMsg('Passwort erfolgreich geaendert'); + setPasswordMsg('Passwort erfolgreich geändert'); setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setPasswordError( - error.response?.data?.message ?? 'Fehler beim Aendern des Passworts', + error.response?.data?.message ?? 'Fehler beim Ändern des Passworts', ); } finally { setPasswordLoading(false); @@ -209,7 +211,7 @@ export function ProfilePage() { } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setTfaError( - error.response?.data?.message ?? 'Ungueltiger Code', + error.response?.data?.message ?? 'Ungültiger Code', ); setTfaLoading(false); return; @@ -255,6 +257,9 @@ export function ProfilePage() { refreshUser().catch(() => {}); }; + // --- Tab-Navigation --- + const [activeTab, setActiveTab] = useState('personal'); + return (

- {/* === Sektion 1: Persoenliche Informationen === */} -
-

Persoenliche Informationen

+ {/* === Tab-Leiste === */} +
+ + + +
- {/* Profilbild */} -
-
fileInputRef.current?.click()} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click(); - }} - > - -
- Bild aendern -
-
- -
- - {avatar && ( + +
+ Bild ändern +
+
+ +
- )} + {avatar && ( + + )} +
+ {avatarMsg &&
{avatarMsg}
} + {avatarError &&
{avatarError}
} + + JPEG, PNG, GIF oder WebP. Wird auf 200x200 Pixel skaliert. +
- {avatarMsg &&
{avatarMsg}
} - {avatarError &&
{avatarError}
} - - JPEG, PNG, GIF oder WebP. Wird auf 200x200 Pixel skaliert. - + +
+ {profileMsg &&
{profileMsg}
} + {profileError &&
{profileError}
} + +
+ + + E-Mail-Adresse kann nicht geändert werden +
+ +
+
+ + setFirstName(e.target.value)} + required + maxLength={100} + /> +
+
+ + setLastName(e.target.value)} + required + maxLength={100} + /> +
+
+ +
+ + +
+ + +
+ )} -
- {profileMsg &&
{profileMsg}
} - {profileError &&
{profileError}
} + {/* === Tab: Experten Profil (Platzhalter) === */} + {activeTab === 'expert' && ( +
+

Experten Profil

+

+ Hier können Sie zukünftig Ihr Experten-Profil verwalten. +

+
+ )} -
- - - E-Mail-Adresse kann nicht geaendert werden -
+ {/* === Tab: Passwort ändern === */} + {activeTab === 'password' && ( +
+

Passwort ändern

+ + {passwordMsg &&
{passwordMsg}
} + {passwordError && ( +
{passwordError}
+ )} -
- + setFirstName(e.target.value)} + id="currentPassword" + type="password" + value={currentPassword} + onChange={(e) => setCurrentPassword(e.target.value)} required - maxLength={100} + minLength={8} />
+
- + setLastName(e.target.value)} + id="newPassword" + type="password" + value={newPassword} + onChange={(e) => setNewPassword(e.target.value)} required - maxLength={100} + minLength={8} />
-
-
- - -
+
+ + setConfirmPassword(e.target.value)} + required + minLength={8} + /> +
- - -
+ + +

+ )} - {/* === Sektion 2: Passwort aendern === */} -
-

Passwort aendern

-
- {passwordMsg &&
{passwordMsg}
} - {passwordError && ( -
{passwordError}
- )} - -
- - setCurrentPassword(e.target.value)} - required - minLength={8} - /> -
- -
- - setNewPassword(e.target.value)} - required - minLength={8} - /> -
- -
- - setConfirmPassword(e.target.value)} - required - minLength={8} - /> -
- - -
-
- - {/* === Sektion 3: Zwei-Faktor-Authentifizierung === */} + {/* === Sicherheit: Zwei-Faktor-Authentifizierung (immer sichtbar) === */}

Zwei-Faktor-Authentifizierung (2FA) @@ -486,17 +528,17 @@ export function ProfilePage() {

- QR-Code fuer 2FA + QR-Code für 2FA
- + {setupData.secret}
- + )} - {/* Deaktivierung mit Passwort bestaetigen */} + {/* Deaktivierung mit Passwort bestätigen */} {twoFactorEnabled && showDisableConfirm && (

- {/* Oeffentliche Routen */} + {/* Öffentliche Routen */} } /> - {/* Geschuetzte Routen */} + {/* Geschützte Routen */}