Фронт: вход по паролю, окно установки пароля, профиль и админка
Страница входа начинается с карточки «Вход» (ник и пароль), в ней же переключатель на регистрацию с повтором пароля. 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:
@@ -1,10 +1,10 @@
|
||||
import { Pencil } from "lucide-react";
|
||||
import { KeyRound, Pencil } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ApiError } from "../../api/client";
|
||||
import { Spinner } from "../../components/Spinner";
|
||||
import { useToast } from "../../context/ToastContext";
|
||||
import { useAdminUpdateUser, useAdminUsers } from "../../hooks/admin";
|
||||
import { useAdminSetPassword, useAdminUpdateUser, useAdminUsers } from "../../hooks/admin";
|
||||
// DEV-ТОЛЬКО: удаление аккаунтов. Импорт используется лишь под import.meta.env.DEV,
|
||||
// поэтому в прод/тест-сборке вырезается тришейкингом (как и dev-вход).
|
||||
import { DevDeleteAccountButton } from "./DevDeleteAccountButton";
|
||||
@@ -12,17 +12,40 @@ import { DevDeleteAccountButton } from "./DevDeleteAccountButton";
|
||||
export function AdminAccountsPage() {
|
||||
const { data: users, isLoading } = useAdminUsers();
|
||||
const update = useAdminUpdateUser();
|
||||
const setPassword = useAdminSetPassword();
|
||||
const toast = useToast();
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState("");
|
||||
// Новый пароль игроку (восстановление забытого): отдельный режим строки, как переименование.
|
||||
const [passwordId, setPasswordId] = useState<number | null>(null);
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
|
||||
const startEdit = (id: number, nickname: string) => {
|
||||
setPasswordId(null);
|
||||
setEditId(id);
|
||||
setEditName(nickname);
|
||||
};
|
||||
|
||||
const startPassword = (id: number) => {
|
||||
setEditId(null);
|
||||
setPasswordId(id);
|
||||
setNewPassword("");
|
||||
};
|
||||
|
||||
const savePassword = async () => {
|
||||
if (passwordId == null || !newPassword) return;
|
||||
try {
|
||||
await setPassword.mutateAsync({ userId: passwordId, password: newPassword });
|
||||
toast.show("Пароль задан — сообщите его игроку");
|
||||
setPasswordId(null);
|
||||
setNewPassword("");
|
||||
} catch (e) {
|
||||
toast.show(e instanceof ApiError ? e.message : "Не удалось задать пароль");
|
||||
}
|
||||
};
|
||||
|
||||
const saveName = async () => {
|
||||
const name = editName.trim();
|
||||
if (editId == null || !name) return;
|
||||
@@ -73,12 +96,39 @@ export function AdminAccountsPage() {
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
) : passwordId === u.id ? (
|
||||
// Режим «задать пароль».
|
||||
<div className="row">
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder={`Новый пароль для ${u.nickname}`}
|
||||
value={newPassword}
|
||||
autoFocus
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") savePassword();
|
||||
if (e.key === "Escape") setPasswordId(null);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary small"
|
||||
disabled={setPassword.isPending || !newPassword}
|
||||
onClick={savePassword}
|
||||
>
|
||||
Задать
|
||||
</button>
|
||||
<button className="btn btn-ghost small" onClick={() => setPasswordId(null)}>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row-between">
|
||||
<div>
|
||||
<b>{u.nickname}</b> {u.role === "admin" && <span className="badge">админ</span>}
|
||||
<div className="muted small">
|
||||
#{u.id} · {u.auth_provider} · {u.is_active ? "активен" : "отключён"}
|
||||
{u.role !== "admin" && !u.has_password && " · без пароля"}
|
||||
</div>
|
||||
</div>
|
||||
{u.role !== "admin" && (
|
||||
@@ -90,6 +140,13 @@ export function AdminAccountsPage() {
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost small"
|
||||
title="Задать пароль"
|
||||
onClick={() => startPassword(u.id)}
|
||||
>
|
||||
<KeyRound size={16} />
|
||||
</button>
|
||||
{import.meta.env.DEV && (
|
||||
<DevDeleteAccountButton userId={u.id} nickname={u.nickname} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user