feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers

This commit is contained in:
2026-04-02 11:19:50 +08:00
parent e59a77bc49
commit dcc1f186f8
298 changed files with 62603 additions and 0 deletions

147
tools/db_check.go Normal file
View File

@@ -0,0 +1,147 @@
//go:build ignore
// 数据库完整性检查工具
package main
import (
"fmt"
"log"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func main() {
db, err := gorm.Open(sqlite.Open("./data/user_management.db"), &gorm.Config{})
if err != nil {
log.Fatal("open db:", err)
}
fmt.Println("=== 数据库完整性检查 ===\n")
// 1. 表存在性检查
tables := []string{"users", "roles", "permissions", "user_roles", "role_permissions",
"devices", "login_logs", "operation_logs", "social_accounts",
"webhooks", "webhook_deliveries", "password_histories"}
fmt.Println("[1] 表结构检查:")
for _, table := range tables {
var count int64
result := db.Raw("SELECT COUNT(*) FROM " + table).Scan(&count)
if result.Error != nil {
fmt.Printf(" ❌ %s: ERROR - %v\n", table, result.Error)
} else {
fmt.Printf(" ✅ %s: %d rows\n", table, count)
}
}
// 2. 用户数据完整性
fmt.Println("\n[2] 用户数据:")
var users []struct {
ID int64
Username string
Email *string
Status int
CreatedAt string
}
db.Raw("SELECT id, username, email, status, created_at FROM users").Scan(&users)
for _, u := range users {
email := "NULL"
if u.Email != nil {
email = *u.Email
}
fmt.Printf(" User[%d]: %s | email=%s | status=%d | created=%s\n",
u.ID, u.Username, email, u.Status, u.CreatedAt)
}
// 3. 角色-权限绑定
fmt.Println("\n[3] 角色-权限绑定:")
var rolePerms []struct {
RoleID int64
PermissionID int64
}
db.Raw("SELECT role_id, permission_id FROM role_permissions").Scan(&rolePerms)
if len(rolePerms) == 0 {
fmt.Println(" ⚠️ 没有角色-权限绑定数据")
} else {
for _, rp := range rolePerms {
fmt.Printf(" role_id=%d permission_id=%d\n", rp.RoleID, rp.PermissionID)
}
}
// 4. 操作日志近5条
fmt.Println("\n[4] 操作日志最近5条:")
var opLogs []struct {
ID int64
UserID int64
RequestMethod string
RequestPath string
ResponseStatus int
CreatedAt string
}
db.Raw("SELECT id, user_id, request_method, request_path, response_status, created_at FROM operation_logs ORDER BY id DESC LIMIT 5").Scan(&opLogs)
for _, l := range opLogs {
fmt.Printf(" [%d] user=%d %s %s status=%d time=%s\n",
l.ID, l.UserID, l.RequestMethod, l.RequestPath, l.ResponseStatus, l.CreatedAt)
}
// 5. 登录日志
fmt.Println("\n[5] 登录日志:")
var loginLogs []struct {
ID int64
UserID int64
IP string
Status int
CreatedAt string
}
db.Raw("SELECT id, user_id, ip, status, created_at FROM login_logs ORDER BY id DESC LIMIT 10").Scan(&loginLogs)
if len(loginLogs) == 0 {
fmt.Println(" ⚠️ 没有登录日志数据 - 登录时未记录!")
} else {
for _, l := range loginLogs {
fmt.Printf(" [%d] user=%d ip=%s status=%d time=%s\n",
l.ID, l.UserID, l.IP, l.Status, l.CreatedAt)
}
}
// 6. 密码历史
fmt.Println("\n[6] 密码历史:")
var pwdHistory []struct {
ID int64
UserID int64
CreatedAt string
}
db.Raw("SELECT id, user_id, created_at FROM password_histories ORDER BY id DESC LIMIT 5").Scan(&pwdHistory)
for _, ph := range pwdHistory {
fmt.Printf(" [%d] user=%d time=%s\n", ph.ID, ph.UserID, ph.CreatedAt)
}
// 7. 索引检查
fmt.Println("\n[7] 主要唯一约束验证:")
// 检查 users 邮箱唯一
var dupEmails []struct {
Email string
Count int64
}
db.Raw("SELECT email, COUNT(*) as count FROM users WHERE email IS NOT NULL GROUP BY email HAVING count > 1").Scan(&dupEmails)
if len(dupEmails) == 0 {
fmt.Println(" ✅ users.email 唯一性: OK")
} else {
fmt.Printf(" ❌ users.email 重复: %v\n", dupEmails)
}
// 检查 users 用户名唯一
var dupUsernames []struct {
Username string
Count int64
}
db.Raw("SELECT username, COUNT(*) as count FROM users GROUP BY username HAVING count > 1").Scan(&dupUsernames)
if len(dupUsernames) == 0 {
fmt.Println(" ✅ users.username 唯一性: OK")
} else {
fmt.Printf(" ❌ users.username 重复: %v\n", dupUsernames)
}
fmt.Println("\n=== 检查完成 ===")
}

116
tools/init_admin.go Normal file
View File

@@ -0,0 +1,116 @@
//go:build ignore
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/glebarez/sqlite"
"github.com/user-management-system/internal/auth"
"github.com/user-management-system/internal/config"
"github.com/user-management-system/internal/domain"
"gorm.io/gorm"
)
func main() {
username := strings.TrimSpace(os.Getenv("UMS_ADMIN_USERNAME"))
password := os.Getenv("UMS_ADMIN_PASSWORD")
email := strings.TrimSpace(os.Getenv("UMS_ADMIN_EMAIL"))
resetPassword := strings.EqualFold(strings.TrimSpace(os.Getenv("UMS_ADMIN_RESET_PASSWORD")), "true")
if username == "" || password == "" {
log.Fatal("UMS_ADMIN_USERNAME and UMS_ADMIN_PASSWORD are required")
}
db, err := gorm.Open(sqlite.Open(resolveDBPath()), &gorm.Config{})
if err != nil {
log.Fatal("open db:", err)
}
var adminRole domain.Role
if err := db.Where("code = ?", "admin").First(&adminRole).Error; err != nil {
log.Fatal("admin role not found:", err)
}
var user domain.User
err = db.Where("username = ?", username).First(&user).Error
switch {
case err == nil:
if email != "" {
user.Email = &email
}
user.Status = domain.UserStatusActive
if resetPassword {
passwordHash, hashErr := auth.HashPassword(password)
if hashErr != nil {
log.Fatal("hash password:", hashErr)
}
user.Password = passwordHash
}
if saveErr := db.Save(&user).Error; saveErr != nil {
log.Fatal("update admin:", saveErr)
}
case err == gorm.ErrRecordNotFound:
passwordHash, hashErr := auth.HashPassword(password)
if hashErr != nil {
log.Fatal("hash password:", hashErr)
}
user = domain.User{
Username: username,
Email: stringPtr(email),
Password: passwordHash,
Status: domain.UserStatusActive,
Nickname: username,
}
if createErr := db.Create(&user).Error; createErr != nil {
log.Fatal("create admin:", createErr)
}
default:
log.Fatal("query admin:", err)
}
var binding domain.UserRole
bindingErr := db.Where("user_id = ? AND role_id = ?", user.ID, adminRole.ID).First(&binding).Error
if bindingErr == gorm.ErrRecordNotFound {
if err := db.Create(&domain.UserRole{UserID: user.ID, RoleID: adminRole.ID}).Error; err != nil {
log.Fatal("assign admin role:", err)
}
} else if bindingErr != nil {
log.Fatal("query admin role binding:", bindingErr)
}
fmt.Printf("admin initialized: username=%s user_id=%d role_id=%d\n", user.Username, user.ID, adminRole.ID)
}
func stringPtr(value string) *string {
if strings.TrimSpace(value) == "" {
return nil
}
return &value
}
func resolveDBPath() string {
if path := strings.TrimSpace(os.Getenv("UMS_DATABASE_SQLITE_PATH")); path != "" {
return path
}
cfg, err := config.Load(resolveConfigPath())
if err == nil && strings.EqualFold(strings.TrimSpace(cfg.Database.Type), "sqlite") {
if path := strings.TrimSpace(cfg.Database.SQLite.Path); path != "" {
return path
}
}
return "./data/user_management.db"
}
func resolveConfigPath() string {
if path := strings.TrimSpace(os.Getenv("UMS_CONFIG_PATH")); path != "" {
return path
}
return "./configs/config.yaml"
}

82
tools/seed_permissions.py Normal file
View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""Seed default permissions and role-permission bindings into existing DB."""
import sqlite3
from datetime import datetime
DB_PATH = 'data/user_management.db'
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
permissions = [
('User List', 'user:list', 2, 'List users', '/api/v1/users', 'GET', 10),
('View User', 'user:view', 2, 'View user detail', '/api/v1/users/:id', 'GET', 11),
('Edit User', 'user:edit', 2, 'Edit user info', '/api/v1/users/:id', 'PUT', 12),
('Delete User', 'user:delete', 2, 'Delete user', '/api/v1/users/:id', 'DELETE', 13),
('Manage User', 'user:manage', 2, 'Manage user status', '/api/v1/users/:id/status', 'PUT', 14),
('View Profile', 'profile:view', 2, 'View own profile', '/api/v1/auth/userinfo', 'GET', 20),
('Edit Profile', 'profile:edit', 2, 'Edit own profile', '/api/v1/users/:id', 'PUT', 21),
('Change Pwd', 'profile:change_password', 2, 'Change password', '/api/v1/users/:id/password', 'PUT', 22),
('Role Manage', 'role:manage', 2, 'Manage roles', '/api/v1/roles', 'GET', 30),
('Create Role', 'role:create', 2, 'Create role', '/api/v1/roles', 'POST', 31),
('Edit Role', 'role:edit', 2, 'Edit role', '/api/v1/roles/:id', 'PUT', 32),
('Delete Role', 'role:delete', 2, 'Delete role', '/api/v1/roles/:id', 'DELETE', 33),
('Perm Manage', 'permission:manage', 2, 'Manage permissions', '/api/v1/permissions', 'GET', 40),
('View Own Log', 'log:view_own', 2, 'View own login log', '/api/v1/logs/login/me', 'GET', 50),
('View All Logs', 'log:view_all', 2, 'View all logs (admin)','/api/v1/logs/login', 'GET', 51),
('Dashboard', 'stats:view', 2, 'View dashboard stats','/api/v1/admin/stats/dashboard','GET', 60),
('Device Manage', 'device:manage', 2, 'Manage devices', '/api/v1/devices', 'GET', 70),
]
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
perm_ids = {}
for name, code, ptype, desc, path, method, sort in permissions:
cur.execute('SELECT id FROM permissions WHERE code=?', (code,))
row = cur.fetchone()
if row:
perm_ids[code] = row[0]
print(f' skip existing: {code}')
else:
cur.execute(
'INSERT INTO permissions(name,code,type,description,level,path,method,sort,status,created_at,updated_at) VALUES(?,?,?,?,1,?,?,?,1,?,?)',
(name, code, ptype, desc, path, method, sort, now, now)
)
perm_ids[code] = cur.lastrowid
print(f' created: {code}')
conn.commit()
# Admin role: bind all permissions
cur.execute('SELECT id FROM roles WHERE code=?', ('admin',))
admin_role = cur.fetchone()
if admin_role:
rid = admin_role[0]
for code, pid in perm_ids.items():
cur.execute('SELECT 1 FROM role_permissions WHERE role_id=? AND permission_id=?', (rid, pid))
if not cur.fetchone():
cur.execute('INSERT INTO role_permissions(role_id,permission_id) VALUES(?,?)', (rid, pid))
conn.commit()
print(f'Admin role {rid}: bound {len(perm_ids)} permissions')
# User role: bind basic permissions
cur.execute('SELECT id FROM roles WHERE code=?', ('user',))
user_role = cur.fetchone()
if user_role:
rid = user_role[0]
for code in ['profile:view', 'profile:edit', 'log:view_own']:
pid = perm_ids.get(code)
if pid:
cur.execute('SELECT 1 FROM role_permissions WHERE role_id=? AND permission_id=?', (rid, pid))
if not cur.fetchone():
cur.execute('INSERT INTO role_permissions(role_id,permission_id) VALUES(?,?)', (rid, pid))
conn.commit()
print(f'User role {rid}: bound 3 base permissions')
# Summary
cur.execute('SELECT COUNT(*) FROM permissions')
print(f'\nTotal permissions: {cur.fetchone()[0]}')
cur.execute('SELECT COUNT(*) FROM role_permissions')
print(f'Total role_permissions: {cur.fetchone()[0]}')
conn.close()
print('Done.')

View File

@@ -0,0 +1,117 @@
//go:build ignore
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"sort"
"time"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type snapshot struct {
GeneratedAt string `json:"generated_at"`
Path string `json:"path"`
FileSize int64 `json:"file_size"`
Existing []string `json:"existing_tables"`
Missing []string `json:"missing_tables"`
Tables map[string]int64 `json:"tables"`
SampleUsers []string `json:"sample_users"`
}
func main() {
dbPath := flag.String("db", "./data/user_management.db", "sqlite database path")
jsonOutput := flag.Bool("json", false, "emit snapshot as JSON")
flag.Parse()
info, err := os.Stat(*dbPath)
if err != nil {
log.Fatalf("stat db failed: %v", err)
}
db, err := gorm.Open(sqlite.Open(*dbPath), &gorm.Config{})
if err != nil {
log.Fatalf("open db failed: %v", err)
}
tableNames := []string{
"users",
"roles",
"permissions",
"user_roles",
"role_permissions",
"devices",
"login_logs",
"operation_logs",
"social_accounts",
"webhooks",
"webhook_deliveries",
"password_histories",
}
var existingTables []string
if err := db.Raw("SELECT name FROM sqlite_master WHERE type = 'table'").Scan(&existingTables).Error; err != nil {
log.Fatalf("load sqlite table names failed: %v", err)
}
sort.Strings(existingTables)
existingTableSet := make(map[string]struct{}, len(existingTables))
for _, tableName := range existingTables {
existingTableSet[tableName] = struct{}{}
}
tableCounts := make(map[string]int64, len(tableNames))
missingTables := make([]string, 0)
for _, tableName := range tableNames {
if _, ok := existingTableSet[tableName]; !ok {
missingTables = append(missingTables, tableName)
continue
}
var count int64
if err := db.Raw("SELECT COUNT(*) FROM " + tableName).Scan(&count).Error; err != nil {
log.Fatalf("count table %s failed: %v", tableName, err)
}
tableCounts[tableName] = count
}
var sampleUsers []string
if err := db.Raw("SELECT username FROM users ORDER BY id ASC LIMIT 10").Scan(&sampleUsers).Error; err != nil {
log.Fatalf("load sample users failed: %v", err)
}
sort.Strings(sampleUsers)
result := snapshot{
GeneratedAt: time.Now().Format(time.RFC3339),
Path: *dbPath,
FileSize: info.Size(),
Existing: existingTables,
Missing: missingTables,
Tables: tableCounts,
SampleUsers: sampleUsers,
}
if *jsonOutput {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(result); err != nil {
log.Fatalf("encode snapshot failed: %v", err)
}
return
}
fmt.Printf("snapshot generated_at=%s\n", result.GeneratedAt)
fmt.Printf("path=%s size=%d\n", result.Path, result.FileSize)
for _, tableName := range tableNames {
if count, ok := result.Tables[tableName]; ok {
fmt.Printf("%s=%d\n", tableName, count)
continue
}
fmt.Printf("%s=missing\n", tableName)
}
fmt.Printf("sample_users=%v\n", result.SampleUsers)
}

68
tools/verify_admin.go Normal file
View File

@@ -0,0 +1,68 @@
//go:build ignore
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/glebarez/sqlite"
"github.com/user-management-system/internal/auth"
"github.com/user-management-system/internal/config"
"github.com/user-management-system/internal/domain"
"gorm.io/gorm"
)
func main() {
username := strings.TrimSpace(os.Getenv("UMS_ADMIN_USERNAME"))
password := os.Getenv("UMS_ADMIN_PASSWORD")
if username == "" {
username = "admin"
}
db, err := gorm.Open(sqlite.Open(resolveDBPath()), &gorm.Config{})
if err != nil {
log.Fatal("open db:", err)
}
var user domain.User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
log.Fatalf("admin user %q not found: %v", username, err)
}
fmt.Printf("admin user: id=%d username=%s status=%d\n", user.ID, user.Username, user.Status)
if user.Email != nil {
fmt.Printf("email=%s\n", *user.Email)
}
if password == "" {
fmt.Println("password verification skipped; set UMS_ADMIN_PASSWORD to verify credentials")
return
}
fmt.Printf("password valid: %v\n", auth.VerifyPassword(user.Password, password))
}
func resolveDBPath() string {
if path := strings.TrimSpace(os.Getenv("UMS_DATABASE_SQLITE_PATH")); path != "" {
return path
}
cfg, err := config.Load(resolveConfigPath())
if err == nil && strings.EqualFold(strings.TrimSpace(cfg.Database.Type), "sqlite") {
if path := strings.TrimSpace(cfg.Database.SQLite.Path); path != "" {
return path
}
}
return "./data/user_management.db"
}
func resolveConfigPath() string {
if path := strings.TrimSpace(os.Getenv("UMS_CONFIG_PATH")); path != "" {
return path
}
return "./configs/config.yaml"
}