yb 3 viikkoa sitten
vanhempi
commit
8d532091df
13 muutettua tiedostoa jossa 1737 lisäystä ja 17 poistoa
  1. 11 0
      .dev.vars.example
  2. 2 1
      package.json
  3. 144 0
      schema.sql
  4. 43 16
      src/index.ts
  5. 169 0
      src/middleware/auth.ts
  6. 204 0
      src/routes/auth.ts
  7. 4 0
      src/routes/stream.ts
  8. 433 0
      src/routes/user.ts
  9. 291 0
      src/services/auth.ts
  10. 141 0
      src/types/index.ts
  11. 153 0
      src/utils/jwt.ts
  12. 134 0
      src/utils/password.ts
  13. 8 0
      wrangler.toml

+ 11 - 0
.dev.vars.example

@@ -14,3 +14,14 @@ CF_API_TOKEN=your_api_token
 # 从任意视频播放地址获取: https://customer-xxx.cloudflarestream.com
 # 填写 xxx 部分
 CUSTOMER_SUBDOMAIN=your_subdomain
+
+# JWT 配置
+# JWT 签名密钥 (生产环境请使用强随机字符串)
+# 生成方式: openssl rand -base64 32
+JWT_SECRET=your_jwt_secret_key_here
+
+# Access Token 有效期 (秒), 默认 86400 (24小时)
+JWT_EXPIRES_IN=86400
+
+# Refresh Token 有效期 (秒), 默认 604800 (7天)
+REFRESH_EXPIRES_IN=604800

+ 2 - 1
package.json

@@ -6,7 +6,8 @@
   "scripts": {
     "dev": "wrangler dev",
     "deploy": "wrangler deploy",
-    "dev:node": "npx tsx watch src/node.ts"
+    "dev:node": "npx tsx watch src/node.ts",
+    "typecheck": "tsc --noEmit"
   },
   "keywords": [
     "cloudflare",

+ 144 - 0
schema.sql

@@ -0,0 +1,144 @@
+-- ============================================
+-- TG Live Game D1 Database Schema
+-- Database: tg_live_game
+-- Engine: Cloudflare D1 (SQLite)
+-- ============================================
+
+-- 摄像头管理表
+CREATE TABLE IF NOT EXISTS cameras (
+    id TEXT PRIMARY KEY,
+    name TEXT NOT NULL,
+    type TEXT CHECK(type IN ('mac', 'ip', 'rtsp', 'screen')),
+    protocol TEXT CHECK(protocol IN ('rtmps', 'srt', 'whip')),
+    rtsp_url TEXT,
+    location TEXT,
+    status TEXT DEFAULT 'offline' CHECK(status IN ('online', 'offline', 'error')),
+    live_input_id TEXT,  -- Cloudflare Stream Live Input ID
+    meta TEXT,  -- JSON 扩展字段
+    created_at INTEGER DEFAULT (strftime('%s', 'now')),
+    updated_at INTEGER DEFAULT (strftime('%s', 'now'))
+);
+
+CREATE INDEX IF NOT EXISTS idx_cameras_status ON cameras(status);
+CREATE INDEX IF NOT EXISTS idx_cameras_type ON cameras(type);
+CREATE INDEX IF NOT EXISTS idx_cameras_live_input ON cameras(live_input_id);
+
+-- 直播会话表
+CREATE TABLE IF NOT EXISTS live_sessions (
+    id TEXT PRIMARY KEY,
+    camera_id TEXT NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
+    live_input_id TEXT NOT NULL,  -- Cloudflare Stream Live Input ID
+    started_at INTEGER NOT NULL,
+    ended_at INTEGER,
+    duration INTEGER,  -- 秒
+    status TEXT DEFAULT 'live' CHECK(status IN ('live', 'ended', 'error')),
+    viewer_count INTEGER DEFAULT 0,
+    peak_viewers INTEGER DEFAULT 0,
+    recording_id TEXT,  -- 录像视频 ID
+    meta TEXT,  -- JSON 扩展字段
+    created_at INTEGER DEFAULT (strftime('%s', 'now'))
+);
+
+CREATE INDEX IF NOT EXISTS idx_sessions_camera ON live_sessions(camera_id);
+CREATE INDEX IF NOT EXISTS idx_sessions_status ON live_sessions(status);
+CREATE INDEX IF NOT EXISTS idx_sessions_started ON live_sessions(started_at);
+
+-- 视频元数据表 (扩展 Cloudflare Stream 视频)
+CREATE TABLE IF NOT EXISTS videos (
+    id TEXT PRIMARY KEY,
+    cf_uid TEXT UNIQUE NOT NULL,  -- Cloudflare Stream Video UID
+    camera_id TEXT REFERENCES cameras(id) ON DELETE SET NULL,
+    session_id TEXT REFERENCES live_sessions(id) ON DELETE SET NULL,
+    title TEXT,
+    description TEXT,
+    tags TEXT,  -- JSON array
+    thumbnail TEXT,
+    duration INTEGER,  -- 秒
+    size INTEGER,  -- 字节
+    status TEXT DEFAULT 'processing' CHECK(status IN ('ready', 'processing', 'error')),
+    is_public INTEGER DEFAULT 0,
+    view_count INTEGER DEFAULT 0,
+    created_at INTEGER DEFAULT (strftime('%s', 'now')),
+    updated_at INTEGER DEFAULT (strftime('%s', 'now'))
+);
+
+CREATE INDEX IF NOT EXISTS idx_videos_cf_uid ON videos(cf_uid);
+CREATE INDEX IF NOT EXISTS idx_videos_camera ON videos(camera_id);
+CREATE INDEX IF NOT EXISTS idx_videos_session ON videos(session_id);
+CREATE INDEX IF NOT EXISTS idx_videos_status ON videos(status);
+
+-- 用户表
+CREATE TABLE IF NOT EXISTS users (
+    id TEXT PRIMARY KEY,
+    username TEXT UNIQUE NOT NULL,
+    email TEXT UNIQUE,
+    password_hash TEXT,
+    role TEXT DEFAULT 'viewer' CHECK(role IN ('admin', 'operator', 'viewer')),
+    status TEXT DEFAULT 'active' CHECK(status IN ('active', 'disabled')),
+    last_login INTEGER,
+    created_at INTEGER DEFAULT (strftime('%s', 'now')),
+    updated_at INTEGER DEFAULT (strftime('%s', 'now'))
+);
+
+CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
+CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
+CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
+
+-- 用户权限表
+CREATE TABLE IF NOT EXISTS user_permissions (
+    id TEXT PRIMARY KEY,
+    user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+    camera_id TEXT NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
+    permission TEXT NOT NULL CHECK(permission IN ('view', 'control', 'manage')),
+    granted_at INTEGER DEFAULT (strftime('%s', 'now')),
+    granted_by TEXT REFERENCES users(id) ON DELETE SET NULL,
+    UNIQUE(user_id, camera_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_perms_user ON user_permissions(user_id);
+CREATE INDEX IF NOT EXISTS idx_perms_camera ON user_permissions(camera_id);
+
+-- 观看统计表
+CREATE TABLE IF NOT EXISTS view_stats (
+    id TEXT PRIMARY KEY,
+    video_id TEXT REFERENCES videos(id) ON DELETE CASCADE,
+    session_id TEXT REFERENCES live_sessions(id) ON DELETE CASCADE,
+    user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
+    ip_address TEXT,
+    user_agent TEXT,
+    watch_duration INTEGER,  -- 秒
+    started_at INTEGER NOT NULL,
+    ended_at INTEGER,
+    country TEXT,
+    city TEXT,
+    created_at INTEGER DEFAULT (strftime('%s', 'now'))
+);
+
+CREATE INDEX IF NOT EXISTS idx_stats_video ON view_stats(video_id);
+CREATE INDEX IF NOT EXISTS idx_stats_session ON view_stats(session_id);
+CREATE INDEX IF NOT EXISTS idx_stats_started ON view_stats(started_at);
+
+-- 操作日志表
+CREATE TABLE IF NOT EXISTS audit_logs (
+    id TEXT PRIMARY KEY,
+    user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
+    action TEXT NOT NULL CHECK(action IN ('create', 'update', 'delete', 'login', 'logout')),
+    resource TEXT NOT NULL,  -- 'camera', 'video', 'user', 'session'
+    resource_id TEXT,
+    details TEXT,  -- JSON
+    ip_address TEXT,
+    created_at INTEGER DEFAULT (strftime('%s', 'now'))
+);
+
+CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_logs(user_id);
+CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action);
+CREATE INDEX IF NOT EXISTS idx_audit_resource ON audit_logs(resource);
+CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_logs(created_at);
+
+-- ============================================
+-- 初始化数据
+-- ============================================
+
+-- 创建默认管理员用户 (密码需要后续更新)
+INSERT OR IGNORE INTO users (id, username, email, role, status)
+VALUES ('admin-001', 'admin', 'admin@tg-live.local', 'admin', 'active');

+ 43 - 16
src/index.ts

@@ -2,6 +2,9 @@ import { Hono } from 'hono'
 import { cors } from 'hono/cors'
 import { logger } from 'hono/logger'
 import stream from './routes/stream'
+import auth from './routes/auth'
+import user from './routes/user'
+import { authMiddleware } from './middleware/auth'
 import type { Env } from './types'
 
 const app = new Hono<{ Bindings: Env }>()
@@ -21,26 +24,50 @@ app.get('/', (c) => {
     msg: 'TG Live Game API',
     data: {
       version: '1.0.0',
-      endpoints: [
-        'GET  /api/stream/video/list',
-        'GET  /api/stream/video/:videoId',
-        'DELETE /api/stream/video/:videoId',
-        'POST /api/stream/video/import',
-        'POST /api/stream/video/upload-url',
-        'GET  /api/stream/video/:videoId/playback',
-        'GET  /api/stream/live/list',
-        'POST /api/stream/live',
-        'GET  /api/stream/live/:liveInputId',
-        'PUT  /api/stream/live/:liveInputId',
-        'DELETE /api/stream/live/:liveInputId',
-        'GET  /api/stream/live/:liveInputId/playback',
-        'GET  /api/stream/live/:liveInputId/recordings',
-      ]
+      endpoints: {
+        auth: [
+          'POST /api/auth/login',
+          'POST /api/auth/register',
+          'POST /api/auth/refresh',
+          'GET  /api/auth/me',
+          'POST /api/auth/change-password',
+          'POST /api/auth/logout',
+        ],
+        users: [
+          'GET  /api/users',
+          'GET  /api/users/:id',
+          'POST /api/users',
+          'PUT  /api/users/:id',
+          'DELETE /api/users/:id',
+          'GET  /api/users/:id/permissions',
+          'POST /api/users/:id/permissions',
+          'DELETE /api/users/:id/permissions/:permissionId',
+        ],
+        stream: [
+          'GET  /api/stream/video/list',
+          'GET  /api/stream/video/:videoId',
+          'DELETE /api/stream/video/:videoId',
+          'POST /api/stream/video/import',
+          'POST /api/stream/video/upload-url',
+          'GET  /api/stream/video/:videoId/playback',
+          'GET  /api/stream/live/list',
+          'POST /api/stream/live',
+          'GET  /api/stream/live/:liveInputId',
+          'PUT  /api/stream/live/:liveInputId',
+          'DELETE /api/stream/live/:liveInputId',
+          'GET  /api/stream/live/:liveInputId/playback',
+          'GET  /api/stream/live/:liveInputId/recordings',
+        ],
+      }
     }
   })
 })
 
-// 挂载路由
+// 挂载公开路由
+app.route('/api/auth', auth)
+
+// 挂载需要认证的路由
+app.route('/api/users', user)
 app.route('/api/stream', stream)
 
 // 404 处理

+ 169 - 0
src/middleware/auth.ts

@@ -0,0 +1,169 @@
+import { Context, Next } from 'hono'
+import type { Env, JwtPayload, UserPermission } from '../types'
+import { verifyToken } from '../utils/jwt'
+
+// 扩展 Context 类型
+declare module 'hono' {
+  interface ContextVariableMap {
+    user: JwtPayload
+    userId: string
+  }
+}
+
+/**
+ * JWT 认证中间件
+ *
+ * 验证 Authorization: Bearer <token>
+ * 成功后将 user 信息注入到 context
+ */
+export function authMiddleware() {
+  return async (c: Context<{ Bindings: Env }>, next: Next) => {
+    const authHeader = c.req.header('Authorization')
+
+    if (!authHeader || !authHeader.startsWith('Bearer ')) {
+      return c.json({
+        code: 401,
+        msg: '未提供认证令牌',
+        data: null,
+      }, 401)
+    }
+
+    const token = authHeader.substring(7)
+    const result = await verifyToken(token, c.env.JWT_SECRET)
+
+    if (!result.valid || !result.payload) {
+      return c.json({
+        code: 401,
+        msg: result.error || '认证令牌无效',
+        data: null,
+      }, 401)
+    }
+
+    // 注入用户信息
+    c.set('user', result.payload)
+    c.set('userId', result.payload.sub)
+
+    await next()
+  }
+}
+
+/**
+ * 角色验证中间件
+ *
+ * 检查用户是否具有指定角色
+ */
+export function requireRole(...roles: Array<'admin' | 'operator' | 'viewer'>) {
+  return async (c: Context<{ Bindings: Env }>, next: Next) => {
+    const user = c.get('user')
+
+    if (!user) {
+      return c.json({
+        code: 401,
+        msg: '未认证',
+        data: null,
+      }, 401)
+    }
+
+    if (!roles.includes(user.role)) {
+      return c.json({
+        code: 403,
+        msg: '权限不足',
+        data: null,
+      }, 403)
+    }
+
+    await next()
+  }
+}
+
+/**
+ * 资源权限验证中间件
+ *
+ * 检查用户是否对特定资源具有指定权限
+ */
+export function requirePermission(
+  resourceType: 'camera',
+  action: 'view' | 'control' | 'manage',
+  getResourceId: (c: Context) => string
+) {
+  return async (c: Context<{ Bindings: Env }>, next: Next) => {
+    const user = c.get('user')
+
+    if (!user) {
+      return c.json({
+        code: 401,
+        msg: '未认证',
+        data: null,
+      }, 401)
+    }
+
+    // admin 拥有所有权限
+    if (user.role === 'admin') {
+      await next()
+      return
+    }
+
+    const resourceId = getResourceId(c)
+
+    // 查询用户权限
+    const permission = await c.env.DB
+      .prepare(`
+        SELECT permission FROM user_permissions
+        WHERE user_id = ? AND camera_id = ?
+      `)
+      .bind(user.sub, resourceId)
+      .first<UserPermission>()
+
+    if (!permission) {
+      return c.json({
+        code: 403,
+        msg: '无权访问该资源',
+        data: null,
+      }, 403)
+    }
+
+    // 权限等级: manage > control > view
+    const permissionLevels: Record<string, number> = {
+      view: 1,
+      control: 2,
+      manage: 3,
+    }
+
+    const requiredLevel = permissionLevels[action] || 0
+    const userLevel = permissionLevels[permission.permission] || 0
+
+    if (userLevel < requiredLevel) {
+      return c.json({
+        code: 403,
+        msg: '权限不足',
+        data: null,
+      }, 403)
+    }
+
+    await next()
+  }
+}
+
+/**
+ * 可选认证中间件
+ *
+ * 如果提供了 token 则验证,否则继续执行
+ * 用于既支持认证又支持匿名访问的接口
+ */
+export function optionalAuth() {
+  return async (c: Context<{ Bindings: Env }>, next: Next) => {
+    const authHeader = c.req.header('Authorization')
+
+    if (authHeader && authHeader.startsWith('Bearer ')) {
+      const token = authHeader.substring(7)
+      const result = await verifyToken(token, c.env.JWT_SECRET)
+
+      if (result.valid && result.payload) {
+        c.set('user', result.payload)
+        c.set('userId', result.payload.sub)
+      }
+    }
+
+    await next()
+  }
+}

+ 204 - 0
src/routes/auth.ts

@@ -0,0 +1,204 @@
+import { Hono } from 'hono'
+import { AuthService } from '../services/auth'
+import { authMiddleware } from '../middleware/auth'
+import type { Env, ApiResponse, LoginRequest, RegisterRequest, AuthResponse, User } from '../types'
+
+const auth = new Hono<{ Bindings: Env }>()
+
+/**
+ * 创建成功响应
+ */
+function success<T>(data: T, msg = 'success'): ApiResponse<T> {
+  return { code: 200, msg, data }
+}
+
+/**
+ * 创建错误响应
+ */
+function error(msg: string, code = 500): ApiResponse<null> {
+  return { code, msg, data: null }
+}
+
+// ==================== 公开接口 ====================
+
+/**
+ * 用户登录
+ * POST /api/auth/login
+ */
+auth.post('/login', async (c) => {
+  try {
+    const body = await c.req.json<LoginRequest>()
+
+    if (!body.username || !body.password) {
+      return c.json(error('用户名和密码不能为空', 400), 400)
+    }
+
+    const service = new AuthService(c.env)
+    const result = await service.login(body)
+
+    if (!result.success) {
+      return c.json(error(result.error || '登录失败', 401), 401)
+    }
+
+    return c.json(success(result.data, '登录成功'))
+  } catch (err) {
+    console.error('Login error:', err)
+    return c.json(error(err instanceof Error ? err.message : '登录失败'))
+  }
+})
+
+/**
+ * 用户注册
+ * POST /api/auth/register
+ */
+auth.post('/register', async (c) => {
+  try {
+    const body = await c.req.json<RegisterRequest>()
+
+    // 验证必填字段
+    if (!body.username || !body.password) {
+      return c.json(error('用户名和密码不能为空', 400), 400)
+    }
+
+    // 验证用户名格式
+    if (!/^[a-zA-Z0-9_]{3,20}$/.test(body.username)) {
+      return c.json(error('用户名只能包含字母、数字、下划线,长度3-20', 400), 400)
+    }
+
+    // 验证密码强度
+    if (body.password.length < 6) {
+      return c.json(error('密码长度至少6位', 400), 400)
+    }
+
+    // 验证邮箱格式
+    if (body.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
+      return c.json(error('邮箱格式不正确', 400), 400)
+    }
+
+    const service = new AuthService(c.env)
+    const result = await service.register(body)
+
+    if (!result.success) {
+      return c.json(error(result.error || '注册失败', 400), 400)
+    }
+
+    return c.json(success(result.data, '注册成功'))
+  } catch (err) {
+    console.error('Register error:', err)
+    return c.json(error(err instanceof Error ? err.message : '注册失败'))
+  }
+})
+
+/**
+ * 刷新 Token
+ * POST /api/auth/refresh
+ */
+auth.post('/refresh', async (c) => {
+  try {
+    const body = await c.req.json<{ refreshToken: string }>()
+
+    if (!body.refreshToken) {
+      return c.json(error('refreshToken 不能为空', 400), 400)
+    }
+
+    const service = new AuthService(c.env)
+    const result = await service.refreshToken(body.refreshToken)
+
+    if (!result.success) {
+      return c.json(error(result.error || '刷新失败', 401), 401)
+    }
+
+    return c.json(success(result.data, '刷新成功'))
+  } catch (err) {
+    console.error('Refresh error:', err)
+    return c.json(error(err instanceof Error ? err.message : '刷新失败'))
+  }
+})
+
+// ==================== 需要认证的接口 ====================
+
+/**
+ * 获取当前用户信息
+ * GET /api/auth/me
+ */
+auth.get('/me', authMiddleware(), async (c) => {
+  try {
+    const userId = c.get('userId')
+    const service = new AuthService(c.env)
+    const user = await service.getCurrentUser(userId)
+
+    if (!user) {
+      return c.json(error('用户不存在', 404), 404)
+    }
+
+    return c.json(success(user))
+  } catch (err) {
+    console.error('Get me error:', err)
+    return c.json(error(err instanceof Error ? err.message : '获取用户信息失败'))
+  }
+})
+
+/**
+ * 修改密码
+ * POST /api/auth/change-password
+ */
+auth.post('/change-password', authMiddleware(), async (c) => {
+  try {
+    const userId = c.get('userId')
+    const body = await c.req.json<{ oldPassword: string; newPassword: string }>()
+
+    if (!body.oldPassword || !body.newPassword) {
+      return c.json(error('原密码和新密码不能为空', 400), 400)
+    }
+
+    if (body.newPassword.length < 6) {
+      return c.json(error('新密码长度至少6位', 400), 400)
+    }
+
+    const service = new AuthService(c.env)
+    const result = await service.changePassword(userId, body.oldPassword, body.newPassword)
+
+    if (!result.success) {
+      return c.json(error(result.error || '修改密码失败', 400), 400)
+    }
+
+    return c.json(success(null, '密码修改成功'))
+  } catch (err) {
+    console.error('Change password error:', err)
+    return c.json(error(err instanceof Error ? err.message : '修改密码失败'))
+  }
+})
+
+/**
+ * 退出登录
+ * POST /api/auth/logout
+ *
+ * 由于 JWT 是无状态的,服务端不需要做任何操作
+ * 客户端删除本地存储的 token 即可
+ */
+auth.post('/logout', authMiddleware(), async (c) => {
+  // 记录登出日志
+  try {
+    const user = c.get('user')
+    await c.env.DB
+      .prepare(`
+        INSERT INTO audit_logs (id, user_id, action, resource, resource_id, created_at)
+        VALUES (?, ?, ?, ?, ?, ?)
+      `)
+      .bind(
+        crypto.randomUUID().replace(/-/g, ''),
+        user.sub,
+        'logout',
+        'user',
+        user.sub,
+        Math.floor(Date.now() / 1000)
+      )
+      .run()
+  } catch (err) {
+    console.error('Log logout error:', err)
+  }
+
+  return c.json(success(null, '退出成功'))
+})
+
+export default auth

+ 4 - 0
src/routes/stream.ts

@@ -1,5 +1,6 @@
 import { Hono } from 'hono'
 import { CloudflareStreamService } from '../services/cloudflare'
+import { authMiddleware } from '../middleware/auth'
 import type { Env, ApiResponse, PageResponse, CloudflareVideo, CloudflareLiveInput, CreateLiveInputParams } from '../types'
 
 const stream = new Hono<{ Bindings: Env }>()
@@ -18,6 +19,9 @@ function error(msg: string, code = 500): ApiResponse<null> {
   return { code, msg, data: null }
 }
 
+// 所有 stream 路由都需要认证
+stream.use('*', authMiddleware())
+
 // ==================== 视频管理 ====================
 
 /**

+ 433 - 0
src/routes/user.ts

@@ -0,0 +1,433 @@
+import { Hono } from 'hono'
+import { authMiddleware, requireRole } from '../middleware/auth'
+import { hashPassword } from '../utils/password'
+import { generateId } from '../utils/jwt'
+import type { Env, ApiResponse, PageResponse, User, UserPermission } from '../types'
+
+const user = new Hono<{ Bindings: Env }>()
+
+/**
+ * 创建成功响应
+ */
+function success<T>(data: T, msg = 'success'): ApiResponse<T> {
+  return { code: 200, msg, data }
+}
+
+/**
+ * 创建错误响应
+ */
+function error(msg: string, code = 500): ApiResponse<null> {
+  return { code, msg, data: null }
+}
+
+// 所有用户管理接口都需要认证和 admin 角色
+user.use('*', authMiddleware())
+user.use('*', requireRole('admin'))
+
+// ==================== 用户管理 ====================
+
+/**
+ * 获取用户列表
+ * GET /api/users
+ */
+user.get('/', async (c) => {
+  try {
+    const { role, status, search, pageSize = '20', page = '1' } = c.req.query()
+    const limit = Math.min(parseInt(pageSize), 100)
+    const offset = (parseInt(page) - 1) * limit
+
+    let sql = 'SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE 1=1'
+    const params: (string | number)[] = []
+
+    if (role) {
+      sql += ' AND role = ?'
+      params.push(role)
+    }
+
+    if (status) {
+      sql += ' AND status = ?'
+      params.push(status)
+    }
+
+    if (search) {
+      sql += ' AND (username LIKE ? OR email LIKE ?)'
+      params.push(`%${search}%`, `%${search}%`)
+    }
+
+    // 获取总数
+    const countResult = await c.env.DB
+      .prepare(sql.replace('SELECT id, username, email, role, status, last_login, created_at, updated_at', 'SELECT COUNT(*) as count'))
+      .bind(...params)
+      .first<{ count: number }>()
+
+    // 获取分页数据
+    sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'
+    params.push(limit, offset)
+
+    const users = await c.env.DB
+      .prepare(sql)
+      .bind(...params)
+      .all<User>()
+
+    const data: PageResponse<User> = {
+      rows: users.results || [],
+      total: countResult?.count || 0,
+    }
+
+    return c.json(success(data))
+  } catch (err) {
+    console.error('List users error:', err)
+    return c.json(error(err instanceof Error ? err.message : '获取用户列表失败'))
+  }
+})
+
+/**
+ * 获取用户详情
+ * GET /api/users/:id
+ */
+user.get('/:id', async (c) => {
+  try {
+    const userId = c.req.param('id')
+
+    const userData = await c.env.DB
+      .prepare('SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE id = ?')
+      .bind(userId)
+      .first<User>()
+
+    if (!userData) {
+      return c.json(error('用户不存在', 404), 404)
+    }
+
+    return c.json(success(userData))
+  } catch (err) {
+    console.error('Get user error:', err)
+    return c.json(error(err instanceof Error ? err.message : '获取用户详情失败'))
+  }
+})
+
+/**
+ * 创建用户
+ * POST /api/users
+ */
+user.post('/', async (c) => {
+  try {
+    const body = await c.req.json<{
+      username: string
+      password: string
+      email?: string
+      role?: 'admin' | 'operator' | 'viewer'
+    }>()
+
+    if (!body.username || !body.password) {
+      return c.json(error('用户名和密码不能为空', 400), 400)
+    }
+
+    // 检查用户名是否已存在
+    const existing = await c.env.DB
+      .prepare('SELECT id FROM users WHERE username = ?')
+      .bind(body.username)
+      .first()
+
+    if (existing) {
+      return c.json(error('用户名已存在', 400), 400)
+    }
+
+    const userId = generateId()
+    const passwordHash = await hashPassword(body.password)
+    const now = Math.floor(Date.now() / 1000)
+
+    await c.env.DB
+      .prepare(`
+        INSERT INTO users (id, username, email, password_hash, role, status, created_at, updated_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+      `)
+      .bind(
+        userId,
+        body.username,
+        body.email || null,
+        passwordHash,
+        body.role || 'viewer',
+        'active',
+        now,
+        now
+      )
+      .run()
+
+    // 记录操作日志
+    const adminUser = c.get('user')
+    await c.env.DB
+      .prepare(`
+        INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?)
+      `)
+      .bind(generateId(), adminUser.sub, 'create', 'user', userId, JSON.stringify({ username: body.username }), now)
+      .run()
+
+    const newUser = await c.env.DB
+      .prepare('SELECT id, username, email, role, status, created_at, updated_at FROM users WHERE id = ?')
+      .bind(userId)
+      .first<User>()
+
+    return c.json(success(newUser, '用户创建成功'))
+  } catch (err) {
+    console.error('Create user error:', err)
+    return c.json(error(err instanceof Error ? err.message : '创建用户失败'))
+  }
+})
+
+/**
+ * 更新用户
+ * PUT /api/users/:id
+ */
+user.put('/:id', async (c) => {
+  try {
+    const userId = c.req.param('id')
+    const body = await c.req.json<{
+      email?: string
+      role?: 'admin' | 'operator' | 'viewer'
+      status?: 'active' | 'disabled'
+      password?: string
+    }>()
+
+    const existing = await c.env.DB
+      .prepare('SELECT id FROM users WHERE id = ?')
+      .bind(userId)
+      .first()
+
+    if (!existing) {
+      return c.json(error('用户不存在', 404), 404)
+    }
+
+    const updates: string[] = []
+    const params: (string | number)[] = []
+
+    if (body.email !== undefined) {
+      updates.push('email = ?')
+      params.push(body.email)
+    }
+
+    if (body.role) {
+      updates.push('role = ?')
+      params.push(body.role)
+    }
+
+    if (body.status) {
+      updates.push('status = ?')
+      params.push(body.status)
+    }
+
+    if (body.password) {
+      const passwordHash = await hashPassword(body.password)
+      updates.push('password_hash = ?')
+      params.push(passwordHash)
+    }
+
+    if (updates.length === 0) {
+      return c.json(error('没有要更新的字段', 400), 400)
+    }
+
+    const now = Math.floor(Date.now() / 1000)
+    updates.push('updated_at = ?')
+    params.push(now)
+    params.push(userId)
+
+    await c.env.DB
+      .prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`)
+      .bind(...params)
+      .run()
+
+    // 记录操作日志
+    const adminUser = c.get('user')
+    await c.env.DB
+      .prepare(`
+        INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?)
+      `)
+      .bind(generateId(), adminUser.sub, 'update', 'user', userId, JSON.stringify(body), now)
+      .run()
+
+    const updatedUser = await c.env.DB
+      .prepare('SELECT id, username, email, role, status, created_at, updated_at FROM users WHERE id = ?')
+      .bind(userId)
+      .first<User>()
+
+    return c.json(success(updatedUser, '用户更新成功'))
+  } catch (err) {
+    console.error('Update user error:', err)
+    return c.json(error(err instanceof Error ? err.message : '更新用户失败'))
+  }
+})
+
+/**
+ * 删除用户
+ * DELETE /api/users/:id
+ */
+user.delete('/:id', async (c) => {
+  try {
+    const userId = c.req.param('id')
+    const adminUser = c.get('user')
+
+    // 不能删除自己
+    if (userId === adminUser.sub) {
+      return c.json(error('不能删除自己', 400), 400)
+    }
+
+    const existing = await c.env.DB
+      .prepare('SELECT id, username FROM users WHERE id = ?')
+      .bind(userId)
+      .first<User>()
+
+    if (!existing) {
+      return c.json(error('用户不存在', 404), 404)
+    }
+
+    await c.env.DB
+      .prepare('DELETE FROM users WHERE id = ?')
+      .bind(userId)
+      .run()
+
+    // 记录操作日志
+    const now = Math.floor(Date.now() / 1000)
+    await c.env.DB
+      .prepare(`
+        INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?)
+      `)
+      .bind(generateId(), adminUser.sub, 'delete', 'user', userId, JSON.stringify({ username: existing.username }), now)
+      .run()
+
+    return c.json(success(null, '用户删除成功'))
+  } catch (err) {
+    console.error('Delete user error:', err)
+    return c.json(error(err instanceof Error ? err.message : '删除用户失败'))
+  }
+})
+
+// ==================== 权限管理 ====================
+
+/**
+ * 获取用户权限列表
+ * GET /api/users/:id/permissions
+ */
+user.get('/:id/permissions', async (c) => {
+  try {
+    const userId = c.req.param('id')
+
+    const permissions = await c.env.DB
+      .prepare(`
+        SELECT up.*, c.name as camera_name
+        FROM user_permissions up
+        LEFT JOIN cameras c ON up.camera_id = c.id
+        WHERE up.user_id = ?
+        ORDER BY up.granted_at DESC
+      `)
+      .bind(userId)
+      .all<UserPermission & { camera_name: string }>()
+
+    return c.json(success(permissions.results || []))
+  } catch (err) {
+    console.error('List permissions error:', err)
+    return c.json(error(err instanceof Error ? err.message : '获取权限列表失败'))
+  }
+})
+
+/**
+ * 添加用户权限
+ * POST /api/users/:id/permissions
+ */
+user.post('/:id/permissions', async (c) => {
+  try {
+    const userId = c.req.param('id')
+    const adminUser = c.get('user')
+    const body = await c.req.json<{
+      camera_id: string
+      permission: 'view' | 'control' | 'manage'
+    }>()
+
+    if (!body.camera_id || !body.permission) {
+      return c.json(error('camera_id 和 permission 不能为空', 400), 400)
+    }
+
+    // 检查用户是否存在
+    const userExists = await c.env.DB
+      .prepare('SELECT id FROM users WHERE id = ?')
+      .bind(userId)
+      .first()
+
+    if (!userExists) {
+      return c.json(error('用户不存在', 404), 404)
+    }
+
+    // 检查摄像头是否存在
+    const cameraExists = await c.env.DB
+      .prepare('SELECT id FROM cameras WHERE id = ?')
+      .bind(body.camera_id)
+      .first()
+
+    if (!cameraExists) {
+      return c.json(error('摄像头不存在', 404), 404)
+    }
+
+    // 检查是否已存在权限
+    const existing = await c.env.DB
+      .prepare('SELECT id FROM user_permissions WHERE user_id = ? AND camera_id = ?')
+      .bind(userId, body.camera_id)
+      .first()
+
+    const now = Math.floor(Date.now() / 1000)
+
+    if (existing) {
+      // 更新现有权限
+      await c.env.DB
+        .prepare('UPDATE user_permissions SET permission = ?, granted_at = ?, granted_by = ? WHERE user_id = ? AND camera_id = ?')
+        .bind(body.permission, now, adminUser.sub, userId, body.camera_id)
+        .run()
+    } else {
+      // 创建新权限
+      await c.env.DB
+        .prepare(`
+          INSERT INTO user_permissions (id, user_id, camera_id, permission, granted_at, granted_by)
+          VALUES (?, ?, ?, ?, ?, ?)
+        `)
+        .bind(generateId(), userId, body.camera_id, body.permission, now, adminUser.sub)
+        .run()
+    }
+
+    return c.json(success(null, '权限设置成功'))
+  } catch (err) {
+    console.error('Add permission error:', err)
+    return c.json(error(err instanceof Error ? err.message : '设置权限失败'))
+  }
+})
+
+/**
+ * 删除用户权限
+ * DELETE /api/users/:id/permissions/:permissionId
+ */
+user.delete('/:id/permissions/:permissionId', async (c) => {
+  try {
+    const userId = c.req.param('id')
+    const permissionId = c.req.param('permissionId')
+
+    const existing = await c.env.DB
+      .prepare('SELECT id FROM user_permissions WHERE id = ? AND user_id = ?')
+      .bind(permissionId, userId)
+      .first()
+
+    if (!existing) {
+      return c.json(error('权限不存在', 404), 404)
+    }
+
+    await c.env.DB
+      .prepare('DELETE FROM user_permissions WHERE id = ?')
+      .bind(permissionId)
+      .run()
+
+    return c.json(success(null, '权限删除成功'))
+  } catch (err) {
+    console.error('Delete permission error:', err)
+    return c.json(error(err instanceof Error ? err.message : '删除权限失败'))
+  }
+})
+
+export default user

+ 291 - 0
src/services/auth.ts

@@ -0,0 +1,291 @@
+import type { Env, User, AuthResponse, LoginRequest, RegisterRequest } from '../types'
+import { hashPassword, verifyPassword } from '../utils/password'
+import { createAccessToken, createRefreshToken, generateId, verifyToken } from '../utils/jwt'
+
+/**
+ * 认证服务
+ */
+export class AuthService {
+  private db: D1Database
+  private jwtSecret: string
+  private accessTokenExpiry: number
+  private refreshTokenExpiry: number
+
+  constructor(env: Env) {
+    this.db = env.DB
+    this.jwtSecret = env.JWT_SECRET
+    this.accessTokenExpiry = parseInt(env.JWT_EXPIRES_IN || '86400')
+    this.refreshTokenExpiry = parseInt(env.REFRESH_EXPIRES_IN || '604800')
+  }
+
+  /**
+   * 用户登录
+   */
+  async login(data: LoginRequest): Promise<{ success: boolean; data?: AuthResponse; error?: string }> {
+    const { username, password } = data
+
+    // 查询用户
+    const user = await this.db
+      .prepare('SELECT * FROM users WHERE username = ? AND status = ?')
+      .bind(username, 'active')
+      .first<User>()
+
+    if (!user) {
+      return { success: false, error: '用户名或密码错误' }
+    }
+
+    // 验证密码
+    if (!user.password_hash) {
+      return { success: false, error: '账户未设置密码' }
+    }
+
+    const isValid = await verifyPassword(password, user.password_hash)
+    if (!isValid) {
+      // 记录登录失败日志
+      await this.logAudit(user.id, 'login', 'user', user.id, { success: false })
+      return { success: false, error: '用户名或密码错误' }
+    }
+
+    // 生成 Token
+    const accessToken = await createAccessToken(
+      user.id,
+      user.username,
+      user.role,
+      this.jwtSecret,
+      this.accessTokenExpiry
+    )
+
+    const refreshToken = await createRefreshToken(
+      user.id,
+      user.username,
+      user.role,
+      this.jwtSecret,
+      this.refreshTokenExpiry
+    )
+
+    // 更新最后登录时间
+    await this.db
+      .prepare('UPDATE users SET last_login = ? WHERE id = ?')
+      .bind(Math.floor(Date.now() / 1000), user.id)
+      .run()
+
+    // 记录登录成功日志
+    await this.logAudit(user.id, 'login', 'user', user.id, { success: true })
+
+    return {
+      success: true,
+      data: {
+        accessToken,
+        refreshToken,
+        expiresIn: this.accessTokenExpiry,
+        user: {
+          id: user.id,
+          username: user.username,
+          role: user.role,
+        },
+      },
+    }
+  }
+
+  /**
+   * 用户注册
+   */
+  async register(data: RegisterRequest): Promise<{ success: boolean; data?: AuthResponse; error?: string }> {
+    const { username, password, email } = data
+
+    // 检查用户名是否已存在
+    const existing = await this.db
+      .prepare('SELECT id FROM users WHERE username = ?')
+      .bind(username)
+      .first()
+
+    if (existing) {
+      return { success: false, error: '用户名已存在' }
+    }
+
+    // 检查邮箱是否已存在
+    if (email) {
+      const emailExists = await this.db
+        .prepare('SELECT id FROM users WHERE email = ?')
+        .bind(email)
+        .first()
+
+      if (emailExists) {
+        return { success: false, error: '邮箱已被使用' }
+      }
+    }
+
+    // 创建用户
+    const userId = generateId()
+    const passwordHash = await hashPassword(password)
+    const now = Math.floor(Date.now() / 1000)
+
+    await this.db
+      .prepare(`
+        INSERT INTO users (id, username, email, password_hash, role, status, created_at, updated_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+      `)
+      .bind(userId, username, email || null, passwordHash, 'viewer', 'active', now, now)
+      .run()
+
+    // 生成 Token
+    const accessToken = await createAccessToken(
+      userId,
+      username,
+      'viewer',
+      this.jwtSecret,
+      this.accessTokenExpiry
+    )
+
+    const refreshToken = await createRefreshToken(
+      userId,
+      username,
+      'viewer',
+      this.jwtSecret,
+      this.refreshTokenExpiry
+    )
+
+    // 记录注册日志
+    await this.logAudit(userId, 'create', 'user', userId, { username })
+
+    return {
+      success: true,
+      data: {
+        accessToken,
+        refreshToken,
+        expiresIn: this.accessTokenExpiry,
+        user: {
+          id: userId,
+          username,
+          role: 'viewer',
+        },
+      },
+    }
+  }
+
+  /**
+   * 刷新 Token
+   */
+  async refreshToken(token: string): Promise<{ success: boolean; data?: AuthResponse; error?: string }> {
+    const result = await verifyToken(token, this.jwtSecret)
+
+    if (!result.valid || !result.payload) {
+      return { success: false, error: result.error || 'Invalid refresh token' }
+    }
+
+    // 查询用户确保仍然有效
+    const user = await this.db
+      .prepare('SELECT * FROM users WHERE id = ? AND status = ?')
+      .bind(result.payload.sub, 'active')
+      .first<User>()
+
+    if (!user) {
+      return { success: false, error: '用户不存在或已禁用' }
+    }
+
+    // 生成新 Token
+    const accessToken = await createAccessToken(
+      user.id,
+      user.username,
+      user.role,
+      this.jwtSecret,
+      this.accessTokenExpiry
+    )
+
+    const refreshToken = await createRefreshToken(
+      user.id,
+      user.username,
+      user.role,
+      this.jwtSecret,
+      this.refreshTokenExpiry
+    )
+
+    return {
+      success: true,
+      data: {
+        accessToken,
+        refreshToken,
+        expiresIn: this.accessTokenExpiry,
+        user: {
+          id: user.id,
+          username: user.username,
+          role: user.role,
+        },
+      },
+    }
+  }
+
+  /**
+   * 获取当前用户信息
+   */
+  async getCurrentUser(userId: string): Promise<User | null> {
+    return this.db
+      .prepare('SELECT id, username, email, role, status, last_login, created_at, updated_at FROM users WHERE id = ?')
+      .bind(userId)
+      .first<User>()
+  }
+
+  /**
+   * 修改密码
+   */
+  async changePassword(
+    userId: string,
+    oldPassword: string,
+    newPassword: string
+  ): Promise<{ success: boolean; error?: string }> {
+    const user = await this.db
+      .prepare('SELECT * FROM users WHERE id = ?')
+      .bind(userId)
+      .first<User>()
+
+    if (!user || !user.password_hash) {
+      return { success: false, error: '用户不存在' }
+    }
+
+    const isValid = await verifyPassword(oldPassword, user.password_hash)
+    if (!isValid) {
+      return { success: false, error: '原密码错误' }
+    }
+
+    const newHash = await hashPassword(newPassword)
+    await this.db
+      .prepare('UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?')
+      .bind(newHash, Math.floor(Date.now() / 1000), userId)
+      .run()
+
+    await this.logAudit(userId, 'update', 'user', userId, { action: 'change_password' })
+
+    return { success: true }
+  }
+
+  /**
+   * 记录审计日志
+   */
+  private async logAudit(
+    userId: string,
+    action: string,
+    resource: string,
+    resourceId: string,
+    details?: Record<string, unknown>
+  ): Promise<void> {
+    try {
+      await this.db
+        .prepare(`
+          INSERT INTO audit_logs (id, user_id, action, resource, resource_id, details, created_at)
+          VALUES (?, ?, ?, ?, ?, ?, ?)
+        `)
+        .bind(
+          generateId(),
+          userId,
+          action,
+          resource,
+          resourceId,
+          details ? JSON.stringify(details) : null,
+          Math.floor(Date.now() / 1000)
+        )
+        .run()
+    } catch (err) {
+      console.error('Failed to log audit:', err)
+    }
+  }
+}

+ 141 - 0
src/types/index.ts

@@ -3,6 +3,44 @@ export interface Env {
   CF_ACCOUNT_ID: string
   CF_API_TOKEN: string
   CUSTOMER_SUBDOMAIN: string
+  DB: D1Database  // D1 数据库绑定
+  JWT_SECRET: string  // JWT 签名密钥
+  JWT_EXPIRES_IN?: string  // Token 有效期 (秒), 默认 86400
+  REFRESH_EXPIRES_IN?: string  // Refresh Token 有效期 (秒), 默认 604800
+}
+
+// JWT Payload
+export interface JwtPayload {
+  sub: string  // user id
+  username: string
+  role: 'admin' | 'operator' | 'viewer'
+  iat: number
+  exp: number
+}
+
+// 登录请求
+export interface LoginRequest {
+  username: string
+  password: string
+}
+
+// 注册请求
+export interface RegisterRequest {
+  username: string
+  password: string
+  email?: string
+}
+
+// 认证响应
+export interface AuthResponse {
+  accessToken: string
+  refreshToken: string
+  expiresIn: number
+  user: {
+    id: string
+    username: string
+    role: string
+  }
 }
 
 // API 响应格式(RuoYi 兼容)
@@ -132,3 +170,106 @@ export interface CloudflareApiResponse<T> {
     total_count: number
   }
 }
+
+// ==================== D1 数据库实体 ====================
+
+// 摄像头
+export interface Camera {
+  id: string
+  name: string
+  type: 'mac' | 'ip' | 'rtsp' | 'screen'
+  protocol: 'rtmps' | 'srt' | 'whip'
+  rtsp_url?: string
+  location?: string
+  status: 'online' | 'offline' | 'error'
+  live_input_id?: string
+  meta?: string  // JSON
+  created_at: number
+  updated_at: number
+}
+
+// 直播会话
+export interface LiveSession {
+  id: string
+  camera_id: string
+  live_input_id: string
+  started_at: number
+  ended_at?: number
+  duration?: number
+  status: 'live' | 'ended' | 'error'
+  viewer_count: number
+  peak_viewers: number
+  recording_id?: string
+  meta?: string  // JSON
+  created_at: number
+}
+
+// 视频元数据
+export interface Video {
+  id: string
+  cf_uid: string
+  camera_id?: string
+  session_id?: string
+  title?: string
+  description?: string
+  tags?: string  // JSON array
+  thumbnail?: string
+  duration?: number
+  size?: number
+  status: 'ready' | 'processing' | 'error'
+  is_public: number
+  view_count: number
+  created_at: number
+  updated_at: number
+}
+
+// 用户
+export interface User {
+  id: string
+  username: string
+  email?: string
+  password_hash?: string
+  role: 'admin' | 'operator' | 'viewer'
+  status: 'active' | 'disabled'
+  last_login?: number
+  created_at: number
+  updated_at: number
+}
+
+// 用户权限
+export interface UserPermission {
+  id: string
+  user_id: string
+  camera_id: string
+  permission: 'view' | 'control' | 'manage'
+  granted_at: number
+  granted_by?: string
+}
+
+// 观看统计
+export interface ViewStat {
+  id: string
+  video_id?: string
+  session_id?: string
+  user_id?: string
+  ip_address?: string
+  user_agent?: string
+  watch_duration?: number
+  started_at: number
+  ended_at?: number
+  country?: string
+  city?: string
+  created_at: number
+}
+
+// 操作日志
+export interface AuditLog {
+  id: string
+  user_id?: string
+  action: 'create' | 'update' | 'delete' | 'login' | 'logout'
+  resource: string
+  resource_id?: string
+  details?: string  // JSON
+  ip_address?: string
+  created_at: number
+}

+ 153 - 0
src/utils/jwt.ts

@@ -0,0 +1,153 @@
+import type { JwtPayload } from '../types'
+
+/**
+ * Base64 URL 编码
+ */
+function base64UrlEncode(data: ArrayBuffer | string): string {
+  const str = typeof data === 'string'
+    ? data
+    : String.fromCharCode(...new Uint8Array(data))
+  return btoa(str)
+    .replace(/\+/g, '-')
+    .replace(/\//g, '_')
+    .replace(/=+$/, '')
+}
+
+/**
+ * Base64 URL 解码
+ */
+function base64UrlDecode(str: string): string {
+  str = str.replace(/-/g, '+').replace(/_/g, '/')
+  const pad = str.length % 4
+  if (pad) {
+    str += '='.repeat(4 - pad)
+  }
+  return atob(str)
+}
+
+/**
+ * 生成 HMAC-SHA256 签名
+ */
+async function sign(data: string, secret: string): Promise<string> {
+  const encoder = new TextEncoder()
+  const key = await crypto.subtle.importKey(
+    'raw',
+    encoder.encode(secret),
+    { name: 'HMAC', hash: 'SHA-256' },
+    false,
+    ['sign']
+  )
+  const signature = await crypto.subtle.sign(
+    'HMAC',
+    key,
+    encoder.encode(data)
+  )
+  return base64UrlEncode(signature)
+}
+
+/**
+ * 验证 HMAC-SHA256 签名
+ */
+async function verify(data: string, signature: string, secret: string): Promise<boolean> {
+  const expectedSignature = await sign(data, secret)
+  return signature === expectedSignature
+}
+
+/**
+ * 生成 JWT Token
+ */
+export async function createToken(
+  payload: Omit<JwtPayload, 'iat' | 'exp'>,
+  secret: string,
+  expiresIn: number = 86400  // 默认 24 小时
+): Promise<string> {
+  const header = { alg: 'HS256', typ: 'JWT' }
+  const now = Math.floor(Date.now() / 1000)
+
+  const fullPayload: JwtPayload = {
+    ...payload,
+    iat: now,
+    exp: now + expiresIn,
+  }
+
+  const headerEncoded = base64UrlEncode(JSON.stringify(header))
+  const payloadEncoded = base64UrlEncode(JSON.stringify(fullPayload))
+  const dataToSign = `${headerEncoded}.${payloadEncoded}`
+  const signature = await sign(dataToSign, secret)
+
+  return `${dataToSign}.${signature}`
+}
+
+/**
+ * 验证并解析 JWT Token
+ */
+export async function verifyToken(
+  token: string,
+  secret: string
+): Promise<{ valid: boolean; payload?: JwtPayload; error?: string }> {
+  try {
+    const parts = token.split('.')
+    if (parts.length !== 3) {
+      return { valid: false, error: 'Invalid token format' }
+    }
+
+    const [headerEncoded, payloadEncoded, signature] = parts
+    const dataToVerify = `${headerEncoded}.${payloadEncoded}`
+
+    // 验证签名
+    const isValid = await verify(dataToVerify, signature, secret)
+    if (!isValid) {
+      return { valid: false, error: 'Invalid signature' }
+    }
+
+    // 解析 payload
+    const payload: JwtPayload = JSON.parse(base64UrlDecode(payloadEncoded))
+
+    // 检查过期
+    const now = Math.floor(Date.now() / 1000)
+    if (payload.exp && payload.exp < now) {
+      return { valid: false, error: 'Token expired' }
+    }
+
+    return { valid: true, payload }
+  } catch (err) {
+    return { valid: false, error: 'Token parse error' }
+  }
+}
+
+/**
+ * 生成 Access Token
+ */
+export async function createAccessToken(
+  userId: string,
+  username: string,
+  role: 'admin' | 'operator' | 'viewer',
+  secret: string,
+  expiresIn: number = 86400
+): Promise<string> {
+  return createToken({ sub: userId, username, role }, secret, expiresIn)
+}
+
+/**
+ * 生成 Refresh Token
+ */
+export async function createRefreshToken(
+  userId: string,
+  username: string,
+  role: 'admin' | 'operator' | 'viewer',
+  secret: string,
+  expiresIn: number = 604800  // 7 天
+): Promise<string> {
+  return createToken({ sub: userId, username, role }, secret, expiresIn)
+}
+
+/**
+ * 生成随机 ID
+ */
+export function generateId(): string {
+  const bytes = new Uint8Array(16)
+  crypto.getRandomValues(bytes)
+  return Array.from(bytes)
+    .map(b => b.toString(16).padStart(2, '0'))
+    .join('')
+}

+ 134 - 0
src/utils/password.ts

@@ -0,0 +1,134 @@
+/**
+ * 密码工具 - 使用 PBKDF2 算法
+ *
+ * Cloudflare Workers 环境兼容
+ * 替代 bcrypt 的安全方案
+ */
+
+const ITERATIONS = 100000  // 迭代次数
+const KEY_LENGTH = 256     // 密钥长度 (bits)
+const SALT_LENGTH = 16     // 盐长度 (bytes)
+
+/**
+ * 生成随机盐
+ */
+function generateSalt(): Uint8Array {
+  const salt = new Uint8Array(SALT_LENGTH)
+  crypto.getRandomValues(salt)
+  return salt
+}
+
+/**
+ * 将 Uint8Array 转换为十六进制字符串
+ */
+function toHex(buffer: Uint8Array): string {
+  return Array.from(buffer)
+    .map(b => b.toString(16).padStart(2, '0'))
+    .join('')
+}
+
+/**
+ * 将十六进制字符串转换为 Uint8Array
+ */
+function fromHex(hex: string): Uint8Array {
+  const bytes = new Uint8Array(hex.length / 2)
+  for (let i = 0; i < hex.length; i += 2) {
+    bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
+  }
+  return bytes
+}
+
+/**
+ * 使用 PBKDF2 派生密钥
+ */
+async function deriveKey(password: string, salt: Uint8Array): Promise<ArrayBuffer> {
+  const encoder = new TextEncoder()
+  const passwordBuffer = encoder.encode(password)
+
+  // 导入密码作为密钥材料
+  const keyMaterial = await crypto.subtle.importKey(
+    'raw',
+    passwordBuffer,
+    'PBKDF2',
+    false,
+    ['deriveBits']
+  )
+
+  // 派生密钥
+  const derivedBits = await crypto.subtle.deriveBits(
+    {
+      name: 'PBKDF2',
+      salt: salt,
+      iterations: ITERATIONS,
+      hash: 'SHA-256',
+    },
+    keyMaterial,
+    KEY_LENGTH
+  )
+
+  return derivedBits
+}
+
+/**
+ * 哈希密码
+ *
+ * 返回格式: iterations$salt$hash
+ */
+export async function hashPassword(password: string): Promise<string> {
+  const salt = generateSalt()
+  const derivedKey = await deriveKey(password, salt)
+  const hash = new Uint8Array(derivedKey)
+
+  return `${ITERATIONS}$${toHex(salt)}$${toHex(hash)}`
+}
+
+/**
+ * 验证密码
+ */
+export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
+  try {
+    const [iterationsStr, saltHex, hashHex] = storedHash.split('$')
+    const iterations = parseInt(iterationsStr, 10)
+    const salt = fromHex(saltHex)
+    const storedHashBytes = fromHex(hashHex)
+
+    // 使用相同参数派生密钥
+    const encoder = new TextEncoder()
+    const passwordBuffer = encoder.encode(password)
+
+    const keyMaterial = await crypto.subtle.importKey(
+      'raw',
+      passwordBuffer,
+      'PBKDF2',
+      false,
+      ['deriveBits']
+    )
+
+    const derivedBits = await crypto.subtle.deriveBits(
+      {
+        name: 'PBKDF2',
+        salt: salt,
+        iterations: iterations,
+        hash: 'SHA-256',
+      },
+      keyMaterial,
+      KEY_LENGTH
+    )
+
+    const derivedHash = new Uint8Array(derivedBits)
+
+    // 时间安全的比较
+    if (derivedHash.length !== storedHashBytes.length) {
+      return false
+    }
+
+    let result = 0
+    for (let i = 0; i < derivedHash.length; i++) {
+      result |= derivedHash[i] ^ storedHashBytes[i]
+    }
+
+    return result === 0
+  } catch (err) {
+    return false
+  }
+}

+ 8 - 0
wrangler.toml

@@ -16,3 +16,11 @@ compatibility_date = "2024-12-01"
 [dev]
 port = 8787
 local_protocol = "http"
+
+# D1 数据库配置
+# 创建: wrangler d1 create tg_live_game
+# 初始化: wrangler d1 execute tg_live_game --file=./schema.sql
+[[d1_databases]]
+binding = "DB"
+database_name = "tg_live_game"
+database_id = "<your-database-id>"  # 运行 wrangler d1 create 后填入