mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
`GET /api/auth/me` grows an `accountAccess` block: the accounts this caller may act on, the one the browser is inside right now, and the booleans a UI binds directly — `canSwitch`, and `canWrite` per account. Every value is resolved server-side. A client that worked out what it may do from grant rows would be a second program deciding one person's access to a health record, and the two answer differently the first time an expiry or a revocation appears. `canWrite` is false for every grant in v1 because invitations write READ only. It is resolved through the grant state machine rather than hardcoded, so the day a WRITE grant exists the answer changes without this file being edited. The route becomes an actor surface. It has to be: the switcher, the banner naming whose record is open, and the way back out all read this payload, and under a bare `requireAuth()` the whole app shell would refuse the moment a switch was on. Every field it returns is still the caller's own. The active record is published as a full entry, not an id to look up. A banner that had to join two fields could render unnamed the moment they disagreed; here the active entry is one of the listed ones, selected by the pass that built them. A stamp left behind by a grant that lapsed reads as not switched — the same answer the resolver gives the next delegated request. Not published: the other account's avatar. Those bytes are owner-scoped, so a URL here would resolve to a refusal and paint a broken image. Clients keep the initials fallback.
59 lines
2.4 KiB
TypeScript
59 lines
2.4 KiB
TypeScript
/**
|
|
* v1.36.0 — the shape of the sharing block, and nothing else.
|
|
*
|
|
* Types only, with no imports at all, because both ends read them: the route
|
|
* that builds the block (`account-access.ts`, which talks to Postgres) and the
|
|
* client that renders it. Declaring them beside the server code would put a
|
|
* module that imports Prisma on the client's import graph — erased at compile
|
|
* today, one careless value export away from being bundled tomorrow. The
|
|
* boundary is cheaper to keep than to rediscover.
|
|
*
|
|
* What these types are FOR is stated once here and holds everywhere they are
|
|
* used: they carry resolved answers. `canWrite` and `canSwitch` are decisions
|
|
* the server has already made; a client that recomputed either from the rest
|
|
* of the payload would be the second program deciding one person's access.
|
|
*/
|
|
|
|
/** One account this caller may act on. */
|
|
export interface AccountAccessEntry {
|
|
/** The account whose record it is — the value `POST /api/account/switch` takes. */
|
|
accountId: string;
|
|
username: string;
|
|
displayName: string | null;
|
|
/** The grant's level. `"read"` throughout v1. */
|
|
access: "read" | "write";
|
|
/** May this caller change that record. Resolved server-side; `false` in v1. */
|
|
canWrite: boolean;
|
|
}
|
|
|
|
/** The `accountAccess` block on `GET /api/auth/me`. */
|
|
export interface AccountAccess {
|
|
/** Every account this caller may open, newest grant first. */
|
|
accounts: AccountAccessEntry[];
|
|
/**
|
|
* The record this browser is inside right now, resolved to a full entry —
|
|
* not an id the client has to look up. Null when it is in its own record,
|
|
* and always null on the Bearer transport.
|
|
*/
|
|
active: AccountAccessEntry | null;
|
|
/** Is there anywhere to switch to. */
|
|
canSwitch: boolean;
|
|
}
|
|
|
|
/**
|
|
* The name to put in front of a person, given what the payload published.
|
|
*
|
|
* `displayName` when the account set one, the username otherwise. This is a
|
|
* presentation fallback and not a derivation of anything the server decided —
|
|
* both values are published, and which of the two reads better is the client's
|
|
* business. Kept here so the banner, the switcher and the sharing panel cannot
|
|
* disagree about what to call somebody.
|
|
*/
|
|
export function accountLabel(entry: {
|
|
displayName: string | null;
|
|
username: string;
|
|
}): string {
|
|
const named = entry.displayName?.trim();
|
|
return named && named.length > 0 ? named : entry.username;
|
|
}
|