You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import { type Component, createSignal, onMount, Match, Switch } from 'solid-js';
|
|
import { useSearchParams, useNavigate, A } from '@solidjs/router';
|
|
import { CheckCircle, XCircle } from 'lucide-solid';
|
|
import { authApi } from '~/api/auth';
|
|
import { useAuth } from '~/contexts/AuthContext';
|
|
import { useI18n } from '~/i18n';
|
|
import LoadingSpinner from '~/components/ui/LoadingSpinner';
|
|
|
|
/**
|
|
* Magic-link verification page reached via `/auth/verify?token=...`.
|
|
*
|
|
* On mount, the `token` query parameter is extracted from the URL. If present,
|
|
* it is POSTed to the backend's verify endpoint which sets a session cookie.
|
|
* On success the auth context is refreshed and the user is redirected home
|
|
* after a 1.5-second delay. Missing or invalid tokens display an error with
|
|
* a link back to the login page.
|
|
*/
|
|
const AuthVerify: Component = () => {
|
|
const [searchParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const { refreshUser } = useAuth();
|
|
const { t } = useI18n();
|
|
|
|
const [status, setStatus] = createSignal<'verifying' | 'success' | 'error'>(
|
|
'verifying',
|
|
);
|
|
|
|
onMount(async () => {
|
|
const token = searchParams.token;
|
|
if (!token || typeof token !== 'string') {
|
|
setStatus('error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await authApi.verify(token);
|
|
await refreshUser();
|
|
setStatus('success');
|
|
setTimeout(() => navigate('/', { replace: true }), 1500);
|
|
} catch {
|
|
setStatus('error');
|
|
}
|
|
});
|
|
|
|
return (
|
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
|
|
<div class="max-w-md w-full text-center">
|
|
<Switch>
|
|
<Match when={status() === 'verifying'}>
|
|
<LoadingSpinner fullPage={false} />
|
|
<p class="mt-4 text-sm text-gray-500">{t('auth.verifying')}</p>
|
|
</Match>
|
|
|
|
<Match when={status() === 'success'}>
|
|
<CheckCircle class="mx-auto h-12 w-12 text-green-500" />
|
|
<h2 class="mt-4 text-2xl font-bold text-gray-900">
|
|
{t('auth.verifySuccess')}
|
|
</h2>
|
|
</Match>
|
|
|
|
<Match when={status() === 'error'}>
|
|
<XCircle class="mx-auto h-12 w-12 text-red-500" />
|
|
<h2 class="mt-4 text-2xl font-bold text-gray-900">
|
|
{t('auth.verifyError')}
|
|
</h2>
|
|
<p class="mt-2 text-sm text-gray-500">
|
|
{t('auth.verifyErrorDescription')}
|
|
</p>
|
|
<A
|
|
href="/login"
|
|
class="mt-6 inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
>
|
|
{t('auth.backToLogin')}
|
|
</A>
|
|
</Match>
|
|
</Switch>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AuthVerify;
|