diff --git a/backend/src/core/db/base_service.py b/backend/src/core/db/base_service.py index afde80f..662d605 100644 --- a/backend/src/core/db/base_service.py +++ b/backend/src/core/db/base_service.py @@ -2,9 +2,8 @@ from __future__ import annotations from uuid import UUID -from fastapi import HTTPException - from core.auth.models import CurrentUser +from core.http.errors import ApiProblemError, problem_payload class BaseService: @@ -15,7 +14,10 @@ class BaseService: def require_current_user(self) -> CurrentUser: if self._current_user is None: - raise HTTPException(status_code=401, detail="Unauthorized") + raise ApiProblemError( + status_code=401, + detail=problem_payload(code="AUTH_UNAUTHORIZED", detail="Unauthorized"), + ) return self._current_user def require_user_id(self) -> UUID: diff --git a/backend/src/v1/notifications/repository.py b/backend/src/v1/notifications/repository.py index c002225..ee702be 100644 --- a/backend/src/v1/notifications/repository.py +++ b/backend/src/v1/notifications/repository.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from uuid import UUID from sqlalchemy.dialects.postgresql import insert @@ -85,7 +85,7 @@ class NotificationRepository: if un is None: return False un.is_read = True - un.read_at = datetime.now() + un.read_at = datetime.now(timezone.utc) await self._session.flush() return True diff --git a/backend/src/v1/notifications/service.py b/backend/src/v1/notifications/service.py index 640c90e..317e7da 100644 --- a/backend/src/v1/notifications/service.py +++ b/backend/src/v1/notifications/service.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from uuid import UUID from core.http.errors import ApiProblemError, problem_payload @@ -113,7 +113,7 @@ class NotificationService: body=n.body, payload=payload, is_read=True, - read_at=un.read_at or datetime.now(), + read_at=un.read_at or datetime.now(timezone.utc), created_at=un.created_at, ) diff --git a/docs/protocols/auth/session-auth-protocol.md b/docs/protocols/auth/session-auth-protocol.md index eb190f4..4e7bce7 100644 --- a/docs/protocols/auth/session-auth-protocol.md +++ b/docs/protocols/auth/session-auth-protocol.md @@ -36,6 +36,11 @@ Gateway error codes from `backend/src/v1/auth/gateway.py`: - `AUTH_VERIFICATION_CODE_INVALID` - `AUTH_REFRESH_TOKEN_INVALID` - `AUTH_REFRESH_TOKEN_MISSING` +- `AUTH_USER_NOT_FOUND` + +Authorization error codes from `backend/src/v1/users/dependencies.py`: + +- `AUTH_UNAUTHORIZED` ## Frontend route mapping diff --git a/docs/protocols/common/http-error-codes.md b/docs/protocols/common/http-error-codes.md index aa1e395..479010b 100644 --- a/docs/protocols/common/http-error-codes.md +++ b/docs/protocols/common/http-error-codes.md @@ -83,6 +83,12 @@ This document is the source of truth for backend RFC7807 `code` values consumed |---|---:|---|---| | `NOTIFICATION_NOT_FOUND` | 404 | Notification not found or not owned by current user | Show not-found message and refresh list | +## Invite + +| code | status | meaning | frontend handling | +|---|---:|---|---| +| `INVITE_CODE_NOT_FOUND` | 404 | Invite code not found for current user | Show not-found message and trigger invite code bootstrap | + ## Global | code | status | meaning | frontend handling | diff --git a/docs/protocols/common/user-points-chat-data-protocol.md b/docs/protocols/common/user-points-chat-data-protocol.md index d978e3e..def1f73 100644 --- a/docs/protocols/common/user-points-chat-data-protocol.md +++ b/docs/protocols/common/user-points-chat-data-protocol.md @@ -39,12 +39,15 @@ Protocol verification status: ### profiles - PK: `id` (`auth.users.id`, `on delete cascade`) -- Core fields: `username`, `avatar_url`, `bio`, `settings`, `created_at`, `updated_at`, `deleted_at` +- Core fields: `username`, `avatar_url`, `bio`, `settings`, `referred_by`, `created_at`, `updated_at`, `deleted_at` - Constraints: - `username` not empty - Indexes: - `ix_profiles_username` - `ix_profiles_settings_gin` +- Notes: + - `referred_by` is FK to `profiles.id` (`on delete set null`) for invite/referral tracking + - `settings` stores `ProfileSettingsV1` JSON including `preferences`, `privacy`, `notification`, `divination_tutorial` ### user_points diff --git a/docs/protocols/divination/divination-run-protocol.md b/docs/protocols/divination/divination-run-protocol.md index bdba8f5..a771e01 100644 --- a/docs/protocols/divination/divination-run-protocol.md +++ b/docs/protocols/divination/divination-run-protocol.md @@ -161,9 +161,11 @@ During run streaming, backend emits standard AG-UI lifecycle events and two divi "binaryCode": "101001", "changedBinaryCode": "100001", "guaName": "山火贲", + "guaNameHant": "山火賁", "upperName": "艮", "lowerName": "离", "targetGuaName": "山雷颐", + "targetGuaNameHant": "山雷頤", "worldPosition": 1, "responsePosition": 4, "hasChangingYao": true, @@ -192,7 +194,9 @@ During run streaming, backend emits standard AG-UI lifecycle events and two divi { "position": 1, "spiritName": "虎", + "spiritNameHant": "虎", "relationName": "官鬼", + "relationNameHant": "官鬼", "tiganName": "卯", "elementName": "木", "isYang": true, @@ -206,13 +210,27 @@ During run streaming, backend emits standard AG-UI lifecycle events and two divi { "position": 2, "relationName": "父母", + "relationNameHant": "父母", "tiganName": "午", "elementName": "火" } - ] + ], + "specialStatus": [], + "interactions": [], + "timeEffect": [], + "riChenZhangSheng": [] } ``` +Field notes: + +- `guaNameHant`, `targetGuaNameHant`: Traditional Chinese variants for卦名. +- `spiritNameHant`, `relationNameHant`: Traditional Chinese variants for六神/六亲 names. +- `specialStatus`: Special hexagram status indicators. +- `interactions`: 爻位 interaction descriptions. +- `timeEffect`: Time-based effect descriptions. +- `riChenZhangSheng`: 日辰长生相关 information. + ### 2) `TEXT_MESSAGE_END` - Standard final answer event. diff --git a/docs/protocols/invite/invite-protocol.md b/docs/protocols/invite/invite-protocol.md new file mode 100644 index 0000000..2c0b576 --- /dev/null +++ b/docs/protocols/invite/invite-protocol.md @@ -0,0 +1,49 @@ +# Invite Protocol (Frontend <-> Backend) + +This document defines the invite code contract for authenticated users. + +Protocol verification status: + +- Backend route source: `backend/src/v1/invite/router.py` +- Backend service source: `backend/src/v1/invite/service.py` +- Backend schema source: `backend/src/v1/invite/schemas.py` +- Frontend mapping source: `apps/lib/features/settings/data/apis/invite_api.dart` + +## Compatibility strategy + +- Additive evolution only. +- Existing response fields are stable and must remain backward-compatible. + +## Route + +### GET /api/v1/invite/me + +Get the current user's invite code information. + +**Authorization**: Requires authenticated session. User identity from JWT `sub`. + +**Response (200)**: + +```json +{ + "code": "ABC123XYZ", + "used_count": 5 +} +``` + +Field rules: + +- `code`: string, unique invite code assigned to the user +- `used_count`: integer `>= 0`, number of times this code has been used + +## Error contract linkage + +- RFC7807 + extension `code`, optional `params`. +- Shared registry: `docs/protocols/common/http-error-codes.md`. +- Error codes for this feature: + - `INVITE_CODE_NOT_FOUND` (404): Invite code not found for current user + +## Data model linkage + +- Invite codes are stored in `invite_codes` table. +- See `docs/protocols/common/user-points-chat-data-protocol.md` for `profiles.referred_by` field. diff --git a/docs/protocols/notification/notification-inbox-protocol.md b/docs/protocols/notification/notification-inbox-protocol.md index 0d105ef..6300467 100644 --- a/docs/protocols/notification/notification-inbox-protocol.md +++ b/docs/protocols/notification/notification-inbox-protocol.md @@ -82,15 +82,15 @@ Field rules: - `count`: integer `>= 0` - Counts only notifications where `notifications.status = 'published'` and `notifications.deleted_at IS NULL` -### PATCH /api/v1/notifications/{id}/read +### PATCH /api/v1/notifications/{notification_id}/read Mark a single notification as read. Idempotent. -**Authorization**: Requires authenticated session. `id` must belong to the current user's `user_notifications`. +**Authorization**: Requires authenticated session. `notification_id` must belong to the current user's `user_notifications`. **Path parameters**: -- `id`: UUID of the `user_notifications` record +- `notification_id`: UUID of the `user_notifications` record **Response (200)**: diff --git a/docs/protocols/profile/profile-protocol.md b/docs/protocols/profile/profile-protocol.md index cddf9f4..eb8e84e 100644 --- a/docs/protocols/profile/profile-protocol.md +++ b/docs/protocols/profile/profile-protocol.md @@ -109,6 +109,11 @@ Request: "notification": { "allow_notifications": true, "allow_vibration": true + }, + "divination_tutorial": { + "divination_entry_shown": false, + "auto_divination_shown": false, + "manual_divination_shown": false } } } @@ -118,6 +123,7 @@ Rules: - `settings` must conform to `ProfileSettingsV1`. - Additional fields are forbidden. +- `divination_tutorial` tracks user's tutorial completion state for divination flows. Response: diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index 329213c..e89e523 100644 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -5,7 +5,7 @@ include: services: redis: - image: redis:7-alpine + image: redis:7.4.2-alpine container_name: eryao-local-redis restart: unless-stopped ports: diff --git a/infra/docker/supabase/docker-compose.yml b/infra/docker/supabase/docker-compose.yml index dd99eb5..1d59751 100644 --- a/infra/docker/supabase/docker-compose.yml +++ b/infra/docker/supabase/docker-compose.yml @@ -74,7 +74,7 @@ services: rest: container_name: supabase-rest - image: postgrest/postgrest:v14.6 + image: postgrest/postgrest:v14.8 restart: unless-stopped depends_on: db: @@ -92,7 +92,7 @@ services: storage: container_name: supabase-storage - image: supabase/storage-api:v1.44.2 + image: supabase/storage-api:v1.48.26 restart: unless-stopped depends_on: db: @@ -125,7 +125,7 @@ services: meta: container_name: supabase-meta - image: supabase/postgres-meta:v0.95.2 + image: supabase/postgres-meta:v0.96.3 restart: unless-stopped depends_on: db: @@ -146,12 +146,19 @@ services: studio: container_name: supabase-studio - image: supabase/studio:2026.03.16-sha-5528817 + image: supabase/studio:2026.04.08-sha-205cbe7 restart: unless-stopped depends_on: meta: condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "node -e \"require('http').get('http://0.0.0.0:3000/', (r) => process.exit(r.statusCode < 500 ? 0 : 1)).on('error', () => process.exit(1))\""] + interval: 10s + timeout: 10s + retries: 3 + start_period: 15s environment: + HOSTNAME: "0.0.0.0" STUDIO_PG_META_URL: http://meta:8080 POSTGRES_PASSWORD: ${ERYAO_DATABASE__PASSWORD} POSTGRES_HOST: db