fix(config): require a real API base, ship the env template, show the real team
Three configuration defects and one screen of invented people. API base. The client defaulted to https://platform.loyaly.ai when LOYALY_API_BASE was unset, and that host serves THIS console, not the Behavision API - verified live: it answers /api/auth/me with the console's own 404 HTML and a login POST with the console's own BFF envelope. So an unset variable in production made the BFF call its own origin, which fails looking like a broken login form rather than a misconfiguration. There is now no remote fallback: development defaults to 127.0.0.1:8088 and production throws, naming the variable, the way tokenStore already refuses to run without AUTH_SECRET. A wrong host that appears to work is worse than a startup failure that says what is missing. The template. .env.example documented that same wrong host, and .gitignore's `.env*` matched the template itself, so it was never committed - a fresh clone got no template at all, for an app that cannot start in production without AUTH_SECRET. Added `!.env.example` after the ignore rule; .env.local and every other .env* stay ignored. The template carries placeholders only, no values. Team. /settings/team listed five invented people - aravind@nearle.in, Vikram Seth, Priya Sharma - with store names no endpoint supplies and roles that do not exist upstream, behind four controls that mutated local state and were lost on refresh. A merchant could not tell any of it from the real thing. It now reads GET /api/team, which the platform already serves and scopes by session, and renders what actually comes back: name, email, role, whether the account is still active, and last sign-in (or "Never", which is a fact worth seeing). The route used to map each row through toAuthUser, which reads client_name - a field GET /api/team does not send - so organisation was undefined on every row while active, last_login_at and created_at were discarded. ApiTeamMember now describes that payload properly and ApiUser is left to authentication. The screen is READ-ONLY on purpose. Accounts are born from invitations, and that flow already exists in the platform's own web app: a manager mints a code, the holder redeems it and chooses their own password. A second way to create a login does not belong here, least of all on the screen that lists them. Role changes and deactivation are supported upstream by PATCH /api/team/{id} and are deliberately not wired: deactivating revokes every session that person holds immediately, so it wants a confirmation step and 409 last_owner handling, neither of which belongs in a change whose purpose is removing invented data. types.ts also gains the Sales/Floor/Customer interfaces. They are inert here - nothing imports them yet - and land with this commit so the screens that consume them arrive as one reviewable change. Verified against the live local platform: two tenants, correct member lists for each, and no cross-tenant leakage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161AMotQ8FxGPZ9gFGb5wiK
This commit is contained in:
@@ -1,26 +1,43 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {teamApi} from '@/services/api/teamApi';
|
||||
import {serveUpstream} from '@/shared/services/bff';
|
||||
import {toAuthUser} from '@/features/auth/services/userMapper';
|
||||
import type {ApiUser} from '@/services/api/types';
|
||||
import type {ApiTeamMember} from '@/services/api/types';
|
||||
import type {UserRole} from '@/features/auth/types/auth';
|
||||
import type {TeamMember} from '@/features/team/types/team';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/** GET /api/team — console accounts for this company. */
|
||||
function toMember(u: ApiUser): TeamMember {
|
||||
const user = toAuthUser(u);
|
||||
/**
|
||||
* GET /api/team — console accounts for this company.
|
||||
*
|
||||
* The tenant is NOT a parameter. The platform takes it from the session and
|
||||
* scopes the query itself, so this route has nothing to filter by and must not
|
||||
* pretend otherwise — a client-side tenant filter is a check an attacker skips.
|
||||
*
|
||||
* ── What this used to drop ───────────────────────────────────────────────
|
||||
* It ran each row through `toAuthUser`, which reads `client_name` — a field
|
||||
* `GET /api/team` does not send. So `organisation` was `undefined` on every
|
||||
* row, while `active`, `last_login_at` and `created_at` were discarded. The
|
||||
* one field the team screen needs — who still has access — never arrived.
|
||||
*/
|
||||
function toMember(m: ApiTeamMember): TeamMember {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
organisation: user.organisation,
|
||||
id: m.id,
|
||||
// Falls back to the address rather than rendering a blank cell: somebody
|
||||
// invited but not yet named still has to be identifiable.
|
||||
name: m.full_name || m.email,
|
||||
email: m.email,
|
||||
role: m.role as UserRole,
|
||||
active: m.active,
|
||||
// Null rather than '' — "has never signed in" and "signed in at an unknown
|
||||
// time" are different facts, and the screen says so.
|
||||
lastLoginAt: m.last_login_at || null,
|
||||
createdAt: m.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return serveUpstream(req, (token) => teamApi.list(token), (users) =>
|
||||
users.map(toMember),
|
||||
return serveUpstream(req, (token) => teamApi.list(token), (members) =>
|
||||
members.map(toMember),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user