Files
verify/backend/internal/router/install/install.go
T
admin 7ab899b44f fix: JWT密钥YAML格式和401错误处理
- 修复JWT密钥在YAML配置文件中未加引号导致解析错误的问题
- 添加dashboard页面401错误处理,自动清除token并跳转登录页
2026-05-04 09:12:50 +08:00

359 lines
9.7 KiB
Go

package install
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"verification-platform-backend/internal/config"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
"verification-platform-backend/pkg/response"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/spf13/viper"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func SetupRoutes(r *gin.Engine) {
install := r.Group("/api/install")
{
install.GET("/status", handleGetInstallStatus)
install.POST("/setup", handleSetup)
install.POST("/test-db", handleTestDatabase)
}
}
type InstallStatus struct {
Installed bool `json:"installed"`
DbType string `json:"db_type"`
HasAdmin bool `json:"has_admin"`
ConfigExist bool `json:"config_exist"`
}
func handleGetInstallStatus(c *gin.Context) {
configExist := checkConfigFile()
hasAdmin := checkAdminExists()
installed := configExist && hasAdmin
var dbType string
if configExist {
dbType = config.GetString("database.type")
if dbType == "" {
if strings.Contains(config.GetString("database.host"), "") && config.GetString("database.host") != "localhost" {
dbType = "mysql"
} else {
dbType = "sqlite"
}
}
}
response.Success(c, InstallStatus{
Installed: installed,
DbType: dbType,
HasAdmin: hasAdmin,
ConfigExist: configExist,
})
}
type TestDBRequest struct {
DbType string `json:"db_type"`
Host string `json:"host"`
Port string `json:"port"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
}
func handleTestDatabase(c *gin.Context) {
var req TestDBRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
if req.DbType == "sqlite" {
dataDir := config.GetDataDir()
dbPath := filepath.Join(dataDir, "verification_platform.db")
testDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
response.Error(c, 400, "无法创建SQLite数据库: "+err.Error())
return
}
sqlDB, _ := testDB.DB()
sqlDB.Close()
response.Success(c, gin.H{"message": "SQLite数据库连接成功"})
return
}
if req.DbType == "mysql" {
if req.Host == "" || req.Port == "" || req.Name == "" || req.Username == "" {
response.Error(c, 400, "MySQL配置不完整")
return
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
req.Username, req.Password, req.Host, req.Port, req.Name)
testDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
response.Error(c, 400, "MySQL连接失败: "+err.Error())
return
}
sqlDB, _ := testDB.DB()
sqlDB.Close()
response.Success(c, gin.H{"message": "MySQL连接成功"})
return
}
response.Error(c, 400, "不支持的数据库类型")
}
type SetupRequest struct {
DbType string `json:"db_type"`
DbHost string `json:"db_host"`
DbPort string `json:"db_port"`
DbName string `json:"db_name"`
DbUsername string `json:"db_username"`
DbPassword string `json:"db_password"`
JwtSecret string `json:"jwt_secret"`
AdminUser string `json:"admin_user"`
AdminPass string `json:"admin_pass"`
AdminEmail string `json:"admin_email"`
UseRedis bool `json:"use_redis"`
RedisHost string `json:"redis_host"`
RedisPort string `json:"redis_port"`
RedisPass string `json:"redis_password"`
RedisDb int `json:"redis_db"`
}
func handleSetup(c *gin.Context) {
if checkAdminExists() {
response.Error(c, 400, "系统已安装")
return
}
var req SetupRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
if req.AdminUser == "" || req.AdminPass == "" {
response.Error(c, 400, "管理员账号和密码不能为空")
return
}
if len(req.AdminPass) < 6 {
response.Error(c, 400, "密码长度至少6位")
return
}
jwtSecret := req.JwtSecret
if jwtSecret == "" {
jwtSecret = generateRandomSecret(32)
}
var newDB *gorm.DB
var err error
if req.DbType == "sqlite" {
dataDir := config.GetDataDir()
dbPath := filepath.Join(dataDir, "verification_platform.db")
newDB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
response.Error(c, 500, "SQLite数据库创建失败: "+err.Error())
return
}
} else if req.DbType == "mysql" {
if req.DbHost == "" || req.DbPort == "" || req.DbName == "" || req.DbUsername == "" {
response.Error(c, 400, "MySQL配置不完整")
return
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
req.DbUsername, req.DbPassword, req.DbHost, req.DbPort, req.DbName)
newDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
response.Error(c, 500, "MySQL连接失败: "+err.Error())
return
}
} else {
response.Error(c, 400, "不支持的数据库类型")
return
}
if sqlDB, err := newDB.DB(); err == nil {
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
}
if err := runMigrations(newDB); err != nil {
response.Error(c, 500, "数据库迁移失败: "+err.Error())
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.AdminPass), bcrypt.DefaultCost)
if err != nil {
response.Error(c, 500, "密码加密失败")
return
}
admin := model.User{
Username: req.AdminUser,
Password: string(hashedPassword),
Email: &req.AdminEmail,
Role: "admin",
Status: "active",
}
if err := newDB.Create(&admin).Error; err != nil {
response.Error(c, 500, "创建管理员失败: "+err.Error())
return
}
configContent := generateConfigFile(req.DbType, req, jwtSecret)
if err := saveConfigFile(configContent); err != nil {
log.Printf("Warning: failed to save config file: %v", err)
}
viper.Set("app.jwt_secret", jwtSecret)
viper.Set("database.type", req.DbType)
if req.DbType == "mysql" {
viper.Set("database.host", req.DbHost)
viper.Set("database.port", req.DbPort)
viper.Set("database.name", req.DbName)
viper.Set("database.username", req.DbUsername)
viper.Set("database.password", req.DbPassword)
}
database.DB = newDB
response.Success(c, gin.H{
"message": "安装成功",
"jwt_secret": jwtSecret,
})
}
func checkConfigFile() bool {
dataDir := config.GetDataDir()
configPath := filepath.Join(dataDir, "config.yaml")
_, err := os.Stat(configPath)
return err == nil
}
func checkAdminExists() bool {
if database.DB == nil {
return false
}
var count int64
database.DB.Model(&model.User{}).Where("role = ?", "admin").Count(&count)
return count > 0
}
func generateRandomSecret(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
return hex.EncodeToString(bytes)[:length]
}
func runMigrations(db *gorm.DB) error {
return db.AutoMigrate(
&model.User{},
&model.AppUser{},
&model.Application{},
&model.CardType{},
&model.Card{},
&model.RechargeRecord{},
&model.UserDevice{},
&model.DeviceSession{},
&model.Setting{},
&model.ExtensionAPIKey{},
&model.AgentApplication{},
&model.Package{},
&model.Order{},
)
}
func generateConfigFile(dbType string, req SetupRequest, jwtSecret string) string {
var content strings.Builder
content.WriteString("# 应用配置\n")
content.WriteString("app:\n")
content.WriteString(" name: verification-platform\n")
content.WriteString(" env: production\n")
content.WriteString(" port: \"8080\"\n")
content.WriteString(fmt.Sprintf(" jwt_secret: \"%s\"\n", jwtSecret))
content.WriteString(" jwt_expire: 24h\n\n")
content.WriteString("# 数据库配置\n")
content.WriteString("database:\n")
content.WriteString(fmt.Sprintf(" type: %s\n", dbType))
if dbType == "mysql" {
content.WriteString(fmt.Sprintf(" host: %s\n", req.DbHost))
content.WriteString(fmt.Sprintf(" port: %s\n", req.DbPort))
content.WriteString(fmt.Sprintf(" name: %s\n", req.DbName))
content.WriteString(fmt.Sprintf(" username: %s\n", req.DbUsername))
content.WriteString(fmt.Sprintf(" password: %s\n", req.DbPassword))
} else {
content.WriteString(" host: localhost\n")
content.WriteString(" port: \"3306\"\n")
content.WriteString(" name: verification_platform\n")
content.WriteString(" username: root\n")
content.WriteString(" password: \"\"\n")
}
content.WriteString(" charset: utf8mb4\n")
content.WriteString(" max_idle_conns: 10\n")
content.WriteString(" max_open_conns: 100\n\n")
content.WriteString("# Redis配置\n")
content.WriteString("redis:\n")
if req.UseRedis {
content.WriteString(fmt.Sprintf(" host: %s\n", req.RedisHost))
content.WriteString(fmt.Sprintf(" port: %s\n", req.RedisPort))
content.WriteString(fmt.Sprintf(" password: %s\n", req.RedisPass))
content.WriteString(fmt.Sprintf(" db: %d\n", req.RedisDb))
} else {
content.WriteString(" host: localhost\n")
content.WriteString(" port: \"6379\"\n")
content.WriteString(" password: \"\"\n")
content.WriteString(" db: 0\n")
}
content.WriteString("\n# 日志配置\n")
content.WriteString("log:\n")
content.WriteString(" level: info\n")
content.WriteString(" format: json\n")
return content.String()
}
func saveConfigFile(content string) error {
dataDir := config.GetDataDir()
configPath := filepath.Join(dataDir, "config.yaml")
file, err := os.Create(configPath)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(content)
return err
}