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:
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user