feat(quick-260630-gbh-01): account settings page — avatar upload + conditional password form

- AuthUser interface: add isLocalUser? + hasAvatar? fields
- uploadAvatarAction: server action forwarding file to POST /users/me/avatar
- AccountSettingsForm: avatar preview with initial fallback + upload; password form only for local users; LDAP notice
- /settings/general/account page mirroring smtp page structure
- SettingsSidebar: Konto link above SMTP link
- de.json + en.json: categoryAccount + account.* keys
This commit is contained in:
2026-06-30 12:00:40 +02:00
parent 0fba45d2c2
commit 4482a8ce78
6 changed files with 338 additions and 0 deletions
+43
View File
@@ -12,6 +12,8 @@ export interface AuthUser {
role: 'SUPER_ADMIN' | 'ADMIN' | 'USER';
tenantId: string;
mustChangePassword: boolean;
isLocalUser?: boolean;
hasAvatar?: boolean;
}
export interface LoginResult {
@@ -157,6 +159,47 @@ export async function changePasswordAction(
redirect('/');
}
export interface UploadAvatarResult {
success: boolean;
error?: string;
}
/**
* Upload a profile picture for the currently authenticated user.
* Forwards the multipart file to POST /users/me/avatar using the session cookie.
* Returns a discriminated result — does NOT redirect.
*/
export async function uploadAvatarAction(
formData: FormData,
): Promise<UploadAvatarResult> {
const cookieStore = await cookies();
const session = cookieStore.get('session')?.value;
if (!session) {
return { success: false, error: 'notAuthenticated' };
}
try {
const response = await fetch(`${API_URL}/users/me/avatar`, {
method: 'POST',
headers: {
Cookie: `session=${session}`,
},
body: formData,
});
if (!response.ok) {
const data = await response.json().catch(() => null);
return { success: false, error: data?.message ?? 'uploadError' };
}
return { success: true };
} catch (err) {
console.error('[uploadAvatarAction] fetch threw:', err);
return { success: false, error: 'networkError' };
}
}
/**
* Fetch the current authenticated user from the API.
* Uses the session cookie for authentication.