为了添加用户认证功能,支持 OAuth 2,并允许用户使用 Google 和 GitHub 账户登录,我们需要进行以下步骤:
- 安装必要的依赖:
bun add @elysiajs/oauth @elysiajs/cookie @elysiajs/jwt-
在 Google Cloud Console 和 GitHub Developer Settings 中创建 OAuth 应用,并获取客户端 ID 和密钥。
-
更新
src/index.ts文件以添加认证功能:
import { Elysia } from 'elysia';
import { html } from '@elysiajs/html';
import { staticPlugin } from '@elysiajs/static';
import { oauth, OAuth2RequestError } from '@elysiajs/oauth';
import { cookie } from '@elysiajs/cookie';
import { jwt } from '@elysiajs/jwt';
import { renderToString } from 'react-dom/server';
import { Blog } from './components/Blog';
import { db } from './db';
import { posts, users } from './db/schema';
import { eq } from 'drizzle-orm';
import { marked } from 'marked';
const app = new Elysia()
.use(html())
.use(staticPlugin())
.use(cookie())
.use(jwt({
name: 'jwt',
secret: process.env.JWT_SECRET!
}))
.use(oauth({
providers: {
google: {
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectURI: 'http://localhost:3000/auth/google/callback',
scope: ['profile', 'email']
},
github: {
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectURI: 'http://localhost:3000/auth/github/callback',
scope: ['user:email']
}
}
}))
.derive(({ cookie, jwt, setCookie }) => ({
getUser: async () => {
const token = cookie.auth;
if (!token) return null;
try {
const payload = await jwt.verify(token);
return payload;
} catch {
return null;
}
},
setUser: (user: any) => {
const token = jwt.sign(user);
setCookie('auth', token, {
httpOnly: true,
maxAge: 7 * 86400, // 7 days
});
}
}))
.get('/', async ({ html, getUser }) => {
const user = await getUser();
const allPosts = await db.select().from(posts).all();
const blogHtml = renderToString(Blog({ posts: allPosts, user }));
return html(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Blog</title>
<link rel="stylesheet" href="/public/styles.css">
<script src="https://unpkg.com/htmx.org@1.9.0"></script>
<script src="https://unpkg.com/marked@4.0.16/marked.min.js"></script>
</head>
<body>
<div id="root">${blogHtml}</div>
</body>
</html>
`);
})
.get('/login', ({ html }) => {
return html(`
<h1>Login</h1>
<a href="/auth/google">Login with Google</a>
<br>
<a href="/auth/github">Login with GitHub</a>
`);
})
.get('/auth/:provider', async ({ params }) => {
if (params.provider === 'google') {
return await app.oauth.google.redirect();
} else if (params.provider === 'github') {
return await app.oauth.github.redirect();
}
})
.get('/auth/:provider/callback', async ({ params, oauth, set, setUser }) => {
try {
let profile;
if (params.provider === 'google') {
const { userInfo } = await app.oauth.google.validate();
profile = {
provider: 'google',
id: userInfo.sub,
email: userInfo.email,
name: userInfo.name
};
} else if (params.provider === 'github') {
const { userInfo } = await app.oauth.github.validate();
profile = {
provider: 'github',
id: userInfo.id,
email: userInfo.email,
name: userInfo.name
};
} else {
set.status = 400;
return 'Invalid provider';
}
let user = await db.select().from(users).where(eq(users.providerId, profile.id)).get();
if (!user) {
user = await db.insert(users).values({
providerId: profile.id,
provider: profile.provider,
email: profile.email,
name: profile.name
}).returning().get();
}
setUser(user);
set.redirect = '/';
} catch (e) {
if (e instanceof OAuth2RequestError) {
set.status = 400;
return 'Invalid code';
}
set.status = 500;
return 'Internal Server Error';
}
})
.get('/logout', ({ setCookie, set }) => {
setCookie('auth', '', { maxAge: 0 });
set.redirect = '/';
})
.get('/api/post/:id', async ({ params, getUser }) => {
const user = await getUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const post = await db.select().from(posts).where(eq(posts.id, parseInt(params.id))).get();
if (!post) {
return new Response('Post not found', { status: 404 });
}
return `
<article>
<h2>${post.title}</h2>
<div>${marked(post.content)}</div>
<small>Created at: ${new Date(post.createdAt).toLocaleString()}</small>
</article>
`;
})
.post('/api/post', async ({ body, getUser }) => {
const user = await getUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const { title, content } = body as { title: string; content: string };
const newPost = await db.insert(posts).values({ title, content, authorId: user.id }).returning().get();
return `
<article>
<h2>${newPost.title}</h2>
<div>${marked(newPost.content)}</div>
<small>Created at: ${new Date(newPost.createdAt).toLocaleString()}</small>
<button hx-get="/api/post/${newPost.id}" hx-target="closest article" hx-swap="outerHTML">
Load Full Post
</button>
<button hx-delete="/api/post/${newPost.id}" hx-target="closest article" hx-swap="outerHTML">
Delete Post
</button>
</article>
`;
})
.put('/api/post/:id', async ({ params, body, getUser }) => {
const user = await getUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const { title, content } = body as { title: string; content: string };
const updatedPost = await db.update(posts)
.set({ title, content })
.where(eq(posts.id, parseInt(params.id)))
.returning()
.get();
return updatedPost;
})
.delete('/api/post/:id', async ({ params, getUser }) => {
const user = await getUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
await db.delete(posts).where(eq(posts.id, parseInt(params.id))).run();
return { success: true };
})
.listen(3000);
console.log(`Server is running at http://localhost:${app.server?.port}`);- 更新
src/db/schema.ts以添加用户表:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey(),
providerId: text('provider_id').notNull(),
provider: text('provider').notNull(),
email: text('email').notNull(),
name: text('name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: integer('author_id').notNull().references(() => users.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
});- 更新
src/components/Blog.tsx以显示登录状态和登出按钮:
import React, { useState } from 'react';
import Editor from './Editor';
import { marked } from 'marked';
interface Post {
id: number;
title: string;
content: string;
createdAt: number;
}
interface User {
id: number;
name: string;
email: string;
}
interface BlogProps {
posts: Post[];
user: User | null;
}
export const Blog: React.FC<BlogProps> = ({ posts, user }) => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// @ts-ignore
htmx.trigger('#new-post-form', 'submit');
};
return (
<div>
<h1>My Blog</h1>
{user ? (
<>
<p>Welcome, {user.name}! <a href="/logout">Logout</a></p>
<form id="new-post-form" hx-post="/api/post" hx-swap="beforeend" hx-target="#posts" onSubmit={handleSubmit}>
<input
type="text"
name="title"
placeholder="Title"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Editor onChange={setContent} />
<input type="hidden" name="content" value={content} />
<button type="submit">Create Post</button>
</form>
</>
) : (
<p><a href="/login">Login to create posts</a></p>
)}
<div id="posts">
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<div dangerouslySetInnerHTML={{ __html: marked(post.content) }} />
<small>Created at: {new Date(post.createdAt).toLocaleString()}</small>
{user && (
<>
<button hx-get={`/api/post/${post.id}`} hx-target="closest article" hx-swap="outerHTML">
Load Full Post
</button>
<button hx-delete={`/api/post/${post.id}`} hx-target="closest article" hx-swap="outerHTML">
Delete Post
</button>
</>
)}
</article>
))}
</div>
</div>
);
};- 更新环境变量:
确保在你的环境中设置了以下变量:
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
JWT_SECRET=your_jwt_secret
- 运行数据库迁移:
bun run generate
bun run push这个实现添加了以下功能:
- 用户可以使用 Google 或 GitHub 账户登录。
- 登录后,用户信息会被保存在数据库中。
- 用户登录状态通过 JWT 保存在 cookie 中。
- 只有登录用户才能创建、编辑和删除博客文章。
- 博客文章现在与作者关联。
这个实现还可以进一步改进,例如:
- 添加用户个人资料页面。
- 实现邮箱验证。
- 添加更多的 OAuth 提供商,如 Facebook、Twitter 等。
- 实现基于角色的访问控制(RBAC)。
- 添加密码重置功能。
- 实现记住我功能。
- 添加登录尝试限制以防止暴力攻击。
记住,在实际部署时,你需要使用 HTTPS 来保护用户的敏感信息,并考虑其他安全最佳实践,如 CSRF 保护、适当的错误处理等。