Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
AUTH_URL=http://localhost:3000
AUTH_SECRET=replace-with-32-byte-secret
AUTH_GITHUB_ID=your-github-oauth-client-id
AUTH_GITHUB_SECRET=your-github-oauth-client-secret
GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxx
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ Agents.md
.env

# VS code
.vscode
.vscode
/generated/prisma
17 changes: 17 additions & 0 deletions app/api/auth/[...nextauth]/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Login Flow Overview

这个目录承载站点的 NextAuth 路由处理。下面是登录流程与依赖的简要说明:

- **身份验证框架**:使用 [NextAuth.js 5 (Auth.js)](https://authjs.dev/) 作为认证核心,通过 `app/auth.ts` 暴露的 `handlers` 响应 GET/POST 请求。
- **OAuth provider**:接入 GitHub OAuth(`next-auth/providers/github`),读取用户的 `id/name/email/avatar` 并持久化到本地用户表。
- **数据库适配器**:优先使用 [@auth/neon-adapter](https://authjs.dev/reference/adapter/neon) 将用户、账户、会话数据写入 Neon Postgres。若运行环境缺少 `DATABASE_URL`,系统会回退到 JWT 会话策略,不再访问数据库,方便协作者在无 Neon 凭据的情况下开发。
- **会话策略**:有 Neon 配置时启用数据库会话(`strategy: "database"`),否则改用默认的 JWT 签名。
- **必要环境变量**:`AUTH_SECRET`/`NEXTAUTH_SECRET` 用于签名;`AUTH_GITHUB_ID`、`AUTH_GITHUB_SECRET` 用于 GitHub OAuth;`DATABASE_URL` 控制 Neon 连接(可选)。开发环境缺少这些变量时会给出控制台警告并使用安全兜底逻辑,以保证本地能跑通。

### 本地无 `.env` 的执行策略

- 如果 `.env` 没有配置,只要 GitHub 登录仍在,NextAuth 会使用内置的开发密钥和 JWT 会话继续工作,登录流程不会报错。
- Neon 数据库适配器会被自动禁用,此时用户信息只保存在 cookie/JWT 中,不会写入 `users / sessions` 表;适合纯前端协作者快速启动项目。
- 控制台会输出显式警告,提示缺少密钥或数据库连接,确保真正部署前补齐配置。

如需扩展更多 provider 或调整会话策略,可直接修改 `auth.config.ts` 与 `auth.ts`。
2 changes: 2 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
8 changes: 7 additions & 1 deletion app/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import { ThemeToggle } from "./ThemeToggle";
import { Button } from "../../components/ui/button";
import { MessageCircle } from "lucide-react";
import { Github as GithubIcon } from "./icons/Github";
import { SignInButton } from "./SignInButton";
import { auth } from "@/auth";
import { UserMenu } from "./UserMenu";

export function Header() {
export async function Header() {
const session = await auth();
const user = session?.user;
return (
<header className="fixed top-0 w-full z-50 bg-background/80 backdrop-blur-lg border-b border-border">
<div className="container mx-auto px-6 h-16 flex items-center justify-between">
Expand Down Expand Up @@ -62,6 +67,7 @@ export function Header() {
</a>
</Button>
<ThemeToggle />
{user ? <UserMenu user={user} /> : <SignInButton />}
</div>
</div>
</header>
Expand Down
22 changes: 22 additions & 0 deletions app/components/SignInButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { signIn } from "@/auth";
import { Button } from "@/app/components/ui/button";

interface SignInButtonProps {
className?: string;
}

export function SignInButton({ className }: SignInButtonProps) {
return (
<form
className={className}
action={async () => {
"use server";
await signIn("github");
}}
>
<Button type="submit" size="sm" variant="outline">
Sign in with GitHub
</Button>
</form>
);
}
62 changes: 62 additions & 0 deletions app/components/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { signOut } from "@/auth";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/app/components/ui/avatar";

interface UserMenuProps {
user: {
name?: string | null;
email?: string | null;
image?: string | null;
};
}

export function UserMenu({ user }: UserMenuProps) {
const initials = user.name?.[0] ?? user.email?.[0] ?? "?";

return (
<details className="relative inline-block text-left">
<summary
className="flex cursor-pointer list-none items-center rounded-full border border-border bg-background p-0.5 transition hover:border-primary/60 [&::-webkit-details-marker]:hidden"
aria-label="Account menu"
>
<Avatar className="size-9">
{user.image ? (
<AvatarImage src={user.image} alt={user.name ?? "User avatar"} />
) : (
<AvatarFallback>{initials}</AvatarFallback>
)}
</Avatar>
</summary>

<div className="absolute right-0 mt-2 w-60 overflow-hidden rounded-md border border-border bg-popover shadow-lg">
<div className="border-b border-border bg-muted/40 px-4 py-3">
<p className="text-sm font-medium text-foreground">
{user.name ?? "Signed in"}
</p>
{user.email ? (
<p className="text-xs text-muted-foreground" title={user.email}>
{user.email}
</p>
) : null}
</div>

<form
action={async () => {
"use server";
await signOut();
}}
>
<button
type="submit"
className="w-full px-4 py-2 text-left text-sm text-foreground transition hover:bg-muted"
>
Sign out
</button>
</form>
</div>
</details>
);
}
64 changes: 64 additions & 0 deletions auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { NextAuthConfig } from "next-auth";
import GitHub from "next-auth/providers/github";

// 在本地开发环境允许没有 .env 的协作者运行站点,因此先尝试读取两个常见的密钥变量,缺失时再使用内置的开发兜底值。
const envSecret = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET;
const secret =
envSecret ??
(process.env.NODE_ENV !== "production"
? "__involutionhell_dev_secret__"
: undefined);

if (!envSecret && process.env.NODE_ENV !== "production") {
console.warn(
"[auth] AUTH_SECRET missing – using development fallback secret",
);
}

if (!secret) {
throw new Error("[auth] AUTH_SECRET is required in production environments");
}

export const authConfig = {
secret,
pages: {
signIn: "/login",
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnProtectedRoute = nextUrl.pathname.startsWith("/dashboard");

if (isOnProtectedRoute) {
if (isLoggedIn) return true;
return false;
}

return true;
},
async signIn({ user, account, profile }) {
console.log("[auth] signIn payload", { user, account, profile });
return true;
},
async session({ session, token }) {
console.log("[auth] session payload", { session, token });
return session;
},
async jwt({ token, user, account, profile }) {
console.log("[auth] jwt payload", { token, user, account, profile });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Remove sensitive payload logging in auth callbacks

The signIn, session, and jwt callbacks log their entire payloads (console.log("[auth] …", { user, account, profile, token })). In production these objects contain OAuth access tokens and user data, so the change will leak credentials and personal information into application logs. Consider gating the logs behind a development flag or removing them before release.

Useful? React with 👍 / 👎.

return token;
},
},
providers: [
GitHub({
profile(profile) {
return {
id: `github-${profile.id}`,
name: profile.name ?? profile.login,
email: profile.email,
image: profile.avatar_url,
};
},
}),
],
} satisfies NextAuthConfig;
51 changes: 51 additions & 0 deletions auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
import GitHub from "next-auth/providers/github";
import { Pool } from "@neondatabase/serverless";
import NeonAdapter from "@auth/neon-adapter";

type NeonAdapterPool = Parameters<typeof NeonAdapter>[0];

export const { handlers, auth, signIn, signOut } = NextAuth(() => {
// Neon 连接只在有数据库配置时启用;本地协作者若没有 `.env`,将回退为纯 JWT 会话,避免直接抛错阻塞开发。
const databaseUrl = process.env.DATABASE_URL;
const adapter = databaseUrl
? NeonAdapter(
new Pool({
connectionString: databaseUrl,
}) as unknown as NeonAdapterPool,
)
: undefined;

if (!databaseUrl) {
console.warn("[auth] DATABASE_URL missing – running without Neon adapter");
}

return {
...authConfig,
providers: [
GitHub({
profile(profile) {
return {
id: `github-${profile.id}`, // 让 User.id 直接对应 GitHub ID
name: profile.name ?? profile.login,
email: profile.email,
image: profile.avatar_url,
};
},
}),
],
...(adapter
? {
adapter,
session: {
strategy: "database" as const,
},
}
: {
session: {
strategy: "jwt" as const,
},
}),
};
});
6 changes: 6 additions & 0 deletions lib/layout.shared.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import { SignInButton } from "@/app/components/SignInButton";

export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: "Involution Hell",
children: (
<div className="ms-auto flex justify-end">
<SignInButton />
</div>
),
},
};
}
10 changes: 10 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";

export default NextAuth(authConfig).auth;

export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
runtime: "nodejs",
};
36 changes: 21 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
"dependencies": {
"@ai-sdk/google": "^2.0.14",
"@ai-sdk/openai": "^2.0.32",
"@ai-sdk/react": "^2.0.45",
"@assistant-ui/react": "^0.11.10",
"@ai-sdk/react": "^2.0.48",
"@assistant-ui/react": "^0.11.14",
"@assistant-ui/react-ai-sdk": "^1.1.0",
"@assistant-ui/react-markdown": "^0.11.0",
"@auth/neon-adapter": "^1.10.0",
"@giscus/react": "^3.1.0",
"@orama/orama": "^3.1.13",
"@orama/tokenizers": "^3.1.13",
"@neondatabase/serverless": "^1.0.1",
"@orama/orama": "^3.1.14",
"@orama/tokenizers": "^3.1.14",
"@prisma/client": "^6.16.2",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
Expand All @@ -31,34 +34,36 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@types/mdx": "^2.0.13",
"@vercel/speed-insights": "^1.2.0",
"ai": "^5.0.45",
"antd": "^5.27.3",
"ai": "^5.0.48",
"antd": "^5.27.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"fumadocs-core": "^15.7.11",
"fumadocs-mdx": "^11.9.1",
"fumadocs-ui": "^15.7.11",
"fumadocs-core": "^15.7.13",
"fumadocs-mdx": "^11.10.1",
"fumadocs-ui": "^15.7.13",
"lucide-react": "^0.544.0",
"motion": "^12.23.14",
"motion": "^12.23.16",
"next": "^15.5.3",
"next-intl": "^4.3.8",
"next-auth": "5.0.0-beta.29",
"next-intl": "^4.3.9",
"prisma": "^6.16.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.9",
"zod": "^4.1.11",
"zustand": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.13",
"@types/node": "latest",
"@types/react": "^19.1.12",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"autoprefixer": "^10.4.21",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"eslint-config-next": "^15.5.3",
"husky": "^9.1.7",
"katex": "^0.16.22",
Expand All @@ -69,7 +74,8 @@
"remark-math": "^6.0.0",
"tailwindcss": "^4.1.13",
"tw-animate-css": "^1.3.8",
"typescript": "^5.6.3"
"typescript": "^5.9.2",
"vercel": "^48.1.0"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
Expand Down
Loading
Loading