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:
John Doe
2026-03-12 02:42:57 +03:00
parent 9d81467937
commit 6826149a8f
27 changed files with 833 additions and 17 deletions

View File

@@ -1070,6 +1070,21 @@
</p>
</div>
<div class="space-y-6 p-6">
<!-- Backend Mode -->
<div
class="flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
>
<div>
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{{ t('admin.settings.site.backendMode') }}
</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.site.backendModeDescription') }}
</p>
</div>
<Toggle v-model="form.backend_mode_enabled" />
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -1785,6 +1800,7 @@ const form = reactive<SettingsForm>({
contact_info: '',
doc_url: '',
home_content: '',
backend_mode_enabled: false,
hide_ccs_import_button: false,
purchase_subscription_enabled: false,
purchase_subscription_url: '',
@@ -1962,6 +1978,7 @@ async function loadSettings() {
try {
const settings = await adminAPI.settings.getSettings()
Object.assign(form, settings)
form.backend_mode_enabled = settings.backend_mode_enabled
form.default_subscriptions = Array.isArray(settings.default_subscriptions)
? settings.default_subscriptions
.filter((item) => item.group_id > 0 && item.validity_days > 0)
@@ -2060,6 +2077,7 @@ async function saveSettings() {
contact_info: form.contact_info,
doc_url: form.doc_url,
home_content: form.home_content,
backend_mode_enabled: form.backend_mode_enabled,
hide_ccs_import_button: form.hide_ccs_import_button,
purchase_subscription_enabled: form.purchase_subscription_enabled,
purchase_subscription_url: form.purchase_subscription_url,