- internal/utils:ClientIP 按序枚举 CDN/代理头(含 RFC 7239 Forwarded),仅在可信代理来源时采信,否则回退直连 IP;RemoteIP 取直连地址;RandomString 生成安全随机串 - 新增 server.trusted_proxies 配置(IP/CIDR,ConfigVersion 2→3 自动补全),启动时同步应用到 gin 与 utils - 初始管理员密码生成改用 utils.RandomString,原密码测试迁至 utils
101 lines
2.7 KiB
Go
101 lines
2.7 KiB
Go
package database
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
|
|
"rill/internal/model"
|
|
"rill/internal/utils"
|
|
)
|
|
|
|
const (
|
|
adminUsername = "admin"
|
|
adminEmail = "admin@example.com"
|
|
adminPasswordLen = 16
|
|
adminPasswordFilename = "admin_password.txt"
|
|
)
|
|
|
|
// 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 := utils.RandomString(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)
|
|
}
|