- Go 工程:cmd/usernode CLI(serve/migrate/admin create/reset-password/user otp)
+ internal/{config,model,service,system,auth,cron,api,router,server,pkg,webui}
- 配置:TOML + 环境变量覆盖(USERNODE_<SEC>_<FIELD>),config.example.toml
- 数据:GORM 双驱动(SQLite/MySQL)8 表模型,migrate 子命令可跑通
- 系统层:system.Manager 接口(useradd/userdel/passwd 白名单,dev dry-run)
- HTTP:Gin 路由骨架(/api/v1 + 501 占位),healthz,slog 结构化日志,优雅启停
- 前端:Vite + Vue3 + TS + Element Plus 最小可运行(web/),go:embed 打通
- 构建:deploy/Containerfile 多阶段单二进制镜像,web/Containerfile.dev 前端 dev
镜像,Makefile(build/test/dev/web-build/image);go.mod 锁定 go 1.22
- 验证:go build/vet/test 通过;podman 镜像构建运行 healthz+embed 通过
187 lines
7.4 KiB
Go
187 lines
7.4 KiB
Go
// Package model 定义 GORM 模型与数据库接入。
|
||
//
|
||
// 兼容性约束(见 PLAN §6):
|
||
// - 避免平台特有类型,时间统一 UTC,字段尽量用通用类型;
|
||
// - detail 等 JSON 内容以 string 存储(入库前序列化),避免依赖
|
||
// datatypes.JSON 的平台差异;
|
||
// - 由本包 AutoMigrate 保证 SQLite / MySQL 均可平滑迁移。
|
||
package model
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"github.com/glebarez/sqlite"
|
||
"gorm.io/driver/mysql"
|
||
"gorm.io/gorm"
|
||
gormlogger "gorm.io/gorm/logger"
|
||
)
|
||
|
||
// 外部用户账号状态。
|
||
const (
|
||
UserStatusActive = "active" // 正常可用
|
||
UserStatusDisabled = "disabled" // 管理员禁用
|
||
UserStatusExpired = "expired" // 已到期,等待回收或延期
|
||
)
|
||
|
||
// 密钥 / 申请 / 审计结果等通用状态。
|
||
const (
|
||
StatusActive = "active"
|
||
StatusRevoked = "revoked"
|
||
StatusPending = "pending"
|
||
StatusApproved = "approved"
|
||
StatusRejected = "rejected"
|
||
ResultSuccess = "success"
|
||
ResultFailed = "failed"
|
||
)
|
||
|
||
// AdminUser 管理端账号。
|
||
type AdminUser struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
|
||
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
||
Email string `gorm:"size:255" json:"email"`
|
||
Role string `gorm:"size:32;not null;default:admin" json:"role"`
|
||
Status string `gorm:"size:16;not null;default:active" json:"status"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// User 外部用户(1:1 对应系统账号 ext_<name>)。
|
||
type User struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"` // 含 ext_ 前缀
|
||
Email string `gorm:"size:255;not null" json:"email"`
|
||
Supervisor string `gorm:"size:128" json:"supervisor"` // 挂靠老师
|
||
Purpose string `gorm:"size:512" json:"purpose"` // 用途
|
||
Status string `gorm:"size:16;not null;default:active;index" json:"status"`
|
||
ExpireAt *time.Time `gorm:"index" json:"expire_at"`
|
||
Shell string `gorm:"size:64;not null" json:"shell"`
|
||
CreatedBy uint `json:"created_by"`
|
||
LastLoginAt *time.Time `json:"last_login_at"`
|
||
RecycledAt *time.Time `json:"recycled_at"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// SSHKey 用户上传的 SSH 公钥。仅用户自行上传(source=user_uploaded),管理员不代签。
|
||
type SSHKey struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||
Name string `gorm:"size:64;not null" json:"name"`
|
||
KeyType string `gorm:"size:32;not null" json:"key_type"` // ssh-ed25519 / ssh-rsa ...
|
||
PublicKey string `gorm:"type:text;not null" json:"public_key"`
|
||
Fingerprint string `gorm:"size:64;index" json:"fingerprint"`
|
||
Status string `gorm:"size:16;not null;default:active;index" json:"status"`
|
||
Source string `gorm:"size:32;not null;default:user_uploaded" json:"source"`
|
||
CreatedBy uint `json:"created_by"`
|
||
RevokedAt *time.Time `json:"revoked_at"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// Approval 新账号申请单(仅新账号申请走审批流,延期由管理员直接操作)。
|
||
type Approval struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
UsernameRequested string `gorm:"size:64;index;not null" json:"username_requested"` // 不含 ext_ 前缀
|
||
Email string `gorm:"size:255;not null" json:"email"`
|
||
Supervisor string `gorm:"size:128" json:"supervisor"`
|
||
Purpose string `gorm:"size:512" json:"purpose"`
|
||
Status string `gorm:"size:16;not null;default:pending;index" json:"status"`
|
||
ReviewerID *uint `json:"reviewer_id"`
|
||
ReviewedAt *time.Time `json:"reviewed_at"`
|
||
Reason string `gorm:"size:512" json:"reason"` // 拒绝理由
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// AuditLog 审计日志,append-only:业务代码只允许 Create,禁止 Update/Delete。
|
||
type AuditLog struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
ActorID uint `gorm:"index" json:"actor_id"`
|
||
ActorName string `gorm:"size:64" json:"actor_name"`
|
||
Action string `gorm:"size:64;index;not null" json:"action"`
|
||
ResourceType string `gorm:"size:32;index" json:"resource_type"`
|
||
ResourceID string `gorm:"size:64;index" json:"resource_id"`
|
||
Detail string `gorm:"type:text" json:"detail"` // JSON 序列化后的详情
|
||
IP string `gorm:"size:64" json:"ip"`
|
||
Result string `gorm:"size:16;not null" json:"result"`
|
||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||
}
|
||
|
||
// Session 服务端会话(cookie 会话,DB 存储,兼容多实例)。
|
||
type Session struct {
|
||
ID string `gorm:"primaryKey;size:64" json:"session_id"`
|
||
UserType string `gorm:"size:16;not null" json:"user_type"` // admin / user
|
||
RefID uint `gorm:"index;not null" json:"ref_id"`
|
||
ExpireAt time.Time `gorm:"index;not null" json:"expire_at"`
|
||
IP string `gorm:"size:64" json:"ip"`
|
||
UserAgent string `gorm:"size:255" json:"user_agent"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// Setting 系统设置(SMTP、默认有效期等,config 提供默认值,settings 表可覆盖)。
|
||
type Setting struct {
|
||
Key string `gorm:"primaryKey;size:128" json:"key"`
|
||
Value string `gorm:"type:text" json:"value"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// MailLog 邮件发送记录(队列 + 重试 + 失败记录)。
|
||
type MailLog struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
To string `gorm:"size:255;index;not null" json:"to"`
|
||
Subject string `gorm:"size:255" json:"subject"`
|
||
Status string `gorm:"size:16;not null;default:pending;index" json:"status"`
|
||
Error string `gorm:"type:text" json:"error"`
|
||
RetryCount int `gorm:"not null;default:0" json:"retry_count"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// AllModels 供 AutoMigrate 使用的全部模型。
|
||
func AllModels() []any {
|
||
return []any{
|
||
&AdminUser{}, &User{}, &SSHKey{}, &Approval{},
|
||
&AuditLog{}, &Session{}, &Setting{}, &MailLog{},
|
||
}
|
||
}
|
||
|
||
// Open 按 driver 打开 GORM 连接。
|
||
//
|
||
// driver: sqlite | mysql
|
||
// dsn: sqlite 为文件路径(自动创建父目录);mysql 为 DSN 字符串
|
||
func Open(driver, dsn string, debug bool) (*gorm.DB, error) {
|
||
logLevel := gormlogger.Warn
|
||
if debug {
|
||
logLevel = gormlogger.Info
|
||
}
|
||
cfg := &gorm.Config{
|
||
Logger: gormlogger.Default.LogMode(logLevel),
|
||
// 表名默认复数(users / ssh_keys / approvals / ...),与 PLAN §6 一致。
|
||
}
|
||
|
||
var dialector gorm.Dialector
|
||
switch driver {
|
||
case "sqlite":
|
||
if dsn != ":memory:" {
|
||
if err := os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
|
||
return nil, fmt.Errorf("model: create sqlite dir: %w", err)
|
||
}
|
||
}
|
||
dialector = sqlite.Open(dsn)
|
||
case "mysql":
|
||
dialector = mysql.Open(dsn)
|
||
default:
|
||
return nil, fmt.Errorf("model: unsupported driver %q", driver)
|
||
}
|
||
return gorm.Open(dialector, cfg)
|
||
}
|
||
|
||
// Migrate 执行 AutoMigrate(CLI migrate 子命令入口)。
|
||
func Migrate(db *gorm.DB) error {
|
||
return db.AutoMigrate(AllModels()...)
|
||
}
|