forked from jappel/leistungsbilanz-ts
Add login-required user management (admin/user roles, Config->Benutzer)
No self-signup: admins create every account in Config -> Benutzer with a typed start password; a forced password change gates first use. Session- cookie auth protects all /api/* routes; /api/users additionally requires the admin role. See docs/authentication.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b50c00c0ac
commit
c56cfb30ad
36 changed files with 4928 additions and 38 deletions
86
src/frontend/utils/auth-api.ts
Normal file
86
src/frontend/utils/auth-api.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Small, self-contained fetch helper for the auth/user endpoints - kept
|
||||
// separate from the large domain api.ts so this feature doesn't have to
|
||||
// touch that file's shared `request()` internals.
|
||||
|
||||
export type UserRole = "admin" | "user";
|
||||
|
||||
export interface AuthStatusDto {
|
||||
signedIn: boolean;
|
||||
mustChangePassword: boolean;
|
||||
}
|
||||
|
||||
export interface AuthUserDto {
|
||||
id: string;
|
||||
username: string;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
mustChangePassword: boolean;
|
||||
}
|
||||
|
||||
export interface UserDto {
|
||||
id: string;
|
||||
username: string;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
active: boolean;
|
||||
mustChangePassword: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export class AuthApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
headers: { "Content-Type": "application/json", ...init?.headers },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
let message = `Anfrage fehlgeschlagen (Status ${response.status})`;
|
||||
try {
|
||||
const body = (await response.json()) as { error?: unknown };
|
||||
if (typeof body.error === "string") message = body.error;
|
||||
} catch {
|
||||
// Keep the generic message for non-JSON error responses.
|
||||
}
|
||||
throw new AuthApiError(message, response.status);
|
||||
}
|
||||
if (response.status === 204) return undefined as T;
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
status: () => request<AuthStatusDto>("/api/auth/status"),
|
||||
login: (username: string, password: string) =>
|
||||
request<AuthStatusDto>("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
}),
|
||||
me: () => request<AuthUserDto>("/api/auth/me"),
|
||||
changePassword: (currentPassword: string, newPassword: string) =>
|
||||
request<{ changed: true }>("/api/auth/change-password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ currentPassword, newPassword }),
|
||||
}),
|
||||
logout: () => request<{ signedOut: true }>("/api/auth/logout", { method: "POST" }),
|
||||
};
|
||||
|
||||
export const userApi = {
|
||||
list: () => request<UserDto[]>("/api/users"),
|
||||
create: (input: { username: string; name: string; role: UserRole; initialPassword: string }) =>
|
||||
request<UserDto>("/api/users", { method: "POST", body: JSON.stringify(input) }),
|
||||
update: (userId: string, patch: { role?: UserRole; active?: boolean }) =>
|
||||
request<UserDto>(`/api/users/${userId}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
resetPassword: (userId: string, newPassword: string) =>
|
||||
request<UserDto>(`/api/users/${userId}/reset-password`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ newPassword }),
|
||||
}),
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue