Protect Tabularium with persistent authenticator-code login
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m28s
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m28s
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authConfig, sessionCookie, signSession } from "@/lib/auth";
|
||||
import { consumeCode } from "@/lib/auth-store";
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
if (req.method !== "POST") {
|
||||
res.setHeader("Allow", "POST");
|
||||
return res.status(405).end();
|
||||
}
|
||||
try {
|
||||
const origin = req.headers.origin;
|
||||
if (
|
||||
!origin ||
|
||||
new URL(origin).host !== req.headers.host ||
|
||||
req.headers["sec-fetch-site"] === "cross-site"
|
||||
)
|
||||
return res.status(403).json({ error: "请求来源不受信任" });
|
||||
const config = authConfig();
|
||||
if (!config)
|
||||
return res
|
||||
.status(503)
|
||||
.json({ error: "尚未配置动态码登录,请联系管理员" });
|
||||
const code = typeof req.body?.code === "string" ? req.body.code.trim() : "";
|
||||
const result = await consumeCode(config, code);
|
||||
if (result === "limited") {
|
||||
res.setHeader("Retry-After", "300");
|
||||
return res.status(429).json({ error: "尝试次数过多,请五分钟后再试" });
|
||||
}
|
||||
if (result !== "ok")
|
||||
return res
|
||||
.status(401)
|
||||
.json({ error: "动态码不正确或已使用,请等待下一组动态码" });
|
||||
res.setHeader("Set-Cookie", sessionCookie(signSession(config)));
|
||||
return res.status(200).json({ ok: true });
|
||||
} catch {
|
||||
return res.status(503).json({ error: "登录服务暂时不可用,请稍后重试" });
|
||||
}
|
||||
}
|
||||
export const config = { api: { bodyParser: { sizeLimit: "1kb" } } };
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { sessionCookie } from "@/lib/auth";
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
if (req.method !== "POST") {
|
||||
res.setHeader("Allow", "POST");
|
||||
return res.status(405).end();
|
||||
}
|
||||
try {
|
||||
if (
|
||||
!req.headers.origin ||
|
||||
new URL(req.headers.origin).host !== req.headers.host ||
|
||||
req.headers["sec-fetch-site"] === "cross-site"
|
||||
)
|
||||
return res.status(403).end();
|
||||
} catch {
|
||||
return res.status(403).end();
|
||||
}
|
||||
res.setHeader("Set-Cookie", sessionCookie("", true));
|
||||
return res.status(200).json({ ok: true });
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export default function DocPage({
|
||||
</div>
|
||||
<div className="article-meta">
|
||||
<span className="eyebrow">{section.group}</span>
|
||||
{doc.slug.includes("draft") && (
|
||||
{section.label.includes("草案") && (
|
||||
<span className="draft-badge">讨论草案</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -183,7 +183,7 @@ export default function DocPage({
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote
|
||||
className={
|
||||
doc.slug.includes("draft")
|
||||
section.label.includes("草案")
|
||||
? "callout callout-draft"
|
||||
: "callout"
|
||||
}
|
||||
@@ -239,7 +239,7 @@ export default function DocPage({
|
||||
<div className="toc-note">
|
||||
<span className="status-dot" /> {section.label}
|
||||
<p>
|
||||
{doc.slug.includes("draft")
|
||||
{section.label.includes("草案")
|
||||
? "这里记录正在讨论的设计,尚不代表现有实现。"
|
||||
: "每一次理解,都成为下一步的起点。"}
|
||||
</p>
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function Home({
|
||||
<p className="card-description">{section.description}</p>
|
||||
<div className="card-bottom">
|
||||
<span>{section.label}</span>
|
||||
{doc.slug.includes("draft") && (
|
||||
{section.label.includes("草案") && (
|
||||
<span className="draft-badge">草案</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Brand } from "@/components/Shell";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [code, setCode] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ code }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
setError(result.error || "登录没有完成");
|
||||
setCode("");
|
||||
return;
|
||||
}
|
||||
const next = router.query.next;
|
||||
window.location.assign(
|
||||
typeof next === "string" &&
|
||||
next.startsWith("/") &&
|
||||
!next.startsWith("//") &&
|
||||
!/[\\\r\n]/.test(next) &&
|
||||
!next.startsWith("/login")
|
||||
? next
|
||||
: "/",
|
||||
);
|
||||
} catch {
|
||||
setError("暂时无法连接,请稍后重试");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>动态码登录 · Tabularium</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Head>
|
||||
<main className="auth-page">
|
||||
<div className="auth-brand">
|
||||
<Brand />
|
||||
</div>
|
||||
<form className="auth-card" onSubmit={submit}>
|
||||
<p className="eyebrow">PRIVATE KNOWLEDGE ARCHIVE</p>
|
||||
<h1>输入动态码</h1>
|
||||
<p className="auth-description">
|
||||
打开验证器,输入当前的 6 位动态码。
|
||||
</p>
|
||||
<label htmlFor="auth-code">动态码</label>
|
||||
<input
|
||||
id="auth-code"
|
||||
autoFocus
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
pattern="[0-9]{6}"
|
||||
maxLength={6}
|
||||
required
|
||||
value={code}
|
||||
onChange={(event) => setCode(event.target.value.replace(/\D/g, ""))}
|
||||
placeholder="000000"
|
||||
aria-describedby="auth-error"
|
||||
/>
|
||||
<div className="auth-error" id="auth-error" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
<button type="submit" disabled={busy || code.length !== 6}>
|
||||
{busy ? "正在验证…" : "进入文档"}
|
||||
</button>
|
||||
<p className="auth-footnote">尚未绑定验证器?请联系管理员。</p>
|
||||
</form>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user