Фронт: вход по паролю, окно установки пароля, профиль и админка
Страница входа начинается с карточки «Вход» (ник и пароль), в ней же переключатель на регистрацию с повтором пароля. Telegram стоит ниже вторым способом. Под ним подсказка: аккаунт, созданный по паролю, сначала привязывают к Telegram в профиле, иначе вход через Telegram заведёт новый аккаунт. Поле логина без автозаглавной буквы и автоисправления, у полей правильный autocomplete для менеджеров паролей. SetPasswordDialog показывается в AppShell всем, у кого has_password=false: аккаунтам из Telegram и созданным до паролей. Закрыть окно нельзя, только задать пароль или выйти. Успешный ответ кладёт в кэш профиль с has_password=true, и окно исчезает само. Кнопка «Позже (dev)» есть лишь под import.meta.env.DEV: в прод-бандле её строки нет, это проверено по dist. В профиле появилась карточка «Вход в аккаунт»: смена пароля (текущий, если он задан, новый и повтор) и привязка Telegram тем же виджетом, что при входе. У смены ника подсказка, что ник — это и логин. В админке у игрока появилась кнопка «Задать пароль» с вводом в той же строке, как у переименования, а в строке игрока — пометка «без пароля». schema.d.ts перегенерирован с живого бэкенда. #24 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
This commit is contained in:
@@ -6,6 +6,7 @@ import { useServerEvents } from "../hooks/useServerEvents";
|
||||
import { BottomBar } from "./BottomBar";
|
||||
import { NotificationBell } from "./NotificationBell";
|
||||
import { NotificationToaster } from "./NotificationToaster";
|
||||
import { SetPasswordDialog } from "./SetPasswordDialog";
|
||||
import { SideMenu } from "./SideMenu";
|
||||
|
||||
const TITLES: Record<string, string> = {
|
||||
@@ -51,6 +52,9 @@ export function AppShell() {
|
||||
|
||||
<SideMenu open={menuOpen} onClose={() => setMenuOpen(false)} />
|
||||
<BottomBar onMenu={() => setMenuOpen(true)} />
|
||||
|
||||
{/* Без пароля дальше не пускаем: логин = ник, пароль нужен для входа. */}
|
||||
{me && !me.has_password && <SetPasswordDialog nickname={me.nickname} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { ApiError } from "../api/client";
|
||||
import { useLogout, useSetPassword } from "../hooks/auth";
|
||||
|
||||
/**
|
||||
* Обязательная установка пароля — для аккаунтов из Telegram и созданных до появления
|
||||
* паролей. Закрыть окно нельзя: только задать пароль или выйти. Кнопка «Позже» есть
|
||||
* лишь в dev-сборке (import.meta.env.DEV) — чтобы тестовые stub-игроки не упирались в окно.
|
||||
*/
|
||||
export function SetPasswordDialog({ nickname }: { nickname: string }) {
|
||||
const setPassword = useSetPassword();
|
||||
const logout = useLogout();
|
||||
const navigate = useNavigate();
|
||||
const [password, setPasswordValue] = useState("");
|
||||
const [repeat, setRepeat] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [skipped, setSkipped] = useState(false);
|
||||
|
||||
if (skipped) return null;
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (password !== repeat) {
|
||||
setError("Пароли не совпадают.");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
// Успех кладёт в кэш профиль с has_password=true — окно исчезает само.
|
||||
await setPassword.mutateAsync({ new_password: password });
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "Не удалось сохранить пароль");
|
||||
}
|
||||
};
|
||||
|
||||
const doLogout = async () => {
|
||||
await logout.mutateAsync().catch(() => {});
|
||||
navigate("/login", { replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<form className="modal" onSubmit={submit}>
|
||||
<div className="modal-title">Задайте пароль</div>
|
||||
<p className="modal-msg">
|
||||
Теперь на сайт входят по логину и паролю. Ваш логин — никнейм <b>{nickname}</b>.
|
||||
Придумайте пароль, чтобы продолжить; вход через Telegram тоже останется.
|
||||
</p>
|
||||
{/* Скрытый логин — чтобы менеджер паролей сохранил пару целиком. */}
|
||||
<input type="text" name="username" autoComplete="username" value={nickname} readOnly hidden />
|
||||
<div className="field">
|
||||
<label className="label" htmlFor="set-password">
|
||||
Пароль (не короче 8 символов)
|
||||
</label>
|
||||
<input
|
||||
id="set-password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
autoFocus
|
||||
value={password}
|
||||
onChange={(e) => setPasswordValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label" htmlFor="set-password-repeat">
|
||||
Повторите пароль
|
||||
</label>
|
||||
<input
|
||||
id="set-password-repeat"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={repeat}
|
||||
onChange={(e) => setRepeat(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="error-text">{error}</p>}
|
||||
<div className="modal-actions">
|
||||
{import.meta.env.DEV && (
|
||||
<button type="button" className="btn btn-ghost" onClick={() => setSkipped(true)}>
|
||||
Позже (dev)
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="btn btn-ghost" onClick={doLogout}>
|
||||
Выйти
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={setPassword.isPending || !password}>
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user