Files
auth-portal/apps/web/src/routes/_auth.admin.index.tsx
T
DenozordecandClaude Sonnet 5.5 9b05fcf03a
quality / changes (push) Successful in 7s
quality / api (push) Skipped
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 53s
CD / quality (push) Successful in 1m2s
CD / publish (push) Successful in 1m37s
feat(web): перенести OIDC, ссылки приложений и пользователей на новый дизайн
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-23 14:09:04 +07:00

234 lines
7.0 KiB
TypeScript

/**
* Admin users directory — thin wiring over AdminUsersGrid.
* Preview: https://reui.io/preview/base/solution-users-1
*/
import { useCallback, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { AdminUser, CreateUserRequest } from '@authportal/shared'
import { AdminUsersGrid } from '@/components/reui-kit/admin-users-grid'
import { CreateUserSheet } from '@/components/reui-kit/create-user-sheet'
import { UserAccessSheet } from '@/components/reui-kit/user-access-sheet'
import { UserAuditSheet } from '@/components/reui-kit/user-audit-sheet'
import { api, ApiError } from '@/lib/api-client'
import { usersQueryKey, usersQueryOptions } from '@/queries/auth'
import { adminResetPasskeys } from '@/queries/webauthn'
import { Button } from '@authportal/ui/components/button'
export const Route = createFileRoute('/_auth/admin/')({
component: AdminUsersPage,
})
function AdminUsersPage() {
const queryClient = useQueryClient()
const { data: users = [], isLoading, error, refetch } = useQuery(
usersQueryOptions,
)
const [createOpen, setCreateOpen] = useState(false)
const [accessUserId, setAccessUserId] = useState<string | null>(null)
const [auditUser, setAuditUser] = useState<AdminUser | null>(null)
const invalidateUsers = useCallback(async () => {
await queryClient.invalidateQueries({ queryKey: usersQueryKey })
}, [queryClient])
const createMutation = useMutation({
mutationFn: (body: CreateUserRequest) =>
api.post<AdminUser>('/api/v1/admin/users', body),
onSuccess: async (user) => {
await invalidateUsers()
setCreateOpen(false)
setAccessUserId(user.id)
toast.success('Пользователь создан')
},
onError: (err) => {
toast.error(
err instanceof ApiError ? err.message : 'Не удалось создать пользователя',
)
},
})
const patchMutation = useMutation({
mutationFn: ({
id,
...body
}: {
id: string
is_admin?: boolean
disabled?: boolean
}) => api.patch<AdminUser>(`/api/v1/admin/users/${id}`, body),
onSuccess: async () => {
await invalidateUsers()
},
onError: (err) => {
toast.error(
err instanceof ApiError ? err.message : 'Не удалось обновить пользователя',
)
},
})
const deleteMutation = useMutation({
mutationFn: (id: string) => api.delete(`/api/v1/admin/users/${id}`),
onSuccess: async () => {
await invalidateUsers()
toast.success('Пользователь удалён')
},
onError: (err) => {
toast.error(
err instanceof ApiError ? err.message : 'Не удалось удалить пользователя',
)
},
})
const handleDeactivate = useCallback(
(user: AdminUser) => {
patchMutation.mutate(
{ id: user.id, disabled: true },
{
onSuccess: () => {
toast.message('Пользователь отключён', {
description: `${user.email} больше не может войти.`,
})
},
},
)
},
[patchMutation],
)
const handleDelete = useCallback(
(user: AdminUser) => {
deleteMutation.mutate(user.id)
},
[deleteMutation],
)
const resetPasskeysMutation = useMutation({
mutationFn: (id: string) => adminResetPasskeys(id),
onSuccess: async (_data, id) => {
await invalidateUsers()
const user = users.find((u) => u.id === id)
toast.success('Passkeys сброшены', {
description: user?.email,
})
},
onError: (err) => {
toast.error(
err instanceof ApiError ? err.message : 'Не удалось сбросить passkeys',
)
},
})
const handleResetPasskeys = useCallback(
(user: AdminUser) => {
resetPasskeysMutation.mutate(user.id)
},
[resetPasskeysMutation],
)
const handleBulkSetRole = useCallback(
(userIds: string[], isAdmin: boolean) => {
Promise.all(
userIds.map((id) =>
api.patch<AdminUser>(`/api/v1/admin/users/${id}`, {
is_admin: isAdmin,
}),
),
)
.then(async () => {
await invalidateUsers()
toast.success(
`Роль обновлена для ${userIds.length} пользовател${userIds.length === 1 ? 'я' : 'ей'}`,
)
})
.catch((err) => {
toast.error(
err instanceof ApiError ? err.message : 'Ошибка массового обновления',
)
})
},
[invalidateUsers],
)
const handleBulkDeactivate = useCallback(
(userIds: string[]) => {
Promise.all(
userIds.map((id) =>
api.patch<AdminUser>(`/api/v1/admin/users/${id}`, { disabled: true }),
),
)
.then(async () => {
await invalidateUsers()
toast.message('Пользователи отключены', {
description: `${userIds.length} учётных записей.`,
})
})
.catch((err) => {
toast.error(
err instanceof ApiError ? err.message : 'Ошибка массового отключения',
)
})
},
[invalidateUsers],
)
return (
<>
{error ? (
<div className="flex flex-col gap-2">
<p className="text-destructive text-sm">
{error instanceof ApiError ? error.message : 'Ошибка загрузки'}
</p>
<Button variant="outline" size="sm" className="w-fit" onClick={() => refetch()}>
Повторить
</Button>
</div>
) : (
<AdminUsersGrid
users={users}
isLoading={isLoading}
onCreateClick={() => setCreateOpen(true)}
onOpenAudit={setAuditUser}
onOpenAccess={(user) => setAccessUserId(user.id)}
onDeactivate={handleDeactivate}
onDelete={handleDelete}
onResetPasskeys={handleResetPasskeys}
onBulkSetRole={handleBulkSetRole}
onBulkDeactivate={handleBulkDeactivate}
/>
)}
<CreateUserSheet
open={createOpen}
onOpenChange={setCreateOpen}
pending={createMutation.isPending}
error={
createMutation.error instanceof ApiError
? createMutation.error.message
: createMutation.error
? 'Не удалось создать'
: null
}
onSubmit={(values) => createMutation.mutate(values)}
/>
<UserAccessSheet
userId={accessUserId}
open={Boolean(accessUserId)}
onOpenChange={(next) => {
if (!next) setAccessUserId(null)
}}
/>
<UserAuditSheet
user={auditUser}
open={Boolean(auditUser)}
onOpenChange={(next) => {
if (!next) setAuditUser(null)
}}
/>
</>
)
}