Implement rate limiting and enhance authentication flow
CI / Backend (build + test) (push) Successful in 1m17s
CI / Frontend (lint + typecheck + build) (push) Successful in 35s

- 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:
Leonid Pershin
2026-07-02 12:40:23 +03:00
parent ed07221ca5
commit 8067be3c35
106 changed files with 8823 additions and 172 deletions
@@ -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>
</>
)
}
+18
View File
@@ -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}`)
}