From 8f58eccecfe425963381bf216e0757a3359d0644 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 30 Sep 2026 09:08:21 +0800 Subject: [PATCH] feat(legal): terms and privacy pages, login consent recorded per version, re-consent gate (draft text, placeholders) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- BLOCKED.md | 7 + CHANGELOG.md | 8 + CONTEXT.md | 4 + deploy/README.md | 10 + docs/tasks/PROGRESS-legal-consent-20260930.md | 35 +++ docs/testing/legal-consent-20260930.md | 11 + frontend/DESIGN.md | 7 + frontend/docs/VOICE.md | 6 + frontend/src/app/(app)/layout.tsx | 2 + frontend/src/app/api/account/consent/route.ts | 63 ++++++ frontend/src/app/globals.css | 33 +++ frontend/src/app/privacy/page.tsx | 11 + frontend/src/app/terms/page.tsx | 11 + frontend/src/components/app-sidebar.tsx | 6 + frontend/src/components/email-otp-login.tsx | 46 +++- .../components/legal/legal-consent-gate.tsx | 74 +++++++ .../components/legal/legal-document-page.tsx | 39 ++++ .../src/components/legal/login-consent.tsx | 42 ++++ frontend/src/lib/legal-consent-client.ts | 22 ++ frontend/src/lib/legal-consent-server.ts | 13 ++ frontend/src/lib/legal-consent.ts | 38 ++++ frontend/src/lib/legal-documents.ts | 208 ++++++++++++++++++ .../20260930010000_user_consents.sql | 47 ++++ .../tests/identity-login-provider.test.ts | 6 +- .../tests/legal-consent-20260930.test.tsx | 133 +++++++++++ 25 files changed, 876 insertions(+), 6 deletions(-) create mode 100644 docs/tasks/PROGRESS-legal-consent-20260930.md create mode 100644 docs/testing/legal-consent-20260930.md create mode 100644 frontend/src/app/api/account/consent/route.ts create mode 100644 frontend/src/app/privacy/page.tsx create mode 100644 frontend/src/app/terms/page.tsx create mode 100644 frontend/src/components/legal/legal-consent-gate.tsx create mode 100644 frontend/src/components/legal/legal-document-page.tsx create mode 100644 frontend/src/components/legal/login-consent.tsx create mode 100644 frontend/src/lib/legal-consent-client.ts create mode 100644 frontend/src/lib/legal-consent-server.ts create mode 100644 frontend/src/lib/legal-consent.ts create mode 100644 frontend/src/lib/legal-documents.ts create mode 100644 frontend/supabase/migrations/20260930010000_user_consents.sql create mode 100644 frontend/tests/legal-consent-20260930.test.tsx diff --git a/BLOCKED.md b/BLOCKED.md index 2a139b37..af86b953 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -11,6 +11,13 @@ - 产物隐私补扫未通过:tracked 守卫 63 passed 不覆盖本轮未跟踪文件。显式复用隐私规则扫描新增产物时,M0 baseline JSON 的时刻数组 R003 命中 3 次;旧 baseline quick 日志混合编码无法以 UTF-8 或 GB18030 严格解码。未打印标记原值、未改既有隐私例外、未删除命中数据;需独立核查精确字段碰撞及完成日志保真扫描后,才可考虑纳入提交。证据 `artifacts/varga-resolution/closure-explicit-privacy.log`。 - M0 两次字节复跑未完成、M2/M3 not_started 是研究欠项,**不是上述环境失败的推断后果**。不得据此关单或宣称已交付。 +## 合规 A:用户协议 / 隐私政策 / 登录同意(2026-09-30) + +- **律师审定**:`frontend/src/lib/legal-documents.ts` 是依据代码写的初稿,页面顶部标「草稿 · 待律师审定」。其中 【待确认】 事实点共 13 条(`pendingLegalConfirmations()` 可列出),定稿前逐条确认;定稿后改 `legal-entity.ts` 的 `legalDocumentsVersion` 与 `legalDocumentsDraft: false`,老用户会在下次登录后看到一次「协议已更新」确认。 +- **运营主体信息待产品提供**:`legal-entity.ts` 的 operatorName / address / contactEmail 仍是占位;生产发布前设 `JYOTISHA_REQUIRE_LEGAL_ENTITY=1` 跑 `tests/legal-consent-20260930.test.tsx` 会失败(见 deploy/README.md)。 +- **数据库测试**:新表 `user_consents`(`20260930010000_user_consents.sql`,只加不改)没有跑 `npm run test:db`(本机无 Docker);staging 部署时由迁移步骤首次真实执行,部署后需用受控账号登录一次核对写入。 +- **真机**:登录页勾选、协议页阅读、账户菜单链接未在真机走查(清单 `docs/testing/legal-consent-20260930.md`)。 + ## TASK-chart-surface-polish:受控登录、实体手机与基线全量失败(2026-09-29) - 本轮 Chrome、Edge、Docker 均可用,不套用历史“无 Chrome / 无 Docker”。真实 Chrome + golden 的本地组件验收及骨架高度修复后复验已完成(70+8 项通过);没有受控线上登录账号、实体手机和读屏实测;不能代替完整账户/人物/请求链路。清单见 `docs/testing/chart-surface-polish-20260929.md`。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ece7281..96b4ade1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 印度占星 Skill 更新日志 +## 2026-09-30 — 用户协议、隐私政策与登录同意(草稿,待律师审定) + +- 新增「用户协议」`/terms` 与「隐私政策」`/privacy` 两个页面,未登录也能打开;页面顶部标明「草稿,待律师审定」。内容按产品实际情况写:收集哪些信息(含出生资料与档案里的他人资料)、交给哪些服务商(模型、邮件、地点搜索等,部分在境外)、保存多久、如何注销(7 天冷静期)、内容安全、未成年人、AI 生成内容说明等。 +- 登录页新增「我已阅读并同意《用户协议》和《隐私政策》」勾选框,不勾选不能发验证码或密码登录;登录成功时记录同意的版本与时间。登录页底部和账户菜单底部都有两份文件的链接。 +- 文本定稿、版本号更新后,已登录的老用户会在下次进入时看到一次「协议已更新」确认;草稿阶段不打扰老用户。 +- 运营主体、地址、客服邮箱目前是占位,产品提供后替换一处即可。 +- Skill 版本不 bump。新增一张表 `user_consents`(只加不改)。 + ## 2026-09-30 — 首页去掉校正提示、星盘类型文字完整显示、加载改为轨道环动画、「那一刻的天空」换小星座图标(待验收) - 首页不再显示「上次那次校正还没完成,可以在历史对话里接着做。」(BUG-1111)。 diff --git a/CONTEXT.md b/CONTEXT.md index 0a745f17..53e0084f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -40,6 +40,10 @@ _避免使用_:会话评分、点赞记录 同一份个人报告的中文版与英文版,由同一次引擎计算、同一个数据包分别渲染,数字逐项一致。英文版是从属版本:缺失或未通过检查时报告仍以中文交付;英文版上线前生成的报告只有中文版。 _避免使用_:翻译版、机翻、英文报告(指另一份报告时) +**同意记录**: +用户在某个时间接受了某一版本的《用户协议》或《隐私政策》的记录(`user_consents`,只追加不修改)。协议版本变化后需要重新同意。 +_避免使用_:授权、签约 + **不满意原因**: 负向回复评价附带的一个或多个原因分类,可包含用户补充说明。 _避免使用_:投诉、差评文本 diff --git a/deploy/README.md b/deploy/README.md index 3fd540d2..b2966ce0 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -128,6 +128,16 @@ docker stats --no-stream UFW permits only the confirmed SSH port, HTTP, and HTTPS. PostgreSQL, Web, API, and the Docker API remain private. +## Legal entity and documents gate (2026-09-30) + +Before a production release, run the legal-consent test with the production flag on: + +```bash +cd frontend && JYOTISHA_REQUIRE_LEGAL_ENTITY=1 npx tsx --test tests/legal-consent-20260930.test.tsx +``` + +It fails while `frontend/src/lib/legal-entity.ts` still holds placeholder operator details or the terms / privacy text is still marked draft. The workflows are not changed for this (they are product-owner territory); the step is manual until the product owner wires it in. + ## Manual production deployment with Gitea Actions GitHub production deployment is retired. Production changes are released only by manually dispatching `.gitea/workflows/deploy-production.yml`. The workflow requires an exact SHA shared by `main` and `staging`, an exact-SHA staging push gate and image manifest, the manual release gate, and matching public staging health. It deploys immutable registry digests and never builds application images on the production host. diff --git a/docs/tasks/PROGRESS-legal-consent-20260930.md b/docs/tasks/PROGRESS-legal-consent-20260930.md new file mode 100644 index 00000000..d4221f8c --- /dev/null +++ b/docs/tasks/PROGRESS-legal-consent-20260930.md @@ -0,0 +1,35 @@ +# PROGRESS · 合规 A:用户协议 / 隐私政策 / 登录同意 · 2026-09-30 + +> 产品 2026-09-30「合规与法律的前四个你可以帮我做吗」;主体与联系方式先用占位(产品选定)。执行:Claude fork 子代理 A;分支 `codex/legal-consent-20260930`,基线 `codex/compliance-base-20260930`(= origin/staging `5208f19b` + `legal-entity.ts` 占位)。 + +## 交付 + +| 项 | 实现 | +|---|---| +| 协议 / 政策页 | `/terms`、`/privacy`(公开、静态),正文在 `lib/legal-documents.ts`,律师定稿只换这一个文件;草稿标识读 `legal-entity.ts` | +| 登录同意 | `components/legal/login-consent.tsx`(勾选框 + 页脚);`email-otp-login.tsx` 在发送验证码 / 密码登录前检查;所有成功路径收成 `enterApp()`,先 `POST /api/account/consent` 再进入 | +| 同意记录 | 表 `user_consents`(`20260930010000_user_consents.sql`,只加不改、用户只能查 / 插自己的行、随账号级联删除);IP 仅在配置 `CONSENT_IP_SALT` 时存加盐哈希 | +| 再次同意 | `components/legal/legal-consent-gate.tsx` 挂在 `(app)` 外壳;草稿阶段(`legalDocumentsDraft: true`)不启用 | +| 入口 | 登录页页脚、账户菜单底部;ICP 号配置后显示在登录页页脚 | +| 生产护栏 | `JYOTISHA_REQUIRE_LEGAL_ENTITY=1` 时测试要求无占位且非草稿(deploy/README.md 记录为发布前手动步骤;未改 workflow) | + +## 待确认事实(交律师 / 产品) + +见 `pendingLegalConfirmations()`:在线支付是否开放及方式、退款规则与时限、管辖法院、日志是否含 IP 及保存期限、正式站启用的模型服务商与所在地、地点服务商、VedAstro 是否启用、支付服务商名称、出境合规路径、订单流水保存年限、内容安全日志保存期限、服务器日志保存期限、备份与加密范围。 + +## 既有断言改动(原值 / 新值 / 原因) + +| 文件 | 原值 | 新值 | 原因 | +|---|---|---|---| +| `tests/identity-login-provider.test.ts` | `window.location.assign(successPath)` 出现 5 次 | 出现 1 次(在 `enterApp` 内),`await/void enterApp()` 5 次 | 登录成功先记录同意再跳转,落点不变 | + +## 验证 + +| 项 | 结果 | +|---|---| +| tsc | 0 错 | +| lint | 0 error / 126 warning(同基线) | +| npm test 全量(Node 22) | 4,429 / fail 24;失败名单与基线(origin/staging `5208f19b` 的全量结果)逐条相同,新增 11 条、消失 0 条 | +| next build | `/`、`/terms`、`/privacy` 均 `○ Static` | +| 首屏 gzip-9 | 646,480 B → 647,749 B(+0.20%;外壳多了再次同意组件) | +| 数据库 | 未跑 `test:db`(无 Docker),见 BLOCKED.md | diff --git a/docs/testing/legal-consent-20260930.md b/docs/testing/legal-consent-20260930.md new file mode 100644 index 00000000..be667e17 --- /dev/null +++ b/docs/testing/legal-consent-20260930.md @@ -0,0 +1,11 @@ +# 真机清单 · 用户协议 / 隐私政策 / 登录同意(2026-09-30) + +| # | 操作 | 应该看到 | 结果 | +|---|---|---|---| +| 1 | 退出登录,打开登录页 | 表单下方有未勾选的「我已阅读并同意《用户协议》和《隐私政策》」;页面最下方有「用户协议 · 隐私政策」 | | +| 2 | 不勾选,输入邮箱点「发送验证码」 | 不发送,框下出现一行红字「请先阅读并勾选同意,再继续。」 | | +| 3 | 点《用户协议》 | 新标签打开协议页,顶部写「草稿 · 版本 2026-09-30-draft,待律师审定」,手机上能正常阅读 | | +| 4 | 勾选后正常登录 | 红字消失,能收到验证码并进入首页 | | +| 5 | 登录后打开账户菜单 | 最底下有「用户协议 · 隐私政策」两个小链接 | | +| 6 | 用管理后台地址登录 | 后台登录页没有勾选框 | | +| 7 | (运营)查数据库 `user_consents` | 第 4 步的账号有 terms、privacy 两行,版本 2026-09-30-draft | | diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 18a47e70..18b9deb1 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -567,6 +567,13 @@ one skeleton, none of them carrying the sidebar. The chart page, the ephemeris and the report list wait with one motion: the home loading screen's orbit ring at 40px (`OrbitWaiting`), centred in the empty chart grid or in the page. No sentence is shown; the old 「……还没拿到。」 copy is kept only as the screen-reader status. The chart's breathing empty grid stays (BUG-1016 exception). +### Login consent and the legal pages (2026-09-30) + +- **Login:** under the form, an unticked checkbox 「我已阅读并同意《用户协议》和《隐私政策》」 (`.auth-consent`, caption size, links underlined, open in a new tab). Sending a code or a password sign-in with the box unticked does nothing but show one red caption line under it; ticking clears it. The operators' admin login (password-only, `/admin`) has no box. A centred caption footer under the login card links 用户协议 · 隐私政策 and, once configured, the ICP number to beian.miit.gov.cn. +- **Account menu:** a caption row at the very bottom, 「用户协议 · 隐私政策」, below 退出登录. +- **/terms and /privacy:** public, static, one 720px column on `--color-canvas-soft`; a top row with 「返回 Jyotisha」 and a link to the other document; while the text is a draft, a warning-tinted note 「草稿 · 版本 …,待律师审定」 above the title; h1 in the display face, h2 per section, bullet lists for enumerations. +- **Re-consent:** when a final version replaces the one a signed-in user accepted, one modal 「协议已更新」 with the same checkbox and 「同意并继续」; it cannot be dismissed without agreeing. Off while the documents are a draft. + ### Personal report centre - **Structure:** inside the app shell, not a page of its own. The name 「我的报告」 sits alone in the 46px header. The body opens with the **generate card** (`.report-center-create`, 2026-09-29, product sketch): a centred `--color-canvas` sheet, `--space-6` below the header (BUG-1103: it used to sit flush under it), with a file icon, the title 「完整本命报告」 (what you get), one line 「星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。」 and the filled 「生成报告」 button (what it does) — title and button no longer say the same thing — the page's only generate entry; the header button was deleted rather than kept as a second one. Below it, 「过往的报告」 heads the **row list** of reports; with none yet, a single quiet line 「还没有个人报告。」 replaces the old second big empty card. The supporting paragraph (「有填报到分钟的出生时间即可生成……」) and the 「共 N 份 · 已完成 N 份」 overview line above it were removed on 2026-09-28 (TASK-self-edit-avatar-menu-20260928 S3, product: 「这里的提示去掉」); the minute requirement still lives in the 生成 button's hover title. diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md index 6db8a520..89b96c61 100644 --- a/frontend/docs/VOICE.md +++ b/frontend/docs/VOICE.md @@ -153,6 +153,12 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 星盘、星历、报告列表几秒内就会出现,等待时只放轨道环动画,不在屏幕上写「这一张盘还没拿到。」一类句子;这些句子只给读屏器。首页不再写「上次那次校正还没完成,可以在历史对话里接着做。」。 +## 协议与同意(2026-09-30) + +- 勾选框:「我已阅读并同意《用户协议》和《隐私政策》」;未勾选就提交:「请先阅读并勾选同意,再继续。」(只在框下一行,不弹 toast)。 +- 协议更新弹窗:标题「协议已更新」;正文「我们更新了《用户协议》和《隐私政策》(版本 …)。请阅读后确认,以便继续使用。」;按钮「同意并继续」;记录失败「没能记录,请稍后再试。」 +- 协议正文用「我们 / 你」,不用「您」;不写「最终解释权归…」这类单方条款;待核实的事实写 【待确认:…】,不得凭猜测写死。 + ## 报告里的限制说明 普通报告只留下人能读的正文、结论、行动建议和必要限制。不写字段名、状态码、评分、权重或执行账本。说不到日期就写「这次说不到具体哪一天。」依据没补上就写「有些依据还没补上,相关说法不能当成确定预测。」判断没闭合就写「有些判断还没闭合,不能写成确定结论。」不出现 `technique_truth`、`workflow_route` 这类键。 diff --git a/frontend/src/app/(app)/layout.tsx b/frontend/src/app/(app)/layout.tsx index 3a7b9159..2697d7ac 100644 --- a/frontend/src/app/(app)/layout.tsx +++ b/frontend/src/app/(app)/layout.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { AppSidebar } from "@/components/app-sidebar"; +import { LegalConsentGate } from "@/components/legal/legal-consent-gate"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { SessionListProvider, sessionListSidebarModel, useSessionList, useShellRegistration } from "@/hooks/use-session-list"; import "../site-styles"; @@ -32,6 +33,7 @@ function AppShell({ children }: { children: ReactNode }) { > {children} + ); diff --git a/frontend/src/app/api/account/consent/route.ts b/frontend/src/app/api/account/consent/route.ts new file mode 100644 index 00000000..5c896c93 --- /dev/null +++ b/frontend/src/app/api/account/consent/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from "next/server"; + +import { CURRENT_LEGAL_VERSION, LEGAL_CONSENT_DOCUMENTS, LEGAL_RECONSENT_BLOCKING, consentOutdated, isCurrentConsentRequest, latestAccepted } from "@/lib/legal-consent"; +import { consentIpHash } from "@/lib/legal-consent-server"; +import { isSupabaseConfigurationError } from "@/lib/supabase/config"; +import { createServerSupabaseClient } from "@/lib/supabase/server"; + +export const runtime = "nodejs"; + +type ConsentRow = { document: string; version: string; accepted_at: string }; + +/** + * GET: which versions this user accepted, and whether a (blocking) re-consent + * is required now. POST: record acceptance of the current version of both + * documents. Rows are append-only (see 20260930010000_user_consents.sql). + */ +export async function GET() { + try { + const supabase = await createServerSupabaseClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 }); + const { data, error } = await supabase.from("user_consents").select("document,version,accepted_at").eq("user_id", user.id); + if (error) return NextResponse.json({ error: "暂时读不到同意记录" }, { status: 503 }); + const accepted = latestAccepted((data ?? []) as ConsentRow[]); + return NextResponse.json({ + current: CURRENT_LEGAL_VERSION, + accepted, + required: LEGAL_RECONSENT_BLOCKING && consentOutdated(accepted), + }); + } catch (caught) { + if (isSupabaseConfigurationError(caught)) return NextResponse.json({ error: "服务未配置" }, { status: 503 }); + throw caught; + } +} + +export async function POST(request: Request) { + try { + if (!isCurrentConsentRequest(await request.json().catch(() => null))) { + return NextResponse.json({ error: "协议版本已更新,请刷新页面后重新确认" }, { status: 409 }); + } + const supabase = await createServerSupabaseClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 }); + const { data: existing, error: readError } = await supabase + .from("user_consents").select("document,version,accepted_at") + .eq("user_id", user.id).eq("version", CURRENT_LEGAL_VERSION); + if (readError) return NextResponse.json({ error: "暂时无法记录" }, { status: 503 }); + const have = new Set(((existing ?? []) as ConsentRow[]).map(row => row.document)); + const missing = LEGAL_CONSENT_DOCUMENTS.filter(document => !have.has(document)); + if (missing.length) { + const ipHash = consentIpHash(request.headers); + const userAgent = request.headers.get("user-agent")?.slice(0, 160) ?? null; + const { error } = await supabase.from("user_consents").insert(missing.map(document => ({ + user_id: user.id, document, version: CURRENT_LEGAL_VERSION, ip_hash: ipHash, user_agent: userAgent, + }))); + if (error) return NextResponse.json({ error: "暂时无法记录" }, { status: 503 }); + } + return NextResponse.json({ recorded: missing, version: CURRENT_LEGAL_VERSION }); + } catch (caught) { + if (isSupabaseConfigurationError(caught)) return NextResponse.json({ error: "服务未配置" }, { status: 503 }); + throw caught; + } +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 7e6d0ddb..4f4b7103 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -2043,6 +2043,39 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .auth-story h2 { max-width: 500px; margin: var(--space-4) 0 var(--space-5); font-family: var(--font-display); font-size: var(--type-display-lg); font-weight: 500; letter-spacing: -1px; line-height: 1.15; text-wrap: balance; } .auth-story > div > p { max-width: 480px; margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-md); line-height: 1.65; text-wrap: pretty; } .auth-footnote { margin: 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); } +/* Consent to the terms and privacy policy on the login screen (2026-09-30). + Unticked by default; sending a code or a password sign-in waits for it. */ +.auth-consent { display: grid; gap: var(--space-1); margin-top: var(--space-4); } +.auth-consent label, label.auth-consent { display: flex; align-items: flex-start; gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.6; cursor: pointer; } +.auth-consent input[type="checkbox"] { flex-shrink: 0; width: 16px; height: 16px; margin-top: 3px; accent-color: var(--color-action); } +.auth-consent a { color: var(--color-ink); text-decoration: underline; text-underline-offset: 2px; } +.auth-consent-hint { margin: 0; color: var(--color-danger); font-size: var(--type-caption); } +.auth-legal-footer { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); padding: var(--space-4); color: var(--color-ink-tertiary); font-size: var(--type-caption); } +.auth-legal-footer a { color: inherit; text-decoration: none; } +.auth-legal-footer a:hover { color: var(--color-ink); text-decoration: underline; } +.account-menu-legal { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3) var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-caption); } +.account-menu-legal a { min-height: 0; padding: 0; color: inherit; text-decoration: none; } +.account-menu-legal a:hover, .account-menu-legal a[data-highlighted] { color: var(--color-ink); text-decoration: underline; } +/* 用户协议 / 隐私政策: one readable column, the draft notice first. */ +.legal-page { background: var(--color-canvas-soft); } +.legal-document { width: min(720px, 100%); margin: 0 auto; padding: var(--space-8) var(--space-5) var(--space-16); color: var(--color-ink); line-height: 1.75; } +.legal-nav { display: flex; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-6); font-size: var(--type-body-sm); } +.legal-nav a { color: var(--color-ink-secondary); text-decoration: none; } +.legal-nav a:hover { color: var(--color-ink); } +.legal-draft { margin: 0 0 var(--space-4); padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); background: color-mix(in srgb, var(--color-warning) 12%, transparent); color: var(--color-warning); font-size: var(--type-caption); } +.legal-version { margin: 0 0 var(--space-4); color: var(--color-ink-tertiary); font-size: var(--type-caption); } +.legal-document h1 { margin: 0 0 var(--space-4); font-family: var(--font-display); font-size: var(--type-display-sm); font-weight: 500; } +.legal-summary { color: var(--color-ink-secondary); } +.legal-document h2 { margin: var(--space-8) 0 var(--space-2); font-size: var(--type-title-md); font-weight: 600; } +.legal-document p { margin: 0 0 var(--space-3); } +.legal-document ul { margin: 0 0 var(--space-3); padding-left: 1.25em; } +.legal-document li { margin-bottom: var(--space-2); } +.legal-consent-dialog { width: min(440px, 92vw); padding: var(--space-6); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); color: var(--color-ink); box-shadow: var(--shadow-elevated); } +.legal-consent-dialog::backdrop { background: var(--color-scrim); } +.legal-consent-dialog h2 { margin: 0 0 var(--space-2); font-size: var(--type-title-md); } +.legal-consent-dialog p { margin: 0 0 var(--space-3); color: var(--color-ink-secondary); font-size: var(--type-body-sm); } +.legal-consent-dialog [data-slot="button"] { width: 100%; margin-top: var(--space-4); } + .auth-panel { width: 100%; min-width: 0; display: flex; flex-direction: column; justify-content: center; padding: var(--space-12); border: 0; border-radius: 0; background: var(--color-canvas); } .auth-brand { align-items: center; gap: var(--space-3); font-size: var(--type-title-lg); display: none; margin-bottom: var(--space-10); } .auth-panel h1 { font-size: var(--type-display-md); } diff --git a/frontend/src/app/privacy/page.tsx b/frontend/src/app/privacy/page.tsx new file mode 100644 index 00000000..d20988a9 --- /dev/null +++ b/frontend/src/app/privacy/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; + +import { LegalDocumentPage } from "@/components/legal/legal-document-page"; +import { PRIVACY } from "@/lib/legal-documents"; +import "../site-styles"; + +export const metadata: Metadata = { title: "隐私政策 · Jyotisha" }; + +export default function PrivacyPage() { + return ; +} diff --git a/frontend/src/app/terms/page.tsx b/frontend/src/app/terms/page.tsx new file mode 100644 index 00000000..744a8810 --- /dev/null +++ b/frontend/src/app/terms/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; + +import { LegalDocumentPage } from "@/components/legal/legal-document-page"; +import { TERMS } from "@/lib/legal-documents"; +import "../site-styles"; + +export const metadata: Metadata = { title: "用户协议 · Jyotisha" }; + +export default function TermsPage() { + return ; +} diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 999702a7..c270ca4f 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -418,6 +418,12 @@ export function AppSidebar({ {accountEntry("logout", onOpenLogout, "account-menu-item account-menu-danger", ( <>