feat: add legal pages for Terms of Service and Privacy Policy with localization support
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { LegalDocumentPage } from "@/components/legal-document-page"
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return <LegalDocumentPage type="privacy" />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { LegalDocumentPage } from "@/components/legal-document-page"
|
||||
|
||||
export default function TermsPage() {
|
||||
return <LegalDocumentPage type="terms" />
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { LocaleSwitcher } from "@/components/locale-switcher"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useAppLocale, useI18n } from "@/i18n/provider"
|
||||
import enUSMessages from "@/messages/en-US.json"
|
||||
import zhCNMessages from "@/messages/zh-CN.json"
|
||||
|
||||
type LegalPageType = "terms" | "privacy"
|
||||
|
||||
type LegalSection = {
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
|
||||
type LegalDocument = {
|
||||
title: string
|
||||
description: string
|
||||
updatedAt: string
|
||||
relatedLabel: string
|
||||
relatedLink: string
|
||||
sections: LegalSection[]
|
||||
}
|
||||
|
||||
const messages = {
|
||||
"zh-CN": zhCNMessages,
|
||||
"en-US": enUSMessages,
|
||||
}
|
||||
|
||||
export function LegalDocumentPage({ type }: { type: LegalPageType }) {
|
||||
const t = useI18n()
|
||||
const { locale } = useAppLocale()
|
||||
const document = messages[locale].legal[type] as LegalDocument
|
||||
const relatedHref = type === "terms" ? "/legal/privacy" : "/legal/terms"
|
||||
|
||||
return (
|
||||
<main className="min-h-svh bg-muted px-6 py-8 md:px-10">
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6">
|
||||
<header className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<Image
|
||||
src="/images/logo.svg"
|
||||
alt={t("app.brand")}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain"
|
||||
priority
|
||||
/>
|
||||
<span>{t("app.brand")}</span>
|
||||
</div>
|
||||
<LocaleSwitcher />
|
||||
</header>
|
||||
|
||||
<Card className="bg-card/95">
|
||||
<CardHeader className="gap-3 border-b px-6 py-6 md:px-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-3xl font-semibold tracking-tight">
|
||||
{document.title}
|
||||
</CardTitle>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{document.description}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{document.updatedAt}</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-8 px-6 py-7 md:px-8">
|
||||
{document.sections.map((section) => (
|
||||
<section key={section.title} className="space-y-2">
|
||||
<h2 className="text-lg font-semibold tracking-tight">
|
||||
{section.title}
|
||||
</h2>
|
||||
<p className="text-sm leading-7 text-muted-foreground">
|
||||
{section.body}
|
||||
</p>
|
||||
</section>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<footer className="flex justify-end text-sm text-muted-foreground">
|
||||
<p>
|
||||
{document.relatedLabel}{" "}
|
||||
<Link
|
||||
href={relatedHref}
|
||||
className="font-medium text-foreground underline-offset-4 hover:underline"
|
||||
>
|
||||
{document.relatedLink}
|
||||
</Link>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { startTransition, useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
@@ -229,8 +230,15 @@ export function LoginForm({
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
{t("auth.termsPrefix")} <a href="#">{t("auth.termsOfService")}</a>{" "}
|
||||
{t("auth.termsJoiner")} <a href="#">{t("auth.privacyPolicy")}</a>.
|
||||
{t("auth.termsPrefix")}{" "}
|
||||
<Link href="/legal/terms" target="_blank" rel="noreferrer">
|
||||
{t("auth.termsOfService")}
|
||||
</Link>{" "}
|
||||
{t("auth.termsJoiner")}{" "}
|
||||
<Link href="/legal/privacy" target="_blank" rel="noreferrer">
|
||||
{t("auth.privacyPolicy")}
|
||||
</Link>
|
||||
.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import { readFile } from "node:fs/promises"
|
||||
|
||||
async function loadMessages(locale) {
|
||||
const source = await readFile(new URL(`../messages/${locale}.json`, import.meta.url), "utf8")
|
||||
return JSON.parse(source)
|
||||
}
|
||||
|
||||
test("legal pages have localized document content", async () => {
|
||||
for (const locale of ["zh-CN", "en-US"]) {
|
||||
const messages = await loadMessages(locale)
|
||||
|
||||
assert.equal(typeof messages.legal.terms.title, "string")
|
||||
assert.equal(typeof messages.legal.privacy.title, "string")
|
||||
assert.ok(messages.legal.terms.sections.length >= 6)
|
||||
assert.ok(messages.legal.privacy.sections.length >= 6)
|
||||
|
||||
for (const page of [messages.legal.terms, messages.legal.privacy]) {
|
||||
assert.equal(typeof page.updatedAt, "string")
|
||||
assert.equal(typeof page.relatedLabel, "string")
|
||||
assert.equal(typeof page.relatedLink, "string")
|
||||
for (const section of page.sections) {
|
||||
assert.equal(typeof section.title, "string")
|
||||
assert.equal(typeof section.body, "string")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -127,6 +127,82 @@
|
||||
"oidcSigningIn": "Signing in with OIDC",
|
||||
"checkingTicket": "Verifying your sign-in ticket and opening the dashboard."
|
||||
},
|
||||
"legal": {
|
||||
"terms": {
|
||||
"title": "Terms of Service",
|
||||
"description": "These terms describe the basic rules for accessing and using this system. This is general product text and may be replaced by formal legal terms from the service provider.",
|
||||
"updatedAt": "Last updated: 2026-05-30",
|
||||
"relatedLabel": "Also read",
|
||||
"relatedLink": "Privacy Policy",
|
||||
"sections": [
|
||||
{
|
||||
"title": "1. Service Description",
|
||||
"body": "This system helps support teams manage conversations, tickets, knowledge bases, AI capabilities, and related operational data. You may access and use it only within your authorized business scope."
|
||||
},
|
||||
{
|
||||
"title": "2. Accounts and Access",
|
||||
"body": "You are responsible for protecting your account, password, tokens, and third-party sign-in credentials. Activity caused by shared, leaked, or misused credentials may be treated as activity by the account holder."
|
||||
},
|
||||
{
|
||||
"title": "3. Acceptable Use",
|
||||
"body": "You must not use the system for unlawful activity, rights infringement, service disruption, privilege bypassing, malicious code, unrelated data, or unauthorized sensitive information."
|
||||
},
|
||||
{
|
||||
"title": "4. Data and Content Responsibility",
|
||||
"body": "Content you create, upload, sync, or process in the system should be lawful, accurate, and aligned with internal management requirements. Necessary activity records may be retained for security, audit, operations, and compliance."
|
||||
},
|
||||
{
|
||||
"title": "5. Service Changes and Availability",
|
||||
"body": "System features may change due to product iteration, maintenance, security fixes, or third-party service changes. The service provider aims to keep the service stable but does not promise that any feature will remain unchanged or uninterrupted."
|
||||
},
|
||||
{
|
||||
"title": "6. Disclaimer",
|
||||
"body": "AI-generated content, automatic assignment, summaries, and suggestions are assistive only. Final business judgment, response content, and handling outcomes should be confirmed by authorized personnel."
|
||||
},
|
||||
{
|
||||
"title": "7. Contact and Updates",
|
||||
"body": "If you have questions about these terms, contact your system administrator or the service provider through published contact channels. The latest version displayed on this page applies after updates."
|
||||
}
|
||||
]
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy Policy",
|
||||
"description": "This policy explains the types of information this system may process when providing support administration capabilities, why it is used, and how it is protected. This is general product text and may be replaced by formal legal text from the service provider.",
|
||||
"updatedAt": "Last updated: 2026-05-30",
|
||||
"relatedLabel": "Also read",
|
||||
"relatedLink": "Terms of Service",
|
||||
"sections": [
|
||||
{
|
||||
"title": "1. Information We Process",
|
||||
"body": "The system may process account profiles, roles and permissions, sign-in records, activity logs, support conversations, ticket content, customer contact information, knowledge base content, and operational diagnostics."
|
||||
},
|
||||
{
|
||||
"title": "2. Purposes of Use",
|
||||
"body": "This information is used for authentication, access control, support collaboration, issue follow-up, quality review, security audit, operations, feature improvement, and applicable compliance requirements."
|
||||
},
|
||||
{
|
||||
"title": "3. Storage and Protection",
|
||||
"body": "The system uses access control, audit logging, transport protection, and appropriate data isolation measures to protect information. You should also avoid entering sensitive personal information unless there is a clear business need."
|
||||
},
|
||||
{
|
||||
"title": "4. Third-Party Services and Integrations",
|
||||
"body": "When WeCom, OIDC, messaging channels, AI models, or other external integrations are enabled, relevant information may be sent to those services according to configuration. Processing is also governed by integration provider rules and organizational settings."
|
||||
},
|
||||
{
|
||||
"title": "5. User and Administrator Rights",
|
||||
"body": "Authorized administrators may query, correct, export, or delete relevant business data according to organizational rules. Other users can contact their system administrator for personal-information requests."
|
||||
},
|
||||
{
|
||||
"title": "6. Cookies and Local Storage",
|
||||
"body": "The system may use browser local storage to keep session state, language, theme, and interface preferences in order to maintain access, support security, and improve the user experience."
|
||||
},
|
||||
{
|
||||
"title": "7. Contact and Updates",
|
||||
"body": "If you have questions about privacy protection, contact your system administrator or the service provider through published contact channels. The latest version displayed on this page applies after updates."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"passwordRequired": "Enter a new password.",
|
||||
"confirmPasswordRequired": "Confirm your new password.",
|
||||
|
||||
@@ -127,6 +127,82 @@
|
||||
"oidcSigningIn": "OIDC 登录中",
|
||||
"checkingTicket": "正在校验登录票据并进入系统,请稍候。"
|
||||
},
|
||||
"legal": {
|
||||
"terms": {
|
||||
"title": "服务条款",
|
||||
"description": "本条款用于说明你访问和使用本系统时应遵守的基本规则。该文本为通用说明,正式法律文本可由服务提供方另行替换。",
|
||||
"updatedAt": "最后更新:2026-05-30",
|
||||
"relatedLabel": "也请阅读",
|
||||
"relatedLink": "隐私政策",
|
||||
"sections": [
|
||||
{
|
||||
"title": "1. 服务说明",
|
||||
"body": "本系统用于客服团队管理会话、工单、知识库、AI 能力和相关运营数据。你应仅在获得授权的业务范围内访问和使用本系统。"
|
||||
},
|
||||
{
|
||||
"title": "2. 账号与访问权限",
|
||||
"body": "你需要妥善保管账号、密码、令牌和第三方登录凭据。因账号共享、泄露或越权使用产生的操作记录,可能会被视为该账号持有人的行为。"
|
||||
},
|
||||
{
|
||||
"title": "3. 合理使用",
|
||||
"body": "你不得利用本系统实施违法违规、侵犯他人权益、干扰系统稳定性或绕过权限控制的行为,也不得上传恶意代码、无关数据或未经授权的敏感信息。"
|
||||
},
|
||||
{
|
||||
"title": "4. 数据与内容责任",
|
||||
"body": "你在系统中创建、上传、同步或处理的内容应来源合法、真实且符合内部管理要求。服务提供方可基于安全、审计、运维和合规目的保留必要操作记录。"
|
||||
},
|
||||
{
|
||||
"title": "5. 服务变更与可用性",
|
||||
"body": "系统功能可能因产品迭代、维护、安全修复或第三方服务变化而调整。服务提供方会尽力保持服务稳定,但不承诺任何功能永久不变或绝对不中断。"
|
||||
},
|
||||
{
|
||||
"title": "6. 免责声明",
|
||||
"body": "AI 生成内容、自动分配、摘要和建议仅作为辅助信息,最终业务判断、回复内容和处理结果应由具备权限的人员确认。"
|
||||
},
|
||||
{
|
||||
"title": "7. 联系与更新",
|
||||
"body": "如你对本条款有疑问,请通过系统管理员或服务提供方公布的联系方式联系。本条款更新后,将以页面展示的最新版本为准。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"privacy": {
|
||||
"title": "隐私政策",
|
||||
"description": "本政策说明本系统在提供客服后台能力时可能处理的信息类型、使用目的和保护方式。该文本为通用说明,正式法律文本可由服务提供方另行替换。",
|
||||
"updatedAt": "最后更新:2026-05-30",
|
||||
"relatedLabel": "也请阅读",
|
||||
"relatedLink": "服务条款",
|
||||
"sections": [
|
||||
{
|
||||
"title": "1. 我们处理的信息",
|
||||
"body": "系统可能处理账号资料、角色权限、登录记录、操作日志、客服会话、工单内容、客户联系信息、知识库内容以及系统运行诊断数据。"
|
||||
},
|
||||
{
|
||||
"title": "2. 使用目的",
|
||||
"body": "上述信息用于身份认证、权限控制、客服协作、问题跟进、质量复盘、安全审计、系统运维、功能优化和满足适用的合规要求。"
|
||||
},
|
||||
{
|
||||
"title": "3. 存储与保护",
|
||||
"body": "系统会采用访问控制、日志审计、传输保护和必要的数据隔离措施保护信息安全。你也应避免在无业务必要时录入敏感个人信息。"
|
||||
},
|
||||
{
|
||||
"title": "4. 第三方服务与集成",
|
||||
"body": "当启用企业微信、OIDC、消息通道、AI 模型或其他外部系统集成时,相关信息可能按配置发送至对应服务。具体处理方式受集成方规则和组织配置约束。"
|
||||
},
|
||||
{
|
||||
"title": "5. 用户与管理员权利",
|
||||
"body": "具备权限的管理员可按组织规则查询、更正、导出或删除相关业务数据。普通用户如需处理个人信息相关请求,可联系系统管理员。"
|
||||
},
|
||||
{
|
||||
"title": "6. Cookie 与本地存储",
|
||||
"body": "系统可能使用浏览器本地存储保存登录态、语言、主题和界面偏好,用于维持会话、安全访问和改善使用体验。"
|
||||
},
|
||||
{
|
||||
"title": "7. 联系与更新",
|
||||
"body": "如你对隐私保护有疑问,请通过系统管理员或服务提供方公布的联系方式联系。本政策更新后,将以页面展示的最新版本为准。"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"passwordRequired": "新密码不能为空",
|
||||
"confirmPasswordRequired": "确认密码不能为空",
|
||||
|
||||
Reference in New Issue
Block a user