fix(auth): handle unregistered users in authentication flow

- Updated the authentication logic in both Auth and useAuth functions to redirect unregistered users to the '/unregistered' page.
- Enhanced error handling in the authOptions to check for user registration status using the Telegram ID.
- Improved the matcher configuration in middleware to exclude the '/unregistered' route from authentication checks.
This commit is contained in:
vchikalkin 2025-09-10 18:27:31 +03:00
parent c9187816a1
commit 17ce24ae04
6 changed files with 139 additions and 3 deletions

View File

@ -23,6 +23,17 @@ export default function Auth() {
callbackUrl: '/profile',
redirect: false,
telegramId: user?.id?.toString(),
}).then((result) => {
if (
result?.error &&
(result?.error?.includes('CredentialsSignin') ||
result?.error?.includes('UNREGISTERED'))
) {
// Пользователь не зарегистрирован
redirect('/unregistered');
} else if (result?.ok) {
redirect('/profile');
}
});
});
}

View File

@ -30,7 +30,17 @@ function useAuth() {
callbackUrl: '/profile',
redirect: false,
telegramId: initDataUser.id.toString(),
}).then(() => redirect('/profile'));
}).then((result) => {
if (
result?.error &&
(result?.error?.includes('CredentialsSignin') || result?.error?.includes('UNREGISTERED'))
) {
// Пользователь не зарегистрирован
redirect('/unregistered');
} else if (result?.ok) {
redirect('/profile');
}
});
}
}, [initDataUser?.id, status]);
}

View File

@ -0,0 +1,54 @@
import { UnregisteredClient } from './unregistered-client';
import { Container } from '@/components/layout';
import { env } from '@/config/env';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@repo/ui/components/ui/card';
import { Bot, MessageCircle } from 'lucide-react';
export default function UnregisteredPage() {
return (
<Container>
<div className="flex min-h-screen items-center justify-center">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
<Bot className="size-8 text-blue-600 dark:text-blue-400" />
</div>
<CardTitle className="text-xl">Давайте познакомимся</CardTitle>
<CardDescription>
Для использования приложения необходимо поделиться своим номером телефона с ботом
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-lg bg-muted p-4">
<div className="space-y-3">
<div className="flex items-start gap-3">
<MessageCircle className="mt-0.5 size-5 text-blue-500" />
<div className="text-sm">
<p className="mb-1 font-medium text-foreground">Как поделиться:</p>
<ol className="list-inside list-decimal space-y-1 text-muted-foreground">
<li>Вернитесь к Telegram боту</li>
<li>
Отправьте команду{' '}
<code className="rounded bg-muted px-1 py-0.5 text-xs">/start</code>
</li>
<li>Нажмите на появившуюся кнопку "Отправить номер телефона"</li>
<li>Закройте и откройте это приложение еще раз</li>
</ol>
</div>
</div>
</div>
</div>
<UnregisteredClient botUrl={env.BOT_URL} />
</CardContent>
</Card>
</div>
</Container>
);
}

View File

@ -0,0 +1,37 @@
'use client';
import { Button } from '@repo/ui/components/ui/button';
import { Bot, ExternalLink } from 'lucide-react';
import Link from 'next/link';
import { signOut } from 'next-auth/react';
type UnregisteredClientProps = {
readonly botUrl: string;
};
export function UnregisteredClient({ botUrl }: UnregisteredClientProps) {
const handleSignOut = () => {
signOut({ callbackUrl: '/' });
};
const handleRefresh = () => {
window.location.reload();
};
return (
<div className="flex flex-col gap-2">
<Button asChild className="w-full">
<Link href={botUrl} rel="noopener noreferrer" target="_blank">
<Bot className="mr-2 size-4" />
Перейти к боту
<ExternalLink className="ml-2 size-4" />
</Link>
</Button>
<Button className="w-full" onClick={handleRefresh} variant="outline">
Обновить страницу
</Button>
<Button className="w-full" onClick={handleSignOut} variant="outline">
Выйти из аккаунта
</Button>
</div>
);
}

View File

@ -1,3 +1,5 @@
import { getClientWithToken } from '@repo/graphql/apollo/client';
import * as GQL from '@repo/graphql/types';
import { type AuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
@ -32,7 +34,29 @@ export const authOptions: AuthOptions = {
throw new TypeError('Invalid Telegram ID format');
}
return { id: parsedTelegramId.toString(), telegramId: parsedTelegramId };
try {
// Проверяем, зарегистрирован ли пользователь
const { query } = await getClientWithToken();
const result = await query({
query: GQL.GetCustomerDocument,
variables: { telegramId: parsedTelegramId },
});
const customer = result.data.customers.at(0);
if (!customer) {
// Пользователь не зарегистрирован - перенаправляем на страницу регистрации
throw new Error('UNREGISTERED');
}
return { id: parsedTelegramId.toString(), telegramId: parsedTelegramId };
} catch (error) {
if (error instanceof Error && error.message.includes('UNREGISTERED')) {
throw error;
}
throw new Error('Authentication failed');
}
},
credentials: {
telegramId: { label: 'Telegram ID', type: 'text' },

View File

@ -11,5 +11,5 @@ export default withAuth({
});
export const config = {
matcher: ['/((?!auth|browser|telegram|api|_next/static|_next/image|favicon.ico).*)'],
matcher: ['/((?!auth|browser|telegram|unregistered|api|_next/static|_next/image|favicon.ico).*)'],
};