store user login

This commit is contained in:
2026-08-25 18:00:16 +05:30
parent 1dc582ba07
commit 517a99d577
62 changed files with 8059 additions and 650 deletions

View File

@@ -84,5 +84,8 @@ export const HOME_ROUTE: Record<ConsoleRole, string> = {
export const ROLE_LABEL: Record<ConsoleRole, string> = {
'nearle-admin': 'Nearle Admin',
'store-admin': 'Store Admin',
'store-manager': 'Store Manager',
// The backend's own word for it: an `app_users` row with roleid 0, bound to
// one `tenantlocations.locationid`. "Manager" implied a rank the record does
// not carry.
'store-manager': 'Store user',
};

View File

@@ -26,31 +26,50 @@ export class PasswordSetupRequiredError extends Error {
interface LoginBody {
authname: string;
password: string;
roleid?: number;
configid?: number;
/**
* Not optional in practice.
*
* The lookup behind every login is `WHERE authname = ? AND configid = ?`
* (`userRepository.go:218`). Omitted, Go receives 0, the query matches no
* row, and every account on the platform answers "Invalid Email". The console
* surface is configid 1 — `createtenantuser` hard-codes it into the account it
* spawns for exactly this reason.
*/
configid: number;
}
/** The console's surface. Every account this app can sign in carries it. */
const CONFIG_ID = 1;
/**
* Signs in against the web login endpoint.
* Signs in.
*
* 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.
* `applogin`, not `tenant/weblogin`. The latter carries a check the former does
* not — `request.roleid == app_users.roleid` (`userService.go:224`) — and since
* Go's zero value is 0, a request without a roleid means "roleid must be 0".
* That is the branch-user role, so weblogin silently locked out every Store
* Admin and every platform operator with a 403 reading "Unauthorized email".
*
* The handler answers HTTP 200 with `status: false` for most failures, so the
* envelope is inspected rather than the HTTP status.
*/
export async function login(email: string, password: string): Promise<SessionUser> {
const body: LoginBody = { authname: email.trim(), password };
const body: LoginBody = { authname: email.trim(), password, configid: CONFIG_ID };
const envelope = await api.envelope<FiestaUser & { setup?: boolean; userid?: number }>(
`${WEB}/users/tenant/weblogin`,
`${WEB}/users/applogin`,
{ method: 'POST', body },
);
// A brand-new account — `createtenantlocation` spawns branch logins with an
// empty password — answers `status: true` with a 409 and the userid to set
// one against. It is not a failure, it is the first step.
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');
throw new Error(loginMessage(envelope.code, envelope.message));
}
const session = toSessionUser(envelope.details);
@@ -58,6 +77,29 @@ export async function login(email: string, password: string): Promise<SessionUse
return session;
}
/**
* The backend's own words, where they are usable, and ours where they are not.
*
* "Invalid Email" is technically true and unhelpful — the same answer covers a
* typo and a till account, because roleids 7 and 8 are excluded from every web
* login lookup. A cashier is not refused here, they are not found, so the copy
* must not say "wrong password".
*/
function loginMessage(code: number | undefined, message: string | undefined): string {
if (code === 409) {
return 'We do not recognise that email. Till accounts (supervisor or cashier) sign in at the terminal, not here.';
}
if (code === 401) {
return message?.toLowerCase().includes('required')
? 'Enter your password.'
: 'That password is not right.';
}
// 403 covers both an inactive account and an inactive store, and the two
// messages differ — pass the backend's through rather than flattening them.
if (code === 403) return message ?? 'This account cannot sign in. Contact your administrator.';
return message ?? 'Sign-in failed';
}
export function persist(session: SessionUser): void {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session));
}