Files
bedolaga-cabinet/src/api/auth.ts
Fringg 0f21629ba5 feat(auth): чекбоксы согласия с офертой и политикой при первой авторизации
Новый пользователь попадал в кабинет молча — никаких документов ему не показывали.
Теперь при создании аккаунта он подтверждает, что ознакомился с офертой и политикой.

Набор документов и сам факт включения гейта приходят с бэка
(GET /cabinet/info/legal-consent): там же учитывается, заполнены ли документы и
видны ли они в вебе. Фронт знает только ключи и адреса публичных страниц /offer
и /privacy — тех же, что уже линкует футер логина.

Два пути входа устроены по-разному, поэтому и UI разный:
- регистрация по email — чекбоксы прямо в форме, кнопка заблокирована до галочек;
- Telegram-вход происходит сам собой, спрашивать заранее не у чего. Поэтому ловим
  428 от бэка и показываем отдельный экран согласия, а замыкание помнит, какой
  именно вход повторить после простановки галочек. Тот же приём страхует и
  email-форму: конфиг мог протухнуть, если админ включил гейт между загрузкой
  страницы и отправкой.

Неизвестный ключ документа всё равно рисуем (без ссылки): бэк его требует, и молча
спрятанный чекбокс превратился бы в «кнопка не работает и непонятно почему».

Локали ru/en/zh/fa.
2026-07-30 00:31:47 +03:00

359 lines
11 KiB
TypeScript

import apiClient from './client';
import { getYandexCid } from '../utils/yandexCid';
import type {
AuthResponse,
LinkCallbackResponse,
LinkedProvidersResponse,
MergePreviewResponse,
MergeResponse,
OAuthProvider,
RegisterResponse,
ServerCompleteResponse,
TokenResponse,
User,
} from '../types';
export const authApi = {
loginTelegram: async (
initData: string,
campaignSlug?: string | null,
referralCode?: string | null,
acceptedLegalDocuments?: string[],
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram', {
init_data: initData,
campaign_slug: campaignSlug || undefined,
referral_code: referralCode || undefined,
yandex_cid: getYandexCid() || undefined,
accepted_legal_documents: acceptedLegalDocuments,
});
return response.data;
},
loginTelegramWidget: async (
data: {
id: number;
first_name: string;
last_name?: string;
username?: string;
photo_url?: string;
auth_date: number;
hash: string;
},
campaignSlug?: string | null,
referralCode?: string | null,
acceptedLegalDocuments?: string[],
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/widget', {
...data,
campaign_slug: campaignSlug || undefined,
referral_code: referralCode || undefined,
yandex_cid: getYandexCid() || undefined,
accepted_legal_documents: acceptedLegalDocuments,
});
return response.data;
},
loginTelegramOIDC: async (
idToken: string,
campaignSlug?: string | null,
referralCode?: string | null,
acceptedLegalDocuments?: string[],
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/oidc', {
id_token: idToken,
campaign_slug: campaignSlug || undefined,
referral_code: referralCode || undefined,
yandex_cid: getYandexCid() || undefined,
accepted_legal_documents: acceptedLegalDocuments,
});
return response.data;
},
loginEmail: async (
email: string,
password: string,
campaignSlug?: string | null,
referralCode?: string | null,
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/login', {
email,
password,
campaign_slug: campaignSlug || undefined,
referral_code: referralCode || undefined,
yandex_cid: getYandexCid() || undefined,
});
return response.data;
},
registerEmail: async (
email: string,
password: string,
): Promise<{
message: string;
email?: string;
merge_required?: boolean;
// 'email_code' means the email belongs to another account: a confirmation
// code was mailed to it and must be verified before a merge token is issued.
merge_verification?: 'email_code';
merge_token?: string | null;
}> => {
const response = await apiClient.post('/cabinet/auth/email/register', {
email,
password,
yandex_cid: getYandexCid() || undefined,
});
return response.data;
},
// Confirm an email account merge with the code sent to the existing account.
verifyEmailMerge: async (
code: string,
): Promise<{ message: string; merge_required?: boolean; merge_token?: string }> => {
const response = await apiClient.post('/cabinet/auth/email/merge/verify', { code });
return response.data;
},
registerEmailStandalone: async (data: {
email: string;
password: string;
first_name?: string;
language?: string;
referral_code?: string;
campaign_slug?: string;
accepted_legal_documents?: string[];
}): Promise<RegisterResponse> => {
const response = await apiClient.post<RegisterResponse>(
'/cabinet/auth/email/register/standalone',
{ ...data, yandex_cid: getYandexCid() || undefined },
);
return response.data;
},
verifyEmail: async (token: string, campaignSlug?: string | null): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/verify', {
token,
campaign_slug: campaignSlug || undefined,
});
return response.data;
},
resendVerification: async (): Promise<{ message: string }> => {
const response = await apiClient.post('/cabinet/auth/email/resend');
return response.data;
},
refreshToken: async (refreshToken: string): Promise<TokenResponse> => {
const response = await apiClient.post<TokenResponse>('/cabinet/auth/refresh', {
refresh_token: refreshToken,
});
return response.data;
},
logout: async (refreshToken: string): Promise<void> => {
await apiClient.post('/cabinet/auth/logout', {
refresh_token: refreshToken,
});
},
forgotPassword: async (email: string): Promise<{ message: string }> => {
const response = await apiClient.post('/cabinet/auth/password/forgot', { email });
return response.data;
},
resetPassword: async (token: string, password: string): Promise<{ message: string }> => {
const response = await apiClient.post('/cabinet/auth/password/reset', {
token,
password,
});
return response.data;
},
getMe: async (): Promise<User> => {
const response = await apiClient.get<User>('/cabinet/auth/me');
return response.data;
},
requestEmailChange: async (
newEmail: string,
): Promise<{ message: string; new_email: string; expires_in_minutes: number }> => {
const response = await apiClient.post('/cabinet/auth/email/change', {
new_email: newEmail,
});
return response.data;
},
verifyEmailChange: async (code: string): Promise<{ message: string; email: string }> => {
const response = await apiClient.post('/cabinet/auth/email/change/verify', {
code,
});
return response.data;
},
getOAuthProviders: async (): Promise<{ providers: OAuthProvider[] }> => {
const response = await apiClient.get<{ providers: OAuthProvider[] }>(
'/cabinet/auth/oauth/providers',
);
return response.data;
},
getOAuthAuthorizeUrl: async (
provider: string,
): Promise<{ authorize_url: string; state: string }> => {
const response = await apiClient.get<{ authorize_url: string; state: string }>(
`/cabinet/auth/oauth/${encodeURIComponent(provider)}/authorize`,
);
return response.data;
},
oauthCallback: async (
provider: string,
code: string,
state: string,
deviceId?: string | null,
campaignSlug?: string | null,
referralCode?: string | null,
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>(
`/cabinet/auth/oauth/${encodeURIComponent(provider)}/callback`,
{
code,
state,
device_id: deviceId || undefined,
campaign_slug: campaignSlug || undefined,
referral_code: referralCode || undefined,
yandex_cid: getYandexCid() || undefined,
},
);
return response.data;
},
getLinkedProviders: async (): Promise<LinkedProvidersResponse> => {
const response = await apiClient.get<LinkedProvidersResponse>(
'/cabinet/auth/account/linked-providers',
);
return response.data;
},
linkProviderInit: async (provider: string): Promise<{ authorize_url: string; state: string }> => {
const response = await apiClient.get<{ authorize_url: string; state: string }>(
`/cabinet/auth/account/link/${encodeURIComponent(provider)}/init`,
);
return response.data;
},
linkProviderCallback: async (
provider: string,
code: string,
state: string,
deviceId?: string,
): Promise<LinkCallbackResponse> => {
const response = await apiClient.post<LinkCallbackResponse>(
`/cabinet/auth/account/link/${encodeURIComponent(provider)}/callback`,
{
code,
state,
device_id: deviceId,
},
);
return response.data;
},
linkTelegram: async (
data:
| { init_data: string }
| { id_token: string }
| {
id: number;
first_name: string;
last_name?: string;
username?: string;
photo_url?: string;
auth_date: number;
hash: string;
},
): Promise<LinkCallbackResponse> => {
const response = await apiClient.post<LinkCallbackResponse>(
'/cabinet/auth/account/link/telegram',
data,
);
return response.data;
},
linkServerComplete: async (
code: string,
state: string,
deviceId?: string,
): Promise<ServerCompleteResponse> => {
const response = await apiClient.post<ServerCompleteResponse>(
'/cabinet/auth/account/link/server-complete',
{
code,
state,
device_id: deviceId,
},
);
return response.data;
},
unlinkProvider: async (provider: string): Promise<{ success: boolean }> => {
const response = await apiClient.post<{ success: boolean }>(
`/cabinet/auth/account/unlink/${encodeURIComponent(provider)}`,
);
return response.data;
},
autoLogin: async (token: string): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/login/auto', { token });
return response.data;
},
requestDeepLinkToken: async (): Promise<{
token: string;
bot_username: string;
expires_in: number;
}> => {
const response = await apiClient.post<{
token: string;
bot_username: string;
expires_in: number;
}>('/cabinet/auth/deeplink/request');
return response.data;
},
pollDeepLinkToken: async (token: string, campaignSlug?: string | null): Promise<AuthResponse> => {
// validateStatus: only treat 200 as success.
// Server returns 202 for "pending" and 410 for "expired" —
// these must reject so the polling catch-block can handle them.
// Without this, axios resolves on 202 (it's 2xx), causing
// loginWithDeepLink to set undefined tokens + isAuthenticated=true,
// which triggers checkAdminStatus() → 401 → safeRedirectToLogin() → infinite reload.
const response = await apiClient.post<AuthResponse>(
'/cabinet/auth/deeplink/poll',
{ token, campaign_slug: campaignSlug || undefined },
{ validateStatus: (status) => status === 200 },
);
return response.data;
},
getMergePreview: async (mergeToken: string): Promise<MergePreviewResponse> => {
const response = await apiClient.get<MergePreviewResponse>(
`/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`,
);
return response.data;
},
executeMerge: async (
mergeToken: string,
keepSubscriptionFrom: number,
): Promise<MergeResponse> => {
const response = await apiClient.post<MergeResponse>(
`/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`,
{
keep_subscription_from: keepSubscriptionFrom,
},
);
return response.data;
},
};