diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index 30f5a670e..e6a867a31 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -2,6 +2,8 @@ package v1 import ( "errors" + "fmt" + "strconv" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/dto" @@ -260,11 +262,23 @@ func (b *BaseApi) CleanMonitor(c *gin.Context) { // @Tags System Setting // @Summary Load mfa info // @Description 获取 mfa 信息 +// @Param interval path string true "request" // @Success 200 {object} mfa.Otp // @Security ApiKeyAuth -// @Router /settings/mfa [get] +// @Router /settings/mfa/:interval [get] func (b *BaseApi) GetMFA(c *gin.Context) { - otp, err := mfa.GetOtp("admin") + intervalStr, ok := c.Params.Get("interval") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error interval in path")) + return + } + interval, err := strconv.Atoi(intervalStr) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("type conversion failed, err: %v", err)) + return + } + + otp, err := mfa.GetOtp("admin", interval) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return @@ -288,12 +302,17 @@ func (b *BaseApi) MFABind(c *gin.Context) { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return } - success := mfa.ValidCode(req.Code, req.Secret) + success := mfa.ValidCode(req.Code, req.Interval, req.Secret) if !success { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, errors.New("code is not valid")) return } + if err := settingService.Update("MFAInterval", req.Interval); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + if err := settingService.Update("MFAStatus", "enable"); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return diff --git a/backend/app/dto/auth.go b/backend/app/dto/auth.go index 1e831c63f..6b9602fb3 100644 --- a/backend/app/dto/auth.go +++ b/backend/app/dto/auth.go @@ -12,8 +12,9 @@ type UserLoginInfo struct { } type MfaCredential struct { - Secret string `json:"secret"` - Code string `json:"code"` + Secret string `json:"secret"` + Code string `json:"code"` + Interval string `json:"interval"` } type Login struct { diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index cc8484041..b46dcc761 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -28,6 +28,7 @@ type SettingInfo struct { ComplexityVerification string `json:"complexityVerification"` MFAStatus string `json:"mfaStatus"` MFASecret string `json:"mfaSecret"` + MFAInterval string `json:"mfaInterval"` MonitorStatus string `json:"monitorStatus"` MonitorInterval string `json:"monitorInterval"` diff --git a/backend/app/service/auth.go b/backend/app/service/auth.go index 5ddc7cc8c..421eaa26e 100644 --- a/backend/app/service/auth.go +++ b/backend/app/service/auth.go @@ -76,7 +76,11 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin) (*dto.UserLogi if err != nil { return nil, err } - success := mfa.ValidCode(info.Code, mfaSecret.Value) + mfaInterval, err := settingRepo.Get(settingRepo.WithByKey("MFAInterval")) + if err != nil { + return nil, err + } + success := mfa.ValidCode(info.Code, mfaInterval.Value, mfaSecret.Value) if !success { return nil, constant.ErrAuth } diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 2742d23f5..dbb170784 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -32,6 +32,7 @@ func Init() { migrations.UpdateCronjobWithSecond, migrations.UpdateWebsite, migrations.AddBackupAccountDir, + migrations.AddMfaInterval, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index d20fc342f..f590e1864 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -400,3 +400,13 @@ var AddBackupAccountDir = &gormigrate.Migration{ return nil }, } + +var AddMfaInterval = &gormigrate.Migration{ + ID: "20230625-add-mfa-interval", + Migrate: func(tx *gorm.DB) error { + if err := tx.Create(&model.Setting{Key: "MFAInterval", Value: "30"}).Error; err != nil { + return err + } + return nil + }, +} diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index 61ee20302..57c43ebe1 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -29,7 +29,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { settingRouter.GET("/time/option", baseApi.LoadTimeZone) settingRouter.POST("/time/sync", baseApi.SyncTime) settingRouter.POST("/monitor/clean", baseApi.CleanMonitor) - settingRouter.GET("/mfa", baseApi.GetMFA) + settingRouter.GET("/mfa/:interval", baseApi.GetMFA) settingRouter.POST("/mfa/bind", baseApi.MFABind) settingRouter.POST("/snapshot", baseApi.CreateSnapshot) diff --git a/backend/utils/mfa/mfa.go b/backend/utils/mfa/mfa.go index 76ac8a957..0ece19eb5 100644 --- a/backend/utils/mfa/mfa.go +++ b/backend/utils/mfa/mfa.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/1Panel-dev/1Panel/backend/global" "github.com/skip2/go-qrcode" "github.com/xlzd/gotp" ) @@ -17,10 +18,10 @@ type Otp struct { QrImage string `json:"qrImage"` } -func GetOtp(username string) (otp Otp, err error) { +func GetOtp(username string, interval int) (otp Otp, err error) { secret := gotp.RandomSecret(secretLength) otp.Secret = secret - totp := gotp.NewDefaultTOTP(secret) + totp := gotp.NewTOTP(secret, 6, interval, nil) uri := totp.ProvisioningUri(username, "1Panel") subImg, err := qrcode.Encode(uri, qrcode.Medium, 256) dist := make([]byte, 3000) @@ -31,8 +32,13 @@ func GetOtp(username string) (otp Otp, err error) { return } -func ValidCode(code string, secret string) bool { - totp := gotp.NewDefaultTOTP(secret) +func ValidCode(code, intervalStr, secret string) bool { + interval, err := strconv.Atoi(intervalStr) + if err != nil { + global.LOG.Errorf("type conversion failed, err: %v", err) + return false + } + totp := gotp.NewTOTP(secret, 6, interval, nil) now := time.Now().Unix() strInt64 := strconv.FormatInt(now, 10) id16, _ := strconv.Atoi(strInt64) diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 0bbc1a5f5..95aa2aad2 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -27,6 +27,7 @@ export namespace Setting { complexityVerification: string; mfaStatus: string; mfaSecret: string; + mfaInterval: string; monitorStatus: string; monitorInterval: number; @@ -71,6 +72,7 @@ export namespace Setting { export interface MFABind { secret: string; code: string; + interval: string; } export interface SnapshotCreate { from: string; diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index e544a36f9..0f8f7e514 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -46,8 +46,8 @@ export const cleanMonitors = () => { return http.post(`/settings/monitor/clean`, {}); }; -export const getMFA = () => { - return http.get(`/settings/mfa`, {}); +export const getMFA = (interval: number) => { + return http.get(`/settings/mfa/${interval}`, {}); }; export const loadDaemonJsonPath = () => { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index c754dcad9..1f94a1799 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1050,6 +1050,8 @@ const message = { allowIPEgs: 'If multiple ip authorizations exist, newlines need to be displayed. For example, \n172.16.10.111 \n172.16.10.112', mfa: 'MFA', + mfaInterval: 'Refresh interval (s)', + mfaIntervalHelper: 'Please rescan or manually add key information after modifying the refresh time.', mfaAlert: 'MFA password is generated based on the current time. Please ensure that the server time is synchronized.', mfaHelper: 'After this function is enabled, the mobile application verification code will be verified', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index e0b25b30d..b4b8ab557 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1058,6 +1058,8 @@ const message = { mfaHelper2: '使用手机应用扫描以下二维码,获取 6 位验证码', mfaHelper3: '输入手机应用上的 6 位数字', mfaCode: '验证码', + mfaInterval: '刷新时间(秒)', + mfaIntervalHelper: '修改刷新时间后,请重新扫描或手动添加密钥信息!', sslChangeHelper: 'https 设置修改需要重启服务,是否继续?', sslDisable: '禁用', sslDisableHelper: '禁用 https 服务,需要重启面板才能生效,是否继续?', diff --git a/frontend/src/views/setting/safe/index.vue b/frontend/src/views/setting/safe/index.vue index ad46eec78..24e871f5e 100644 --- a/frontend/src/views/setting/safe/index.vue +++ b/frontend/src/views/setting/safe/index.vue @@ -194,6 +194,7 @@ const form = reactive({ expirationTime: '', complexityVerification: 'disable', mfaStatus: 'disable', + mfaInterval: 30, allowIPs: '', bindDomain: '', }); @@ -213,6 +214,7 @@ const search = async () => { form.expirationTime = res.data.expirationTime; form.complexityVerification = res.data.complexityVerification; form.mfaStatus = res.data.mfaStatus; + form.mfaInterval = Number(res.data.mfaInterval); form.allowIPs = res.data.allowIPs.replaceAll(',', '\n'); form.bindDomain = res.data.bindDomain; }; @@ -236,7 +238,7 @@ const onSaveComplexity = async () => { const handleMFA = async () => { if (form.mfaStatus === 'enable') { - mfaRef.value.acceptParams(); + mfaRef.value.acceptParams({ interval: form.mfaInterval }); return; } loading.value = true; diff --git a/frontend/src/views/setting/safe/mfa/index.vue b/frontend/src/views/setting/safe/mfa/index.vue index c0e827ee2..7c3723bdf 100644 --- a/frontend/src/views/setting/safe/mfa/index.vue +++ b/frontend/src/views/setting/safe/mfa/index.vue @@ -17,7 +17,14 @@ - + @@ -45,7 +52,15 @@ - + + + + + {{ $t('setting.mfaIntervalHelper') }} + + @@ -65,7 +80,7 @@