domain change

This commit is contained in:
2026-09-28 18:08:55 +05:30
parent c9616edad9
commit eeef8851e4
34 changed files with 99 additions and 4211 deletions

View File

@@ -1,90 +1,63 @@
import type { ConsoleRole } from './roles';
/**
* Which console this build is.
* Who may sign in to the merchant console.
*
* ── Why one codebase produces two sites ─────────────────────────────────────
* ── Why this is a constant ──────────────────────────────────────────────────
*
* 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.
* There was a `VITE_WORKSPACE` build flag here while one codebase served both
* consoles and had to be told which it was. That is over: Nearle's own staff
* have their own application, `nearle-platform`, and this repository is the
* merchant console and nothing else. A flag would only be a way to deploy this
* application as something it is not.
*
* 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 separation is a refusal, not a redirect ─────────────────────────────
*
* ── The default is `merchant`, deliberately ─────────────────────────────────
* A Nearle staff account is turned away here with a sentence naming where it
* belongs. It is not bounced to the other site carrying a half-made session.
*
* 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.
* The role is not known until the password has been checked — `applogin`
* returns it — so the refusal can only happen after credentials are verified.
* `login` applies it BEFORE the session is written and `restore` applies it to
* what is already stored. That order is the point: a session persisted first
* and refused afterwards leaves somebody signed in by every measure the shell
* uses, with a nav built from a role this application serves no routes for.
*/
export type Workspace = 'platform' | 'merchant';
export const WORKSPACE = 'merchant' as const;
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']),
};
const ALLOWED: ReadonlySet<ConsoleRole> = new Set<ConsoleRole>(['store-admin', 'store-manager']);
export function isAllowedHere(role: ConsoleRole): boolean {
return ALLOWED[WORKSPACE].has(role);
return ALLOWED.has(role);
}
/**
* The other console's address, for the sentence shown to somebody in the wrong
* place.
* The platform console's address, for the sentence shown to a staff member who
* signs in at the wrong site.
*
* 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.
* A build variable rather than a constant, because the two are separate
* deployments and either can move. The fallback is the production hostname,
* which is right far more often than saying nothing would be.
*/
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',
};
const PLATFORM_HOST =
(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.
* Names the host rather than linking to it. A live link from one sign-in screen
* to another 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.`;
return `${roleWord(role)} accounts sign in at ${PLATFORM_HOST}, not here.`;
}
function roleWord(role: ConsoleRole): string {
switch (role) {
case 'nearle-admin':
return 'Nearle staff';
// Unreachable: both merchant roles are allowed here, so neither reaches the
// refusal. Present because the switch is exhaustive over the union and a
// missing arm would be a type error the day a role is added.
case 'store-admin':
return 'Store admin';
case 'store-manager':