87 lines
3.0 KiB
Markdown
87 lines
3.0 KiB
Markdown
# Local JSON MVP Persistence and API Contracts
|
|
|
|
## Scenario: Single-Repo MVP Before Database Adoption
|
|
|
|
### 1. Scope / Trigger
|
|
|
|
- Trigger: implementing full-stack CRUD, auth, RBAC, or audit logging before Drizzle/Postgres/oRPC/better-auth are installed.
|
|
- Applies to this single-package Next.js app when the feature must be real and durable in local/runtime execution, but production database infrastructure is not yet available.
|
|
- Store boundary: `modules/core/server/store.ts`.
|
|
|
|
### 2. Signatures
|
|
|
|
- `readData(): Promise<AppData>`
|
|
- `writeData(data: AppData): Promise<void>`
|
|
- `updateData<T>(mutator: (data: AppData) => T): Promise<T>`
|
|
- `getCurrentAuthContext(): Promise<AuthContext | null>`
|
|
- `requirePermission(permission: Permission, auditContext: DeniedAuditContext): Promise<PermissionCheckSuccess | PermissionCheckFailure>`
|
|
- Route Handlers use standard `GET`, `POST`, `PATCH`, and `DELETE` exports and return `Response`.
|
|
|
|
### 3. Contracts
|
|
|
|
- Default data file: `.data/teatea.json`.
|
|
- Override key: `TEATEA_DATA_DIR` points to the directory containing `teatea.json`.
|
|
- `.data/` must stay ignored; it may contain account hashes and session IDs.
|
|
- API response shape:
|
|
|
|
```ts
|
|
type ApiResult<T extends Record<string, unknown>> =
|
|
| ({ success: true; reason: string } & T)
|
|
| { success: false; reason: string };
|
|
```
|
|
|
|
- Session cookie:
|
|
- Name: `teatea_session`
|
|
- Flags: `httpOnly`, `sameSite: "lax"`, `path: "/"`
|
|
- Lifetime: 7 days
|
|
- Passwords:
|
|
- Never persist plaintext.
|
|
- Use Node crypto salt + scrypt hash until a dedicated auth library is introduced.
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
- Missing/expired session -> `401` with `{ success: false, reason: "未登录或会话已过期" }`.
|
|
- Missing permission -> `403` with `{ success: false, reason: "权限不足" }` and a denied audit log.
|
|
- Invalid JSON body -> `400` with a Chinese user-facing `reason`.
|
|
- Missing record by ID -> `404` with `{ success: false, reason: "<entity>不存在" }`.
|
|
- Duplicate account email -> `409` with `{ success: false, reason: "账号已存在" }`.
|
|
|
|
### 5. Good/Base/Bad Cases
|
|
|
|
- Good: UI submits to Route Handler, Route Handler validates input, calls `requirePermission`, mutates data through `updateData`, writes audit log, returns `ApiResult`.
|
|
- Base: Server Component reads data directly via `readData` after session/permission checks.
|
|
- Bad: UI or route handler reads `.data/teatea.json` directly, bypasses `requirePermission`, or stores auth state in localStorage.
|
|
|
|
### 6. Tests Required
|
|
|
|
- `pnpm lint`
|
|
- `pnpm type-check`
|
|
- `pnpm build`
|
|
- Manual or automated integration assertions:
|
|
- first setup creates admin and cookie session
|
|
- protected app route redirects without cookie
|
|
- CRUD mutation persists across reload/API list
|
|
- denied permission returns 403 and writes an audit log
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
#### Wrong
|
|
|
|
```ts
|
|
window.localStorage.setItem("teatea.session", JSON.stringify(session));
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```ts
|
|
const cookieStore = await cookies();
|
|
cookieStore.set({
|
|
name: "teatea_session",
|
|
value: sessionId,
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
path: "/",
|
|
});
|
|
```
|
|
|