为了添加用户认证功能,支持 Google 和 GitHub 账户登录,并保存登录用户的信息,我们可以使用 NextAuth. Js。这是一个专为 Next. Js 设计的认证解决方案,可以轻松集成多种认证提供者。以下是实现步骤:
- 安装必要的依赖:
bun add next-auth @auth/drizzle-adapter- 更新数据库 schema(
db/schema.ts)以包含用户相关表:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
name: text('name'),
email: text('email').notNull(),
emailVerified: integer('emailVerified', { mode: 'timestamp_ms' }),
image: text('image'),
});
export const accounts = sqliteTable('accounts', {
id: text('id').primaryKey(),
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
type: text('type').notNull(),
provider: text('provider').notNull(),
providerAccountId: text('providerAccountId').notNull(),
refresh_token: text('refresh_token'),
access_token: text('access_token'),
expires_at: integer('expires_at'),
token_type: text('token_type'),
scope: text('scope'),
id_token: text('id_token'),
session_state: text('session_state'),
});
export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(),
sessionToken: text('sessionToken').notNull(),
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
expires: integer('expires', { mode: 'timestamp_ms' }).notNull(),
});
export const verificationTokens = sqliteTable('verificationToken', {
identifier: text('identifier').notNull(),
token: text('token').notNull(),
expires: integer('expires', { mode: 'timestamp_ms' }).notNull(),
},
(vt) => ({
compoundKey: [vt.identifier, vt.token],
}));
export const categories = sqliteTable('categories', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
userId: text('userId').notNull().references(() => users.id),
});
export const todos = sqliteTable('todos', {
id: integer('id').primaryKey(),
text: text('text').notNull(),
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
categoryId: integer('category_id').references(() => categories.id),
userId: text('userId').notNull().references(() => users.id),
});
export const usersRelations = relations(users, ({ many }) => ({
todos: many(todos),
categories: many(categories),
}));
export const categoriesRelations = relations(categories, ({ one }) => ({
user: one(users, { fields: [categories.userId], references: [users.id] }),
}));
export const todosRelations = relations(todos, ({ one }) => ({
category: one(categories, { fields: [todos.categoryId], references: [categories.id] }),
user: one(users, { fields: [todos.userId], references: [users.id] }),
}));- 创建 NextAuth 配置文件
pages/api/auth/[...nextauth].ts:
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import GithubProvider from "next-auth/providers/github"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "../../../db"
export default NextAuth({
adapter: DrizzleAdapter(db),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
GithubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
],
callbacks: {
session: async ({ session, user }) => {
if (session?.user) {
session.user.id = user.id;
}
return session;
},
},
pages: {
signIn: '/auth/signin',
},
})- 创建一个自定义登录页面
pages/auth/signin.tsx:
import { getProviders, signIn } from "next-auth/react"
import { GetServerSideProps } from "next"
export default function SignIn({ providers }: { providers: any }) {
return (
<div className="flex flex-col items-center justify-center min-h-screen py-2">
<h1 className="text-4xl font-bold mb-6">Sign In</h1>
{Object.values(providers).map((provider: any) => (
<div key={provider.name} className="mb-4">
<button
onClick={() => signIn(provider.id)}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Sign in with {provider.name}
</button>
</div>
))}
</div>
)
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const providers = await getProviders()
return {
props: { providers },
}
}- 更新
pages/_app.tsx以包含 SessionProvider:
import { SessionProvider } from "next-auth/react"
import type { AppProps } from 'next/app'
import 'uno.css'
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
export default MyApp- 更新
pages/index.tsx以使用用户认证:
import { useState, useEffect } from 'react';
import { useSession, signIn, signOut } from "next-auth/react"
import { marked } from 'marked';
// ... (保留之前的 interface 定义)
export default function Home() {
const { data: session } = useSession()
// ... (保留之前的 state 定义)
useEffect(() => {
if (session) {
fetchTodos();
fetchCategories();
}
}, [session]);
// ... (保留之前的函数定义,但需要修改 API 调用以包含用户 ID)
const addTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (newTodo.trim() && session?.user?.id) {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: newTodo, categoryId: selectedCategory, userId: session.user.id })
});
const data = await response.json();
setTodos([...todos, data]);
setNewTodo('');
}
};
// ... (类似地修改其他 API 调用函数)
if (!session) {
return (
<div className="flex flex-col items-center justify-center min-h-screen py-2">
<h1 className="text-4xl font-bold mb-6">Welcome to Todo App</h1>
<button onClick={() => signIn()} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Sign in
</button>
</div>
)
}
return (
<div className="p-4 max-w-2xl mx-auto">
<div className="flex justify-between items-center mb-4">
<h1 className="text-3xl font-bold">Todo App with Categories</h1>
<button onClick={() => signOut()} className="bg-red-500 text-white p-2 rounded hover:bg-red-600">
Sign out
</button>
</div>
{/* ... (保留之前的 JSX,包括表单和列表) */}
</div>
);
}- 更新
server.ts文件中的 API 路由以包含用户 ID:
// ... (previous imports)
import { getSession } from "next-auth/react"
const app = new Elysia()
.use(html())
// ... (previous routes)
.post('/api/todos', async ({ body, request }) => {
const session = await getSession({ req: request });
if (!session?.user?.id) {
return new Response('Unauthorized', { status: 401 });
}
const [newTodo] = await db.insert(todos).values({ ...body, userId: session.user.id }).returning();
return newTodo;
}, {
body: t.Object({
text: t.String(),
categoryId: t.Optional(t.Number())
})
})
// ... (similarly update other routes to check for authentication and include userId)
.listen(3000);- 更新
.env文件以包含 OAuth 凭证:
DATABASE_URL=libsql://your-database-url
DATABASE_AUTH_TOKEN=your-auth-token
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_ID=your-github-client-id
GITHUB_SECRET=your-github-client-secret
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-nextauth-secret
确保在 Google Cloud Console 和 GitHub Developer Settings 中创建 OAuth 应用并获取相应的客户端 ID 和密钥。
- 运行数据库迁移以创建新的用户相关表:
bun run db:generate
bun run db:push这个更新后的 Todo 应用现在支持用户认证,允许用户使用 Google 或 GitHub 账户登录。它还将用户信息与 Todo 项目和分类关联起来,确保每个用户只能看到和管理自己的数据。
要运行这个项目,使用以下命令:
bun run dev这将启动 Elysia 服务器和 Next. Js 开发服务器。你可以在浏览器中访问 http://localhost:3000 来使用这个带有用户认证的 Todo 应用。
请注意,你需要正确配置 OAuth 提供者(Google 和 GitHub)并在 . Env 文件中设置相应的环境变量。此外,确保你的 Turso 数据库已经更新以包含新的用户相关表。