24.03-用户体系
要点
- 用户属于租户,每个用户有角色(admin/member/viewer)
- 认证使用 JWT,token 中包含
userId和tenantId - 密码使用 bcrypt 加密存储
- 支持用户邀请、角色管理、权限控制
内容
1. 用户数据模型
-- 用户表
CREATE TABLE users (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
name TEXT,
role TEXT NOT NULL DEFAULT 'member', -- admin/member/viewer
status TEXT NOT NULL DEFAULT 'active', -- active/invited/suspended
last_login_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
UNIQUE(tenant_id, email) -- 同一租户内邮箱唯一
)
-- 用户会话表(可选,用于主动失效 token)
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token_hash TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)TypeScript 类型:
// src/types/user.ts
export interface User {
id: string
tenantId: string
email: string
passwordHash: string
name?: string
role: UserRole
status: UserStatus
lastLoginAt?: number
createdAt: number
updatedAt: number
}
export type UserRole = 'admin' | 'member' | 'viewer'
export type UserStatus = 'active' | 'invited' | 'suspended'
// 返回给前端的用户信息(不包含敏感字段)
export interface UserProfile {
id: string
email: string
name?: string
role: UserRole
status: UserStatus
lastLoginAt?: number
}2. 用户注册与登录
2.1 密码加密
// src/lib/auth.ts
import { hash, compare } from 'bcryptjs'
export async function hashPassword(password: string): Promise<string> {
return hash(password, 10) // salt rounds
}
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return compare(password, hash)
}2.2 登录接口
// src/routes/auth.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { sign } from 'hono/jwt'
import { verifyPassword } from '../lib/auth'
const app = new Hono()
app.post('/login', zValidator('json', z.object({
email: z.string().email(),
password: z.string(),
})), async (c) => {
const { email, password } = c.req.valid('json')
// 1. 查找用户
const user = await c.env.DB.prepare(`
SELECT * FROM users WHERE email = ?
`).bind(email).first()
if (!user) {
return c.json({ error: '邮箱或密码错误' }, 401)
}
// 2. 验证密码
const valid = await verifyPassword(password, user.password_hash)
if (!valid) {
return c.json({ error: '邮箱或密码错误' }, 401)
}
// 3. 检查用户状态
if (user.status !== 'active') {
return c.json({ error: '账号已被禁用' }, 403)
}
// 4. 生成 JWT
const token = await sign(
{
userId: user.id,
tenantId: user.tenant_id,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7 天
},
c.env.JWT_SECRET
)
// 5. 更新最后登录时间
await c.env.DB.prepare(`
UPDATE users SET last_login_at = ? WHERE id = ?
`).bind(Date.now(), user.id).run()
return c.json({
token,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
tenantId: user.tenant_id,
},
})
})2.3 JWT 中间件
// src/middleware/auth.ts
import { Context, Next } from 'hono'
import { verify } from 'hono/jwt'
export async function authMiddleware(c: Context, next: Next) {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401)
}
const token = authHeader.slice(7)
try {
const payload = await verify(token, c.env.JWT_SECRET)
const userId = payload.userId as string
const tenantId = payload.tenantId as string
// 验证用户是否存在且状态正常
const user = await c.env.DB.prepare(`
SELECT id, status FROM users WHERE id = ? AND tenant_id = ?
`).bind(userId, tenantId).first()
if (!user) {
return c.json({ error: 'User not found' }, 404)
}
if (user.status !== 'active') {
return c.json({ error: 'Account is suspended' }, 403)
}
// 存储到上下文
c.set('userId', userId)
c.set('tenantId', tenantId)
await next()
} catch (error) {
return c.json({ error: 'Invalid token' }, 401)
}
}3. 用户邀请
3.1 邀请流程
- 管理员生成邀请链接(包含邀请 token)
- 被邀请人点击链接,填写密码
- 系统创建用户账号,状态为
active - 被邀请人可以登录
3.2 邀请接口
// src/routes/users.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { generateId } from '../lib/utils'
import { sign } from 'hono/jwt'
const app = new Hono()
// 邀请用户
app.post('/invite', zValidator('json', z.object({
email: z.string().email(),
role: z.enum(['admin', 'member', 'viewer']),
})), async (c) => {
const tenantId = c.get('tenantId')
const inviterId = c.get('userId')
const { email, role } = c.req.valid('json')
// 1. 检查用户是否已存在
const existing = await c.env.DB.prepare(`
SELECT id FROM users WHERE tenant_id = ? AND email = ?
`).bind(tenantId, email).first()
if (existing) {
return c.json({ error: '用户已存在' }, 409)
}
// 2. 创建邀请记录(状态为 invited)
const userId = generateId()
const now = Date.now()
await c.env.DB.prepare(`
INSERT INTO users (id, tenant_id, email, password_hash, name, role, status, created_at, updated_at)
VALUES (?, ?, ?, '', NULL, ?, 'invited', ?, ?)
`).bind(userId, tenantId, email, role, now, now).run()
// 3. 生成邀请 token(24 小时有效)
const inviteToken = await sign(
{
userId,
tenantId,
email,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24,
},
c.env.JWT_SECRET
)
// 4. 发送邀请邮件
const inviteUrl = `${process.env.FRONTEND_URL}/invite?token=${inviteToken}`
c.executionCtx.waitUntil(
sendInviteEmail(email, inviteUrl, tenantId)
)
return c.json({ success: true, inviteUrl })
})
// 接受邀请(设置密码)
app.post('/accept-invite', zValidator('json', z.object({
token: z.string(),
password: z.string().min(8),
name: z.string().optional(),
})), async (c) => {
const { token, password, name } = c.req.valid('json')
// 1. 验证 token
const payload = await verify(token, c.env.JWT_SECRET)
const { userId, tenantId, email } = payload
// 2. 查找用户
const user = await c.env.DB.prepare(`
SELECT * FROM users WHERE id = ? AND status = 'invited'
`).bind(userId).first()
if (!user) {
return c.json({ error: '邀请无效或已过期' }, 400)
}
// 3. 设置密码,激活用户
const passwordHash = await hashPassword(password)
const now = Date.now()
await c.env.DB.prepare(`
UPDATE users
SET password_hash = ?, name = ?, status = 'active', updated_at = ?
WHERE id = ?
`).bind(passwordHash, name || null, now, userId).run()
return c.json({ success: true })
})4. 角色与权限
4.1 角色定义
| 角色 | 权限 |
|---|---|
| admin | 管理租户、管理用户、管理配置、所有业务操作 |
| member | 使用 AI 功能、管理自己的资源 |
| viewer | 只读访问 |
4.2 权限中间件
// src/middleware/permission.ts
import { Context, Next } from 'hono'
export function requireRole(...roles: string[]) {
return async (c: Context, next: Next) => {
const userId = c.get('userId')
const tenantId = c.get('tenantId')
const user = await c.env.DB.prepare(`
SELECT role FROM users WHERE id = ? AND tenant_id = ?
`).bind(userId, tenantId).first()
if (!user || !roles.includes(user.role)) {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
}
}
// 使用
app.delete('/users/:id', requireRole('admin'), async (c) => {
const userId = c.req.param('id')
// 只有 admin 可以删除用户
})
app.get('/settings', requireRole('admin', 'member'), async (c) => {
// admin 和 member 可以访问
})4.3 细粒度权限
对于更复杂的场景,可以使用权限列表:
// src/types/permission.ts
export type Permission =
| 'user:read'
| 'user:write'
| 'user:delete'
| 'config:read'
| 'config:write'
| 'chat:use'
| 'rag:use'
| 'rag:manage'
export const ROLE_PERMISSIONS: Record<string, Permission[]> = {
admin: [
'user:read', 'user:write', 'user:delete',
'config:read', 'config:write',
'chat:use', 'rag:use', 'rag:manage',
],
member: [
'user:read',
'config:read',
'chat:use', 'rag:use',
],
viewer: [
'user:read',
'config:read',
'rag:use', // 只能使用,不能管理
],
}
export function hasPermission(role: string, permission: Permission): boolean {
const permissions = ROLE_PERMISSIONS[role] || []
return permissions.includes(permission)
}// 使用
app.post('/rag/knowledge-base', async (c) => {
const userId = c.get('userId')
const tenantId = c.get('tenantId')
const user = await c.env.DB.prepare(`
SELECT role FROM users WHERE id = ? AND tenant_id = ?
`).bind(userId, tenantId).first()
if (!hasPermission(user.role, 'rag:manage')) {
return c.json({ error: 'Forbidden' }, 403)
}
// 创建知识库
})5. 用户管理
5.1 获取租户内用户列表
// src/routes/users.ts
app.get('/', async (c) => {
const tenantId = c.get('tenantId')
const page = parseInt(c.req.query('page') || '1')
const pageSize = parseInt(c.req.query('pageSize') || '20')
const offset = (page - 1) * pageSize
const users = await c.env.DB.prepare(`
SELECT id, email, name, role, status, last_login_at, created_at
FROM users
WHERE tenant_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`).bind(tenantId, pageSize, offset).all()
const total = await c.env.DB.prepare(`
SELECT COUNT(*) as count FROM users WHERE tenant_id = ?
`).bind(tenantId).first()
return c.json({
users: users.results,
pagination: {
page,
pageSize,
total: total.count,
},
})
})5.2 更新用户信息
app.put('/me', zValidator('json', z.object({
name: z.string().optional(),
})), async (c) => {
const userId = c.get('userId')
const { name } = c.req.valid('json')
await c.env.DB.prepare(`
UPDATE users SET name = ?, updated_at = ? WHERE id = ?
`).bind(name, Date.now(), userId).run()
return c.json({ success: true })
})
// 管理员更新其他用户
app.put('/:id', requireRole('admin'), zValidator('json', z.object({
role: z.enum(['admin', 'member', 'viewer']).optional(),
status: z.enum(['active', 'suspended']).optional(),
})), async (c) => {
const targetUserId = c.req.param('id')
const tenantId = c.get('tenantId')
const updates = c.req.valid('json')
// 不能修改自己的角色
if (targetUserId === c.get('userId') && updates.role) {
return c.json({ error: 'Cannot change your own role' }, 400)
}
const setClauses = []
const values = []
if (updates.role) {
setClauses.push('role = ?')
values.push(updates.role)
}
if (updates.status) {
setClauses.push('status = ?')
values.push(updates.status)
}
if (setClauses.length === 0) {
return c.json({ error: 'No updates provided' }, 400)
}
setClauses.push('updated_at = ?')
values.push(Date.now())
values.push(targetUserId)
values.push(tenantId)
await c.env.DB.prepare(`
UPDATE users SET ${setClauses.join(', ')} WHERE id = ? AND tenant_id = ?
`).bind(...values).run()
return c.json({ success: true })
})5.3 删除用户
app.delete('/:id', requireRole('admin'), async (c) => {
const targetUserId = c.req.param('id')
const tenantId = c.get('tenantId')
const currentUserId = c.get('userId')
// 不能删除自己
if (targetUserId === currentUserId) {
return c.json({ error: 'Cannot delete yourself' }, 400)
}
// 检查是否是最后一个 admin
const adminCount = await c.env.DB.prepare(`
SELECT COUNT(*) as count FROM users
WHERE tenant_id = ? AND role = 'admin' AND status = 'active'
`).bind(tenantId).first()
const targetUser = await c.env.DB.prepare(`
SELECT role FROM users WHERE id = ? AND tenant_id = ?
`).bind(targetUserId, tenantId).first()
if (targetUser.role === 'admin' && adminCount.count === 1) {
return c.json({ error: 'Cannot delete the last admin' }, 400)
}
await c.env.DB.prepare(`
DELETE FROM users WHERE id = ? AND tenant_id = ?
`).bind(targetUserId, tenantId).run()
return c.json({ success: true })
})6. 修改密码
app.post('/change-password', zValidator('json', z.object({
oldPassword: z.string(),
newPassword: z.string().min(8),
})), async (c) => {
const userId = c.get('userId')
const { oldPassword, newPassword } = c.req.valid('json')
// 1. 获取用户
const user = await c.env.DB.prepare(`
SELECT password_hash FROM users WHERE id = ?
`).bind(userId).first()
// 2. 验证旧密码
const valid = await verifyPassword(oldPassword, user.password_hash)
if (!valid) {
return c.json({ error: '旧密码错误' }, 400)
}
// 3. 更新密码
const newPasswordHash = await hashPassword(newPassword)
await c.env.DB.prepare(`
UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?
`).bind(newPasswordHash, Date.now(), userId).run()
return c.json({ success: true })
})7. 实战:完整的用户管理
// src/routes/users.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { generateId } from '../lib/utils'
import { hashPassword, verifyPassword } from '../lib/auth'
import { requireRole } from '../middleware/permission'
import { sign, verify } from 'hono/jwt'
const app = new Hono()
// 获取用户列表
app.get('/', async (c) => {
const tenantId = c.get('tenantId')
const users = await c.env.DB.prepare(`
SELECT id, email, name, role, status, last_login_at, created_at
FROM users WHERE tenant_id = ?
ORDER BY created_at DESC
`).bind(tenantId).all()
return c.json(users.results)
})
// 获取当前用户信息
app.get('/me', async (c) => {
const userId = c.get('userId')
const user = await c.env.DB.prepare(`
SELECT id, email, name, role, status, last_login_at
FROM users WHERE id = ?
`).bind(userId).first()
return c.json(user)
})
// 更新当前用户信息
app.put('/me', zValidator('json', z.object({
name: z.string().optional(),
})), async (c) => {
const userId = c.get('userId')
const { name } = c.req.valid('json')
await c.env.DB.prepare(`
UPDATE users SET name = ?, updated_at = ? WHERE id = ?
`).bind(name, Date.now(), userId).run()
return c.json({ success: true })
})
// 邀请用户
app.post('/invite', requireRole('admin'), zValidator('json', z.object({
email: z.string().email(),
role: z.enum(['admin', 'member', 'viewer']),
})), async (c) => {
const tenantId = c.get('tenantId')
const { email, role } = c.req.valid('json')
const existing = await c.env.DB.prepare(`
SELECT id FROM users WHERE tenant_id = ? AND email = ?
`).bind(tenantId, email).first()
if (existing) {
return c.json({ error: '用户已存在' }, 409)
}
const userId = generateId()
const now = Date.now()
await c.env.DB.prepare(`
INSERT INTO users (id, tenant_id, email, password_hash, role, status, created_at, updated_at)
VALUES (?, ?, ?, '', ?, 'invited', ?, ?)
`).bind(userId, tenantId, email, role, now, now).run()
const inviteToken = await sign(
{ userId, tenantId, email, exp: Math.floor(Date.now() / 1000) + 86400 },
c.env.JWT_SECRET
)
c.executionCtx.waitUntil(sendInviteEmail(email, inviteToken))
return c.json({ success: true })
})
// 接受邀请
app.post('/accept-invite', zValidator('json', z.object({
token: z.string(),
password: z.string().min(8),
name: z.string().optional(),
})), async (c) => {
const { token, password, name } = c.req.valid('json')
const payload = await verify(token, c.env.JWT_SECRET)
const passwordHash = await hashPassword(password)
await c.env.DB.prepare(`
UPDATE users
SET password_hash = ?, name = ?, status = 'active', updated_at = ?
WHERE id = ? AND status = 'invited'
`).bind(passwordHash, name || null, Date.now(), payload.userId).run()
return c.json({ success: true })
})
// 修改密码
app.post('/change-password', zValidator('json', z.object({
oldPassword: z.string(),
newPassword: z.string().min(8),
})), async (c) => {
const userId = c.get('userId')
const { oldPassword, newPassword } = c.req.valid('json')
const user = await c.env.DB.prepare(`
SELECT password_hash FROM users WHERE id = ?
`).bind(userId).first()
const valid = await verifyPassword(oldPassword, user.password_hash)
if (!valid) {
return c.json({ error: '旧密码错误' }, 400)
}
const newPasswordHash = await hashPassword(newPassword)
await c.env.DB.prepare(`
UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?
`).bind(newPasswordHash, Date.now(), userId).run()
return c.json({ success: true })
})
// 管理员更新用户
app.put('/:id', requireRole('admin'), zValidator('json', z.object({
role: z.enum(['admin', 'member', 'viewer']).optional(),
status: z.enum(['active', 'suspended']).optional(),
})), async (c) => {
const targetUserId = c.req.param('id')
const tenantId = c.get('tenantId')
const updates = c.req.valid('json')
const setClauses = []
const values = []
if (updates.role) {
setClauses.push('role = ?')
values.push(updates.role)
}
if (updates.status) {
setClauses.push('status = ?')
values.push(updates.status)
}
if (setClauses.length === 0) {
return c.json({ error: 'No updates' }, 400)
}
setClauses.push('updated_at = ?')
values.push(Date.now())
values.push(targetUserId)
values.push(tenantId)
await c.env.DB.prepare(`
UPDATE users SET ${setClauses.join(', ')} WHERE id = ? AND tenant_id = ?
`).bind(...values).run()
return c.json({ success: true })
})
// 删除用户
app.delete('/:id', requireRole('admin'), async (c) => {
const targetUserId = c.req.param('id')
const tenantId = c.get('tenantId')
const currentUserId = c.get('userId')
if (targetUserId === currentUserId) {
return c.json({ error: 'Cannot delete yourself' }, 400)
}
await c.env.DB.prepare(`
DELETE FROM users WHERE id = ? AND tenant_id = ?
`).bind(targetUserId, tenantId).run()
return c.json({ success: true })
})
export default app8. 小结
用户体系的关键点:
- 数据模型:用户属于租户,包含角色和状态
- 认证:JWT token 包含
userId和tenantId,中间件统一验证 - 密码安全:使用 bcrypt 加密,不存储明文
- 用户邀请:生成邀请 token,被邀请人设置密码后激活
- 角色权限:admin/member/viewer,中间件统一检查权限
- 用户管理:列表、更新、删除,管理员可以管理租户内用户
一句话带走:
用户体系的核心是「认证 + 授权」。JWT 做认证,角色做授权,中间件统一处理。密码用 bcrypt 加密,用户邀请用 token 验证,权限检查用中间件拦截。