106 lines
3.6 KiB
TypeScript
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;
|
|
}
|