forked from jappel/leistungsbilanz-ts
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>
86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
// 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 }),
|
|
}),
|
|
};
|