Add Super Admin login routing and wire tenant onboarding to auto-create a location

super_admin is now a real LoginRole, derived from the server's
issuperadmin flag (not a client-guessable roleid) — routes exclusively
to a new minimal SuperAdminPage, never the merchant console.

That page reuses the existing tenant/store/rider onboarding wizard
(AdminConsole) rather than duplicating it — its Tenant tab was already
calling the right composite endpoint but had nowhere to send location
data, and was never actually reachable from anywhere in the app. Now
it collects a primary outlet name + business category and sends a
nested tenantlocations object, so one submit provisions tenant +
active location + admin user instead of leaving the tenant
locationless. createTenantUser also moves off the mob API base onto
the web one, matching where this is actually called from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-07-16 20:46:07 +05:30
parent 186546f1fe
commit ecbae9d8fc
5 changed files with 143 additions and 9 deletions

View File

@@ -68,6 +68,11 @@ const RESPONSE_FIELDS = {
applocation: 'applocation',
locationid: 'locationid',
locationname: 'locationname',
// Server-derived flag, checked before roleid: a platform operator who
// isn't scoped to any tenant. Never inferred client-side (a roleid the
// client could send/expect would be spoofable) — this comes back on the
// login row itself, gated server-side in AppLogin.
issuperadmin: 'issuperadmin',
} as const;
/**
@@ -79,7 +84,7 @@ const ADMIN_ROLE_IDS = new Set<number>([1, 3]);
// ──────────────────────────────────────────────────────────────────────────────
export type LoginRole = 'admin' | 'user';
export type LoginRole = 'admin' | 'user' | 'super_admin';
export interface AuthUser {
role: LoginRole;
@@ -87,6 +92,8 @@ export interface AuthUser {
email: string;
userid?: number;
roleid?: number;
/** Platform operator, not scoped to any tenant — see RESPONSE_FIELDS.issuperadmin. */
issuperadmin?: boolean;
/** Phone number on the user record. */
contactno?: string;
/** The merchant/tenant this user belongs to — scopes every Fiesta query. */
@@ -229,15 +236,20 @@ export function matchTenantUser(users: Row[], email: string): Row | null {
/** Assemble the final AuthUser (role + identity) from a resolved user record. */
export function buildAuthUser(row: Row | null, email: string): AuthUser {
const roleid = row ? num(row[RESPONSE_FIELDS.roleid]) : 0;
const issuperadmin = Boolean(row && row[RESPONSE_FIELDS.issuperadmin]);
const applocation = row ? str(row[RESPONSE_FIELDS.applocation]).trim() : '';
const locationname = row ? str(row[RESPONSE_FIELDS.locationname]).trim() : '';
const contactno = row ? str(row[RESPONSE_FIELDS.contactno]).trim() : '';
return {
role: roleFromRoleId(roleid),
// issuperadmin is checked first: it's a server-derived flag from the
// login row, not something the client asserts, so it takes priority
// over the roleid-based admin/user split.
role: issuperadmin ? 'super_admin' : roleFromRoleId(roleid),
name: displayName(row ?? {}, email),
email,
userid: row && row[RESPONSE_FIELDS.userid] != null ? num(row[RESPONSE_FIELDS.userid]) : undefined,
roleid,
issuperadmin: issuperadmin || undefined,
contactno: contactno || undefined,
tenantid: row && row[RESPONSE_FIELDS.tenantid] != null ? num(row[RESPONSE_FIELDS.tenantid]) : undefined,
applocationid:

View File

@@ -1015,6 +1015,18 @@ export async function updateUser(input: UpdateUserInput): Promise<Row> {
return fiestaSend<Row>('users/update', 'PUT', input);
}
export interface CreateTenantLocationPayload {
locationname: string;
email?: string;
contactno?: string;
address?: string;
suburb?: string;
city?: string;
state?: string;
postcode?: string;
applocationid?: number;
}
export interface CreateTenantInput {
tenantname: string;
companyname: string;
@@ -1027,11 +1039,15 @@ export interface CreateTenantInput {
postcode?: string;
approved?: number;
status?: string;
applocationid?: number;
categoryid?: number;
/** Auto-provisions the tenant's primary (Active) location in the same call. */
tenantlocations?: CreateTenantLocationPayload;
}
/** POST /tenants/createtenantuser — Onboard a new tenant and create their admin user. */
/** POST /tenants/createtenantuser — Onboard a new tenant, primary location, and admin user. */
export async function createTenantUser(input: CreateTenantInput): Promise<Row> {
const res = await fetch(`${FIESTA_MOB_BASE}/tenants/createtenantuser`, {
const res = await fetch(`${FIESTA_BASE}/tenants/createtenantuser`, {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(input),