Implement rate limiting and enhance authentication flow
- Added rate limiting configuration for authentication endpoints, allowing customizable request limits via environment variables. - Updated authentication flow to utilize HttpRequest for cookie management, ensuring secure handling of refresh tokens. - Introduced a new endpoint to retrieve user subscription details. - Enhanced the handling of Telegram bot token validation to prevent errors with empty tokens. - Updated the application to serialize enums as strings for better documentation and compatibility with TypeScript. - Improved test coverage for new features and adjustments in command handlers.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { useState } from 'react'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { listUsers } from '@/features/admin/users/api'
|
||||
import { UserManageDialog } from '@/features/admin/users/UserManageDialog'
|
||||
import type { UserSummaryDto } from '@/shared/api/types'
|
||||
|
||||
export const Route = createFileRoute('/admin/users')({ component: AdminUsersPage })
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
function AdminUsersPage() {
|
||||
const { t } = useTranslation()
|
||||
const [search, setSearch] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [managing, setManaging] = useState<UserSummaryDto | null>(null)
|
||||
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ['admin-users', page, search],
|
||||
queryFn: () => listUsers(page, PAGE_SIZE, search || undefined),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
placeholder={t('admin.users.searchPlaceholder')}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value)
|
||||
setPage(1)
|
||||
}}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
|
||||
{isLoading && <p className="text-sm text-muted-foreground">…</p>}
|
||||
|
||||
{isError && (
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">{t('auth.genericError')}</p>
|
||||
<Button variant="outline" size="sm" onClick={() => void refetch()}>
|
||||
{t('activation.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-muted-foreground">
|
||||
<th className="py-2 font-medium">{t('admin.users.userName')}</th>
|
||||
<th className="py-2 font-medium">{t('admin.users.role')}</th>
|
||||
<th className="py-2 font-medium">{t('admin.users.statusLabel')}</th>
|
||||
<th className="py-2" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((user) => (
|
||||
<tr key={user.id} className="border-b border-border">
|
||||
<td className="py-2">{user.userName}</td>
|
||||
<td className="py-2">{user.role}</td>
|
||||
<td className="py-2">
|
||||
<Badge variant={user.isBlocked ? 'destructive' : user.isActivated ? 'success' : 'warning'}>
|
||||
{user.isBlocked
|
||||
? t('admin.users.status.blocked')
|
||||
: user.isActivated
|
||||
? t('admin.users.status.active')
|
||||
: t('admin.users.status.pending')}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
<Button size="sm" variant="outline" onClick={() => setManaging(user)}>
|
||||
{t('admin.users.manage')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{data.items.length === 0 && <p className="text-sm text-muted-foreground">{t('admin.users.empty')}</p>}
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">{t('admin.users.total', { count: data.total })}</span>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>
|
||||
{t('admin.prev')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={page * PAGE_SIZE >= data.total} onClick={() => setPage((p) => p + 1)}>
|
||||
{t('admin.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{managing && <UserManageDialog user={managing} open={!!managing} onOpenChange={(open) => !open && setManaging(null)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user