First Commit
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 5m26s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

This commit is contained in:
Denozordec
2026-07-18 13:27:37 +07:00
commit bac95bdb2e
154 changed files with 26610 additions and 0 deletions
@@ -0,0 +1,146 @@
import { useState, type FormEvent } from 'react'
import { useNavigate, useSearch } from '@tanstack/react-router'
import { useQueryClient } from '@tanstack/react-query'
import { EyeIcon, EyeOffIcon } from 'lucide-react'
import { buildSsoRedirectUrl, isReturnToAllowed } from '@authportal/shared'
import { Button } from '@authportal/ui/components/button'
import { Field, FieldGroup, FieldLabel } from '@authportal/ui/components/field'
import { Input } from '@authportal/ui/components/input'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from '@authportal/ui/components/input-group'
import {
Alert,
AlertDescription,
AlertTitle,
} from '@/components/reui/alert'
import { setToken } from '@/lib/auth'
import { ApiError } from '@/lib/api-client'
import { login, meQueryKey } from '@/queries/auth'
import { AuthLogo } from '@/components/blocks/auth-18/components/auth-logo'
/** Dev default matches auth-portal .env.example; prod should pass via Vite if needed. */
const RETURN_TO_ALLOWLIST =
import.meta.env.VITE_RETURN_TO_ALLOWLIST ??
'.shnt.top,localhost,http://localhost:5173'
export function PortalLoginForm() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const search = useSearch({ from: '/' }) as { return_to?: string }
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState<string | null>(null)
const [pending, setPending] = useState(false)
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
setError(null)
setPending(true)
const form = new FormData(event.currentTarget)
const email = String(form.get('email') ?? '')
const password = String(form.get('password') ?? '')
const returnTo = search.return_to
try {
const res = await login(email, password, returnTo)
setToken(res.access_token)
queryClient.setQueryData(meQueryKey, res.user)
if (
returnTo &&
isReturnToAllowed(returnTo, RETURN_TO_ALLOWLIST)
) {
window.location.href = buildSsoRedirectUrl(
returnTo,
res.access_token,
res.expires_at,
)
return
}
if (res.user.is_admin) {
await navigate({ to: '/admin' })
} else {
await navigate({ to: '/apps' })
}
} catch (err) {
setError(
err instanceof ApiError ? err.message : 'Не удалось войти',
)
} finally {
setPending(false)
}
}
return (
<section className="flex min-h-svh min-w-0 flex-col justify-center py-8">
<div className="mx-auto flex w-full max-w-90 flex-col gap-6 px-4">
<div className="flex flex-col items-center gap-3 text-center">
<AuthLogo />
<div className="flex flex-col gap-1">
<h1 className="text-xl font-semibold tracking-tight">
Auth Portal
</h1>
<p className="text-muted-foreground text-sm">
Единый вход в приложения shnt.top
</p>
</div>
</div>
{error ? (
<Alert variant="destructive">
<AlertTitle>Ошибка входа</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<FieldGroup className="gap-3.5">
<Field className="gap-2">
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
autoComplete="username"
placeholder="[email protected]"
className="bg-background"
required
/>
</Field>
<Field className="gap-2">
<FieldLabel htmlFor="password">Пароль</FieldLabel>
<InputGroup className="bg-background w-full">
<InputGroupInput
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
placeholder="Пароль"
required
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
size="icon-xs"
aria-label={showPassword ? 'Скрыть пароль' : 'Показать пароль'}
onClick={() => setShowPassword((v) => !v)}
>
{showPassword ? <EyeOffIcon /> : <EyeIcon />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</FieldGroup>
<Button type="submit" className="w-full" disabled={pending}>
{pending ? 'Вход…' : 'Войти'}
</Button>
</form>
</div>
</section>
)
}