From 38a55c41ab0475a356809e7a8c35af6166c21c40 Mon Sep 17 00:00:00 2001 From: yangshangbo <42314632+yangshangbo@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:09:52 +0800 Subject: [PATCH] feat: add user role menu management api --- README_AUTH_SYSTEM.md | 361 ++++++++++++++++++++++++++++++++++ auth_example.v | 145 ++++++++++++++ hono/auth.v | 404 ++++++++++++++++++++++++++++++++++++++ hono/auth_routes.v | 223 +++++++++++++++++++++ public/auth.html | 436 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1569 insertions(+) create mode 100644 README_AUTH_SYSTEM.md create mode 100644 auth_example.v create mode 100644 hono/auth.v create mode 100644 hono/auth_routes.v create mode 100644 public/auth.html diff --git a/README_AUTH_SYSTEM.md b/README_AUTH_SYSTEM.md new file mode 100644 index 0000000..96ed2cc --- /dev/null +++ b/README_AUTH_SYSTEM.md @@ -0,0 +1,361 @@ +# 用户角色菜单管理系统 + +基于 v-hono 框架的用户认证和权限管理系统,提供完整的用户角色管理和动态菜单功能。 + +## 功能特性 + +### 🔐 用户认证 +- 用户注册和登录 +- JWT 令牌认证 +- 密码加密存储 +- 会话管理 + +### 👥 角色管理 +- 多级角色系统(admin、manager、user、guest) +- 基于角色的权限控制 +- 灵活的权限分配 + +### 📋 菜单管理 +- 动态菜单系统 +- 树形菜单结构 +- 基于权限的菜单显示 +- 菜单排序和图标支持 + +### 🛡️ 安全特性 +- 密码 SHA256 加密 +- 令牌过期机制 +- 中间件权限验证 +- 防止路径遍历攻击 + +## 系统架构 + +``` +v-hono/ +├── hono/ +│ ├── auth.v # 认证核心模块 +│ ├── auth_routes.v # 认证路由 +│ ├── database.v # 数据库管理 +│ ├── request.v # 请求处理 +│ └── app.v # 应用框架 +├── auth_example.v # 完整示例 +└── README_AUTH_SYSTEM.md # 本文档 +``` + +## 快速开始 + +### 1. 编译和运行 + +```bash +# 编译示例应用 +v auth_example.v + +# 运行应用 +./auth_example +``` + +### 2. 访问系统 + +打开浏览器访问:`http://localhost:3000` + +### 3. 默认用户 + +系统预创建了以下示例用户: + +| 用户名 | 密码 | 角色 | 权限 | +|--------|------|------|------| +| admin | admin123 | admin | 所有权限 | +| manager | manager123 | manager | 读写管理权限 | +| user | user123 | user | 读写权限 | +| guest | guest123 | guest | 只读权限 | + +## API 接口 + +### 认证接口 + +#### 用户注册 +```http +POST /api/auth/register +Content-Type: application/json + +{ + "username": "newuser", + "email": "user@example.com", + "password": "password123", + "role": "user" +} +``` + +#### 用户登录 +```http +POST /api/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "admin123" +} +``` + +响应: +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_at": 1640995200 +} +``` + +#### 获取用户信息 +```http +GET /api/auth/profile +Authorization: +``` + +#### 用户注销 +```http +POST /api/auth/logout +Authorization: +``` + +### 菜单接口 + +#### 获取用户菜单 +```http +GET /api/auth/menus +Authorization: +``` + +#### 获取所有菜单(管理员) +```http +GET /api/auth/menus/all +Authorization: +``` + +#### 创建菜单项(管理员) +```http +POST /api/auth/menus +Authorization: +Content-Type: application/json + +{ + "name": "新菜单", + "path": "/new-menu", + "icon": "📄", + "parent_id": 0, + "sort_order": 1, + "permissions": ["read", "write"] +} +``` + +### 受保护的路由 + +#### 用户路由 +```http +GET /api/protected/user +Authorization: +``` + +#### 管理员路由 +```http +GET /api/protected/admin +Authorization: +``` + +## 角色权限说明 + +### Admin(超级管理员) +- 拥有所有权限 +- 可以管理所有用户和菜单 +- 可以访问所有系统功能 + +### Manager(管理员) +- 拥有读写和管理权限 +- 可以管理用户和菜单 +- 可以访问大部分系统功能 + +### User(普通用户) +- 拥有读写权限 +- 可以查看和编辑内容 +- 无法管理系统设置 + +### Guest(访客) +- 只有只读权限 +- 只能查看公开内容 +- 无法进行任何修改操作 + +## 菜单权限配置 + +菜单项通过 `permissions` 字段控制访问权限: + +```json +{ + "name": "用户管理", + "path": "/users", + "icon": "👥", + "permissions": ["read", "write", "manage"] +} +``` + +权限说明: +- `read`: 只读权限 +- `write`: 读写权限 +- `manage`: 管理权限 +- `*`: 所有权限(仅管理员) + +## 中间件使用 + +### 认证中间件 +```v +// 添加认证中间件 +app.use(hono.auth_middleware(auth_manager)) +``` + +### 权限中间件 +```v +// 添加权限检查中间件 +app.use(hono.permission_middleware('manage')) +``` + +## 数据库结构 + +### users 表 +```sql +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL, + status BOOLEAN DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); +``` + +### menu_items 表 +```sql +CREATE TABLE menu_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + path TEXT NOT NULL, + icon TEXT, + parent_id INTEGER DEFAULT 0, + sort_order INTEGER DEFAULT 0, + permissions TEXT, + status BOOLEAN DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); +``` + +### user_sessions 表 +```sql +CREATE TABLE user_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + token TEXT UNIQUE NOT NULL, + expires_at INTEGER NOT NULL, + created_at INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) +); +``` + +## 开发指南 + +### 添加新的认证功能 + +1. 在 `auth.v` 中添加新的方法 +2. 在 `auth_routes.v` 中添加对应的路由 +3. 更新前端界面 + +### 自定义权限 + +1. 修改 `UserRole` 枚举 +2. 更新 `check_permission` 方法 +3. 调整菜单权限配置 + +### 扩展菜单功能 + +1. 添加菜单编辑和删除接口 +2. 实现菜单拖拽排序 +3. 添加菜单缓存机制 + +## 安全建议 + +1. **密码安全** + - 使用强密码策略 + - 定期更换密码 + - 启用密码复杂度检查 + +2. **令牌安全** + - 设置合理的过期时间 + - 实现令牌刷新机制 + - 支持令牌撤销 + +3. **权限控制** + - 最小权限原则 + - 定期审查权限 + - 记录权限变更日志 + +4. **数据安全** + - 数据库加密 + - 定期备份 + - 访问日志记录 + +## 故障排除 + +### 常见问题 + +1. **数据库连接失败** + - 检查数据库文件权限 + - 确保 SQLite 支持 + - 验证数据库路径 + +2. **认证失败** + - 检查用户名密码 + - 验证令牌格式 + - 确认令牌未过期 + +3. **权限不足** + - 检查用户角色 + - 验证菜单权限配置 + - 确认中间件配置 + +### 调试模式 + +启用调试输出: +```v +// 在代码中添加调试信息 +println('Debug: $debug_info') +``` + +## 性能优化 + +1. **数据库优化** + - 添加适当的索引 + - 使用连接池 + - 优化查询语句 + +2. **缓存策略** + - 菜单缓存 + - 用户信息缓存 + - 权限缓存 + +3. **并发处理** + - 使用连接池 + - 实现请求队列 + - 优化锁机制 + +## 贡献指南 + +1. Fork 项目 +2. 创建功能分支 +3. 提交更改 +4. 创建 Pull Request + +## 许可证 + +MIT License + +## 联系方式 + +如有问题或建议,请提交 Issue 或联系开发团队。 \ No newline at end of file diff --git a/auth_example.v b/auth_example.v new file mode 100644 index 0000000..1483bfc --- /dev/null +++ b/auth_example.v @@ -0,0 +1,145 @@ +module main + +import hono +import net.http +import json + +fn main() { + // 创建数据库管理器 + db_manager := hono.new_database_manager('auth_system.db') or { + eprintln('Failed to create database manager: $err') + return + } + + // 创建认证管理器 + mut auth_manager := hono.new_auth_manager(db_manager) + + // 初始化认证相关表 + auth_manager.init_tables() or { + eprintln('Failed to initialize auth tables: $err') + return + } + + // 创建 Hono 应用 + mut app := hono.Hono{} + + // 注册认证路由 + hono.register_auth_routes(mut app, mut auth_manager) + + // 添加一些示例菜单项 + create_sample_menus(mut auth_manager) + + // 添加一些示例用户 + create_sample_users(mut auth_manager) + + // 添加一些受保护的路由示例 + app.get('/api/protected/user', fn [auth_manager] (mut c hono.Context) http.Response { + token := c.req.header.get_custom('Authorization') or { '' } + user := auth_manager.verify_token(token) or { + c.status(401) + return c.json(json.encode({ + 'error': 'User not found' + })) + } + return c.json(json.encode({ + 'message': 'This is a protected user route', + 'user_id': user.id.str(), + 'username': user.username, + 'email': user.email, + 'role': user.role.str() + })) + }) + + app.get('/api/protected/admin', fn [auth_manager] (mut c hono.Context) http.Response { + token := c.req.header.get_custom('Authorization') or { '' } + user := auth_manager.verify_token(token) or { + c.status(401) + return c.json(json.encode({ + 'error': 'User not found' + })) + } + if !auth_manager.check_permission(user, 'manage') { + c.status(403) + return c.json(json.encode({ + 'error': 'Admin access required' + })) + } + return c.json(json.encode({ + 'message': 'This is a protected admin route', + 'user_id': user.id.str(), + 'username': user.username, + 'email': user.email, + 'role': user.role.str() + })) + }) + + // 添加静态文件服务 + app.get('/', fn (mut c hono.Context) http.Response { + return c.file('public/auth.html') + }) + + println('🚀 用户角色菜单管理系统已启动') + println('📱 访问地址: http://localhost:3000') + println('') + println('📋 示例用户:') + println(' 用户名: admin, 密码: admin123, 角色: admin') + println(' 用户名: manager, 密码: manager123, 角色: manager') + println(' 用户名: user, 密码: user123, 角色: user') + println(' 用户名: guest, 密码: guest123, 角色: guest') + println('') + + // 启动服务器 + app.listen(':3000') +} + +// 创建示例菜单 +fn create_sample_menus(mut auth_manager hono.AuthManager) { + // 创建根菜单 + auth_manager.create_menu_item('仪表板', '/dashboard', '📊', 0, 1, ['read']) or { println('仪表板插入失败') } + auth_manager.create_menu_item('用户管理', '/users', '👥', 0, 2, ['read', 'write', 'manage']) or { println('用户管理插入失败') } + auth_manager.create_menu_item('系统设置', '/settings', '⚙️', 0, 3, ['manage']) or { println('系统设置插入失败') } + auth_manager.create_menu_item('文件管理', '/files', '📁', 0, 4, ['read', 'write']) or { println('文件管理插入失败') } + + // 获取用户管理菜单ID(动态获取) + user_menu_id := auth_manager.get_menu_id_by_path('/users') + + // 创建子菜单 + auth_manager.create_menu_item('用户列表', '/users/list', '📋', user_menu_id, 1, ['read']) or { println('用户列表插入失败') } + auth_manager.create_menu_item('添加用户', '/users/add', '➕', user_menu_id, 2, ['write']) or { println('添加用户插入失败') } + auth_manager.create_menu_item('编辑用户', '/users/edit', '✏️', user_menu_id, 3, ['write']) or { println('编辑用户插入失败') } + auth_manager.create_menu_item('删除用户', '/users/delete', '🗑️', user_menu_id, 4, ['manage']) or { println('删除用户插入失败') } + + // 更多测试菜单 + auth_manager.create_menu_item('报表中心', '/reports', '📈', 0, 5, ['read', 'write']) or { println('报表中心插入失败') } + reports_menu_id := auth_manager.get_menu_id_by_path('/reports') + auth_manager.create_menu_item('销售报表', '/reports/sales', '💹', reports_menu_id, 1, ['read']) or { println('销售报表插入失败') } + auth_manager.create_menu_item('财务报表', '/reports/finance', '💰', reports_menu_id, 2, ['read']) or { println('财务报表插入失败') } + auth_manager.create_menu_item('消息中心', '/messages', '📨', 0, 6, ['read']) or { println('消息中心插入失败') } + auth_manager.create_menu_item('个人中心', '/profile', '👤', 0, 7, ['read', 'write']) or { println('个人中心插入失败') } + auth_manager.create_menu_item('测试菜单A', '/test/a', '🅰️', 0, 8, ['read']) or { println('测试菜单A插入失败') } + auth_manager.create_menu_item('测试菜单B', '/test/b', '🅱️', 0, 9, ['read']) or { println('测试菜单B插入失败') } + auth_manager.create_menu_item('测试菜单C', '/test/c', '🆑', 0, 10, ['read']) or { println('测试菜单C插入失败') } + test_a_id := auth_manager.get_menu_id_by_path('/test/a') + auth_manager.create_menu_item('A-子菜单1', '/test/a/1', '1️⃣', test_a_id, 1, ['read']) or { println('A-子菜单1插入失败') } + auth_manager.create_menu_item('A-子菜单2', '/test/a/2', '2️⃣', test_a_id, 2, ['read']) or { println('A-子菜单2插入失败') } + + println('✅ 示例菜单已创建') + println('✅ 更多测试菜单已创建') +} + +// 创建示例用户 +fn create_sample_users(mut auth_manager hono.AuthManager) { + // 创建管理员用户 + auth_manager.create_user('admin', 'admin@example.com', 'admin123', hono.UserRole.admin) or { return } + + // 创建经理用户 + auth_manager.create_user('manager', 'manager@example.com', 'manager123', hono.UserRole.manager) or { return } + + // 创建普通用户 + auth_manager.create_user('user', 'user@example.com', 'user123', hono.UserRole.user) or { return } + + // 创建访客用户 + auth_manager.create_user('guest', 'guest@example.com', 'guest123', hono.UserRole.guest) or { return } + + println('✅ 示例用户已创建') +} \ No newline at end of file diff --git a/hono/auth.v b/hono/auth.v new file mode 100644 index 0000000..86f4f50 --- /dev/null +++ b/hono/auth.v @@ -0,0 +1,404 @@ +module hono + +import crypto.sha256 +import crypto.rand +import time +import json + +// 用户角色枚举 +pub enum UserRole { + admin + manager + user + guest +} + +// 用户结构 +pub struct User { +pub: + id int + username string + email string + password_hash string + role UserRole + status bool // true: active, false: inactive + created_at int + updated_at int +} + +// 菜单项结构 +pub struct MenuItem { +pub: + id int + name string + path string + icon string + parent_id int + sort_order int + permissions []string + status bool + created_at int + updated_at int +mut: + children []MenuItem +} + +// 用户会话结构 +pub struct UserSession { +pub: + user_id int + token string + expires_at int + created_at int +} + +// 认证管理器 +pub struct AuthManager { +mut: + db DatabaseManager +} + +// 创建认证管理器 +pub fn new_auth_manager(db DatabaseManager) AuthManager { + return AuthManager{ + db: db + } +} + +// 初始化认证相关表 +pub fn (mut auth AuthManager) init_tables() ! { + // 创建用户表 + auth.db.db.exec('CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL, + status BOOLEAN DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + );') or { + return error('Failed to create users table: $err') + } + + // 创建菜单表 + auth.db.db.exec('CREATE TABLE IF NOT EXISTS menu_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + path TEXT NOT NULL, + icon TEXT, + parent_id INTEGER DEFAULT 0, + sort_order INTEGER DEFAULT 0, + permissions TEXT, + status BOOLEAN DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + );') or { + return error('Failed to create menu_items table: $err') + } + + // 创建用户会话表 + auth.db.db.exec('CREATE TABLE IF NOT EXISTS user_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + token TEXT UNIQUE NOT NULL, + expires_at INTEGER NOT NULL, + created_at INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) + );') or { + return error('Failed to create user_sessions table: $err') + } + + // 创建索引 + auth.db.db.exec('CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);') or { + return error('Failed to create index: $err') + } + auth.db.db.exec('CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);') or { + return error('Failed to create index: $err') + } + auth.db.db.exec('CREATE INDEX IF NOT EXISTS idx_menu_parent ON menu_items(parent_id);') or { + return error('Failed to create index: $err') + } + auth.db.db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_token ON user_sessions(token);') or { + return error('Failed to create index: $err') + } +} + +// 密码哈希 +fn hash_password(password string) string { + return sha256.sum(password.bytes()).hex() +} + +// 生成随机令牌 +fn generate_token() string { + random_bytes := rand.bytes(32) or { return '' } + mut token := '' + for byte in random_bytes { + token += '${byte:02x}' + } + return token +} + +// 创建用户 +pub fn (mut auth AuthManager) create_user(username string, email string, password string, role UserRole) !User { + now := int(time.now().unix()) + password_hash := hash_password(password) + role_str := role.str() + + auth.db.db.exec('INSERT INTO users (username, email, password_hash, role, status, created_at, updated_at) VALUES ("$username", "$email", "$password_hash", "$role_str", 1, $now, $now)') or { + return error('Failed to create user: $err') + } + + // 获取插入的用户ID + rows := auth.db.db.exec('SELECT last_insert_rowid()') or { + return error('Failed to get user ID: $err') + } + user_id := rows[0].vals[0].int() + + return User{ + id: user_id + username: username + email: email + password_hash: password_hash + role: role + status: true + created_at: now + updated_at: now + } +} + +// 用户登录 +pub fn (mut auth AuthManager) login(username string, password string) !UserSession { + password_hash := hash_password(password) + + rows := auth.db.db.exec('SELECT id, username, email, password_hash, role, status FROM users WHERE username = "$username" AND password_hash = "$password_hash" AND status = 1') or { + return error('Failed to query user: $err') + } + + if rows.len == 0 { + return error('Invalid username or password') + } + + user := rows[0] + user_id := user.vals[0].int() + + // 生成会话令牌 + token := generate_token() + now := int(time.now().unix()) + expires_at := now + (24 * 60 * 60) // 24小时后过期 + + // 保存会话 + auth.db.db.exec('INSERT INTO user_sessions (user_id, token, expires_at, created_at) VALUES ($user_id, "$token", $expires_at, $now)') or { + return error('Failed to create session: $err') + } + + return UserSession{ + user_id: user_id + token: token + expires_at: expires_at + created_at: now + } +} + +// 验证令牌 +pub fn (auth AuthManager) verify_token(token string) !User { + rows := auth.db.db.exec('SELECT u.id, u.username, u.email, u.password_hash, u.role, u.status FROM users u JOIN user_sessions s ON u.id = s.user_id WHERE s.token = "$token" AND s.expires_at > ${int(time.now().unix())} AND u.status = 1') or { + return error('Failed to verify token: $err') + } + + if rows.len == 0 { + return error('Invalid or expired token') + } + + user := rows[0] + role_str := user.vals[4] + role := match role_str { + 'admin' { UserRole.admin } + 'manager' { UserRole.manager } + 'user' { UserRole.user } + 'guest' { UserRole.guest } + else { UserRole.guest } + } + + return User{ + id: user.vals[0].int() + username: user.vals[1] + email: user.vals[2] + password_hash: user.vals[3] + role: role + status: user.vals[5].int() == 1 + created_at: 0 + updated_at: 0 + } +} + +// 注销会话 +pub fn (mut auth AuthManager) logout(token string) ! { + auth.db.db.exec('DELETE FROM user_sessions WHERE token = "$token"') or { + return error('Failed to logout: $err') + } +} + +// 创建菜单项 +pub fn (mut auth AuthManager) create_menu_item(name string, path string, icon string, parent_id int, sort_order int, permissions []string) !MenuItem { + now := int(time.now().unix()) + permissions_json := json.encode(permissions) + permissions_sql := permissions_json.replace('"', "''") + + auth.db.db.exec('INSERT INTO menu_items (name, path, icon, parent_id, sort_order, permissions, status, created_at, updated_at) VALUES ("$name", "$path", "$icon", $parent_id, $sort_order, "$permissions_sql", 1, $now, $now)') + or { + eprintln('菜单插入SQL错误: $err') + return error('Failed to create menu item: $err') + } + + // 获取插入的菜单ID + rows := auth.db.db.exec('SELECT last_insert_rowid()') or { + return error('Failed to get menu ID: $err') + } + menu_id := rows[0].vals[0].int() + + return MenuItem{ + id: menu_id + name: name + path: path + icon: icon + parent_id: parent_id + sort_order: sort_order + permissions: permissions + status: true + created_at: now + updated_at: now + } +} + +// 获取所有菜单项 +pub fn (auth AuthManager) get_all_menu_items() ![]MenuItem { + rows := auth.db.db.exec('SELECT id, name, path, icon, parent_id, sort_order, permissions, status FROM menu_items ORDER BY parent_id, sort_order') or { + return error('Failed to query all menu items: $err') + } + + mut menus := []MenuItem{} + for row in rows { + permissions_str := row.vals[6] + permissions := json.decode([]string, permissions_str) or { []string{} } + + menus << MenuItem{ + id: row.vals[0].int() + name: row.vals[1] + path: row.vals[2] + icon: row.vals[3] + parent_id: row.vals[4].int() + sort_order: row.vals[5].int() + permissions: permissions + status: row.vals[7].int() == 1 + created_at: 0 + updated_at: 0 + } + } + + return menus +} + +// 获取用户菜单 +pub fn (auth AuthManager) get_user_menus(user User) ![]MenuItem { + // 根据用户角色获取菜单 + role_permissions := match user.role { + .admin { ['*'] } // 管理员拥有所有权限 + .manager { ['read', 'write', 'manage'] } + .user { ['read', 'write'] } + .guest { ['read'] } + } + + // 构建权限查询条件 + mut permission_conditions := []string{} + for permission in role_permissions { + if permission == '*' { + permission_conditions << 'permissions LIKE "%*%"' + } else { + permission_conditions << 'permissions LIKE "%$permission%"' + } + } + permission_where := permission_conditions.join(' OR ') + + rows := auth.db.db.exec('SELECT id, name, path, icon, parent_id, sort_order, permissions, status FROM menu_items WHERE status = 1 AND ($permission_where) ORDER BY parent_id, sort_order') or { + return error('Failed to query menu items: $err') + } + + mut menus := []MenuItem{} + for row in rows { + permissions_str := row.vals[6] + permissions := json.decode([]string, permissions_str) or { []string{} } + + menus << MenuItem{ + id: row.vals[0].int() + name: row.vals[1] + path: row.vals[2] + icon: row.vals[3] + parent_id: row.vals[4].int() + sort_order: row.vals[5].int() + permissions: permissions + status: row.vals[7].int() == 1 + created_at: 0 + updated_at: 0 + } + } + + return menus +} + +// 构建菜单树 +pub fn (auth AuthManager) build_menu_tree(menus []MenuItem) []MenuItem { + mut menu_map := map[int]MenuItem{} + mut root_menus := []MenuItem{} + + // 创建菜单映射 + for menu in menus { + menu_map[menu.id] = menu + } + + // 构建树结构 + for menu in menus { + if menu.parent_id == 0 { + // 根菜单 + mut root_menu := menu + root_menu.children = auth.get_children(menu.id, menu_map) + root_menus << root_menu + } + } + + return root_menus +} + +// 获取子菜单 +fn (auth AuthManager) get_children(parent_id int, menu_map map[int]MenuItem) []MenuItem { + mut children := []MenuItem{} + + for _, menu in menu_map { + if menu.parent_id == parent_id { + mut child := menu + child.children = auth.get_children(menu.id, menu_map) + children << child + } + } + + return children +} + +// 检查用户权限 +pub fn (auth AuthManager) check_permission(user User, required_permission string) bool { + return match user.role { + .admin { true } // 管理员拥有所有权限 + .manager { required_permission in ['read', 'write', 'manage'] } + .user { required_permission in ['read', 'write'] } + .guest { required_permission == 'read' } + } +} + +// 获取菜单id(通过path) +pub fn (auth AuthManager) get_menu_id_by_path(path string) int { + rows := auth.db.db.exec('SELECT id FROM menu_items WHERE path = "$path"') or { return 0 } + if rows.len > 0 { + return rows[0].vals[0].int() + } + return 0 +} \ No newline at end of file diff --git a/hono/auth_routes.v b/hono/auth_routes.v new file mode 100644 index 0000000..cde9531 --- /dev/null +++ b/hono/auth_routes.v @@ -0,0 +1,223 @@ +module hono + +import json +import net.http + +// 登录请求结构 +pub struct LoginRequest { +pub: + username string + password string +} + +// 注册请求结构 +pub struct RegisterRequest { +pub: + username string + email string + password string + role string +} + +// 菜单创建请求结构 +pub struct MenuCreateRequest { +pub: + name string + path string + icon string + parent_id int + sort_order int + permissions []string +} + +// 认证中间件(只做校验,不注入 user) +pub fn auth_middleware(auth_manager AuthManager) ContextMiddleware { + return fn [auth_manager] (mut c Context, next fn (mut Context) http.Response) http.Response { + token := c.req.header.get_custom('Authorization') or { '' } + if token == '' { + c.status(401) + return c.json(json.encode({ + 'error': 'Authorization token required' + })) + } + _ := auth_manager.verify_token(token) or { + c.status(401) + return c.json(json.encode({ + 'error': 'Invalid or expired token' + })) + } + return next(mut c) + } +} + +// 权限检查中间件(直接校验 token 权限) +pub fn permission_middleware(auth_manager AuthManager, required_permission string) ContextMiddleware { + return fn [auth_manager, required_permission] (mut c Context, next fn (mut Context) http.Response) http.Response { + token := c.req.header.get_custom('Authorization') or { '' } + user := auth_manager.verify_token(token) or { + c.status(401) + return c.json(json.encode({ + 'error': 'Invalid or expired token' + })) + } + if !auth_manager.check_permission(user, required_permission) { + c.status(403) + return c.json(json.encode({ + 'error': 'Insufficient permissions' + })) + } + return next(mut c) + } +} + +// 注册认证路由 +pub fn register_auth_routes(mut app Hono, mut auth_manager AuthManager) { + // 登录路由 + app.post('/api/auth/login', fn [mut auth_manager] (mut c Context) http.Response { + body := c.body + login_req := json.decode(LoginRequest, body) or { + c.status(400) + return c.json(json.encode({ + 'error': 'Invalid request body' + })) + } + session := auth_manager.login(login_req.username, login_req.password) or { + c.status(401) + return c.json(json.encode({ + 'error': err.msg() + })) + } + return c.json(json.encode({ + 'token': session.token.str() + 'expires_at': session.expires_at.str() + })) + }) + + // 注册路由 + app.post('/api/auth/register', fn [mut auth_manager] (mut c Context) http.Response { + body := c.body + register_req := json.decode(RegisterRequest, body) or { + c.status(400) + return c.json(json.encode({ + 'error': 'Invalid request body' + })) + } + role := match register_req.role { + 'admin' { UserRole.admin } + 'manager' { UserRole.manager } + 'user' { UserRole.user } + 'guest' { UserRole.guest } + else { UserRole.user } + } + user := auth_manager.create_user(register_req.username, register_req.email, register_req.password, role) or { + c.status(400) + return c.json(json.encode({ + 'error': err.msg() + })) + } + return c.json(json.encode({ + 'user_id': user.id.str() + 'username': user.username + 'email': user.email + 'role': user.role.str() + })) + }) + + // 注销路由 + app.post('/api/auth/logout', fn [mut auth_manager] (mut c Context) http.Response { + token := c.req.header.get_custom('Authorization') or { '' } + if token == '' { + c.status(401) + return c.json(json.encode({ + 'error': 'Authorization token required' + })) + } + auth_manager.logout(token) or { + c.status(500) + return c.json(json.encode({ + 'error': err.msg() + })) + } + return c.json(json.encode({ + 'message': 'Logged out successfully' + })) + }) + + // 获取用户信息路由 + app.get('/api/auth/profile', fn [auth_manager] (mut c Context) http.Response { + token := c.req.header.get_custom('Authorization') or { '' } + user := auth_manager.verify_token(token) or { + c.status(401) + return c.json(json.encode({ + 'error': 'Invalid or expired token' + })) + } + return c.json(json.encode({ + 'user_id': user.id.str() + 'username': user.username + 'email': user.email + 'role': user.role.str() + 'active': user.status.str() + })) + }) + + // 获取用户菜单路由 + app.get('/api/auth/menus', fn [auth_manager] (mut c Context) http.Response { + token := c.req.header.get_custom('Authorization') or { '' } + user := auth_manager.verify_token(token) or { + c.status(401) + return c.json(json.encode({ + 'error': 'Invalid or expired token' + })) + } + menus := auth_manager.get_user_menus(user) or { + c.status(500) + return c.json(json.encode({ + 'error': err.msg() + })) + } + return c.json(json.encode({ + 'menus': menus + })) + }) + + // 创建菜单项路由 (需要管理员权限) + app.post('/api/auth/menus', fn [mut auth_manager] (mut c Context) http.Response { + body := c.body + menu_req := json.decode(MenuCreateRequest, body) or { + c.status(400) + return c.json(json.encode({ + 'error': 'Invalid request body' + })) + } + menu := auth_manager.create_menu_item(menu_req.name, menu_req.path, menu_req.icon, menu_req.parent_id, menu_req.sort_order, menu_req.permissions) or { + c.status(400) + return c.json(json.encode({ + 'error': err.msg() + })) + } + return c.json(json.encode({ + 'menu_id': menu.id.str() + 'name': menu.name + 'path': menu.path + 'icon': menu.icon + 'parent_id': menu.parent_id.str() + 'sort_order': menu.sort_order.str() + 'permissions': json.encode(menu.permissions) + 'active': menu.status.str() + })) + }) + + // 获取所有菜单项路由 (需要管理员权限) + app.get('/api/auth/menus/all', fn [auth_manager] (mut c Context) http.Response { + menus := auth_manager.get_all_menu_items() or { + c.status(500) + return c.json(json.encode({ + 'error': err.msg() + })) + } + return c.json(json.encode({ + 'menus': menus + })) + }) +} \ No newline at end of file diff --git a/public/auth.html b/public/auth.html new file mode 100644 index 0000000..947576e --- /dev/null +++ b/public/auth.html @@ -0,0 +1,436 @@ + + + + + + 用户角色菜单管理系统 + + + +
+
+

用户角色菜单管理系统

+

基于 v-hono 框架的用户认证和权限管理

+
+ +
+ +
+

用户注册

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+

用户登录

+
+ + +
+
+ + +
+ + +
+ + +
+

用户信息

+ + +
+ + +
+

菜单管理

+ + + +
+ + +
+

权限测试

+ + + +
+ + +
+

注销

+ + +
+
+
+ + + + \ No newline at end of file