backend integration started
This commit is contained in:
@@ -1,18 +1,16 @@
|
||||
import { createContext, use, useCallback, useMemo, useState, type ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState, type ReactNode } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { HOME_ROUTE, type ConsoleRole, type SessionUser } from './roles';
|
||||
import { clear, login as loginRequest, persist, restore } from './session';
|
||||
import { disableDemo, enableDemo } from '@/demo';
|
||||
import { clear, login as loginRequest, restore } from './session';
|
||||
import { AuthContext, useAuth } from './context';
|
||||
|
||||
interface AuthContextValue {
|
||||
user: SessionUser | null;
|
||||
signIn: (email: string, password: string) => Promise<SessionUser>;
|
||||
/** Development only — see `src/demo`. Absent from a production build. */
|
||||
signInAsDemo: (session: SessionUser) => void;
|
||||
signOut: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
/**
|
||||
* The context object and `useAuth` live in `./context`, which exports no
|
||||
* components — see the note there. Both are re-exported from here so every
|
||||
* existing import keeps working; the split is invisible to callers.
|
||||
*/
|
||||
export { useAuth } from './context';
|
||||
export type { AuthContextValue } from './context';
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<SessionUser | null>(() => restore());
|
||||
@@ -23,38 +21,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
return session;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Seeds a session without touching the API. Demo mode is switched on at the
|
||||
* same moment, so the fixture backend and the fixture session can never be
|
||||
* out of step — a demo user paired with live data would be the worst of both.
|
||||
*/
|
||||
const signInAsDemo = useCallback((session: SessionUser) => {
|
||||
if (!import.meta.env.DEV) return;
|
||||
enableDemo();
|
||||
persist(session);
|
||||
setUser(session);
|
||||
}, []);
|
||||
|
||||
const signOut = useCallback(() => {
|
||||
if (import.meta.env.DEV) disableDemo();
|
||||
clear();
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ user, signIn, signInAsDemo, signOut }),
|
||||
[user, signIn, signInAsDemo, signOut],
|
||||
() => ({ user, signIn, signOut }),
|
||||
[user, signIn, signOut],
|
||||
);
|
||||
|
||||
return <AuthContext value={value}>{children}</AuthContext>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const context = use(AuthContext);
|
||||
if (!context) throw new Error('useAuth must be used inside <AuthProvider>');
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route guard.
|
||||
*
|
||||
|
||||
45
src/auth/context.ts
Normal file
45
src/auth/context.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createContext, use } from 'react';
|
||||
import type { SessionUser } from './roles';
|
||||
|
||||
/**
|
||||
* The auth context object, kept in a module that exports NO components.
|
||||
*
|
||||
* That separation is the whole point of this file, and it is not style.
|
||||
*
|
||||
* `createContext()` returns an object whose IDENTITY is the key React matches a
|
||||
* provider to a consumer by. React Fast Refresh re-executes a module when it or
|
||||
* its dependents change, and a module that exports components is a refresh
|
||||
* boundary — so while `AuthContext` lived beside `AuthProvider`, a refresh could
|
||||
* mint a NEW context object for the provider while consumers that were not
|
||||
* re-executed still held the OLD one. The provider then publishes into a
|
||||
* context nobody is reading, `use(AuthContext)` returns null, and `useAuth`
|
||||
* throws `useAuth must be used inside <AuthProvider>` — from a component that
|
||||
* is unmistakably inside it.
|
||||
*
|
||||
* That error is a lie about the component tree, which is what makes it so
|
||||
* expensive: it sends you looking at `main.tsx`, where the nesting is correct
|
||||
* and always was. A file with no component exports is not a refresh boundary,
|
||||
* so the object created here is created once per page load and cannot be
|
||||
* duplicated by an edit anywhere else.
|
||||
*
|
||||
* Rule for this file: no components, ever. Adding one re-arms the bug.
|
||||
*/
|
||||
|
||||
export interface AuthContextValue {
|
||||
user: SessionUser | null;
|
||||
signIn: (email: string, password: string) => Promise<SessionUser>;
|
||||
signOut: () => void;
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const context = use(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useAuth must be used inside <AuthProvider>. If the tree looks right, the dev server ' +
|
||||
'is serving a stale module — stop it, delete node_modules/.vite, and start it again.',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -77,6 +77,35 @@ export async function login(email: string, password: string): Promise<SessionUse
|
||||
return session;
|
||||
}
|
||||
|
||||
/** The backend's floor, enforced here too so the refusal is instant. */
|
||||
export const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
/**
|
||||
* Sets the password on an account that has never had one.
|
||||
*
|
||||
* `PUT /users/update` doubles as the password call. There is no dedicated
|
||||
* endpoint and no reset flow — the controller says so in as many words
|
||||
* (`userController.go:145`): "this endpoint also doubles as the
|
||||
* password-setup/reset call (userid + password only, everything else left zero
|
||||
* so GORM's `Updates` skips it)". Sending only those two fields is therefore
|
||||
* load-bearing: a struct with any other field populated would write it.
|
||||
*
|
||||
* This is reachable only with the `userid` that `applogin` just handed back for
|
||||
* an account it confirmed has an empty password. It is not a "change my
|
||||
* password" call and must not be wired up as one — nothing here verifies the
|
||||
* old password, because there is no old password.
|
||||
*
|
||||
* Passwords are stored in clear on this backend. That is not something the
|
||||
* console can fix, and it is the reason this flow exists at all rather than an
|
||||
* emailed setup link.
|
||||
*/
|
||||
export async function setInitialPassword(userid: number, password: string): Promise<void> {
|
||||
if (password.length < MIN_PASSWORD_LENGTH) {
|
||||
throw new Error(`Use at least ${MIN_PASSWORD_LENGTH} characters.`);
|
||||
}
|
||||
await api.put<unknown>(`${WEB}/users/update`, { userid, password });
|
||||
}
|
||||
|
||||
/**
|
||||
* The backend's own words, where they are usable, and ours where they are not.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user