mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-25 17:14:45 +08:00
feat: add Backend Mode toggle to disable user self-service
Add a system-wide "Backend Mode" that disables user self-registration and self-service while keeping admin panel and API gateway fully functional. When enabled, only admin can log in; all user-facing routes return 403. Backend: - New setting key `backend_mode_enabled` with atomic cached reads (60s TTL) - BackendModeUserGuard middleware blocks non-admin authenticated routes - BackendModeAuthGuard middleware blocks registration/password-reset auth routes - Login/Login2FA/RefreshToken handlers reject non-admin when enabled - TokenPairWithUser struct for role-aware token refresh - 20 unit tests (middleware + service layer) Frontend: - Router guards redirect unauthenticated users to /login - Admin toggle in Settings page - Login page hides register link and footer in backend mode - 9 unit tests for router guard logic - i18n support (en/zh) 27 files changed, 833 insertions(+), 17 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
199
backend/internal/service/setting_service_backend_mode_test.go
Normal file
199
backend/internal/service/setting_service_backend_mode_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type bmRepoStub struct {
|
||||
getValueFn func(ctx context.Context, key string) (string, error)
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *bmRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
|
||||
panic("unexpected Get call")
|
||||
}
|
||||
|
||||
func (s *bmRepoStub) GetValue(ctx context.Context, key string) (string, error) {
|
||||
s.calls++
|
||||
if s.getValueFn == nil {
|
||||
panic("unexpected GetValue call")
|
||||
}
|
||||
return s.getValueFn(ctx, key)
|
||||
}
|
||||
|
||||
func (s *bmRepoStub) Set(ctx context.Context, key, value string) error {
|
||||
panic("unexpected Set call")
|
||||
}
|
||||
|
||||
func (s *bmRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
||||
panic("unexpected GetMultiple call")
|
||||
}
|
||||
|
||||
func (s *bmRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
|
||||
panic("unexpected SetMultiple call")
|
||||
}
|
||||
|
||||
func (s *bmRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
|
||||
panic("unexpected GetAll call")
|
||||
}
|
||||
|
||||
func (s *bmRepoStub) Delete(ctx context.Context, key string) error {
|
||||
panic("unexpected Delete call")
|
||||
}
|
||||
|
||||
type bmUpdateRepoStub struct {
|
||||
updates map[string]string
|
||||
getValueFn func(ctx context.Context, key string) (string, error)
|
||||
}
|
||||
|
||||
func (s *bmUpdateRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
|
||||
panic("unexpected Get call")
|
||||
}
|
||||
|
||||
func (s *bmUpdateRepoStub) GetValue(ctx context.Context, key string) (string, error) {
|
||||
if s.getValueFn == nil {
|
||||
panic("unexpected GetValue call")
|
||||
}
|
||||
return s.getValueFn(ctx, key)
|
||||
}
|
||||
|
||||
func (s *bmUpdateRepoStub) Set(ctx context.Context, key, value string) error {
|
||||
panic("unexpected Set call")
|
||||
}
|
||||
|
||||
func (s *bmUpdateRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
||||
panic("unexpected GetMultiple call")
|
||||
}
|
||||
|
||||
func (s *bmUpdateRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
|
||||
s.updates = make(map[string]string, len(settings))
|
||||
for k, v := range settings {
|
||||
s.updates[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *bmUpdateRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
|
||||
panic("unexpected GetAll call")
|
||||
}
|
||||
|
||||
func (s *bmUpdateRepoStub) Delete(ctx context.Context, key string) error {
|
||||
panic("unexpected Delete call")
|
||||
}
|
||||
|
||||
func resetBackendModeTestCache(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
backendModeCache.Store((*cachedBackendMode)(nil))
|
||||
t.Cleanup(func() {
|
||||
backendModeCache.Store((*cachedBackendMode)(nil))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsBackendModeEnabled_ReturnsTrue(t *testing.T) {
|
||||
resetBackendModeTestCache(t)
|
||||
|
||||
repo := &bmRepoStub{
|
||||
getValueFn: func(ctx context.Context, key string) (string, error) {
|
||||
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
||||
return "true", nil
|
||||
},
|
||||
}
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
require.True(t, svc.IsBackendModeEnabled(context.Background()))
|
||||
require.Equal(t, 1, repo.calls)
|
||||
}
|
||||
|
||||
func TestIsBackendModeEnabled_ReturnsFalse(t *testing.T) {
|
||||
resetBackendModeTestCache(t)
|
||||
|
||||
repo := &bmRepoStub{
|
||||
getValueFn: func(ctx context.Context, key string) (string, error) {
|
||||
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
||||
return "false", nil
|
||||
},
|
||||
}
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
require.False(t, svc.IsBackendModeEnabled(context.Background()))
|
||||
require.Equal(t, 1, repo.calls)
|
||||
}
|
||||
|
||||
func TestIsBackendModeEnabled_ReturnsFalseOnNotFound(t *testing.T) {
|
||||
resetBackendModeTestCache(t)
|
||||
|
||||
repo := &bmRepoStub{
|
||||
getValueFn: func(ctx context.Context, key string) (string, error) {
|
||||
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
||||
return "", ErrSettingNotFound
|
||||
},
|
||||
}
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
require.False(t, svc.IsBackendModeEnabled(context.Background()))
|
||||
require.Equal(t, 1, repo.calls)
|
||||
}
|
||||
|
||||
func TestIsBackendModeEnabled_ReturnsFalseOnDBError(t *testing.T) {
|
||||
resetBackendModeTestCache(t)
|
||||
|
||||
repo := &bmRepoStub{
|
||||
getValueFn: func(ctx context.Context, key string) (string, error) {
|
||||
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
||||
return "", errors.New("db down")
|
||||
},
|
||||
}
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
require.False(t, svc.IsBackendModeEnabled(context.Background()))
|
||||
require.Equal(t, 1, repo.calls)
|
||||
}
|
||||
|
||||
func TestIsBackendModeEnabled_CachesResult(t *testing.T) {
|
||||
resetBackendModeTestCache(t)
|
||||
|
||||
repo := &bmRepoStub{
|
||||
getValueFn: func(ctx context.Context, key string) (string, error) {
|
||||
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
||||
return "true", nil
|
||||
},
|
||||
}
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
require.True(t, svc.IsBackendModeEnabled(context.Background()))
|
||||
require.True(t, svc.IsBackendModeEnabled(context.Background()))
|
||||
require.Equal(t, 1, repo.calls)
|
||||
}
|
||||
|
||||
func TestUpdateSettings_InvalidatesBackendModeCache(t *testing.T) {
|
||||
resetBackendModeTestCache(t)
|
||||
|
||||
backendModeCache.Store(&cachedBackendMode{
|
||||
value: true,
|
||||
expiresAt: time.Now().Add(backendModeCacheTTL).UnixNano(),
|
||||
})
|
||||
|
||||
repo := &bmUpdateRepoStub{
|
||||
getValueFn: func(ctx context.Context, key string) (string, error) {
|
||||
require.Equal(t, SettingKeyBackendModeEnabled, key)
|
||||
return "true", nil
|
||||
},
|
||||
}
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
err := svc.UpdateSettings(context.Background(), &SystemSettings{
|
||||
BackendModeEnabled: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "false", repo.updates[SettingKeyBackendModeEnabled])
|
||||
require.False(t, svc.IsBackendModeEnabled(context.Background()))
|
||||
}
|
||||
Reference in New Issue
Block a user