Files
usernode/internal/model/model.go
T

221 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 邮件发送记录(队列 + 重试 + 失败记录)。
// Body 保存原文以便失败后按原内容重试(队列的载荷),仅用于重发,不对外展示。
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"`
Body string `gorm:"type:text" json:"-"`
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"`
}
// OTPCode 外部用户 OTP 验证码(DB 存储,邮件与 CLI 双通道共享)。
//
// 每用户一行(username 唯一):邮件通道经 Send 生成,CLI 通道经 Current
// 读取同一验证码,保证"同一验证码、同一有效期、同一冷却与失败限速"(PLAN §2.2)。
// 验证码为 6 位数字,生命周期短(10 分钟)且一次性消费,存储明文以便 CLI
// 复用返回;并发写由单实例部署的串行事务保证(多实例需改 DB 行锁/Redis,PLAN §6)。
type OTPCode struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
Code string `gorm:"size:16;not null" json:"-"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
CooldownUntil time.Time `json:"cooldown_until"` // Send 冷却截止
Failures int `json:"failures"`
FailedAt *time.Time `json:"failed_at"` // 失败计数窗口起点(窗口内达阈值限速)
ConsumedAt *time.Time `json:"consumed_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PasswordResetToken 管理员密码重置令牌(邮件重置)。DB 只存哈希,
// 明文令牌仅经邮件/日志发送给管理员,一次有效。
type PasswordResetToken struct {
ID uint `gorm:"primaryKey" json:"id"`
AdminID uint `gorm:"index;not null" json:"admin_id"`
TokenHash string `gorm:"size:64;not null" json:"-"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
UsedAt *time.Time `json:"used_at"`
IP string `gorm:"size:64" json:"ip"`
CreatedAt time.Time `json:"created_at"`
}
// AllModels 供 AutoMigrate 使用的全部模型。
func AllModels() []any {
return []any{
&AdminUser{}, &User{}, &SSHKey{}, &Approval{},
&AuditLog{}, &Session{}, &Setting{}, &MailLog{},
&OTPCode{}, &PasswordResetToken{},
}
}
// 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 执行 AutoMigrateCLI migrate 子命令入口)。
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(AllModels()...)
}