Files
ARTEX/web/src/app/(auth)/login/page.tsx
T

314 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { AlertTriangle, ShieldCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/lib/api";
import { auth } from "@/lib/auth";
export default function LoginPage() {
const router = useRouter();
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [checking, setChecking] = useState(true);
const [agreed, setAgreed] = useState(false);
const [termsOpen, setTermsOpen] = useState(false);
const [readToEnd, setReadToEnd] = useState(false);
const termsBodyRef = useRef<HTMLDivElement>(null);
// 滚动到条款底部(含无需滚动即可完整展示的情况)方可点击「同意」。
function handleTermsScroll() {
const el = termsBodyRef.current;
if (!el) return;
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 8) setReadToEnd(true);
}
useEffect(() => {
if (!termsOpen) return;
// 打开时重置,并处理内容本就不足一屏、无法触发滚动的场景。
setReadToEnd(false);
const el = termsBodyRef.current;
if (el && el.scrollHeight <= el.clientHeight + 8) setReadToEnd(true);
}, [termsOpen]);
useEffect(() => {
// 已登录直接进主界面(静态导出下无 middleware 代劳这层跳转)。
const token = auth.getToken();
if (token) {
// localStorage 可能仍有凭据但 cookie 已丢失。先同步,再发起全新请求,
// 避免服务端守卫或路由缓存把跳转送回仍处于 checking 状态的登录页。
auth.setToken(token);
window.location.replace("/function/tasks");
return;
}
api
.authStatus()
.then(({ initialized }) => {
if (!initialized) router.replace("/setup");
})
.catch(() => setError("无法连接到后端服务"))
.finally(() => setChecking(false));
}, [router]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!agreed) {
setError("请先阅读并同意《使用须知》");
return;
}
setLoading(true);
setError("");
try {
const { token } = await api.login("ARTEX", password);
auth.setToken(token);
window.location.replace("/function/tasks");
} catch {
setError("用户名或密码错误");
} finally {
setLoading(false);
}
}
if (checking) {
return (
<div role="status" className="flex min-h-dvh items-center justify-center text-muted-foreground">
正在检查登录状态…
</div>
);
}
return (
<div className="flex h-dvh">
{/* Left panel */}
<div className="hidden flex-col items-center justify-center bg-primary p-12 text-center lg:flex lg:w-1/3">
<div className="relative flex items-center justify-center">
<div className="absolute size-80 rounded-full border border-primary-foreground/10" />
<div className="absolute size-60 rounded-full border border-primary-foreground/15" />
<div className="absolute size-40 rounded-full border border-primary-foreground/20" />
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/logo.png" alt="ARTEX" width={160} height={160} className="relative brightness-0 invert" />
</div>
</div>
{/* Right panel */}
<div className="flex w-full items-center justify-center bg-background p-8 lg:w-2/3">
<div className="w-full max-w-md space-y-10 py-24 lg:py-32">
<div className="space-y-4 text-center">
<h2 className="text-2xl font-medium tracking-tight">登录</h2>
<p className="mx-auto max-w-xl text-muted-foreground">欢迎回来,请输入密码以继续使用 ARTEX</p>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="space-y-1.5">
<Label htmlFor="username">用户名</Label>
<Input id="username" value="ARTEX" readOnly className="bg-muted text-muted-foreground" />
</div>
<div className="space-y-1.5">
<Label htmlFor="password">密码</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
autoFocus
autoComplete="current-password"
/>
</div>
<div className="flex items-start gap-2">
<Checkbox
id="agree-terms"
checked={agreed}
onCheckedChange={(v) => setAgreed(v === true)}
className="mt-0.5"
/>
<Label htmlFor="agree-terms" className="text-sm font-normal leading-relaxed text-muted-foreground">
我已阅读并同意
<button
type="button"
onClick={() => setTermsOpen(true)}
className="mx-0.5 font-medium text-primary underline-offset-4 hover:underline"
>
《使用须知》
</button>
</Label>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<Button type="submit" className="w-full" disabled={loading || !password || !agreed}>
{loading ? "登录中..." : "登录"}
</Button>
</form>
</div>
</div>
<Dialog open={termsOpen} onOpenChange={setTermsOpen}>
<DialogContent className="gap-0 p-0 sm:max-w-2xl">
<DialogHeader className="flex-row items-center gap-3 border-b px-6 py-4">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<ShieldCheck className="size-5" />
</div>
<div className="space-y-0.5">
<DialogTitle className="text-base">ARTEX 使用须知与免责声明</DialogTitle>
<p className="text-xs text-muted-foreground">
版本 v1.0 · 生效日期 2026-09-18 · 请在登录前完整阅读以下全部条款
</p>
</div>
</DialogHeader>
<div
ref={termsBodyRef}
onScroll={handleTermsScroll}
className="max-h-[60vh] space-y-5 overflow-y-auto px-6 py-5 text-sm leading-relaxed text-muted-foreground"
>
<p className="rounded-lg border bg-muted/40 p-3 text-foreground/80">
本《使用须知与免责声明》(以下简称"本声明")是您与 ARTEX
项目作者及贡献者之间就使用本软件所达成的约定。请您在使用前审慎阅读、充分理解各条款内容,特别是以粗体或色块标注的免责、责任限制及禁止性条款。
<span className="font-medium text-foreground">
{" "}
一旦您下载、安装、访问或以任何方式使用本软件,即视为您已阅读、理解并同意接受本声明的全部约束。
</span>
</p>
<section className="space-y-1.5">
<h4 className="flex items-center gap-2 font-medium text-foreground">
<span className="flex size-5 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
1
</span>
第一条 · 定义与开源许可
</h4>
<p className="pl-7">
本软件(ARTEX)是一款基于 GNU Affero General Public License
v3.0(AGPL-3.0)发布的开源程序。您可依据该协议自由使用、复制、修改和分发本软件;但任何衍生作品(包括通过网络向第三方提供的在线服务)均须同样以
AGPL-3.0 协议开源并向使用者公开对应的完整源代码。AGPL-3.0 完整条款以随附的 LICENSE 文件为准。
</p>
</section>
<section className="space-y-1.5">
<h4 className="flex items-center gap-2 font-medium text-foreground">
<span className="flex size-5 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
2
</span>
第二条 · 授权使用范围
</h4>
<p className="pl-7">
本软件仅供个人学习、代码研究、安全技术原理探讨,以及在您自行搭建的本地隔离环境中进行技术验证之用,适用于学习、学术研究、代码审阅等非攻击性、非破坏性用途。除本条明确许可的情形外,您不得将本软件用于任何其他目的。
</p>
</section>
<section className="space-y-2">
<h4 className="flex items-center gap-2 font-medium text-destructive">
<span className="flex size-5 items-center justify-center rounded-md bg-destructive/10 text-xs font-semibold text-destructive">
3
</span>
<AlertTriangle className="size-4" />
第三条 · 禁止行为
</h4>
<ul className="ml-7 list-decimal space-y-1.5 rounded-lg border border-destructive/20 bg-destructive/5 p-3 pl-8 text-foreground/80 marker:text-destructive/70">
<li>
严禁对任何网站、线上服务、他人或第三方所有的联网系统发起扫描、探测、利用或攻击(无论是否已获得授权、是否为您自有资产);
</li>
<li>严禁将本软件用于任何实际的渗透测试、攻防对抗、红蓝演练或生产环境;</li>
<li>严禁将本软件用于非法入侵、数据窃取、勒索、拒绝服务(DoS/DDoS)或任何破坏性、犯罪性活动;</li>
<li>严禁移除、篡改或规避本软件及其输出中的任何版权、许可或安全提示信息;</li>
<li>严禁从事任何违反您所在国家或地区法律、法规及监管规定的行为。</li>
</ul>
</section>
<section className="space-y-1.5">
<h4 className="flex items-center gap-2 font-medium text-foreground">
<span className="flex size-5 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
4
</span>
第四条 · 知识产权
</h4>
<p className="pl-7">
本软件的著作权及相关知识产权归项目作者及贡献者所有,并在 AGPL-3.0
协议约定的范围内向您授予相应权利。除该协议明确授予的权利外,本声明未以明示或默示方式授予您任何其他权利。
</p>
</section>
<section className="space-y-1.5">
<h4 className="flex items-center gap-2 font-medium text-foreground">
<span className="flex size-5 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
5
</span>
第五条 · 数据与隐私
</h4>
<p className="pl-7">
本软件为可自行部署的开源程序,作者不运营任何集中式服务、亦不会收集或上传您的使用数据。您在使用过程中产生、处理或接触的一切数据,均由您自行掌控并负责其合法性与安全性;因数据处理不当引发的任何后果由您自行承担。
</p>
</section>
<section className="space-y-1.5">
<h4 className="flex items-center gap-2 font-medium text-foreground">
<span className="flex size-5 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
6
</span>
第六条 · 合规与法律责任
</h4>
<p className="pl-7">
您应自行遵守所在国家或地区关于网络安全、数据安全与个人信息保护、计算机犯罪等方面的全部法律法规(在中国大陆包括但不限于《网络安全法》《数据安全法》《个人信息保护法》及相关司法解释)。
<span className="font-medium text-foreground">
{" "}
因您违反上述法律法规或本声明约定而产生的一切法律责任与后果,均由您本人独立承担,与本软件作者及贡献者无关。
</span>
</p>
</section>
<section className="space-y-1.5">
<h4 className="flex items-center gap-2 font-medium text-foreground">
<span className="flex size-5 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
7
</span>
第七条 · 免责声明与责任限制
</h4>
<p className="pl-7">
本软件按"现状(AS IS)"与"现有(AS
AVAILABLE)"状态提供,不附带任何明示或默示的担保,包括但不限于对适销性、特定用途适用性、准确性及不侵权的担保。在适用法律允许的最大范围内,本软件作者及贡献者不对因使用或无法使用本软件(无论使用方式是否得当)而导致的任何直接、间接、偶然、特殊或后果性损失承担责任,包括但不限于数据丢失、系统损坏、业务中断、利润损失或法律纠纷。
</p>
</section>
<section className="space-y-1.5">
<h4 className="flex items-center gap-2 font-medium text-foreground">
<span className="flex size-5 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
8
</span>
第八条 · 条款变更与最终解释
</h4>
<p className="pl-7">
作者有权根据法律法规或项目发展需要不时更新本声明,更新后的版本将随项目发布并自公布之日起生效;您继续使用本软件即视为接受修订后的条款。在法律允许的范围内,本声明的最终解释权归项目作者所有。若本声明任一条款被认定为无效,不影响其余条款的效力。
</p>
</section>
</div>
<DialogFooter className="mx-0 mb-0 flex-col items-stretch gap-2 rounded-b-xl px-6 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs text-muted-foreground">
{readToEnd ? "您已浏览全部条款" : "请将条款滚动至底部后再确认"}
</p>
<DialogClose asChild>
<Button
type="button"
disabled={!readToEnd}
onClick={() => {
setAgreed(true);
setError("");
}}
>
我已阅读并同意全部条款
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}