Previously "Continue" on the token step only checked the field was non-empty; a wrong token wasn't caught until the final submit, after the user had filled in email + password. Add a non-burning verify: - backend: POST /setup/verify-token constant-time compares the token without consuming it (createInitialAdmin still claims it atomically on submit), gated on no-admin-exists and rate-limited like /setup/admin. - frontend: step-1 "Continue" calls verifyToken and only advances on a valid token; a wrong token shows the invalidToken error on the field, 429 -> too-many-attempts, 409 -> redirect to login. Adds integration tests for accept-without-burn / reject / closed-once-set.
42 lines
1.3 KiB
TypeScript
42 lines
1.3 KiB
TypeScript
import { api } from '../config/api';
|
|
|
|
export interface SetupStatus {
|
|
needsAdmin: boolean;
|
|
complete: boolean;
|
|
}
|
|
|
|
export interface SetupAdminUser {
|
|
id: number;
|
|
username: string;
|
|
email: string;
|
|
role: { name: string; displayName?: string };
|
|
}
|
|
|
|
export interface CreateInitialAdminInput {
|
|
token: string;
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
// First-run bootstrap. Public endpoints that self-close once an admin exists.
|
|
export const setupService = {
|
|
async getSetupStatus(): Promise<SetupStatus> {
|
|
const response = await api.get<SetupStatus>('/setup/status');
|
|
return response.data;
|
|
},
|
|
|
|
// Step-1 pre-flight: confirm the token is valid before advancing to the
|
|
// account step. Rejects (400, field: 'token') on a wrong token without
|
|
// burning it. Throws on non-2xx so the caller can branch on the status.
|
|
async verifyToken(token: string): Promise<{ valid: boolean }> {
|
|
const response = await api.post<{ valid: boolean }>('/setup/verify-token', { token });
|
|
return response.data;
|
|
},
|
|
|
|
async createInitialAdmin(input: CreateInitialAdminInput): Promise<{ user: SetupAdminUser }> {
|
|
// Admin JWT is returned as an HttpOnly cookie (mirrors login); body carries the user.
|
|
const response = await api.post<{ user: SetupAdminUser }>('/setup/admin', input);
|
|
return response.data;
|
|
},
|
|
};
|