Files
loyaly-merchant/src/features/auth/mock/users.mock.ts

106 lines
3.6 KiB
TypeScript

import type {AuthUser} from '@/features/auth/types/auth';
/**
* The mock user directory. Server-only — nothing here is ever bundled to the
* client, because the only importers are route handlers.
*
* ── On the plaintext passwords ────────────────────────────────────────────
* They are plaintext BECAUSE this is a fixture, and the fixture is the one
* place where that is safe: it never leaves the server, and there is no real
* credential in it. What matters architecturally is that `verifyCredentials`
* below is the ONLY function that ever sees a password. When a real backend
* arrives, that function's body becomes an HTTP call (or an argon2/bcrypt
* comparison against a user table) and every other file in the app is
* untouched — including the login form, which already only knows how to ask.
*
* Do not add a "demo" user that accepts any password. The whole point of this
* layer is that wrong credentials fail.
*/
interface MockUserRecord extends AuthUser {
password: string;
}
const USERS: MockUserRecord[] = [
{
id: 'usr_owner_01',
email: 'aravind@nealre.in',
name: 'Aravind',
role: 'owner',
organisation: 'Loyaly Retail Pvt Ltd',
password: 'admin123',
},
{
id: 'usr_owner_02',
email: 'aravind@nearle.in',
name: 'Aravind',
role: 'owner',
organisation: 'Loyaly Retail Pvt Ltd',
password: 'admin123',
},
{
id: 'usr_mgr_01',
email: 'priya.sharma@loyaly.ai',
name: 'Priya Sharma',
role: 'manager',
organisation: 'Loyaly Retail Pvt Ltd',
password: 'indiranagar@01',
},
{
id: 'usr_analyst_01',
email: 'analyst@loyaly.ai',
name: 'Rahul Menon',
role: 'analyst',
organisation: 'Loyaly Retail Pvt Ltd',
password: 'reports@2026',
},
];
/** Public projection — the record minus the credential. */
function toAuthUser(record: MockUserRecord): AuthUser {
// Field-by-field rather than a rest-spread that drops `password`: a spread
// silently carries anything added to the record later, and this projection
// is the boundary that keeps credentials out of every response.
return {
id: record.id,
email: record.email,
name: record.name,
role: record.role,
organisation: record.organisation,
};
}
export type CredentialCheck =
| {outcome: 'ok'; user: AuthUser}
| {outcome: 'unknown_email'}
| {outcome: 'wrong_password'};
/**
* The single credential-checking seam.
*
* Note the two distinct failure outcomes. That is a deliberate product choice
* — the brief asks for "wrong email" and "wrong password" to read differently
* — and it is worth knowing that it also makes the login form a user
* enumeration oracle: an attacker can discover which addresses have accounts.
* If that trade stops being acceptable, collapse both branches at the ROUTE
* (map them to one 'invalid_credentials' message) rather than here, so the
* distinction stays available to audit logging.
*/
export function verifyCredentials(
email: string,
password: string,
): CredentialCheck {
const record = USERS.find(
(u) => u.email.toLowerCase() === email.trim().toLowerCase(),
);
if (!record) return {outcome: 'unknown_email'};
if (record.password !== password) return {outcome: 'wrong_password'};
return {outcome: 'ok', user: toAuthUser(record)};
}
/** Used by the session endpoint to re-resolve a cookie subject to a user. */
export function findUserById(id: string): AuthUser | null {
const record = USERS.find((u) => u.id === id);
return record ? toAuthUser(record) : null;
}