backend integration started

This commit is contained in:
2026-08-26 18:37:28 +05:30
parent 517a99d577
commit 3545819be8
50 changed files with 3461 additions and 3241 deletions

View File

@@ -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
View 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;
}

View File

@@ -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.
*