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,87 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { Send } from 'lucide-react'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
|
||||
import { applyAuthResponse } from '@/features/auth/api'
|
||||
import { createLoginRequest, getLoginRequestStatus } from './api'
|
||||
import type { TelegramLoginStatus } from '@/shared/api/types'
|
||||
|
||||
const TERMINAL: TelegramLoginStatus[] = ['Rejected', 'Expired', 'Consumed']
|
||||
|
||||
export function TelegramLoginButton() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [requestId, setRequestId] = useState<string | null>(null)
|
||||
const [deepLink, setDeepLink] = useState<string | null>(null)
|
||||
|
||||
const startMutation = useMutation({
|
||||
mutationFn: createLoginRequest,
|
||||
onSuccess: (data) => {
|
||||
setRequestId(data.requestId)
|
||||
setDeepLink(data.deepLink)
|
||||
setOpen(true)
|
||||
},
|
||||
onError: () => toast.error(t('auth.genericError')),
|
||||
})
|
||||
|
||||
const statusQuery = useQuery({
|
||||
queryKey: ['telegram-login-status', requestId],
|
||||
queryFn: () => getLoginRequestStatus(requestId!),
|
||||
enabled: open && !!requestId,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status
|
||||
return status && (status === 'Approved' || TERMINAL.includes(status)) ? false : 2000
|
||||
},
|
||||
})
|
||||
|
||||
const status = statusQuery.data?.status
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'Approved' || !statusQuery.data?.accessToken || !statusQuery.data.user) return
|
||||
applyAuthResponse({
|
||||
accessToken: statusQuery.data.accessToken,
|
||||
expiresAt: statusQuery.data.expiresAt!,
|
||||
user: statusQuery.data.user,
|
||||
})
|
||||
setOpen(false)
|
||||
void navigate({ to: '/dashboard' })
|
||||
}, [status, statusQuery.data, navigate])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="button" variant="outline" className="w-full" onClick={() => startMutation.mutate()} disabled={startMutation.isPending}>
|
||||
<Send className="h-4 w-4" />
|
||||
{t('auth.loginViaTelegram')}
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('auth.loginViaTelegram')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
{deepLink && (
|
||||
<>
|
||||
<QRCodeSVG value={deepLink} size={200} />
|
||||
<a href={deepLink} target="_blank" rel="noreferrer" className="text-sm text-primary hover:underline">
|
||||
{deepLink}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{!deepLink && <p className="text-sm text-muted-foreground">{t('auth.telegramBotNotConfigured')}</p>}
|
||||
|
||||
{status === 'Pending' && <p className="text-sm text-muted-foreground">{t('auth.waitingForConfirmation')}</p>}
|
||||
{status === 'Rejected' && <p className="text-sm text-red-500">{t('auth.telegramLoginRejected')}</p>}
|
||||
{status === 'Expired' && <p className="text-sm text-red-500">{t('auth.telegramLoginExpired')}</p>}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { apiRequest } from '@/shared/api/client'
|
||||
import type { LinkTokenResponse, TelegramLoginRequestResponse, TelegramLoginStatusResponse } from '@/shared/api/types'
|
||||
|
||||
export function createLinkToken() {
|
||||
return apiRequest<LinkTokenResponse>('/auth/telegram/link-token', { method: 'POST' })
|
||||
}
|
||||
|
||||
export function unlinkTelegram() {
|
||||
return apiRequest<void>('/auth/telegram/unlink', { method: 'POST' })
|
||||
}
|
||||
|
||||
export function createLoginRequest() {
|
||||
return apiRequest<TelegramLoginRequestResponse>('/auth/telegram/login-request', { method: 'POST' })
|
||||
}
|
||||
|
||||
export function getLoginRequestStatus(id: string) {
|
||||
return apiRequest<TelegramLoginStatusResponse>(`/auth/telegram/login-request/${id}`)
|
||||
}
|
||||
Reference in New Issue
Block a user