feat: add real account workspace controls

This commit is contained in:
2026-07-02 19:52:29 -07:00
parent 1b8fae0116
commit 1cdf89c608
12 changed files with 759 additions and 203 deletions

View File

@@ -4,6 +4,60 @@ This document covers backend authentication integration using better-auth, inclu
## 1. Overview
### Current Project Auth Contract: Session Organization and Profile APIs
#### 1. Scope / Trigger
- Trigger: app shell and account settings need authenticated current-tenant switching and current-account profile updates.
- This repository currently uses the custom `teatea_session` HTTP-only cookie plus Drizzle tables (`sessions`, `accounts`, `organizations`, `memberships`, `roles`), not a better-auth runtime.
#### 2. Signatures
- `GET /api/auth/session`: returns current account, active organization, available organization options, membership, and permissions.
- `POST /api/auth/organization`: switches `sessions.activeOrganizationId` for the current session.
- `PATCH /api/account/profile`: updates the authenticated account's `name` and `avatarUrl`.
#### 3. Contracts
- `GET /api/auth/session` response payload:
- `account: PublicAccount | null`
- `organization: Organization | null`
- `organizations: AccountOrganizationOption[]`
- `membership: Membership | null`
- `permissions: Permission[]`
- `POST /api/auth/organization` request payload: `{ organizationId: string }`.
- `PATCH /api/account/profile` request payload: `{ name: string; avatarUrl: string }`.
- All responses use `{ success, reason, ...payload }` and `Cache-Control: no-store`.
#### 4. Validation & Error Matrix
- Missing/expired session -> `success: false`, `401`, `未登录或会话已过期`.
- Empty `organizationId` -> `success: false`, `400`, `机构不能为空`.
- Organization not in authenticated account's available organization list -> `success: false`, `403`, `无权切换到该机构`.
- Empty profile `name` -> `success: false`, `400`, `用户名称不能为空`.
#### 5. Good/Base/Bad Cases
- Good: platform account with organization-read permission can switch among active organizations.
- Base: organization user can switch only among active memberships.
- Bad: never trust a client-provided organization ID without comparing against server-computed organization options.
#### 6. Tests Required
- Session API asserts `organizations` includes `isActive`, `slug`, and `roleLabel`.
- Organization switch asserts session row changes and forbidden org IDs are rejected.
- Profile update asserts current account only is updated and audit log is recorded.
#### 7. Wrong vs Correct
Wrong:
```typescript
await database.update(sessions).set({ activeOrganizationId: body.organizationId });
```
Correct:
```typescript
const target = context.organizations.find((organization) => organization.id === organizationId);
if (!target) {
throw new Error("无权切换到该机构");
}
await database.update(sessions).set({ activeOrganizationId: organizationId }).where(eq(sessions.id, context.session.id));
```
### What is better-auth
better-auth is a modern authentication library for TypeScript applications that provides:

View File

@@ -200,6 +200,23 @@ When a module becomes real, replace the placeholder with a Server Component that
data from the server boundary and pass only persisted, permission-filtered data into
client components.
### App Shell Tenant and Account Menu Contract
The desktop app shell footer is the tenant/account workspace control, not only a sign-out
surface.
- Show a compact organization switcher above the account card.
- Display both organization name and non-empty `slug`; do not hide or fabricate missing
tenant identifiers.
- Switch organizations by calling `POST /api/auth/organization` and then `router.refresh()`;
do not store the active organization in localStorage.
- The account card menu opens a user settings dialog for the current account. Profile edits
call `PATCH /api/account/profile`.
- OIDC binding UI must reflect real backend capability. If binding records/callbacks are not
implemented, show an honest not-connected state instead of fake provider accounts.
- The sidebar nav selected state belongs in a small client component using `usePathname()`;
keep the rest of `AppShell` server-rendered.
### Business Form Defaults Contract
Create forms for persisted business records must not prefill required domain fields with