seperation of nearle admin

This commit is contained in:
2026-09-28 15:43:07 +05:30
parent 153f87b577
commit c9616edad9
7 changed files with 397 additions and 0 deletions

93
src/auth/workspace.ts Normal file
View File

@@ -0,0 +1,93 @@
import type { ConsoleRole } from './roles';
/**
* Which console this build is.
*
* ── Why one codebase produces two sites ─────────────────────────────────────
*
* Nearle's own staff work at `platform.nearledaily.com`; merchants and their
* branch users work at `app.nearledaily.com`. They are the same application
* built twice with this flag set differently, rather than two repositories,
* because every screen below the workspace split — drawers, tables, the
* assistant, the design system — is shared and would otherwise be maintained
* in two places and drift.
*
* What the flag changes is which routes are mounted and which roles may sign
* in. It does not change what is compiled: the branch in `App.tsx` is evaluated
* at runtime, so both workspaces' chunks are built and served, and the
* unmounted one is simply never fetched because nothing routes to it. Removing
* it from the bundle would need the flag to be a literal at each import site,
* which is a separate piece of work and buys nothing for access control.
*
* ── The default is `merchant`, deliberately ─────────────────────────────────
*
* An unset variable is the ordinary state of a developer's machine and of any
* deployment that has not been told about this yet. Defaulting to `merchant`
* means the existing site keeps behaving exactly as it did, and the platform
* build is the one that has to be asked for. The opposite default would turn
* every un-migrated environment into a platform console the day this shipped.
*/
export type Workspace = 'platform' | 'merchant';
const CONFIGURED = (import.meta.env?.['VITE_WORKSPACE'] ?? '').trim().toLowerCase();
export const WORKSPACE: Workspace = CONFIGURED === 'platform' ? 'platform' : 'merchant';
export const IS_PLATFORM = WORKSPACE === 'platform';
/**
* Who may sign in here.
*
* The separation is a REFUSAL, not a redirect. A merchant reaching the platform
* console is told which console their account belongs to and stays where they
* are; they are not bounced across a domain boundary carrying a half-made
* session. Each site serves exactly one audience and says so.
*/
const ALLOWED: Record<Workspace, ReadonlySet<ConsoleRole>> = {
platform: new Set<ConsoleRole>(['nearle-admin']),
merchant: new Set<ConsoleRole>(['store-admin', 'store-manager']),
};
export function isAllowedHere(role: ConsoleRole): boolean {
return ALLOWED[WORKSPACE].has(role);
}
/**
* The other console's address, for the sentence shown to somebody in the wrong
* place.
*
* Named rather than derived from `location.hostname`, because the two sites are
* not a naming convention apart — they are separate deployments and either can
* move. A build that was not told falls back to the production hostnames, which
* is right far more often than saying nothing.
*/
const OTHER_SITE: Record<Workspace, string> = {
platform: (import.meta.env?.['VITE_MERCHANT_HOST'] ?? '').trim() || 'app.nearledaily.com',
merchant: (import.meta.env?.['VITE_PLATFORM_HOST'] ?? '').trim() || 'platform.nearledaily.com',
};
/**
* What to tell somebody whose account belongs to the other console.
*
* Names the host rather than linking to it. A live link from a sign-in screen
* to another sign-in screen reads as a redirect that failed, and this is not a
* failure — it is the right answer to the wrong door.
*/
export function wrongConsoleMessage(role: ConsoleRole): string {
const site = OTHER_SITE[WORKSPACE];
return IS_PLATFORM
? `This is the Nearle platform console. ${roleWord(role)} accounts sign in at ${site}.`
: `${roleWord(role)} accounts sign in at ${site}, not here.`;
}
function roleWord(role: ConsoleRole): string {
switch (role) {
case 'nearle-admin':
return 'Nearle staff';
case 'store-admin':
return 'Store admin';
case 'store-manager':
return 'Store user';
}
}