为了添加用户认证功能,支持 Google 和 GitHub 账户登录,并保存登录用户的信息,我们可以使用 NextAuth. Js。这是一个专为 Next. Js 设计的认证解决方案,可以轻松集成多种认证提供者。以下是实现步骤:

  1. 安装必要的依赖:
bun add next-auth @auth/drizzle-adapter
  1. 更新数据库 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] }),
}));
  1. 创建 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',
  },
})
  1. 创建一个自定义登录页面 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 },
  }
}
  1. 更新 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
  1. 更新 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>
  );
}
  1. 更新 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);
  1. 更新 .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 和密钥。

  1. 运行数据库迁移以创建新的用户相关表:
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 数据库已经更新以包含新的用户相关表。