好的,让我们继续完善认证和授权部分。
第五部分:认证和授权
- 首先,让我们更新
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 } };
});- 创建一个授权中间件
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;
},
}));- 更新
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" };
});- 更新
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}`);- 更新
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>这些更改完善了认证和授权部分。我们添加了用户认证、登录、注册和注销功能,并在需要的地方添加了授权检查。我们还更新了视图以反映用户的登录状态。
接下来,我们可以继续改进用户界面和用户体验。需要我继续吗?