Initial commit
This commit is contained in:
81
src/auth/session.ts
Normal file
81
src/auth/session.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user