feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
This commit is contained in:
147
tools/db_check.go
Normal file
147
tools/db_check.go
Normal 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
116
tools/init_admin.go
Normal 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
82
tools/seed_permissions.py
Normal 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.')
|
||||
117
tools/sqlite_snapshot_check.go
Normal file
117
tools/sqlite_snapshot_check.go
Normal 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
68
tools/verify_admin.go
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user