- 错误响应、健康检查等运行时文案改为英文 - 种子数据英文化,新增迁移 v5 更新存量内置数据(不覆盖手工修改) - Swagger 注释与 docs/ 文档全英文化 - 补充英文文案断言与 v5 迁移测试
120 lines
3.3 KiB
Go
120 lines
3.3 KiB
Go
package database
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
|
|
"rill/internal/model"
|
|
)
|
|
|
|
const (
|
|
adminUsername = "admin"
|
|
adminEmail = "admin@example.com"
|
|
adminPasswordLen = 16
|
|
adminPasswordFilename = "admin_password.txt"
|
|
)
|
|
|
|
// adminPasswordCharset 去掉了易混淆字符(0/O、1/l/I)。
|
|
const adminPasswordCharset = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
|
|
// seedAdminUser 创建初始管理员用户并加入 admin 组,密码随机生成且仅在迁移时打印一次。
|
|
func seedAdminUser(tx *gorm.DB) error {
|
|
var count int64
|
|
if err := tx.Model(&model.User{}).Where("username = ?", adminUsername).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
slog.Warn("已存在同名用户,跳过初始管理员创建", "username", adminUsername)
|
|
return nil
|
|
}
|
|
|
|
password, err := generatePassword(adminPasswordLen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user := model.User{
|
|
Username: adminUsername,
|
|
Email: adminEmail,
|
|
PasswordHash: string(hash),
|
|
Nickname: "Administrator",
|
|
Status: 1,
|
|
}
|
|
if err := tx.Create(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Exec(
|
|
"INSERT INTO user_group_members (user_id, user_group_id) VALUES (?, ?)",
|
|
user.ID, model.GroupIDAdmin,
|
|
).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
passwordFile := adminPasswordPath(tx)
|
|
if err := writeAdminPasswordFile(passwordFile, password); err != nil {
|
|
slog.Warn("初始管理员密码文件保存失败", "path", passwordFile, "err", err)
|
|
}
|
|
fmt.Printf(
|
|
"\n初始管理员账号已创建,密码仅显示这一次,请立即保存:\n 用户名: %s\n 密码: %s\n 密码文件: %s(登录后请立即删除该文件)\n\n",
|
|
adminUsername, password, passwordFile,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// adminPasswordPath 密码文件与 SQLite 数据库同目录;其他驱动回落到 ./data。
|
|
func adminPasswordPath(db *gorm.DB) string {
|
|
if db.Dialector.Name() == "sqlite" {
|
|
if file := sqliteDatabaseFile(db); file != "" && file != ":memory:" {
|
|
return filepath.Join(filepath.Dir(file), adminPasswordFilename)
|
|
}
|
|
}
|
|
return filepath.Join("data", adminPasswordFilename)
|
|
}
|
|
|
|
func sqliteDatabaseFile(db *gorm.DB) string {
|
|
var rows []struct {
|
|
File string
|
|
}
|
|
if err := db.Raw("PRAGMA database_list").Scan(&rows).Error; err != nil || len(rows) == 0 {
|
|
return ""
|
|
}
|
|
return rows[0].File
|
|
}
|
|
|
|
func writeAdminPasswordFile(path, password string) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
content := fmt.Sprintf(
|
|
"初始管理员账号(密码仅明文保存在此,登录后请立即删除本文件)\n\n用户名: %s\n密码: %s\n",
|
|
adminUsername, password,
|
|
)
|
|
return os.WriteFile(path, []byte(content), 0o600)
|
|
}
|
|
|
|
func generatePassword(length int) (string, error) {
|
|
limit := big.NewInt(int64(len(adminPasswordCharset)))
|
|
var builder strings.Builder
|
|
builder.Grow(length)
|
|
for i := 0; i < length; i++ {
|
|
n, err := rand.Int(rand.Reader, limit)
|
|
if err != nil {
|
|
return "", fmt.Errorf("生成随机密码失败: %w", err)
|
|
}
|
|
builder.WriteByte(adminPasswordCharset[n.Int64()])
|
|
}
|
|
return builder.String(), nil
|
|
}
|