Initial commit

This commit is contained in:
2026-08-24 20:35:18 +05:30
commit 1dc582ba07
85 changed files with 21081 additions and 0 deletions

81
src/auth/session.ts Normal file
View File

@@ -0,0 +1,81 @@
/**
* Sign-in and session persistence.
*
* There is no token to hold. `TenantWebLogin` returns the user record and
* nothing else, so the session IS that record. It is kept in sessionStorage
* rather than localStorage: a shared back-office machine should not stay signed
* in after the browser closes, and there is no server-side session to revoke.
*/
import { api, WEB } from '@/api/client';
import type { FiestaUser } from '@/api/types';
import { toSessionUser, type SessionUser } from './roles';
const STORAGE_KEY = 'nearle.session.v1';
/** Thrown when the account exists but has never had a password set. */
export class PasswordSetupRequiredError extends Error {
readonly userid: number;
constructor(userid: number) {
super('This account needs a password before it can sign in.');
this.name = 'PasswordSetupRequiredError';
this.userid = userid;
}
}
interface LoginBody {
authname: string;
password: string;
roleid?: number;
configid?: number;
}
/**
* Signs in against the web login endpoint.
*
* The handler answers HTTP 200 with `status: false` for a wrong password, and
* with `code: 409` plus `details.setup` when no password has been set, so the
* envelope is inspected rather than trusting the HTTP status.
*/
export async function login(email: string, password: string): Promise<SessionUser> {
const body: LoginBody = { authname: email.trim(), password };
const envelope = await api.envelope<FiestaUser & { setup?: boolean; userid?: number }>(
`${WEB}/users/tenant/weblogin`,
{ method: 'POST', body },
);
if (envelope.code === 409 && envelope.details?.setup === true) {
throw new PasswordSetupRequiredError(envelope.details.userid ?? 0);
}
if (envelope.status !== true || !envelope.details) {
throw new Error(envelope.message ?? 'Sign-in failed');
}
const session = toSessionUser(envelope.details);
persist(session);
return session;
}
export function persist(session: SessionUser): void {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session));
}
export function restore(): SessionUser | null {
const raw = sessionStorage.getItem(STORAGE_KEY);
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as SessionUser;
// A stored blob is only as trustworthy as the tab it came from; a shape
// check keeps a corrupted value from crashing the shell on boot.
if (typeof parsed?.userid !== 'number' || typeof parsed?.role !== 'string') return null;
return parsed;
} catch {
return null;
}
}
export function clear(): void {
sessionStorage.removeItem(STORAGE_KEY);
}