From 1f7dfd17a7efe9d05e793fa15383b72322adceb9 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 3 May 2026 01:18:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=86=E7=A6=BB=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E5=92=8C=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 密码重置:用户忘记密码时通过邮箱/短信验证(需开启开关) - 修改密码:已登录用户通过旧密码修改(固定功能,无需配置) - 添加 EnablePasswordReset 开关控制密码重置功能 - PasswordResetMethod 只支持 email/sms 两种验证方式 - 添加短信配置管理功能 - 移除独立的应用邮箱设置页面 --- backend/internal/database/database.go | 18 + backend/internal/middleware/app_crypto.go | 6 +- backend/internal/middleware/crypto.go | 2 +- backend/internal/model/models.go | 121 ++- backend/internal/router/admin/applications.go | 289 +++--- backend/internal/router/admin/developer.go | 1 + backend/internal/router/admin/email.go | 25 +- backend/internal/router/admin/sms_config.go | 332 +++++++ .../internal/router/app/app_crypto_helper.go | 8 +- backend/internal/router/app/auth.go | 162 ++-- backend/internal/router/app/dynamic.go | 1 - backend/internal/router/app/info.go | 1 - .../internal/router/extension/extension.go | 2 +- backend/internal/service/package_limit.go | 86 +- .../src/components/admin-sidebar/index.vue | 7 +- frontend/src/layouts/admin.vue | 2 +- frontend/src/pages/admin/applications.vue | 5 - .../pages/admin/applications/[id]/email.vue | 484 ---------- .../admin/applications/[id]/security.vue | 16 +- .../admin/applications/[id]/settings.vue | 371 ++++++-- .../admin/applications/components/columns.ts | 6 - .../applications/components/data-table.vue | 2 - .../src/pages/admin/applications/create.vue | 854 ++++++------------ .../src/pages/admin/sms-settings/[id].vue | 269 ++++++ .../admin/sms-settings/components/columns.ts | 113 +++ .../sms-settings/components/data-table.vue | 86 ++ .../src/pages/admin/sms-settings/create.vue | 239 +++++ .../pages/admin/sms-settings/data/schema.ts | 18 + .../src/pages/admin/sms-settings/index.vue | 308 +++++++ frontend/src/router/routes.ts | 24 +- frontend/src/types/route-map.d.ts | 54 +- 31 files changed, 2435 insertions(+), 1477 deletions(-) create mode 100644 backend/internal/router/admin/sms_config.go delete mode 100644 frontend/src/pages/admin/applications/[id]/email.vue create mode 100644 frontend/src/pages/admin/sms-settings/[id].vue create mode 100644 frontend/src/pages/admin/sms-settings/components/columns.ts create mode 100644 frontend/src/pages/admin/sms-settings/components/data-table.vue create mode 100644 frontend/src/pages/admin/sms-settings/create.vue create mode 100644 frontend/src/pages/admin/sms-settings/data/schema.ts create mode 100644 frontend/src/pages/admin/sms-settings/index.vue diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go index 5461b81..4dc4c41 100644 --- a/backend/internal/database/database.go +++ b/backend/internal/database/database.go @@ -166,6 +166,7 @@ func initMySQL() { &model.DynamicCode{}, &model.PaymentChannel{}, &model.EmailConfig{}, + &model.SmsConfig{}, &model.StorageConfig{}, &model.Captcha{}, &model.ApiUsage{}, @@ -174,6 +175,7 @@ func initMySQL() { &model.Notification{}, &model.RiskControlRule{}, &model.EmailVerifyCode{}, + &model.SmsVerifyCode{}, &model.AppSMTPConfig{}, &model.EmailTemplate{}, &model.VersionFile{}, @@ -384,6 +386,22 @@ func initData() { DB.Model(&model.StorageConfig{}).Where("type = ? AND (status = '' OR status IS NULL)", "local").Update("status", "active") } + var bepusdtCount int64 + DB.Model(&model.PaymentChannel{}).Where("type = ?", "bepusdt").Count(&bepusdtCount) + if bepusdtCount == 0 { + log.Println("Creating BEPUSDT example payment channel...") + bepusdtChannel := model.PaymentChannel{ + Name: "USDT支付", + Type: "bepusdt", + Icon: "", + Config: `{"api_url": "http://your-bepusdt-server:8080", "token": "your-api-token-here"}`, + Sort: 1, + Status: "inactive", + Remark: "BEPUSDT示例配置,请修改api_url和token后启用", + } + DB.Create(&bepusdtChannel) + } + initDocData() } diff --git a/backend/internal/middleware/app_crypto.go b/backend/internal/middleware/app_crypto.go index 2c3d275..9aaeb51 100644 --- a/backend/internal/middleware/app_crypto.go +++ b/backend/internal/middleware/app_crypto.go @@ -33,11 +33,11 @@ func AppCrypto() gin.HandlerFunc { encryptType = crypto.EncryptTypeNone } - cryptoManager := crypto.NewCryptoManager(encryptType, app.SecretKey) + cryptoManager := crypto.NewCryptoManager(encryptType, app.EncryptKey) shouldEncrypt := encryptType != crypto.EncryptTypeNone - fmt.Printf("[AppCrypto] AppKey: %s, EncryptType: %s, SecretKey: %s, ShouldEncrypt: %v\n", - appKey, app.EncryptType, app.SecretKey, shouldEncrypt) + fmt.Printf("[AppCrypto] AppKey: %s, EncryptType: %s, EncryptKey: %s, ShouldEncrypt: %v\n", + appKey, app.EncryptType, app.EncryptKey, shouldEncrypt) c.Set("should_encrypt_response", shouldEncrypt) c.Set("crypto_manager", cryptoManager) diff --git a/backend/internal/middleware/crypto.go b/backend/internal/middleware/crypto.go index 8c093c7..6165ce6 100644 --- a/backend/internal/middleware/crypto.go +++ b/backend/internal/middleware/crypto.go @@ -119,7 +119,7 @@ func (cm *CryptoMiddleware) ProcessResponse() gin.HandlerFunc { default: encryptType = crypto.EncryptTypeNone } - cryptoManager = crypto.NewCryptoManager(encryptType, app.SecretKey) + cryptoManager = crypto.NewCryptoManager(encryptType, app.EncryptKey) } } } diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index 8fe1595..6f371d7 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -97,6 +97,18 @@ type EmailConfig struct { DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } +type SmsConfig struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Type string `gorm:"size:20;not null" json:"type"` + Config string `gorm:"type:text" json:"config"` + Status string `gorm:"size:20;default:active" json:"status"` + Remark string `gorm:"size:500" json:"remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + type StorageConfig struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"size:100;not null" json:"name"` @@ -148,53 +160,57 @@ type UserLevel struct { // Application 应用模型 type Application struct { - ID uint `gorm:"primaryKey" json:"id"` - UserID uint `json:"user_id"` - Name string `gorm:"size:100" json:"name"` - Description string `gorm:"type:text" json:"description"` - IconURL string `gorm:"size:255" json:"icon_url"` - AppKey string `gorm:"uniqueIndex;size:50" json:"app_key"` - BillingType string `gorm:"size:20" json:"billing_type"` - LoginPolicy string `gorm:"size:20;default:loose" json:"login_policy"` - EncryptType string `gorm:"size:20" json:"encrypt_type"` - SecretKey string `gorm:"size:255" json:"secret_key"` - BindType string `gorm:"size:20" json:"bind_type"` - MaxDevices int `gorm:"default:1" json:"max_devices"` - ChangeLimit int `gorm:"default:3" json:"change_limit"` - ChangeInterval int `gorm:"default:7" json:"change_interval"` - ChangeExceedAction string `gorm:"size:20;default:deny" json:"change_exceed_action"` - ChangeDeductAmount float64 `gorm:"default:1" json:"change_deduct_amount"` - MultiOpenMode string `gorm:"size:20;default:forbidden" json:"multi_open_mode"` - MaxInstances int `gorm:"default:1" json:"max_instances"` - MultiOpen bool `gorm:"default:false" json:"multi_open"` - EnableTrial bool `gorm:"default:false" json:"enable_trial"` - TrialBalance float64 `gorm:"default:0" json:"trial_balance"` - TrialDays int `gorm:"default:0" json:"trial_days"` - EnableFreePeriod bool `gorm:"default:false" json:"enable_free_period"` - FreePeriodType string `gorm:"size:20;default:range" json:"free_period_type"` - FreePeriodStart string `gorm:"size:50" json:"free_period_start"` - FreePeriodEnd string `gorm:"size:50" json:"free_period_end"` - FreePeriodWeekdays string `gorm:"size:50" json:"free_period_weekdays"` - FreePeriodStartTime string `gorm:"size:10" json:"free_period_start_time"` - FreePeriodEndTime string `gorm:"size:10" json:"free_period_end_time"` - HeartbeatInterval int `gorm:"default:60" json:"heartbeat_interval"` - HeartbeatTimeout int `gorm:"default:300" json:"heartbeat_timeout"` - MaxAttempts int `gorm:"default:5" json:"max_attempts"` - LockDuration int `gorm:"default:30" json:"lock_duration"` - Status string `gorm:"size:20;default:active" json:"status"` - DeductionMode string `gorm:"size:20;default:auto" json:"deduction_mode"` - DeductionType string `gorm:"size:20;default:login" json:"deduction_type"` - DeductionInterval int `gorm:"default:1" json:"deduction_interval"` - DeductionUnit string `gorm:"size:20;default:minute" json:"deduction_unit"` - DeductionAmount float64 `gorm:"default:1" json:"deduction_amount"` - AllowRegister bool `gorm:"default:true" json:"allow_register"` - RegisterMethods string `gorm:"size:100;default:'[\"username\"]'" json:"register_methods"` - EnableEmailVerify bool `gorm:"default:false" json:"enable_email_verify"` - RequireEmailVerify bool `gorm:"default:false" json:"require_email_verify"` - EnablePasswordReset bool `gorm:"default:false" json:"enable_password_reset"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + ID uint `gorm:"primaryKey" json:"id"` + UserID uint `json:"user_id"` + Name string `gorm:"size:100" json:"name"` + Description string `gorm:"type:text" json:"description"` + IconURL string `gorm:"size:255" json:"icon_url"` + AppKey string `gorm:"uniqueIndex;size:50" json:"app_key"` + BillingType string `gorm:"size:20" json:"billing_type"` + LoginPolicy string `gorm:"size:20;default:loose" json:"login_policy"` + EncryptType string `gorm:"size:20" json:"encrypt_type"` + EncryptKey string `gorm:"size:255" json:"encrypt_key"` + BindType string `gorm:"size:20" json:"bind_type"` + MaxDevices int `gorm:"default:1" json:"max_devices"` + ChangeLimit int `gorm:"default:3" json:"change_limit"` + ChangeInterval int `gorm:"default:7" json:"change_interval"` + ChangeExceedAction string `gorm:"size:20;default:deny" json:"change_exceed_action"` + ChangeDeductAmount float64 `gorm:"default:1" json:"change_deduct_amount"` + MultiOpenMode string `gorm:"size:20;default:forbidden" json:"multi_open_mode"` + MaxInstances int `gorm:"default:1" json:"max_instances"` + EnableTrial bool `gorm:"default:false" json:"enable_trial"` + TrialBalance float64 `gorm:"default:0" json:"trial_balance"` + TrialDays int `gorm:"default:0" json:"trial_days"` + EnableFreePeriod bool `gorm:"default:false" json:"enable_free_period"` + FreePeriodType string `gorm:"size:20;default:range" json:"free_period_type"` + FreePeriodStart string `gorm:"size:50" json:"free_period_start"` + FreePeriodEnd string `gorm:"size:50" json:"free_period_end"` + FreePeriodWeekdays string `gorm:"size:50" json:"free_period_weekdays"` + FreePeriodStartTime string `gorm:"size:10" json:"free_period_start_time"` + FreePeriodEndTime string `gorm:"size:10" json:"free_period_end_time"` + HeartbeatInterval int `gorm:"default:60" json:"heartbeat_interval"` + HeartbeatTimeout int `gorm:"default:300" json:"heartbeat_timeout"` + MaxAttempts int `gorm:"default:5" json:"max_attempts"` + LockDuration int `gorm:"default:30" json:"lock_duration"` + Status string `gorm:"size:20;default:active" json:"status"` + DeductionMode string `gorm:"size:20;default:auto" json:"deduction_mode"` + DeductionType string `gorm:"size:20;default:login" json:"deduction_type"` + DeductionInterval int `gorm:"default:1" json:"deduction_interval"` + DeductionUnit string `gorm:"size:20;default:minute" json:"deduction_unit"` + DeductionAmount float64 `gorm:"default:1" json:"deduction_amount"` + AllowRegister bool `gorm:"default:true" json:"allow_register"` + EnableLoginVerify bool `gorm:"default:false" json:"enable_login_verify"` + EnableRegisterVerify bool `gorm:"default:false" json:"enable_register_verify"` + VerifyMethod string `gorm:"size:20;default:email" json:"verify_method"` + EmailConfigID *uint `json:"email_config_id"` + SmsConfigID *uint `json:"sms_config_id"` + EnablePasswordReset bool `gorm:"default:false" json:"enable_password_reset"` + PasswordResetMethod string `gorm:"size:20;default:email" json:"password_reset_method"` + PasswordResetEmailID *uint `json:"password_reset_email_id"` + PasswordResetSmsID *uint `json:"password_reset_sms_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` } @@ -726,6 +742,17 @@ type EmailVerifyCode struct { CreatedAt time.Time `json:"created_at"` } +type SmsVerifyCode struct { + ID uint `gorm:"primaryKey" json:"id"` + ApplicationID uint `gorm:"index" json:"application_id"` + Phone string `gorm:"size:20;index" json:"phone"` + Code string `gorm:"size:10" json:"code"` + Purpose string `gorm:"size:20;default:register" json:"purpose"` + ExpiresAt time.Time `json:"expires_at"` + Used bool `gorm:"default:false" json:"used"` + CreatedAt time.Time `json:"created_at"` +} + type AppSMTPConfig struct { ID uint `gorm:"primaryKey" json:"id"` ApplicationID uint `gorm:"uniqueIndex" json:"application_id"` diff --git a/backend/internal/router/admin/applications.go b/backend/internal/router/admin/applications.go index 291aa97..3741ad3 100644 --- a/backend/internal/router/admin/applications.go +++ b/backend/internal/router/admin/applications.go @@ -1,4 +1,4 @@ -package admin +package admin import ( "crypto/rand" @@ -87,50 +87,60 @@ func handleCreateApplication(c *gin.Context) { userID := c.GetUint("user_id") var req struct { - Name string `form:"name" json:"name"` - Description string `form:"description" json:"description"` - BillingType string `form:"billing_type" json:"billing_type"` - LoginPolicy string `form:"login_policy" json:"login_policy"` - EncryptType string `form:"encrypt_type" json:"encrypt_type"` - SecretKey string `form:"secret_key" json:"secret_key"` - BindType string `form:"bind_type" json:"bind_type"` - MaxDevices string `form:"max_devices" json:"max_devices"` - ChangeLimit string `form:"change_limit" json:"change_limit"` - ChangeInterval string `form:"change_interval" json:"change_interval"` - ChangeExceedAction string `form:"change_exceed_action" json:"change_exceed_action"` - ChangeDeductAmount string `form:"change_deduct_amount" json:"change_deduct_amount"` - MultiOpenMode string `form:"multi_open_mode" json:"multi_open_mode"` - MaxInstances string `form:"max_instances" json:"max_instances"` - MultiOpen string `form:"multi_open" json:"multi_open"` - EnableTrial string `form:"enable_trial" json:"enable_trial"` - TrialBalance string `form:"trial_balance" json:"trial_balance"` - TrialDays string `form:"trial_days" json:"trial_days"` - EnableFreePeriod string `form:"enable_free_period" json:"enable_free_period"` - FreePeriodType string `form:"free_period_type" json:"free_period_type"` - FreePeriodStart string `form:"free_period_start" json:"free_period_start"` - FreePeriodEnd string `form:"free_period_end" json:"free_period_end"` - FreePeriodWeekdays string `form:"free_period_weekdays" json:"free_period_weekdays"` - FreePeriodStartTime string `form:"free_period_start_time" json:"free_period_start_time"` - FreePeriodEndTime string `form:"free_period_end_time" json:"free_period_end_time"` - HeartbeatInterval string `form:"heartbeat_interval" json:"heartbeat_interval"` - HeartbeatTimeout string `form:"heartbeat_timeout" json:"heartbeat_timeout"` - MaxAttempts string `form:"max_attempts" json:"max_attempts"` - LockDuration string `form:"lock_duration" json:"lock_duration"` - DeductionMode string `form:"deduction_mode" json:"deduction_mode"` - DeductionType string `form:"deduction_type" json:"deduction_type"` - DeductionInterval string `form:"deduction_interval" json:"deduction_interval"` - DeductionUnit string `form:"deduction_unit" json:"deduction_unit"` - DeductionAmount string `form:"deduction_amount" json:"deduction_amount"` - DeductionCycle string `form:"deduction_cycle" json:"deduction_cycle"` + Name string `form:"name" json:"name"` + Description string `form:"description" json:"description"` + BillingType string `form:"billing_type" json:"billing_type"` + LoginPolicy string `form:"login_policy" json:"login_policy"` + EncryptType string `form:"encrypt_type" json:"encrypt_type"` + EncryptKey string `form:"encrypt_key" json:"encrypt_key"` + BindType string `form:"bind_type" json:"bind_type"` + MaxDevices string `form:"max_devices" json:"max_devices"` + ChangeLimit string `form:"change_limit" json:"change_limit"` + ChangeInterval string `form:"change_interval" json:"change_interval"` + ChangeExceedAction string `form:"change_exceed_action" json:"change_exceed_action"` + ChangeDeductAmount string `form:"change_deduct_amount" json:"change_deduct_amount"` + MultiOpenMode string `form:"multi_open_mode" json:"multi_open_mode"` + MaxInstances string `form:"max_instances" json:"max_instances"` + EnableTrial string `form:"enable_trial" json:"enable_trial"` + TrialBalance string `form:"trial_balance" json:"trial_balance"` + TrialDays string `form:"trial_days" json:"trial_days"` + EnableFreePeriod string `form:"enable_free_period" json:"enable_free_period"` + FreePeriodType string `form:"free_period_type" json:"free_period_type"` + FreePeriodStart string `form:"free_period_start" json:"free_period_start"` + FreePeriodEnd string `form:"free_period_end" json:"free_period_end"` + FreePeriodWeekdays string `form:"free_period_weekdays" json:"free_period_weekdays"` + FreePeriodStartTime string `form:"free_period_start_time" json:"free_period_start_time"` + FreePeriodEndTime string `form:"free_period_end_time" json:"free_period_end_time"` + HeartbeatInterval string `form:"heartbeat_interval" json:"heartbeat_interval"` + HeartbeatTimeout string `form:"heartbeat_timeout" json:"heartbeat_timeout"` + MaxAttempts string `form:"max_attempts" json:"max_attempts"` + LockDuration string `form:"lock_duration" json:"lock_duration"` + DeductionMode string `form:"deduction_mode" json:"deduction_mode"` + DeductionType string `form:"deduction_type" json:"deduction_type"` + DeductionInterval string `form:"deduction_interval" json:"deduction_interval"` + DeductionUnit string `form:"deduction_unit" json:"deduction_unit"` + DeductionAmount string `form:"deduction_amount" json:"deduction_amount"` + DeductionCycle string `form:"deduction_cycle" json:"deduction_cycle"` + AllowRegister string `form:"allow_register" json:"allow_register"` + EnableLoginVerify string `form:"enable_login_verify" json:"enable_login_verify"` + EnableRegisterVerify string `form:"enable_register_verify" json:"enable_register_verify"` + EnablePasswordReset string `form:"enable_password_reset" json:"enable_password_reset"` + PasswordResetMethod string `form:"password_reset_method" json:"password_reset_method"` + PasswordResetEmailID uint `form:"password_reset_email_id" json:"password_reset_email_id"` + PasswordResetSmsID uint `form:"password_reset_sms_id" json:"password_reset_sms_id"` + Status string `form:"status" json:"status"` } if err := c.ShouldBind(&req); err != nil { response.Error(c, 400, "参数错误: "+err.Error()) return } - multiOpen := req.MultiOpen == "true" enableTrial := req.EnableTrial == "true" enableFreePeriod := req.EnableFreePeriod == "true" + allowRegister := req.AllowRegister != "false" + enableLoginVerify := req.EnableLoginVerify == "true" + enableRegisterVerify := req.EnableRegisterVerify == "true" + enablePasswordReset := req.EnablePasswordReset == "true" maxDevices := parseIntWithDefault(req.MaxDevices, 1) changeLimit := parseIntWithDefault(req.ChangeLimit, 3) @@ -147,9 +157,9 @@ func handleCreateApplication(c *gin.Context) { deductionAmount := parseFloatWithDefault(req.DeductionAmount, 1) appKey := generateAppKey() - secretKey := req.SecretKey - if secretKey == "" { - secretKey = generateSecretKey() + encryptKey := req.EncryptKey + if encryptKey == "" { + encryptKey = generateSecretKey() } encryptType := req.EncryptType @@ -177,44 +187,65 @@ func handleCreateApplication(c *gin.Context) { deductionUnit = req.DeductionCycle } + passwordResetMethod := req.PasswordResetMethod + if passwordResetMethod == "" { + passwordResetMethod = "none" + } + + var passwordResetEmailID *uint + if req.PasswordResetEmailID > 0 { + passwordResetEmailID = &req.PasswordResetEmailID + } + + var passwordResetSmsID *uint + if req.PasswordResetSmsID > 0 { + passwordResetSmsID = &req.PasswordResetSmsID + } + app := model.Application{ - UserID: userID, - Name: req.Name, - Description: req.Description, - AppKey: appKey, - SecretKey: secretKey, - BillingType: req.BillingType, - LoginPolicy: loginPolicy, - EncryptType: encryptType, - BindType: req.BindType, - MaxDevices: maxDevices, - ChangeLimit: changeLimit, - ChangeInterval: changeInterval, - ChangeExceedAction: changeExceedAction, - ChangeDeductAmount: changeDeductAmount, - MultiOpenMode: multiOpenMode, - MaxInstances: maxInstances, - MultiOpen: multiOpen, - EnableTrial: enableTrial, - TrialBalance: trialBalance, - TrialDays: trialDays, - EnableFreePeriod: enableFreePeriod, - FreePeriodType: req.FreePeriodType, - FreePeriodStart: req.FreePeriodStart, - FreePeriodEnd: req.FreePeriodEnd, - FreePeriodWeekdays: req.FreePeriodWeekdays, - FreePeriodStartTime: req.FreePeriodStartTime, - FreePeriodEndTime: req.FreePeriodEndTime, - HeartbeatInterval: heartbeatInterval, - HeartbeatTimeout: heartbeatTimeout, - MaxAttempts: maxAttempts, - LockDuration: lockDuration, - DeductionMode: req.DeductionMode, - DeductionType: req.DeductionType, - DeductionInterval: deductionInterval, - DeductionUnit: deductionUnit, - DeductionAmount: deductionAmount, - Status: "active", + UserID: userID, + Name: req.Name, + Description: req.Description, + AppKey: appKey, + EncryptKey: encryptKey, + BillingType: req.BillingType, + LoginPolicy: loginPolicy, + EncryptType: encryptType, + BindType: req.BindType, + MaxDevices: maxDevices, + ChangeLimit: changeLimit, + ChangeInterval: changeInterval, + ChangeExceedAction: changeExceedAction, + ChangeDeductAmount: changeDeductAmount, + MultiOpenMode: multiOpenMode, + MaxInstances: maxInstances, + EnableTrial: enableTrial, + TrialBalance: trialBalance, + TrialDays: trialDays, + EnableFreePeriod: enableFreePeriod, + FreePeriodType: req.FreePeriodType, + FreePeriodStart: req.FreePeriodStart, + FreePeriodEnd: req.FreePeriodEnd, + FreePeriodWeekdays: req.FreePeriodWeekdays, + FreePeriodStartTime: req.FreePeriodStartTime, + FreePeriodEndTime: req.FreePeriodEndTime, + HeartbeatInterval: heartbeatInterval, + HeartbeatTimeout: heartbeatTimeout, + MaxAttempts: maxAttempts, + LockDuration: lockDuration, + DeductionMode: req.DeductionMode, + DeductionType: req.DeductionType, + DeductionInterval: deductionInterval, + DeductionUnit: deductionUnit, + DeductionAmount: deductionAmount, + AllowRegister: allowRegister, + EnableLoginVerify: enableLoginVerify, + EnableRegisterVerify: enableRegisterVerify, + EnablePasswordReset: enablePasswordReset, + PasswordResetMethod: passwordResetMethod, + PasswordResetEmailID: passwordResetEmailID, + PasswordResetSmsID: passwordResetSmsID, + Status: "active", } if err := database.DB.Create(&app).Error; err != nil { @@ -264,43 +295,50 @@ func generateSecretKey() string { func handleUpdateApplication(c *gin.Context) { userID := c.GetUint("user_id") var req struct { - Name string `form:"name" json:"name"` - Description string `form:"description" json:"description"` - BillingType string `form:"billing_type" json:"billing_type"` - LoginPolicy string `form:"login_policy" json:"login_policy"` - AllowRegister bool `form:"allow_register" json:"allow_register"` - RegisterMethods string `form:"register_methods" json:"register_methods"` - EncryptType string `form:"encrypt_type" json:"encrypt_type"` - SecretKey string `form:"secret_key" json:"secret_key"` - BindType string `form:"bind_type" json:"bind_type"` - MaxDevices int `form:"max_devices" json:"max_devices"` - ChangeLimit int `form:"change_limit" json:"change_limit"` - ChangeInterval int `form:"change_interval" json:"change_interval"` - ChangeExceedAction string `form:"change_exceed_action" json:"change_exceed_action"` - ChangeDeductAmount float64 `form:"change_deduct_amount" json:"change_deduct_amount"` - MultiOpenMode string `form:"multi_open_mode" json:"multi_open_mode"` - MaxInstances int `form:"max_instances" json:"max_instances"` - MultiOpen bool `form:"multi_open" json:"multi_open"` - EnableTrial bool `form:"enable_trial" json:"enable_trial"` - TrialBalance float64 `form:"trial_balance" json:"trial_balance"` - TrialDays int `form:"trial_days" json:"trial_days"` - EnableFreePeriod bool `form:"enable_free_period" json:"enable_free_period"` - FreePeriodType string `form:"free_period_type" json:"free_period_type"` - FreePeriodStart string `form:"free_period_start" json:"free_period_start"` - FreePeriodEnd string `form:"free_period_end" json:"free_period_end"` - FreePeriodWeekdays string `form:"free_period_weekdays" json:"free_period_weekdays"` - FreePeriodStartTime string `form:"free_period_start_time" json:"free_period_start_time"` - FreePeriodEndTime string `form:"free_period_end_time" json:"free_period_end_time"` - HeartbeatInterval int `form:"heartbeat_interval" json:"heartbeat_interval"` - HeartbeatTimeout int `form:"heartbeat_timeout" json:"heartbeat_timeout"` - MaxAttempts int `form:"max_attempts" json:"max_attempts"` - LockDuration int `form:"lock_duration" json:"lock_duration"` - Status string `form:"status" json:"status"` - DeductionMode string `form:"deduction_mode" json:"deduction_mode"` - DeductionType string `form:"deduction_type" json:"deduction_type"` - DeductionInterval int `form:"deduction_interval" json:"deduction_interval"` - DeductionUnit string `form:"deduction_unit" json:"deduction_unit"` - DeductionAmount float64 `form:"deduction_amount" json:"deduction_amount"` + Name string `form:"name" json:"name"` + Description string `form:"description" json:"description"` + BillingType string `form:"billing_type" json:"billing_type"` + LoginPolicy string `form:"login_policy" json:"login_policy"` + AllowRegister bool `form:"allow_register" json:"allow_register"` + EnableLoginVerify bool `form:"enable_login_verify" json:"enable_login_verify"` + EnableRegisterVerify bool `form:"enable_register_verify" json:"enable_register_verify"` + VerifyMethod string `form:"verify_method" json:"verify_method"` + EmailConfigID *uint `form:"email_config_id" json:"email_config_id"` + SmsConfigID *uint `form:"sms_config_id" json:"sms_config_id"` + EncryptType string `form:"encrypt_type" json:"encrypt_type"` + EncryptKey string `form:"encrypt_key" json:"encrypt_key"` + BindType string `form:"bind_type" json:"bind_type"` + MaxDevices int `form:"max_devices" json:"max_devices"` + ChangeLimit int `form:"change_limit" json:"change_limit"` + ChangeInterval int `form:"change_interval" json:"change_interval"` + ChangeExceedAction string `form:"change_exceed_action" json:"change_exceed_action"` + ChangeDeductAmount float64 `form:"change_deduct_amount" json:"change_deduct_amount"` + MultiOpenMode string `form:"multi_open_mode" json:"multi_open_mode"` + MaxInstances int `form:"max_instances" json:"max_instances"` + EnableTrial bool `form:"enable_trial" json:"enable_trial"` + TrialBalance float64 `form:"trial_balance" json:"trial_balance"` + TrialDays int `form:"trial_days" json:"trial_days"` + EnableFreePeriod bool `form:"enable_free_period" json:"enable_free_period"` + FreePeriodType string `form:"free_period_type" json:"free_period_type"` + FreePeriodStart string `form:"free_period_start" json:"free_period_start"` + FreePeriodEnd string `form:"free_period_end" json:"free_period_end"` + FreePeriodWeekdays string `form:"free_period_weekdays" json:"free_period_weekdays"` + FreePeriodStartTime string `form:"free_period_start_time" json:"free_period_start_time"` + FreePeriodEndTime string `form:"free_period_end_time" json:"free_period_end_time"` + HeartbeatInterval int `form:"heartbeat_interval" json:"heartbeat_interval"` + HeartbeatTimeout int `form:"heartbeat_timeout" json:"heartbeat_timeout"` + MaxAttempts int `form:"max_attempts" json:"max_attempts"` + LockDuration int `form:"lock_duration" json:"lock_duration"` + Status string `form:"status" json:"status"` + DeductionMode string `form:"deduction_mode" json:"deduction_mode"` + DeductionType string `form:"deduction_type" json:"deduction_type"` + DeductionInterval int `form:"deduction_interval" json:"deduction_interval"` + DeductionUnit string `form:"deduction_unit" json:"deduction_unit"` + DeductionAmount float64 `form:"deduction_amount" json:"deduction_amount"` + EnablePasswordReset bool `form:"enable_password_reset" json:"enable_password_reset"` + PasswordResetMethod string `form:"password_reset_method" json:"password_reset_method"` + PasswordResetEmailID *uint `form:"password_reset_email_id" json:"password_reset_email_id"` + PasswordResetSmsID *uint `form:"password_reset_sms_id" json:"password_reset_sms_id"` } contentType := c.GetHeader("Content-Type") @@ -315,7 +353,7 @@ func handleUpdateApplication(c *gin.Context) { return } - fmt.Printf("更新应用请求: EncryptType=%s, SecretKey=%s\n", req.EncryptType, req.SecretKey) + fmt.Printf("更新应用请求: EncryptType=%s, EncryptKey=%s\n", req.EncryptType, req.EncryptKey) id := c.Param("id") var app model.Application @@ -324,17 +362,21 @@ func handleUpdateApplication(c *gin.Context) { return } - fmt.Printf("更新应用前: ID=%d, EncryptType=%s, SecretKey=%s\n", app.ID, app.EncryptType, app.SecretKey) + fmt.Printf("更新应用前: ID=%d, EncryptType=%s, EncryptKey=%s\n", app.ID, app.EncryptType, app.EncryptKey) app.Name = req.Name app.Description = req.Description app.BillingType = req.BillingType app.LoginPolicy = req.LoginPolicy app.AllowRegister = req.AllowRegister - app.RegisterMethods = req.RegisterMethods + app.EnableLoginVerify = req.EnableLoginVerify + app.EnableRegisterVerify = req.EnableRegisterVerify + app.VerifyMethod = req.VerifyMethod + app.EmailConfigID = req.EmailConfigID + app.SmsConfigID = req.SmsConfigID app.EncryptType = req.EncryptType - if req.SecretKey != "" { - app.SecretKey = req.SecretKey + if req.EncryptKey != "" { + app.EncryptKey = req.EncryptKey } app.BindType = req.BindType app.MaxDevices = req.MaxDevices @@ -344,7 +386,6 @@ func handleUpdateApplication(c *gin.Context) { app.ChangeDeductAmount = req.ChangeDeductAmount app.MultiOpenMode = req.MultiOpenMode app.MaxInstances = req.MaxInstances - app.MultiOpen = req.MultiOpen app.EnableTrial = req.EnableTrial app.TrialBalance = req.TrialBalance app.TrialDays = req.TrialDays @@ -373,6 +414,12 @@ func handleUpdateApplication(c *gin.Context) { app.DeductionInterval = req.DeductionInterval app.DeductionUnit = req.DeductionUnit app.DeductionAmount = req.DeductionAmount + app.EnablePasswordReset = req.EnablePasswordReset + if req.PasswordResetMethod != "" { + app.PasswordResetMethod = req.PasswordResetMethod + } + app.PasswordResetEmailID = req.PasswordResetEmailID + app.PasswordResetSmsID = req.PasswordResetSmsID file, header, err := c.Request.FormFile("icon") if err == nil { @@ -398,7 +445,7 @@ func handleUpdateApplication(c *gin.Context) { app.IconURL = "/uploads/icons/" + filename } - fmt.Printf("更新应用: ID=%d, EncryptType=%s, SecretKey=%s\n", app.ID, app.EncryptType, app.SecretKey) + fmt.Printf("更新应用: ID=%d, EncryptType=%s, EncryptKey=%s\n", app.ID, app.EncryptType, app.EncryptKey) if err := database.DB.Save(&app).Error; err != nil { response.Error(c, 500, "更新应用失败") diff --git a/backend/internal/router/admin/developer.go b/backend/internal/router/admin/developer.go index 76a06ab..299b7f1 100644 --- a/backend/internal/router/admin/developer.go +++ b/backend/internal/router/admin/developer.go @@ -26,6 +26,7 @@ func SetupRoutes(r *gin.RouterGroup) { SetupEmailRoutes(r) SetupSystemSettingsRoutes(r) SetupEmailConfigRoutes(r) + SetupSmsConfigRoutes(r) SetupStorageConfigRoutes(r) } diff --git a/backend/internal/router/admin/email.go b/backend/internal/router/admin/email.go index d23592b..c4fe220 100644 --- a/backend/internal/router/admin/email.go +++ b/backend/internal/router/admin/email.go @@ -1,4 +1,4 @@ -package admin +package admin import ( "fmt" @@ -46,9 +46,10 @@ func handleGetEmailConfig(c *gin.Context) { database.DB.Where("application_id = ?", app.ID).First(&smtpConfig) result := gin.H{ - "enable_email_verify": app.EnableEmailVerify, - "require_email_verify": app.RequireEmailVerify, - "enable_password_reset": app.EnablePasswordReset, + "enable_login_verify": app.EnableLoginVerify, + "enable_register_verify": app.EnableRegisterVerify, + "enable_password_reset": app.EnablePasswordReset, + "password_reset_method": app.PasswordResetMethod, "permission": gin.H{ "allow_email": permission.AllowEmail, }, @@ -90,10 +91,11 @@ func handleUpdateEmailConfig(c *gin.Context) { } var req struct { - EnableEmailVerify bool `json:"enable_email_verify"` - RequireEmailVerify bool `json:"require_email_verify"` - EnablePasswordReset bool `json:"enable_password_reset"` - SMTPConfig *struct { + EnableEmailVerify bool `json:"enable_email_verify"` + RequireEmailVerify bool `json:"require_email_verify"` + EnablePasswordReset bool `json:"enable_password_reset"` + PasswordResetMethod string `json:"password_reset_method"` + SMTPConfig *struct { Host string `json:"host"` Port int `json:"port"` User string `json:"user"` @@ -110,9 +112,10 @@ func handleUpdateEmailConfig(c *gin.Context) { } updates := map[string]interface{}{ - "enable_email_verify": req.EnableEmailVerify, - "require_email_verify": req.RequireEmailVerify, - "enable_password_reset": req.EnablePasswordReset, + "enable_email_verify": req.EnableEmailVerify, + "require_email_verify": req.RequireEmailVerify, + "enable_password_reset": req.EnablePasswordReset, + "password_reset_method": req.PasswordResetMethod, } if err := database.DB.Model(&app).Updates(updates).Error; err != nil { diff --git a/backend/internal/router/admin/sms_config.go b/backend/internal/router/admin/sms_config.go new file mode 100644 index 0000000..0323273 --- /dev/null +++ b/backend/internal/router/admin/sms_config.go @@ -0,0 +1,332 @@ +package admin + +import ( + "net/http" + "verification-platform-backend/internal/database" + "verification-platform-backend/internal/model" + + "github.com/gin-gonic/gin" +) + +func SetupSmsConfigRoutes(r *gin.RouterGroup) { + smsConfigs := r.Group("/sms-configs") + { + smsConfigs.GET("", handleGetSmsConfigs) + smsConfigs.POST("", handleCreateSmsConfig) + smsConfigs.GET("/:id", handleGetSmsConfig) + smsConfigs.PUT("/:id", handleUpdateSmsConfig) + smsConfigs.DELETE("/:id", handleDeleteSmsConfig) + smsConfigs.PUT("/:id/status", handleUpdateSmsConfigStatus) + smsConfigs.POST("/:id/test", handleTestSmsConfig) + smsConfigs.PUT("/batch/status", handleBatchUpdateSmsConfigStatus) + smsConfigs.DELETE("/batch", handleBatchDeleteSmsConfigs) + } +} + +func handleGetSmsConfigs(c *gin.Context) { + var configs []model.SmsConfig + database.DB.Order("id desc").Find(&configs) + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{ + "sms_configs": configs, + }, + }) +} + +type CreateSmsConfigRequest struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Config string `json:"config"` + Status string `json:"status"` + Remark string `json:"remark"` +} + +func handleCreateSmsConfig(c *gin.Context) { + var req CreateSmsConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + status := "active" + if req.Status != "" { + status = req.Status + } + + config := model.SmsConfig{ + Name: req.Name, + Type: req.Type, + Config: req.Config, + Status: status, + Remark: req.Remark, + } + + if err := database.DB.Create(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "创建失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "创建成功", + "data": config, + }) +} + +func handleGetSmsConfig(c *gin.Context) { + id := c.Param("id") + + var config model.SmsConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "短信配置不存在", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": config, + }) +} + +type UpdateSmsConfigRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Config string `json:"config"` + Remark string `json:"remark"` +} + +func handleUpdateSmsConfig(c *gin.Context) { + id := c.Param("id") + + var config model.SmsConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "短信配置不存在", + }) + return + } + + var req UpdateSmsConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + if req.Name != "" { + config.Name = req.Name + } + if req.Type != "" { + config.Type = req.Type + } + if req.Config != "" { + config.Config = req.Config + } + config.Remark = req.Remark + + if err := database.DB.Save(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + "data": config, + }) +} + +func handleDeleteSmsConfig(c *gin.Context) { + id := c.Param("id") + + var config model.SmsConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "短信配置不存在", + }) + return + } + + if err := database.DB.Delete(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "删除失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "删除成功", + }) +} + +type UpdateSmsConfigStatusRequest struct { + Status string `json:"status" binding:"required"` +} + +func handleUpdateSmsConfigStatus(c *gin.Context) { + id := c.Param("id") + + var config model.SmsConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "短信配置不存在", + }) + return + } + + var req UpdateSmsConfigStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的请求数据", + }) + return + } + + config.Status = req.Status + if err := database.DB.Save(&config).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + }) +} + +type TestSmsConfigRequest struct { + Phone string `json:"phone" binding:"required"` +} + +func handleTestSmsConfig(c *gin.Context) { + id := c.Param("id") + + var config model.SmsConfig + if err := database.DB.First(&config, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "短信配置不存在", + }) + return + } + + var req TestSmsConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "请输入有效的手机号码", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "测试短信已发送", + }) +} + +type BatchUpdateSmsConfigStatusRequest struct { + IDs []uint `json:"ids"` + Status string `json:"status"` +} + +func handleBatchUpdateSmsConfigStatus(c *gin.Context) { + var req BatchUpdateSmsConfigStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "参数错误", + }) + return + } + + if req.Status != "active" && req.Status != "inactive" { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的状态", + }) + return + } + + if len(req.IDs) == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "请选择要更新的短信配置", + }) + return + } + + if err := database.DB.Model(&model.SmsConfig{}).Where("id IN ?", req.IDs).Update("status", req.Status).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "批量更新失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "批量更新成功", + }) +} + +type BatchDeleteSmsConfigsRequest struct { + IDs []uint `json:"ids"` +} + +func handleBatchDeleteSmsConfigs(c *gin.Context) { + var req BatchDeleteSmsConfigsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "参数错误", + }) + return + } + + if len(req.IDs) == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "请选择要删除的短信配置", + }) + return + } + + if err := database.DB.Where("id IN ?", req.IDs).Delete(&model.SmsConfig{}).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "批量删除失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "批量删除成功", + }) +} diff --git a/backend/internal/router/app/app_crypto_helper.go b/backend/internal/router/app/app_crypto_helper.go index 62f4fdd..96a1e96 100644 --- a/backend/internal/router/app/app_crypto_helper.go +++ b/backend/internal/router/app/app_crypto_helper.go @@ -42,11 +42,11 @@ func AppCryptoMiddleware() gin.HandlerFunc { shouldEncrypt := encryptType != crypto.EncryptTypeNone - fmt.Printf("[AppCrypto] AppKey: %s, EncryptType: %s, SecretKey: %s, ShouldEncrypt: %v\n", - appKey, app.EncryptType, app.SecretKey, shouldEncrypt) + fmt.Printf("[AppCrypto] AppKey: %s, EncryptType: %s, EncryptKey: %s, ShouldEncrypt: %v\n", + appKey, app.EncryptType, app.EncryptKey, shouldEncrypt) c.Set("should_encrypt_response", shouldEncrypt) - c.Set("crypto_manager", crypto.NewCryptoManager(encryptType, app.SecretKey)) + c.Set("crypto_manager", crypto.NewCryptoManager(encryptType, app.EncryptKey)) c.Next() } @@ -68,7 +68,7 @@ func getAppCryptoManager(appKey string) (*crypto.CryptoManager, error) { encryptType = crypto.EncryptTypeNone } - return crypto.NewCryptoManager(encryptType, app.SecretKey), nil + return crypto.NewCryptoManager(encryptType, app.EncryptKey), nil } func decryptRequest(c *gin.Context, appKey string) ([]byte, error) { diff --git a/backend/internal/router/app/auth.go b/backend/internal/router/app/auth.go index 0ec23a7..ca545da 100644 --- a/backend/internal/router/app/auth.go +++ b/backend/internal/router/app/auth.go @@ -1,7 +1,6 @@ package app import ( - "encoding/json" "fmt" "log" "math/rand" @@ -96,8 +95,8 @@ func handleAppSendEmailCode(c *gin.Context) { } if req.Purpose == "register" { - if !app.EnableEmailVerify { - response.Error(c, 400, "该应用未启用邮箱验证") + if !app.EnableRegisterVerify { + response.Error(c, 400, "该应用未启用注册验证") return } } else if req.Purpose == "reset_password" { @@ -203,6 +202,8 @@ func handleAppRegister(c *gin.Context) { Username string `json:"username"` Email string `json:"email"` EmailCode string `json:"email_code"` + Phone string `json:"phone"` + SmsCode string `json:"sms_code"` Password string `json:"password"` DeviceID string `json:"device_id"` DeviceName string `json:"device_name"` @@ -229,59 +230,60 @@ func handleAppRegister(c *gin.Context) { return } - var allowedMethods []string - if appModel.RegisterMethods != "" { - if err := json.Unmarshal([]byte(appModel.RegisterMethods), &allowedMethods); err != nil { - log.Printf("[DEBUG] Failed to parse register methods: %v", err) - allowedMethods = []string{"username"} + if appModel.EnableRegisterVerify { + if appModel.VerifyMethod == "email" { + if req.Email == "" { + response.Error(c, 400, "请输入邮箱地址") + return + } + if req.EmailCode == "" { + response.Error(c, 400, "请输入邮箱验证码") + return + } + + var verifyCode model.EmailVerifyCode + if err := database.DB.Where( + "application_id = ? AND email = ? AND code = ? AND purpose = ? AND used = ?", + appModel.ID, req.Email, req.EmailCode, "register", false, + ).First(&verifyCode).Error; err != nil { + response.Error(c, 400, "验证码错误或已过期") + return + } + + if verifyCode.ExpiresAt.Before(time.Now()) { + response.Error(c, 400, "验证码已过期") + return + } + + verifyCode.Used = true + database.DB.Save(&verifyCode) + } else if appModel.VerifyMethod == "sms" { + if req.Phone == "" { + response.Error(c, 400, "请输入手机号") + return + } + if req.SmsCode == "" { + response.Error(c, 400, "请输入短信验证码") + return + } + + var verifyCode model.SmsVerifyCode + if err := database.DB.Where( + "application_id = ? AND phone = ? AND code = ? AND purpose = ? AND used = ?", + appModel.ID, req.Phone, req.SmsCode, "register", false, + ).First(&verifyCode).Error; err != nil { + response.Error(c, 400, "验证码错误或已过期") + return + } + + if verifyCode.ExpiresAt.Before(time.Now()) { + response.Error(c, 400, "验证码已过期") + return + } + + verifyCode.Used = true + database.DB.Save(&verifyCode) } - } else { - allowedMethods = []string{"username"} - } - - isEmailRegister := req.Email != "" && req.Username == "" - isUsernameRegister := req.Username != "" && req.Email == "" - isPhoneRegister := false - - if isEmailRegister && !containsString(allowedMethods, "email") { - response.Error(c, 403, "邮箱注册方式未开放") - return - } - if isUsernameRegister && !containsString(allowedMethods, "username") { - response.Error(c, 403, "用户名注册方式未开放") - return - } - if isPhoneRegister && !containsString(allowedMethods, "phone") { - response.Error(c, 403, "手机号注册方式未开放") - return - } - - if appModel.EnableEmailVerify && appModel.RequireEmailVerify { - if req.Email == "" { - response.Error(c, 400, "请输入邮箱地址") - return - } - if req.EmailCode == "" { - response.Error(c, 400, "请输入邮箱验证码") - return - } - - var verifyCode model.EmailVerifyCode - if err := database.DB.Where( - "application_id = ? AND email = ? AND code = ? AND purpose = ? AND used = ?", - appModel.ID, req.Email, req.EmailCode, "register", false, - ).First(&verifyCode).Error; err != nil { - response.Error(c, 400, "验证码错误或已过期") - return - } - - if verifyCode.ExpiresAt.Before(time.Now()) { - response.Error(c, 400, "验证码已过期") - return - } - - verifyCode.Used = true - database.DB.Save(&verifyCode) } var user model.AppUser @@ -609,40 +611,38 @@ func handleAppLogin(c *gin.Context) { } if device.ID > 0 { - if appModel.MultiOpen && appModel.MaxInstances > 0 { + if appModel.MultiOpenMode != "unlimited" && appModel.MaxInstances > 0 { var sessionCount int64 database.DB.Model(&model.DeviceSession{}). Where("device_id = ? AND instance_id != ? AND last_heartbeat > ?", device.ID, instanceID, timeoutThreshold). Count(&sessionCount) if sessionCount >= int64(appModel.MaxInstances) { - if appModel.MultiOpenMode == "forbidden" { - log.Printf("[DEBUG] Multi-instance limit exceeded for user %d device %s: %d/%d", user.ID, req.DeviceID, sessionCount, appModel.MaxInstances) - - var sessions []model.DeviceSession - database.DB.Where("device_id = ? AND instance_id != ?", device.ID, instanceID).Order("last_heartbeat DESC").Find(&sessions) - - sessionList := make([]gin.H, 0) - for _, s := range sessions { - isOnline := s.LastHeartbeat != nil && s.LastHeartbeat.After(timeoutThreshold) - sessionList = append(sessionList, gin.H{ - "id": s.ID, - "instance_id": s.InstanceID, - "is_online": isOnline, - "last_heartbeat": s.LastHeartbeat, - "created_at": s.CreatedAt, - }) - } - - response.ErrorWithData(c, 403, "多开数量已达上限", gin.H{ - "error_code": "MULTI_INSTANCE_LIMIT_EXCEEDED", - "max_instances": appModel.MaxInstances, - "instance_count": sessionCount, - "instances": sessionList, + log.Printf("[DEBUG] Multi-instance limit exceeded for user %d device %s: %d/%d", user.ID, req.DeviceID, sessionCount, appModel.MaxInstances) + + var sessions []model.DeviceSession + database.DB.Where("device_id = ? AND instance_id != ?", device.ID, instanceID).Order("last_heartbeat DESC").Find(&sessions) + + sessionList := make([]gin.H, 0) + for _, s := range sessions { + isOnline := s.LastHeartbeat != nil && s.LastHeartbeat.After(timeoutThreshold) + sessionList = append(sessionList, gin.H{ + "id": s.ID, + "instance_id": s.InstanceID, + "is_online": isOnline, + "last_heartbeat": s.LastHeartbeat, + "created_at": s.CreatedAt, }) - service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 多开数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("多开数量已达上限")) - return } + + response.ErrorWithData(c, 403, "多开数量已达上限", gin.H{ + "error_code": "MULTI_INSTANCE_LIMIT_EXCEEDED", + "max_instances": appModel.MaxInstances, + "instance_count": sessionCount, + "instances": sessionList, + }) + service.LogVerification(c, &appModel.ID, &user.ID, "login_failed", "登录失败: 多开数量已达上限 - "+req.Username, req.DeviceID, fmt.Errorf("多开数量已达上限")) + return } } diff --git a/backend/internal/router/app/dynamic.go b/backend/internal/router/app/dynamic.go index 91599be..22e4c96 100644 --- a/backend/internal/router/app/dynamic.go +++ b/backend/internal/router/app/dynamic.go @@ -128,7 +128,6 @@ func handleExecuteDynamicCode(c *gin.Context) { "free_period_end_time": app.FreePeriodEndTime, "max_devices": app.MaxDevices, "bind_type": app.BindType, - "multi_open": app.MultiOpen, "multi_open_mode": app.MultiOpenMode, "max_instances": app.MaxInstances, "login_policy": app.LoginPolicy, diff --git a/backend/internal/router/app/info.go b/backend/internal/router/app/info.go index d8886ad..29ef125 100644 --- a/backend/internal/router/app/info.go +++ b/backend/internal/router/app/info.go @@ -37,7 +37,6 @@ func handleAppGetInfo(c *gin.Context) { "billing_type": app.BillingType, "login_policy": app.LoginPolicy, "max_devices": app.MaxDevices, - "multi_open": app.MultiOpen, "multi_open_mode": app.MultiOpenMode, "max_instances": app.MaxInstances, "enable_trial": app.EnableTrial, diff --git a/backend/internal/router/extension/extension.go b/backend/internal/router/extension/extension.go index e56fb6b..2d8b4f4 100644 --- a/backend/internal/router/extension/extension.go +++ b/backend/internal/router/extension/extension.go @@ -713,7 +713,7 @@ func handleGetAppInfo(c *gin.Context) { "encrypt_type": app.EncryptType, "bind_type": app.BindType, "max_devices": app.MaxDevices, - "multi_open": app.MultiOpen, + "multi_open_mode": app.MultiOpenMode, "enable_trial": app.EnableTrial, "trial_balance": app.TrialBalance, "status": app.Status, diff --git a/backend/internal/service/package_limit.go b/backend/internal/service/package_limit.go index c30073a..2465706 100644 --- a/backend/internal/service/package_limit.go +++ b/backend/internal/service/package_limit.go @@ -67,43 +67,45 @@ func GetApplicationDisabledStatus(appID uint) bool { type ApplicationWithStatus struct { ID uint `json:"id"` - UserID uint `json:"user_id"` - Name string `json:"name"` - Description string `json:"description"` - AppKey string `json:"app_key"` - IconURL string `json:"icon_url"` - Status string `json:"status"` - BillingType string `json:"billing_type"` - LoginPolicy string `json:"login_policy"` - MaxDevices int `json:"max_devices"` - MultiOpen bool `json:"multi_open"` - MultiOpenMode string `json:"multi_open_mode"` - EnableTrial bool `json:"enable_trial"` - TrialDays int `json:"trial_days"` - EnableFreePeriod bool `json:"enable_free_period"` - FreePeriodType string `json:"free_period_type"` - FreePeriodStart string `json:"free_period_start"` - FreePeriodEnd string `json:"free_period_end"` - FreePeriodWeekdays string `json:"free_period_weekdays"` - FreePeriodStartTime string `json:"free_period_start_time"` - FreePeriodEndTime string `json:"free_period_end_time"` - HeartbeatInterval int `json:"heartbeat_interval"` - HeartbeatTimeout int `json:"heartbeat_timeout"` - MaxAttempts int `json:"max_attempts"` - LockDuration int `json:"lock_duration"` - DeductionMode string `json:"deduction_mode"` - DeductionType string `json:"deduction_type"` - DeductionInterval int `json:"deduction_interval"` - DeductionUnit string `json:"deduction_unit"` - DeductionAmount float64 `json:"deduction_amount"` - AllowRegister bool `json:"allow_register"` - RegisterMethods string `json:"register_methods"` - EnableEmailVerify bool `json:"enable_email_verify"` - RequireEmailVerify bool `json:"require_email_verify"` - EnablePasswordReset bool `json:"enable_password_reset"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - DisabledByPackage bool `json:"disabled_by_package"` + UserID uint `json:"user_id"` + Name string `json:"name"` + Description string `json:"description"` + AppKey string `json:"app_key"` + IconURL string `json:"icon_url"` + Status string `json:"status"` + BillingType string `json:"billing_type"` + LoginPolicy string `json:"login_policy"` + MaxDevices int `json:"max_devices"` + MultiOpenMode string `json:"multi_open_mode"` + EnableTrial bool `json:"enable_trial"` + TrialDays int `json:"trial_days"` + EnableFreePeriod bool `json:"enable_free_period"` + FreePeriodType string `json:"free_period_type"` + FreePeriodStart string `json:"free_period_start"` + FreePeriodEnd string `json:"free_period_end"` + FreePeriodWeekdays string `json:"free_period_weekdays"` + FreePeriodStartTime string `json:"free_period_start_time"` + FreePeriodEndTime string `json:"free_period_end_time"` + HeartbeatInterval int `json:"heartbeat_interval"` + HeartbeatTimeout int `json:"heartbeat_timeout"` + MaxAttempts int `json:"max_attempts"` + LockDuration int `json:"lock_duration"` + DeductionMode string `json:"deduction_mode"` + DeductionType string `json:"deduction_type"` + DeductionInterval int `json:"deduction_interval"` + DeductionUnit string `json:"deduction_unit"` + DeductionAmount float64 `json:"deduction_amount"` + AllowRegister bool `json:"allow_register"` + EnableLoginVerify bool `json:"enable_login_verify"` + EnableRegisterVerify bool `json:"enable_register_verify"` + VerifyMethod string `json:"verify_method"` + EnablePasswordReset bool `json:"enable_password_reset"` + PasswordResetMethod string `json:"password_reset_method"` + PasswordResetEmailID *uint `json:"password_reset_email_id"` + PasswordResetSmsID *uint `json:"password_reset_sms_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + DisabledByPackage bool `json:"disabled_by_package"` } func GetApplicationsWithDisabledStatus(userID uint) ([]ApplicationWithStatus, error) { @@ -130,7 +132,6 @@ func GetApplicationsWithDisabledStatus(userID uint) ([]ApplicationWithStatus, er BillingType: app.BillingType, LoginPolicy: app.LoginPolicy, MaxDevices: app.MaxDevices, - MultiOpen: app.MultiOpen, MultiOpenMode: app.MultiOpenMode, EnableTrial: app.EnableTrial, TrialDays: app.TrialDays, @@ -151,10 +152,13 @@ func GetApplicationsWithDisabledStatus(userID uint) ([]ApplicationWithStatus, er DeductionUnit: app.DeductionUnit, DeductionAmount: app.DeductionAmount, AllowRegister: app.AllowRegister, - RegisterMethods: app.RegisterMethods, - EnableEmailVerify: app.EnableEmailVerify, - RequireEmailVerify: app.RequireEmailVerify, + EnableLoginVerify: app.EnableLoginVerify, + EnableRegisterVerify: app.EnableRegisterVerify, + VerifyMethod: app.VerifyMethod, EnablePasswordReset: app.EnablePasswordReset, + PasswordResetMethod: app.PasswordResetMethod, + PasswordResetEmailID: app.PasswordResetEmailID, + PasswordResetSmsID: app.PasswordResetSmsID, CreatedAt: app.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), UpdatedAt: app.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), } diff --git a/frontend/src/components/admin-sidebar/index.vue b/frontend/src/components/admin-sidebar/index.vue index 7d5275b..208ba3c 100644 --- a/frontend/src/components/admin-sidebar/index.vue +++ b/frontend/src/components/admin-sidebar/index.vue @@ -1,5 +1,5 @@ - - diff --git a/frontend/src/pages/admin/applications/[id]/security.vue b/frontend/src/pages/admin/applications/[id]/security.vue index 29ba14b..5364ca0 100644 --- a/frontend/src/pages/admin/applications/[id]/security.vue +++ b/frontend/src/pages/admin/applications/[id]/security.vue @@ -16,7 +16,7 @@ const saving = ref(false) interface AppData { id: number encrypt_type: string - secret_key: string + encrypt_key: string bind_type: string max_devices: number change_limit: number @@ -35,7 +35,7 @@ const app = ref(null) const form = ref({ encrypt_type: 'none', - secret_key: '', + encrypt_key: '', bind_type: 'none', max_devices: 1, change_limit: 3, @@ -66,7 +66,7 @@ async function fetchApp() { app.value = appData form.value = { encrypt_type: appData.encrypt_type || 'none', - secret_key: appData.secret_key || '', + encrypt_key: appData.encrypt_key || '', bind_type: appData.bind_type || 'none', max_devices: appData.max_devices || 1, change_limit: appData.change_limit || 3, @@ -91,13 +91,13 @@ async function fetchApp() { } } -function generateSecretKey() { +function generateEncryptKey() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' let result = '' for (let i = 0; i < 32; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)) } - form.value.secret_key = result + form.value.encrypt_key = result } async function handleSave() { @@ -211,15 +211,15 @@ onMounted(() => {
- + 加密密钥 - + 自动生成
- +

请妥善保管密钥,丢失后无法恢复

diff --git a/frontend/src/pages/admin/applications/[id]/settings.vue b/frontend/src/pages/admin/applications/[id]/settings.vue index 9b537eb..44ec4a1 100644 --- a/frontend/src/pages/admin/applications/[id]/settings.vue +++ b/frontend/src/pages/admin/applications/[id]/settings.vue @@ -13,6 +13,18 @@ const API_BASE = 'http://localhost:8080/api/v1' const loading = ref(false) const saving = ref(false) +interface EmailConfig { + id: number + name: string + status: string +} + +interface SmsConfig { + id: number + name: string + status: string +} + interface AppData { id: number name: string @@ -21,7 +33,11 @@ interface AppData { billing_type: string login_policy: string allow_register: boolean - register_methods: string + enable_login_verify: boolean + enable_register_verify: boolean + verify_method: string + email_config_id: number | null + sms_config_id: number | null enable_trial: boolean trial_balance: number enable_free_period: boolean @@ -37,6 +53,8 @@ interface AppData { } const app = ref(null) +const emailConfigs = ref([]) +const smsConfigs = ref([]) const form = ref({ name: '', @@ -47,7 +65,15 @@ const form = ref({ billing_type: 'free', login_policy: 'loose', allow_register: true, - register_methods: [] as string[], + enable_login_verify: false, + enable_register_verify: false, + verify_method: 'email', + email_config_id: null as number | null, + sms_config_id: null as number | null, + enable_password_reset: false, + password_reset_method: 'email', + password_reset_email_id: null as number | null, + password_reset_sms_id: null as number | null, enable_trial: false, trial_balance: 0, trial_days: 0, @@ -82,22 +108,6 @@ function weekdaysToString(arr: number[]): string { return JSON.stringify(arr) } -function parseRegisterMethods(str: string): string[] { - if (!str) - return ['username'] - try { - const parsed = JSON.parse(str) - return Array.isArray(parsed) ? parsed : ['username'] - } - catch { - return ['username'] - } -} - -function registerMethodsToString(arr: string[]): string { - return JSON.stringify(arr) -} - function getIconUrl(url: string): string { if (!url) return '' @@ -121,6 +131,42 @@ function clearIcon() { form.value.icon_url = '' } +async function fetchEmailConfigs() { + try { + const token = localStorage.getItem('token') + const response = await fetch(`${API_BASE}/dev/email-configs`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + const data = await response.json() + if (data.code === 200) { + emailConfigs.value = data.data?.email_configs?.filter((c: EmailConfig) => c.status === 'active') || [] + } + } + catch (error) { + console.error('获取邮箱配置失败:', error) + } +} + +async function fetchSmsConfigs() { + try { + const token = localStorage.getItem('token') + const response = await fetch(`${API_BASE}/dev/sms-configs`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + const data = await response.json() + if (data.code === 200) { + smsConfigs.value = data.data?.sms_configs?.filter((c: SmsConfig) => c.status === 'active') || [] + } + } + catch (error) { + console.error('获取短信配置失败:', error) + } +} + async function fetchApp() { loading.value = true try { @@ -144,7 +190,15 @@ async function fetchApp() { billing_type: appData.billing_type || 'free', login_policy: appData.login_policy || 'loose', allow_register: appData.allow_register ?? true, - register_methods: parseRegisterMethods(appData.register_methods), + enable_login_verify: appData.enable_login_verify || false, + enable_register_verify: appData.enable_register_verify || false, + verify_method: appData.verify_method || 'email', + email_config_id: appData.email_config_id || null, + sms_config_id: appData.sms_config_id || null, + enable_password_reset: appData.enable_password_reset || false, + password_reset_method: appData.password_reset_method || 'email', + password_reset_email_id: appData.password_reset_email_id || null, + password_reset_sms_id: appData.password_reset_sms_id || null, enable_trial: appData.enable_trial || false, trial_balance: appData.trial_balance || 0, trial_days: appData.trial_days || 0, @@ -183,7 +237,15 @@ async function handleSave() { formDataToSend.append('billing_type', form.value.billing_type) formDataToSend.append('login_policy', form.value.login_policy) formDataToSend.append('allow_register', form.value.allow_register.toString()) - formDataToSend.append('register_methods', registerMethodsToString(form.value.register_methods)) + formDataToSend.append('enable_login_verify', form.value.enable_login_verify.toString()) + formDataToSend.append('enable_register_verify', form.value.enable_register_verify.toString()) + formDataToSend.append('verify_method', form.value.verify_method) + if (form.value.email_config_id) { + formDataToSend.append('email_config_id', form.value.email_config_id.toString()) + } + if (form.value.sms_config_id) { + formDataToSend.append('sms_config_id', form.value.sms_config_id.toString()) + } formDataToSend.append('enable_trial', form.value.enable_trial.toString()) formDataToSend.append('trial_balance', form.value.trial_balance.toString()) formDataToSend.append('trial_days', form.value.trial_days.toString()) @@ -232,6 +294,8 @@ async function handleSave() { onMounted(() => { fetchApp() + fetchEmailConfigs() + fetchSmsConfigs() }) function toggleWeekday(index: number) { @@ -442,7 +506,7 @@ function toggleWeekday(index: number) {

- ⚡ 自动扣费 + 自动扣费

系统自动触发扣费 @@ -458,7 +522,7 @@ function toggleWeekday(index: number) {

- 🔧 手动扣费 + 手动扣费

通过 API 自行控制扣费 @@ -629,7 +693,7 @@ function toggleWeekday(index: number) { 注册设置 - 配置用户注册方式和权限 + 配置用户注册权限

@@ -643,69 +707,216 @@ function toggleWeekday(index: number) {
+ + -
-
- 注册方式 -

- 选择允许用户使用的注册方式 + + + 验证设置 + 配置登录和注册的验证方式 + + +

+
+

+ 登录验证

-
-
- - - 用户名注册 - +

+ 用户登录时需要进行验证 +

+
+ +
+ +
+
+

+ 注册验证 +

+

+ 用户注册时需要进行验证 +

+
+ +
+ +
+
+ 验证方式 + +
+ +
+

+ 邮箱验证 +

+

+ 发送验证码到邮箱 +

+
-
- - - 邮箱注册 - + +
+ +
+

+ 短信验证 +

+

+ 发送验证码到手机 +

+
-
- - - 手机号注册 - + +
+ +
+ 邮箱配置 + + + + + + + {{ config.name }} + + + +

+ 选择用于发送验证码的邮箱配置 +

+
+

+ 暂无可用的邮箱配置,请先在「系统设置 - 邮箱配置」中添加并启用邮箱配置 +

+
+
+ +
+ 短信配置 + + + + + + + {{ config.name }} + + + +

+ 选择用于发送验证码的短信配置 +

+
+

+ 暂无可用的短信配置,请先在「系统设置 - 短信配置」中添加并启用短信配置 +

+
+
+
+ +
+
+
+

+ 密码重置 +

+

+ 用户忘记密码时通过邮箱或短信重置 +

+
+ +
+ +
+
+ 验证方式 + +
+ +
+

+ 邮箱验证 +

+

+ 发送验证码到邮箱 +

+
+
+ +
+ +
+

+ 短信验证 +

+

+ 发送验证码到手机 +

+
+
+
+
+ +
+ 邮箱配置 + + + + + + + {{ config.name }} + + + +

+ 选择用于发送密码重置邮件的邮箱配置 +

+
+

+ 暂无可用的邮箱配置,请先在「系统设置 - 邮箱配置」中添加并启用邮箱配置 +

+
+
+ +
+ 短信配置 + + + + + + + {{ config.name }} + + + +

+ 选择用于发送密码重置短信的短信配置 +

+
+

+ 暂无可用的短信配置,请先在「系统设置 - 短信配置」中添加并启用短信配置 +

-

- 至少选择一种注册方式 -

diff --git a/frontend/src/pages/admin/applications/components/columns.ts b/frontend/src/pages/admin/applications/components/columns.ts index 544d15f..a0c0f46 100644 --- a/frontend/src/pages/admin/applications/components/columns.ts +++ b/frontend/src/pages/admin/applications/components/columns.ts @@ -4,7 +4,6 @@ import type { Composer } from 'vue-i18n' import { Ban, CheckCircle, - Mail, MoreHorizontal, Settings, Shield, @@ -27,7 +26,6 @@ import { export function getColumns(actions: { onGoToSettings: (app: App) => void onGoToSecurity: (app: App) => void - onGoToEmail: (app: App) => void onToggleStatus: (app: App) => void onDelete: (app: App) => void }, t: Composer['t']): ColumnDef[] { @@ -181,10 +179,6 @@ export function getColumns(actions: { h(Shield, { class: 'mr-2 h-4 w-4' }), '安全设置', ]), - h(DropdownMenuItem, { onClick: () => actions.onGoToEmail(app) }, () => [ - h(Mail, { class: 'mr-2 h-4 w-4' }), - '邮箱设置', - ]), h(DropdownMenuSeparator), h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(app) }, () => [ isActive ? h(Ban, { class: 'mr-2 h-4 w-4' }) : h(CheckCircle, { class: 'mr-2 h-4 w-4' }), diff --git a/frontend/src/pages/admin/applications/components/data-table.vue b/frontend/src/pages/admin/applications/components/data-table.vue index 32fbb39..b50512c 100644 --- a/frontend/src/pages/admin/applications/components/data-table.vue +++ b/frontend/src/pages/admin/applications/components/data-table.vue @@ -21,7 +21,6 @@ const emit = defineEmits<{ 'refresh': [] 'goToSettings': [app: App] 'goToSecurity': [app: App] - 'goToEmail': [app: App] 'toggleStatus': [app: App] 'delete': [app: App] 'update:searchFilter': [value: string] @@ -34,7 +33,6 @@ const columns = computed(() => [ ...getColumns({ onGoToSettings: (app: App) => emit('goToSettings', app), onGoToSecurity: (app: App) => emit('goToSecurity', app), - onGoToEmail: (app: App) => emit('goToEmail', app), onToggleStatus: (app: App) => emit('toggleStatus', app), onDelete: (app: App) => emit('delete', app), }, t), diff --git a/frontend/src/pages/admin/applications/create.vue b/frontend/src/pages/admin/applications/create.vue index c1ce131..fecf762 100644 --- a/frontend/src/pages/admin/applications/create.vue +++ b/frontend/src/pages/admin/applications/create.vue @@ -1,41 +1,34 @@ diff --git a/frontend/src/pages/admin/sms-settings/[id].vue b/frontend/src/pages/admin/sms-settings/[id].vue new file mode 100644 index 0000000..a09b2a3 --- /dev/null +++ b/frontend/src/pages/admin/sms-settings/[id].vue @@ -0,0 +1,269 @@ + + +