好的,让我们继续完善认证和授权部分。

第五部分:认证和授权

  1. 首先,让我们更新 src/routes/auth.ts 文件,添加一些辅助函数和中间件:
import { Elysia, t } from "elysia";
import { jwt } from "@elysiajs/jwt";
import { cookie } from "@elysiajs/cookie";
import { db } from "../db";
import { users } from "../db/schema";
import { eq } from "drizzle-orm";
import bcrypt from "bcryptjs";
 
const authPlugin = new Elysia()
  .use(jwt({ name: "jwt", secret: process.env.JWT_SECRET! }))
  .use(cookie());
 
export const auth = authPlugin.derive(({ jwt, cookie, setCookie, removeCookie }) => {
  return {
    getCurrentUser: async () => {
      const token = cookie.auth;
      if (!token) return null;
      try {
        const payload = await jwt.verify(token);
        if (!payload) return null;
        const user = await db.select().from(users).where(eq(users.id, payload.id)).get();
        return user;
      } catch (error) {
        return null;
      }
    },
    setAuthCookie: (token: string) => {
      setCookie("auth", token, {
        httpOnly: true,
        maxAge: 7 * 24 * 60 * 60, // 7 days
        path: "/",
      });
    },
    clearAuthCookie: () => {
      removeCookie("auth");
    },
  };
});
 
export const authRoutes = new Elysia()
  .use(auth)
  .post(
    "/login",
    async ({ body, jwt, set, setAuthCookie }) => {
      const { username, password } = body;
      const user = await db.select().from(users).where(eq(users.username, username)).get();
 
      if (!user || !(await bcrypt.compare(password, user.password))) {
        set.status = 401;
        return { error: "Invalid credentials" };
      }
 
      const token = await jwt.sign({ id: user.id, username: user.username });
      setAuthCookie(token);
      return { message: "Logged in successfully" };
    },
    {
      body: t.Object({
        username: t.String(),
        password: t.String(),
      }),
    }
  )
  .post(
    "/register",
    async ({ body, set }) => {
      const { username, password, email } = body;
      const hashedPassword = await bcrypt.hash(password, 10);
      try {
        const newUser = await db
          .insert(users)
          .values({ username, password: hashedPassword, email })
          .returning()
          .get();
        set.status = 201;
        return { message: "User created successfully", user: { id: newUser.id, username: newUser.username, email: newUser.email } };
      } catch (error) {
        set.status = 400;
        return { error: "Username or email already exists" };
      }
    },
    {
      body: t.Object({
        username: t.String(),
        password: t.String(),
        email: t.String(),
      }),
    }
  )
  .get("/logout", ({ clearAuthCookie }) => {
    clearAuthCookie();
    return { message: "Logged out successfully" };
  })
  .get("/me", async ({ getCurrentUser, set }) => {
    const user = await getCurrentUser();
    if (!user) {
      set.status = 401;
      return { error: "Not authenticated" };
    }
    return { user: { id: user.id, username: user.username, email: user.email } };
  });
  1. 创建一个授权中间件 src/middleware/auth.ts:
import { Elysia } from "elysia";
import { auth } from "../routes/auth";
 
export const authMiddleware = new Elysia()
  .use(auth)
  .derive(({ getCurrentUser, set }) => ({
    ensureAuthenticated: async () => {
      const user = await getCurrentUser();
      if (!user) {
        set.status = 401;
        return { error: "Not authenticated" };
      }
      return user;
    },
  }));
  1. 更新 src/routes/posts.ts 以使用授权中间件:
import { Elysia, t } from "elysia";
import { db } from "../db";
import { posts } from "../db/schema";
import { eq } from "drizzle-orm";
import { authMiddleware } from "../middleware/auth";
 
export const postRoutes = new Elysia()
  .use(authMiddleware)
  .get("/posts", async () => {
    return await db.select().from(posts).all();
  })
  .get("/posts/:id", async ({ params }) => {
    const post = await db.select().from(posts).where(eq(posts.id, parseInt(params.id))).get();
    return post || { error: "Post not found" };
  })
  .post(
    "/posts",
    async ({ body, ensureAuthenticated }) => {
      const user = await ensureAuthenticated();
      const { title, content } = body;
      const newPost = await db.insert(posts).values({ title, content, authorId: user.id }).returning().get();
      return newPost;
    },
    {
      body: t.Object({
        title: t.String(),
        content: t.String(),
      }),
    }
  )
  .put(
    "/posts/:id",
    async ({ params, body, ensureAuthenticated }) => {
      const user = await ensureAuthenticated();
      const { title, content } = body;
      const updatedPost = await db
        .update(posts)
        .set({ title, content, updatedAt: new Date() })
        .where(eq(posts.id, parseInt(params.id)))
        .returning()
        .get();
      
      if (!updatedPost) {
        return { error: "Post not found" };
      }
      
      if (updatedPost.authorId !== user.id) {
        return { error: "Not authorized to update this post" };
      }
      
      return updatedPost;
    },
    {
      body: t.Object({
        title: t.String(),
        content: t.String(),
      }),
    }
  )
  .delete("/posts/:id", async ({ params, ensureAuthenticated }) => {
    const user = await ensureAuthenticated();
    const post = await db.select().from(posts).where(eq(posts.id, parseInt(params.id))).get();
    
    if (!post) {
      return { error: "Post not found" };
    }
    
    if (post.authorId !== user.id) {
      return { error: "Not authorized to delete this post" };
    }
    
    await db.delete(posts).where(eq(posts.id, parseInt(params.id)));
    return { message: "Post deleted successfully" };
  });
  1. 更新 src/index.ts 以使用认证中间件:
import { Elysia } from "elysia";
import { html } from "@elysiajs/html";
import { staticPlugin } from "@elysiajs/static";
import { authRoutes, auth } from "./routes/auth";
import { postRoutes } from "./routes/posts";
import { migrate } from "drizzle-orm/libsql/migrator";
import { db } from "./db";
import { renderFile } from "eta";
import path from "path";
 
// 运行迁移
await migrate(db, { migrationsFolder: "./drizzle" });
 
const app = new Elysia()
  .use(html())
  .use(staticPlugin())
  .use(auth)
  .use(authRoutes)
  .use(postRoutes)
  .get("/", async ({ set, getCurrentUser }) => {
    const posts = await db.select().from(posts).limit(5).all();
    const user = await getCurrentUser();
    set.headers['Content-Type'] = 'text/html';
    return await renderFile(path.join(__dirname, "views", "home.eta"), { posts, user });
  })
  .get("/login", async ({ set, getCurrentUser }) => {
    const user = await getCurrentUser();
    if (user) {
      set.redirect = "/";
      return;
    }
    set.headers['Content-Type'] = 'text/html';
    return await renderFile(path.join(__dirname, "views", "login.eta"), {});
  })
  .get("/posts/:id", async ({ params, set, getCurrentUser }) => {
    const post = await db.select().from(posts).where(eq(posts.id, parseInt(params.id))).get();
    if (!post) {
      set.status = 404;
      return "Post not found";
    }
    const user = await getCurrentUser();
    set.headers['Content-Type'] = 'text/html';
    return await renderFile(path.join(__dirname, "views", "post.eta"), { post, user });
  })
  .listen(3000);
 
console.log(`Server is running at ${app.server?.hostname}:${app.server?.port}`);
  1. 更新 src/views/login.eta 以处理登录响应:
<% layout('./layouts/main.eta', { title: 'Login' }) %>
 
<h1>Login</h1>
<form hx-post="/login" hx-swap="outerHTML">
    <div>
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required>
    </div>
    <div>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required>
    </div>
    <button type="submit">Login</button>
</form>
 
<script>
    document.body.addEventListener('htmx:afterRequest', function(event) {
        if (event.detail.successful) {
            window.location.href = '/';
        } else {
            alert('Login failed. Please check your credentials.');
        }
    });
</script>

这些更改完善了认证和授权部分。我们添加了用户认证、登录、注册和注销功能,并在需要的地方添加了授权检查。我们还更新了视图以反映用户的登录状态。

接下来,我们可以继续改进用户界面和用户体验。需要我继续吗?