diff --git a/.env.example b/.env.example
index 4e1f1f1..330f984 100644
--- a/.env.example
+++ b/.env.example
@@ -24,3 +24,9 @@ NODE_ENV=production
# UMAMI_ID=
# UMAMI_URL=
# UMAMI_DOMAINS=
+
+# 登录认证(必填,用于六爻/八字/综合测算与 AI 解读)
+AUTH_USERNAME=
+AUTH_PASSWORD=
+# 会话签名密钥,请使用 32 位以上随机字符串
+AUTH_SESSION_SECRET=
diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts
new file mode 100644
index 0000000..5ede30f
--- /dev/null
+++ b/app/api/auth/login/route.ts
@@ -0,0 +1,41 @@
+import { NextResponse } from "next/server";
+import {
+ isAuthEnabled,
+ verifyCredentials,
+} from "@/lib/auth/config";
+import {
+ createSessionToken,
+ SESSION_COOKIE,
+ SESSION_MAX_AGE_SEC,
+} from "@/lib/auth/session";
+
+export async function POST(req: Request) {
+ if (!isAuthEnabled()) {
+ return NextResponse.json({ ok: true, authEnabled: false });
+ }
+
+ let body: { username?: string; password?: string };
+ try {
+ body = await req.json();
+ } catch {
+ return NextResponse.json({ error: "请求格式错误" }, { status: 400 });
+ }
+
+ const username = body.username?.trim() ?? "";
+ const password = body.password ?? "";
+
+ if (!verifyCredentials(username, password)) {
+ return NextResponse.json({ error: "用户名或密码错误" }, { status: 401 });
+ }
+
+ const token = await createSessionToken(username);
+ const res = NextResponse.json({ ok: true, username });
+ res.cookies.set(SESSION_COOKIE, token, {
+ httpOnly: true,
+ sameSite: "lax",
+ secure: process.env.NODE_ENV === "production",
+ path: "/",
+ maxAge: SESSION_MAX_AGE_SEC,
+ });
+ return res;
+}
diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts
new file mode 100644
index 0000000..2079302
--- /dev/null
+++ b/app/api/auth/logout/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+import { SESSION_COOKIE } from "@/lib/auth/session";
+
+export async function POST() {
+ const res = NextResponse.json({ ok: true });
+ res.cookies.set(SESSION_COOKIE, "", {
+ httpOnly: true,
+ sameSite: "lax",
+ secure: process.env.NODE_ENV === "production",
+ path: "/",
+ maxAge: 0,
+ });
+ return res;
+}
diff --git a/app/api/auth/me/route.ts b/app/api/auth/me/route.ts
new file mode 100644
index 0000000..52089c8
--- /dev/null
+++ b/app/api/auth/me/route.ts
@@ -0,0 +1,19 @@
+import { NextResponse } from "next/server";
+import { cookies } from "next/headers";
+import { isAuthEnabled } from "@/lib/auth/config";
+import { getSessionUsername, SESSION_COOKIE } from "@/lib/auth/session";
+
+export async function GET() {
+ if (!isAuthEnabled()) {
+ return NextResponse.json({ authEnabled: false, loggedIn: true });
+ }
+
+ const token = (await cookies()).get(SESSION_COOKIE)?.value;
+ const username = await getSessionUsername(token);
+
+ return NextResponse.json({
+ authEnabled: true,
+ loggedIn: !!username,
+ username: username ?? undefined,
+ });
+}
diff --git a/app/history/page.tsx b/app/history/page.tsx
new file mode 100644
index 0000000..60640f7
--- /dev/null
+++ b/app/history/page.tsx
@@ -0,0 +1,5 @@
+import HistoryPageClient from "@/components/history/history-page";
+
+export default function HistoryPage() {
+ return ;
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index fc47d96..35aae08 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -4,6 +4,7 @@ import React from "react";
import Umami from "@/components/umami";
import PwaProvider from "@/components/pwa/pwa-provider";
import PwaDisplayMode from "@/components/pwa/pwa-display-mode";
+import { AuthProvider } from "@/components/auth/auth-provider";
import { ThemeProvider } from "next-themes";
export const metadata: Metadata = {
@@ -53,9 +54,11 @@ export default function RootLayout({
defaultTheme="system"
disableTransitionOnChange
>
- {children}
-
-
+
+ {children}
+
+
+