Adds auth pages
This commit is contained in:
@@ -41,3 +41,22 @@ export type ApiResult<T> = { ok: true; data: T } | { ok: false; error: ProblemDe
|
||||
export interface SetupStatus {
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export interface SetupRequest {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface InvitationValidation {
|
||||
email: string;
|
||||
name: string | null;
|
||||
isValid: boolean;
|
||||
errorCode: 'EXPIRED' | 'USED' | 'NOT_FOUND' | null;
|
||||
}
|
||||
|
||||
export interface InviteCompleteRequest {
|
||||
token: string;
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { InvitationValidation, InviteCompleteRequest } from '@/api/types';
|
||||
|
||||
interface ValidateState {
|
||||
data: InvitationValidation | null;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export function useValidateInvitation(token: string | undefined): ValidateState {
|
||||
const [state, setState] = useState<ValidateState>({ data: null, isLoading: token !== undefined, error: null });
|
||||
const mounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (token === undefined) {
|
||||
setState({ data: null, isLoading: false, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
setState({ data: null, isLoading: true, error: null });
|
||||
|
||||
api.get<InvitationValidation>(`/Invitation/validate?token=${encodeURIComponent(token)}`)
|
||||
.then((data) => {
|
||||
if (mounted.current) setState({ data, isLoading: false, error: null });
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (mounted.current)
|
||||
setState({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err : new Error(String(err)),
|
||||
});
|
||||
});
|
||||
}, [token]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
interface CompleteSetupState {
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
interface UseCompleteSetup extends CompleteSetupState {
|
||||
mutate: (data: InviteCompleteRequest) => Promise<void>;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function useCompleteSetup(): UseCompleteSetup {
|
||||
const [state, setState] = useState<CompleteSetupState>({ isLoading: false, error: null });
|
||||
|
||||
const mutate = useCallback(async (data: InviteCompleteRequest): Promise<void> => {
|
||||
setState({ isLoading: true, error: null });
|
||||
try {
|
||||
await api.post('/Invitation/complete', data);
|
||||
setState({ isLoading: false, error: null });
|
||||
} catch (err: unknown) {
|
||||
setState({ isLoading: false, error: err instanceof Error ? err : new Error(String(err)) });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => setState({ isLoading: false, error: null }), []);
|
||||
|
||||
return { ...state, mutate, reset };
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { SetupRequest, SetupStatus } from '@/api/types';
|
||||
|
||||
// Module-level session cache — one fetch per page load (BR-U2-08).
|
||||
let _setupStatusCache: SetupStatus | null = null;
|
||||
let _setupStatusPromise: Promise<SetupStatus> | null = null;
|
||||
|
||||
export function fetchSetupStatus(): Promise<SetupStatus> {
|
||||
if (_setupStatusCache !== null) return Promise.resolve(_setupStatusCache);
|
||||
if (_setupStatusPromise === null) {
|
||||
_setupStatusPromise = api
|
||||
.get<SetupStatus>('/Setup/status')
|
||||
.then((s) => {
|
||||
_setupStatusCache = s;
|
||||
return s;
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
_setupStatusPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return _setupStatusPromise;
|
||||
}
|
||||
|
||||
export function invalidateSetupStatusCache(): void {
|
||||
_setupStatusCache = null;
|
||||
_setupStatusPromise = null;
|
||||
}
|
||||
|
||||
interface SetupStatusState {
|
||||
data: SetupStatus | null;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export function useSetupStatus(): SetupStatusState {
|
||||
const [state, setState] = useState<SetupStatusState>(() =>
|
||||
_setupStatusCache !== null
|
||||
? { data: _setupStatusCache, isLoading: false, error: null }
|
||||
: { data: null, isLoading: true, error: null },
|
||||
);
|
||||
|
||||
const mounted = useRef(true);
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (_setupStatusCache !== null) return;
|
||||
fetchSetupStatus()
|
||||
.then((data) => {
|
||||
if (mounted.current) setState({ data, isLoading: false, error: null });
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (mounted.current)
|
||||
setState({ data: null, isLoading: false, error: err instanceof Error ? err : new Error(String(err)) });
|
||||
});
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
interface CreateOwnerState {
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
interface UseCreateOwner extends CreateOwnerState {
|
||||
mutate: (data: SetupRequest) => Promise<void>;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function useCreateOwner(): UseCreateOwner {
|
||||
const [state, setState] = useState<CreateOwnerState>({ isLoading: false, error: null });
|
||||
|
||||
const mutate = useCallback(async (data: SetupRequest): Promise<void> => {
|
||||
setState({ isLoading: true, error: null });
|
||||
try {
|
||||
await api.post('/Setup', data);
|
||||
_setupStatusCache = { initialized: true };
|
||||
setState({ isLoading: false, error: null });
|
||||
} catch (err: unknown) {
|
||||
setState({ isLoading: false, error: err instanceof Error ? err : new Error(String(err)) });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => setState({ isLoading: false, error: null }), []);
|
||||
|
||||
return { ...state, mutate, reset };
|
||||
}
|
||||
Reference in New Issue
Block a user