Add factory reset functionality and update identity service
- Introduced a new DELETE endpoint `/api/admin/maintenance/factory-reset` for a complete reset of the admin panel, removing all users except the current admin and clearing various data. - Implemented the `FactoryReset` method in `AdminMaintenanceEndpoints` to handle the reset logic. - Added a new method `ListAllUserIdsExceptAsync` in `IIdentityService` to retrieve user IDs excluding a specified user, aiding in the factory reset process. - Updated the frontend to include a confirmation dialog for the factory reset action, enhancing user experience and safety. - Enhanced localization support for the new factory reset feature in both Russian and English, ensuring clarity for all users.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/shared/ui/dialog'
|
||||
import { factoryReset } from './api'
|
||||
|
||||
const CONFIRM_PHRASE_KEY = 'admin.maintenance.factoryReset.confirmPhrase'
|
||||
|
||||
export function FactoryResetDialog() {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [confirmText, setConfirmText] = useState('')
|
||||
const confirmPhrase = t(CONFIRM_PHRASE_KEY)
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: factoryReset,
|
||||
onSuccess: () => {
|
||||
toast.success(t('admin.maintenance.factoryReset.done'))
|
||||
// Почти всё состояние приложения (пользователи/роли/ноды/кэши списков) больше не валидно —
|
||||
// проще перезагрузить всю панель, чем точечно инвалидировать десяток query-ключей.
|
||||
window.location.href = '/admin'
|
||||
},
|
||||
onError: () => toast.error(t('auth.genericError')),
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
setOpen(next)
|
||||
if (!next) setConfirmText('')
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">{t('admin.maintenance.factoryReset.action')}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-red-400">{t('admin.maintenance.factoryReset.dialogTitle')}</DialogTitle>
|
||||
<DialogDescription asChild>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<p>{t('admin.maintenance.factoryReset.warningIntro')}</p>
|
||||
<ul className="list-disc pl-5">
|
||||
{(t('admin.maintenance.factoryReset.warningItems', { returnObjects: true }) as string[]).map(
|
||||
(item) => (
|
||||
<li key={item}>{item}</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
<p className="font-semibold text-red-400">{t('admin.maintenance.factoryReset.irreversible')}</p>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm text-muted-foreground">
|
||||
{t('admin.maintenance.factoryReset.confirmInputLabel', { phrase: confirmPhrase })}
|
||||
</label>
|
||||
<Input value={confirmText} onChange={(e) => setConfirmText(e.target.value)} autoComplete="off" />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={() => setOpen(false)}>
|
||||
{t('settings.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={confirmText !== confirmPhrase || mutation.isPending}
|
||||
onClick={() => mutation.mutate()}
|
||||
>
|
||||
{t('admin.maintenance.factoryReset.confirmButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -14,3 +14,7 @@ export function deleteOldAuditLogs(olderThanDays: number) {
|
||||
export function deleteDisabledApps() {
|
||||
return apiRequest<MaintenanceCleanupResponseDto>('/admin/maintenance/apps/disabled', { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export function factoryReset() {
|
||||
return apiRequest<void>('/admin/maintenance/factory-reset', { method: 'DELETE' })
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from '@/shared/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { deleteClosedTickets, deleteDisabledApps, deleteOldAuditLogs } from '@/features/admin/maintenance/api'
|
||||
import { FactoryResetDialog } from '@/features/admin/maintenance/FactoryResetDialog'
|
||||
|
||||
export const Route = createFileRoute('/admin/maintenance')({ component: AdminMaintenancePage })
|
||||
|
||||
@@ -118,6 +119,25 @@ function AdminMaintenancePage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<details className="group rounded-md border border-red-900/50">
|
||||
<summary className="cursor-pointer select-none px-4 py-3 text-sm font-medium text-red-400">
|
||||
{t('admin.maintenance.factoryReset.dangerZone')}
|
||||
</summary>
|
||||
<div className="border-t border-red-900/50 px-4 py-4">
|
||||
<Card className="border-red-900/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base text-red-400">{t('admin.maintenance.factoryReset.title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted-foreground">{t('admin.maintenance.factoryReset.description')}</p>
|
||||
<div>
|
||||
<FactoryResetDialog />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -370,6 +370,29 @@ const resources = {
|
||||
confirm: 'Удалить все отключённые приложения? Действие необратимо.',
|
||||
deleted: 'Удалено приложений: {{count}}.',
|
||||
},
|
||||
factoryReset: {
|
||||
dangerZone: 'Опасная зона',
|
||||
title: 'Полный сброс панели',
|
||||
description: 'Вернуть панель в состояние только что развёрнутой — удалить всех пользователей, конфиги, ноды и все данные. Ваш текущий аккаунт сохранится.',
|
||||
action: 'Сбросить панель',
|
||||
dialogTitle: 'Полный сброс панели',
|
||||
warningIntro: 'Будет удалено безвозвратно:',
|
||||
warningItems: [
|
||||
'Все пользователи, кроме вашего текущего аккаунта',
|
||||
'Все VPN-конфиги (сначала будут отозваны на нодах 3x-ui)',
|
||||
'Все ноды и инбаунды — регистрация серверов 3x-ui и сохранённые креды',
|
||||
'Все обращения в поддержку вместе с перепиской и вложениями',
|
||||
'Все новости',
|
||||
'Весь журнал аудита',
|
||||
'Все роли, кроме системных admin/user',
|
||||
'Каталог приложений (будет пересеян из стартового набора)',
|
||||
],
|
||||
irreversible: 'Действие необратимо и не может быть отменено.',
|
||||
confirmPhrase: 'СБРОС',
|
||||
confirmInputLabel: 'Введите «{{phrase}}» для подтверждения',
|
||||
confirmButton: 'Сбросить панель безвозвратно',
|
||||
done: 'Панель сброшена до начального состояния.',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
totalUsers: 'Всего пользователей',
|
||||
@@ -752,6 +775,29 @@ const resources = {
|
||||
confirm: 'Delete all disabled apps? This cannot be undone.',
|
||||
deleted: 'Deleted apps: {{count}}.',
|
||||
},
|
||||
factoryReset: {
|
||||
dangerZone: 'Danger zone',
|
||||
title: 'Full panel reset',
|
||||
description: 'Return the panel to a freshly-deployed state — deletes all users, configs, nodes and data. Your current account is kept.',
|
||||
action: 'Reset panel',
|
||||
dialogTitle: 'Full panel reset',
|
||||
warningIntro: 'This will permanently delete:',
|
||||
warningItems: [
|
||||
'All users except your current account',
|
||||
'All VPN configs (revoked on the 3x-ui nodes first)',
|
||||
'All nodes and inbounds — 3x-ui server registrations and stored credentials',
|
||||
'All support tickets with their comments and attachments',
|
||||
'All news posts',
|
||||
'The entire audit log',
|
||||
'All roles except the system admin/user roles',
|
||||
'The app catalog (will be re-seeded from the default set)',
|
||||
],
|
||||
irreversible: 'This cannot be undone.',
|
||||
confirmPhrase: 'RESET',
|
||||
confirmInputLabel: 'Type "{{phrase}}" to confirm',
|
||||
confirmButton: 'Reset the panel permanently',
|
||||
done: 'The panel has been reset to its initial state.',
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
totalUsers: 'Total users',
|
||||
|
||||
Reference in New Issue
Block a user