diff --git a/backend/controllers/login.go b/backend/controllers/login.go index 9383af2..b781a3c 100644 --- a/backend/controllers/login.go +++ b/backend/controllers/login.go @@ -127,8 +127,9 @@ func loginSecurityFields(user *models.User) map[string]any { fields["totpConfigured"] = enabled } fields["recoveryCodesConfigured"] = configured - fields["showRecoverySetupPrompt"] = !configured - fields["showSecuritySetupPrompt"] = !configured || fields["externalEmailConfigured"] != true + needsRecoverySetup := !configured && fields["externalEmailConfigured"] != true + fields["showRecoverySetupPrompt"] = needsRecoverySetup + fields["showSecuritySetupPrompt"] = needsRecoverySetup if user.EffectiveRole() == access.RoleSuperAdmin { required := !configured && fields["externalEmailConfigured"] != true && fields["totpConfigured"] != true fields["securitySetupRequired"] = required diff --git a/backend/controllers/security_setup_test.go b/backend/controllers/security_setup_test.go index add9601..fb22a2f 100644 --- a/backend/controllers/security_setup_test.go +++ b/backend/controllers/security_setup_test.go @@ -189,11 +189,12 @@ func TestSecuritySetupRequiresUsableCredentials(t *testing.T) { fields := loginSecurityFields(&user) emailReady := emailStatus == models.ExternalEmailStatusVerified codeReady := codeStatus == "valid" - wantPrompt := !emailReady || !codeReady - if role == access.RoleSuperAdmin { - wantPrompt = !emailReady && !codeReady - } - if fields["externalEmailConfigured"] != emailReady || fields["showSecuritySetupPrompt"] != wantPrompt { + wantPrompt := !emailReady && !codeReady + wantRequired := role == access.RoleSuperAdmin && wantPrompt + wantRecoveryPrompt := role != access.RoleSuperAdmin && wantPrompt + if fields["externalEmailConfigured"] != emailReady || fields["recoveryCodesConfigured"] != codeReady || + fields["showSecuritySetupPrompt"] != wantPrompt || fields["showRecoverySetupPrompt"] != wantRecoveryPrompt || + fields["securitySetupRequired"] != wantRequired { t.Fatalf("unexpected security state: %#v", fields) } }) @@ -202,6 +203,49 @@ func TestSecuritySetupRequiresUsableCredentials(t *testing.T) { } } +func TestNonSuperAdminRecoverySetupTracksUsableOptions(t *testing.T) { + for _, role := range []access.Role{access.RoleUser, access.RoleAdmin} { + t.Run(string(role), func(t *testing.T) { + engine := setupSecurityPromptTest(t) + user := models.User{Account: "alice", Role: string(role)} + if _, err := engine.Insert(&user); err != nil { + t.Fatal(err) + } + if _, err := engine.Insert(&models.TOTPAuthenticator{UserID: user.ID, Enabled: true}); err != nil { + t.Fatal(err) + } + assertPrompt := func(want bool) { + t.Helper() + fields := loginSecurityFields(&user) + if fields["showSecuritySetupPrompt"] != want || fields["showRecoverySetupPrompt"] != want || + fields["securitySetupRequired"] != false { + t.Fatalf("unexpected security state: %#v", fields) + } + } + assertPrompt(true) + email := models.ExternalEmail{UserID: user.ID, Status: models.ExternalEmailStatusVerified, EmailFingerprint: "fixture"} + if _, err := engine.Insert(&email); err != nil { + t.Fatal(err) + } + assertPrompt(false) + if _, err := engine.ID(email.ID).Cols("status").Update(&models.ExternalEmail{Status: models.ExternalEmailStatusDisabled}); err != nil { + t.Fatal(err) + } + assertPrompt(true) + code := models.RecoveryCode{UserID: user.ID, CodeHash: "fixture"} + if _, err := engine.Insert(&code); err != nil { + t.Fatal(err) + } + assertPrompt(false) + now := time.Now() + if _, err := engine.ID(code.ID).Cols("used_at").Update(&models.RecoveryCode{UsedAt: &now}); err != nil { + t.Fatal(err) + } + assertPrompt(true) + }) + } +} + func TestSecuritySetupRejectsAnonymous(t *testing.T) { recorder := httptest.NewRecorder() AcknowledgeSecuritySetup(nil, recorder, httptest.NewRequest(http.MethodPost, "/api/v1/security/setup/acknowledge", nil)) diff --git a/frontend/src/views/components/SecuritySetupPrompt/SecuritySetupPrompt.vue b/frontend/src/views/components/SecuritySetupPrompt/SecuritySetupPrompt.vue index 25e8508..d131ac8 100644 --- a/frontend/src/views/components/SecuritySetupPrompt/SecuritySetupPrompt.vue +++ b/frontend/src/views/components/SecuritySetupPrompt/SecuritySetupPrompt.vue @@ -8,7 +8,9 @@ :show-close="false" class="security-setup-dialog" > -

{{ isSuperAdmin ? '请至少启用一种安全验证后继续。' : '请设置安全码并绑定外部邮箱。未完成前,每次登录都会提醒。' }}

+

+ {{ isSuperAdmin ? '请至少启用一种安全验证后继续。' : '安全码或外部邮箱至少配置一项。未配置时,每次登录都会提醒。' }} +