Добавлена форма изменения ника при регистрации аккаунта на случай, когда уже существует аакунт с ником, совпадающим с ТГ тегом регистрируемого аккаунта
This commit is contained in:
Vendored
+87
-1
@@ -30,7 +30,15 @@ export interface paths {
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Telegram Login */
|
||||
/**
|
||||
* Telegram Login
|
||||
* @description Вход/регистрация через Telegram.
|
||||
*
|
||||
* Возвращающийся пользователь (личность уже есть) — просто входит. Первый вход —
|
||||
* регистрация под тегом Telegram; если тег занят/некорректен, отдаём 409
|
||||
* TELEGRAM_NICKNAME_REQUIRED, и фронт повторяет регистрацию через /telegram/register
|
||||
* с выбранным ником.
|
||||
*/
|
||||
post: operations["telegram_login_api_auth_telegram_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -38,6 +46,30 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/auth/telegram/register": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Telegram Register
|
||||
* @description Завершение регистрации через Telegram с выбранным ником.
|
||||
*
|
||||
* Подпись виджета проверяется заново (поле nickname в HMAC не входит), поэтому личность
|
||||
* доверенная — подделать чужой telegram_id нельзя. Если пользователь уже успел
|
||||
* зарегистрироваться (гонка) — просто входим.
|
||||
*/
|
||||
post: operations["telegram_register_api_auth_telegram_register_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/auth/logout": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1369,6 +1401,27 @@ export interface components {
|
||||
} & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
/** TelegramRegister */
|
||||
TelegramRegister: {
|
||||
/** Id */
|
||||
id: number;
|
||||
/** Auth Date */
|
||||
auth_date: number;
|
||||
/** Hash */
|
||||
hash: string;
|
||||
/** First Name */
|
||||
first_name?: string | null;
|
||||
/** Last Name */
|
||||
last_name?: string | null;
|
||||
/** Username */
|
||||
username?: string | null;
|
||||
/** Photo Url */
|
||||
photo_url?: string | null;
|
||||
/** Nickname */
|
||||
nickname: string;
|
||||
} & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
/** UserRead */
|
||||
UserRead: {
|
||||
/** Id */
|
||||
@@ -1459,6 +1512,39 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
telegram_register_api_auth_telegram_register_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["TelegramRegister"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["MeRead"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
logout_api_auth_logout_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -44,6 +44,8 @@ export function useStubLogin() {
|
||||
}
|
||||
|
||||
// Вход через Telegram Login Widget: payload виджета проверяется на сервере по HMAC.
|
||||
// При первом входе, если тег занят, бэкенд отдаёт 409 TELEGRAM_NICKNAME_REQUIRED —
|
||||
// тогда вызывается useTelegramRegister с выбранным ником (тот же payload виджета).
|
||||
export function useTelegramLogin() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
@@ -56,6 +58,20 @@ export function useTelegramLogin() {
|
||||
});
|
||||
}
|
||||
|
||||
// Регистрация через Telegram с выбранным ником (когда тег занят/некорректен).
|
||||
// payload — те же данные виджета + nickname; подпись проверяется заново на сервере.
|
||||
export function useTelegramRegister() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (payload: Record<string, unknown>) =>
|
||||
unwrap(await api.POST("/api/auth/telegram/register", { body: payload as never })),
|
||||
onSuccess: (me) => {
|
||||
qc.setQueryData(qk.me, me);
|
||||
qc.invalidateQueries();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateDevUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
|
||||
@@ -11,13 +11,21 @@ import {
|
||||
useMe,
|
||||
useStubLogin,
|
||||
useTelegramLogin,
|
||||
useTelegramRegister,
|
||||
} from "../hooks/auth";
|
||||
|
||||
// Данные виджета Telegram, отложенные до выбора ника (когда тег занят).
|
||||
interface PendingTelegram {
|
||||
payload: Record<string, unknown>;
|
||||
suggested: string;
|
||||
}
|
||||
|
||||
export function LoginPage() {
|
||||
const { data: me, isLoading } = useMe();
|
||||
const { data: config } = useAuthConfig();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pendingTg, setPendingTg] = useState<PendingTelegram | null>(null);
|
||||
|
||||
const methods = config?.methods ?? [];
|
||||
const hasTelegram = methods.includes("telegram");
|
||||
@@ -36,6 +44,12 @@ export function LoginPage() {
|
||||
await telegram.mutateAsync(user);
|
||||
navigate("/", { replace: true });
|
||||
} catch (e) {
|
||||
// Тег занят/некорректен → просим выбрать ник и регистрируем тем же payload.
|
||||
if (e instanceof ApiError && e.code === "TELEGRAM_NICKNAME_REQUIRED") {
|
||||
const suggested = (e.details as { suggested?: string } | undefined)?.suggested ?? "";
|
||||
setPendingTg({ payload: user, suggested });
|
||||
return;
|
||||
}
|
||||
setError(e instanceof ApiError ? e.message : "Не удалось войти через Telegram");
|
||||
}
|
||||
};
|
||||
@@ -48,24 +62,35 @@ export function LoginPage() {
|
||||
</h1>
|
||||
<p className="muted center">Учёт партий</p>
|
||||
|
||||
{hasTelegram && (
|
||||
<div className="card">
|
||||
<h3>Вход через Telegram</h3>
|
||||
{config?.telegram_bot_username ? (
|
||||
<TelegramLoginButton
|
||||
botUsername={config.telegram_bot_username}
|
||||
onAuth={(u) => onTelegram(u as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
) : (
|
||||
<p className="muted small">
|
||||
Telegram не настроен — укажите TELEGRAM_BOT_USERNAME и TELEGRAM_BOT_TOKEN
|
||||
в .env (бот от @BotFather + /setdomain на ваш домен).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{pendingTg ? (
|
||||
<TelegramNicknamePicker
|
||||
pending={pendingTg}
|
||||
onCancel={() => {
|
||||
setPendingTg(null);
|
||||
setError(null);
|
||||
}}
|
||||
onDone={() => navigate("/", { replace: true })}
|
||||
/>
|
||||
) : (
|
||||
hasTelegram && (
|
||||
<div className="card">
|
||||
<h3>Вход через Telegram</h3>
|
||||
{config?.telegram_bot_username ? (
|
||||
<TelegramLoginButton
|
||||
botUsername={config.telegram_bot_username}
|
||||
onAuth={(u) => onTelegram(u as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
) : (
|
||||
<p className="muted small">
|
||||
Telegram не настроен — укажите TELEGRAM_BOT_USERNAME и TELEGRAM_BOT_TOKEN
|
||||
в .env (бот от @BotFather + /setdomain на ваш домен).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{showStub && <DevLogin onError={setError} />}
|
||||
{showStub && !pendingTg && <DevLogin onError={setError} />}
|
||||
|
||||
{error && <p className="error-text center">{error}</p>}
|
||||
|
||||
@@ -83,6 +108,68 @@ export function LoginPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Выбор ника при регистрации через Telegram, когда тег занят/некорректен.
|
||||
// Повторяет регистрацию тем же payload виджета (подпись на сервере проверяется заново).
|
||||
function TelegramNicknamePicker({
|
||||
pending,
|
||||
onCancel,
|
||||
onDone,
|
||||
}: {
|
||||
pending: PendingTelegram;
|
||||
onCancel: () => void;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const register = useTelegramRegister();
|
||||
const [nickname, setNickname] = useState(pending.suggested);
|
||||
const [error, setError] = useState<string | null>(
|
||||
pending.suggested ? `Ник «${pending.suggested}» уже занят — выберите другой.` : null,
|
||||
);
|
||||
|
||||
const submit = async () => {
|
||||
const nick = nickname.trim();
|
||||
if (!nick) {
|
||||
setError("Введите никнейм.");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
await register.mutateAsync({ ...pending.payload, nickname: nick });
|
||||
onDone();
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : "Не удалось зарегистрироваться");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h3>Выберите никнейм</h3>
|
||||
<p className="muted small">
|
||||
Ваш ник в Telegram уже занят другим игроком. Укажите другой никнейм для регистрации.
|
||||
</p>
|
||||
<div className="field">
|
||||
<div className="row">
|
||||
<input
|
||||
placeholder="Никнейм"
|
||||
value={nickname}
|
||||
autoFocus
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") submit();
|
||||
}}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={submit} disabled={register.isPending}>
|
||||
Зарегистрироваться
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className="error-text">{error}</p>}
|
||||
<button className="btn btn-ghost small" onClick={onCancel}>
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// DEV-вход по нику. Вынесен в отдельный компонент; импортируется только в dev-ветке
|
||||
// LoginPage, поэтому в прод-сборке (import.meta.env.DEV=false) удаляется тришейкингом.
|
||||
function DevLogin({ onError }: { onError: (msg: string | null) => void }) {
|
||||
|
||||
Reference in New Issue
Block a user