From 0ca8f78ffa07097c6716eb1d5523f971f505e32a Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 16 Jun 2026 00:21:10 -0600 Subject: [PATCH] feat: add admin routes (list/create/delete users) --- server/routes/admin.test.ts | 98 +++++++++++++++++++++++++++++++++++++ server/routes/admin.ts | 54 ++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 server/routes/admin.test.ts create mode 100644 server/routes/admin.ts diff --git a/server/routes/admin.test.ts b/server/routes/admin.test.ts new file mode 100644 index 0000000..af180c8 --- /dev/null +++ b/server/routes/admin.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it } from 'bun:test' +import type { Database } from 'bun:sqlite' +import { Hono } from 'hono' +import { hashPassword, signAccessToken } from '../auth' +import { createUser, openDb } from '../db' +import { createAdminRouter } from './admin' + +const JWT_SECRET = 'test-secret' + +describe('admin routes', () => { + let app: Hono + let db: Database + let adminToken: string + let userToken: string + let adminId: string + let userId: string + + beforeEach(async () => { + db = openDb(':memory:') + const adminHash = await hashPassword('adminpass') + const userHash = await hashPassword('userpass') + const admin = createUser(db, { username: 'admin', passwordHash: adminHash, role: 'admin' }) + const user = createUser(db, { username: 'alice', passwordHash: userHash, role: 'user' }) + adminId = admin.id + userId = user.id + adminToken = await signAccessToken(admin.id, 'admin', JWT_SECRET) + userToken = await signAccessToken(user.id, 'user', JWT_SECRET) + app = new Hono().route('/api/admin', createAdminRouter(db, JWT_SECRET)) + }) + + it('lists users for admin', async () => { + const res = await app.request('/api/admin/users', { + headers: { Authorization: `Bearer ${adminToken}` }, + }) + expect(res.status).toBe(200) + const body = await res.json() as { id: string; username: string }[] + expect(body.length).toBe(2) + expect(body[0]).not.toHaveProperty('passwordHash') + }) + + it('returns 401 for unauthenticated request', async () => { + const res = await app.request('/api/admin/users') + expect(res.status).toBe(401) + }) + + it('returns 403 for non-admin user', async () => { + const res = await app.request('/api/admin/users', { + headers: { Authorization: `Bearer ${userToken}` }, + }) + expect(res.status).toBe(403) + }) + + it('creates a new user', async () => { + const res = await app.request('/api/admin/users', { + method: 'POST', + headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: 'bob', password: 'pass123', role: 'user' }), + }) + expect(res.status).toBe(200) + const body = await res.json() as { username: string; role: string } + expect(body.username).toBe('bob') + expect(body.role).toBe('user') + expect(body).not.toHaveProperty('passwordHash') + }) + + it('returns 400 when creating user without required fields', async () => { + const res = await app.request('/api/admin/users', { + method: 'POST', + headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: 'bob' }), + }) + expect(res.status).toBe(400) + }) + + it('deletes a user', async () => { + const res = await app.request(`/api/admin/users/${userId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${adminToken}` }, + }) + expect(res.status).toBe(200) + }) + + it('prevents deleting yourself', async () => { + const res = await app.request(`/api/admin/users/${adminId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${adminToken}` }, + }) + expect(res.status).toBe(400) + }) + + it('returns 404 when deleting missing user', async () => { + const res = await app.request('/api/admin/users/missing', { + method: 'DELETE', + headers: { Authorization: `Bearer ${adminToken}` }, + }) + expect(res.status).toBe(404) + }) +}) diff --git a/server/routes/admin.ts b/server/routes/admin.ts new file mode 100644 index 0000000..69ca710 --- /dev/null +++ b/server/routes/admin.ts @@ -0,0 +1,54 @@ +import type { Database } from 'bun:sqlite' +import { Hono } from 'hono' +import { hashPassword } from '../auth' +import { createUser, deleteUser, listUsers } from '../db' +import { bearerAuth, type AuthVariables } from '../middleware/bearerAuth' + +export function createAdminRouter(db: Database, jwtSecret: string) { + const app = new Hono<{ Variables: AuthVariables }>() + + app.use('/*', bearerAuth(jwtSecret)) + app.use('/*', async (c, next) => { + if (c.get('role') !== 'admin') return c.json({ error: '无权限' }, 403) + await next() + }) + + app.get('/users', (c) => { + return c.json(listUsers(db)) + }) + + app.post('/users', async (c) => { + const body = (await c.req.json().catch(() => null)) as { + username?: unknown + password?: unknown + role?: unknown + } | null + + if ( + typeof body?.username !== 'string' || + body.username.trim() === '' || + typeof body?.password !== 'string' || + body.password.length < 1 + ) { + return c.json({ error: '请提供用户名和密码' }, 400) + } + + const role = body.role === 'admin' ? 'admin' : 'user' + const passwordHash = await hashPassword(body.password) + const user = createUser(db, { username: body.username.trim(), passwordHash, role }) + return c.json({ id: user.id, username: user.username, role: user.role, createdAt: user.createdAt }) + }) + + app.delete('/users/:id', (c) => { + const targetId = c.req.param('id') + if (targetId === c.get('userId')) { + return c.json({ error: '不能删除自己的账号' }, 400) + } + if (!deleteUser(db, targetId)) { + return c.json({ error: '用户不存在' }, 404) + } + return c.json({ ok: true }) + }) + + return app +}