Пароль и привязка Telegram в профиле, пароль игрока из админки
PUT /api/users/me/password задаёт или меняет пароль. Первый раз текущий пароль
не нужен: так его задают аккаунты из Telegram и все, кто появился до паролей.
Если пароль уже есть, нужен текущий. Иначе оставленная открытой сессия позволила
бы отобрать аккаунт насовсем, поэтому подбор текущего тоже ограничен: 5 неудач
на аккаунт за 15 минут. Ошибка 403 WRONG_CURRENT_PASSWORD, а не 401, чтобы фронт
не принял её за истёкшую сессию.
POST /api/users/me/telegram привязывает Telegram к аккаунту, созданному по
паролю. Подпись виджета проверяется так же, как при входе, ник не меняется.
Связка пишется в auth_identity, как при регистрации через Telegram, поэтому
следующий вход через Telegram попадает в этот аккаунт. Telegram, привязанный к
другому аккаунту, даёт 409 TELEGRAM_TAKEN, повторная привязка — 409
TELEGRAM_ALREADY_LINKED.
PUT /api/admin/users/{id}/password — способ восстановить забытый пароль: почту
приложение не хранит. Работает только для игроков, пароль админа по-прежнему
задаётся в .env. В аудит пишется только факт смены, без пароля. Сборка
AdminUserRead вынесена в хелпер, в ответе появилось has_password.
#24
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
This commit is contained in:
@@ -10,7 +10,13 @@ from sqlmodel import Session, select
|
||||
from app.auth.password import validate_new_password
|
||||
from app.auth.provider import ExternalIdentity
|
||||
from app.core.config import settings
|
||||
from app.core.errors import NicknameTakenError, NotFoundError, ValidationError
|
||||
from app.core.errors import (
|
||||
NicknameTakenError,
|
||||
NotFoundError,
|
||||
TelegramAlreadyLinkedError,
|
||||
TelegramTakenError,
|
||||
ValidationError,
|
||||
)
|
||||
from app.core.security import hash_password
|
||||
from app.core.timeutil import utcnow
|
||||
from app.models import AuthIdentity, Faction, User
|
||||
@@ -154,6 +160,45 @@ def register_local(session: Session, nickname: str, password: str) -> User:
|
||||
return user
|
||||
|
||||
|
||||
def set_password(session: Session, user: User, new_password: str) -> User:
|
||||
"""Записать новый пароль. Проверку текущего делает вызывающий (игрок — да, админ — нет)."""
|
||||
validate_new_password(new_password)
|
||||
user.password_hash = hash_password(new_password)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
def link_telegram(session: Session, user: User, identity: ExternalIdentity) -> User:
|
||||
"""Привязать Telegram к существующему аккаунту; ник не меняется.
|
||||
|
||||
После привязки вход через Telegram попадает в этот аккаунт: find_by_identity находит
|
||||
его по той же связке provider+external_id, что создаёт регистрация через Telegram."""
|
||||
already = session.exec(
|
||||
select(AuthIdentity).where(
|
||||
AuthIdentity.user_id == user.id, AuthIdentity.provider == identity.provider
|
||||
)
|
||||
).first()
|
||||
if user.telegram_id is not None or already is not None:
|
||||
raise TelegramAlreadyLinkedError()
|
||||
taken_by_id = session.exec(select(User).where(User.telegram_id == identity.telegram_id)).first()
|
||||
if find_by_identity(session, identity) is not None or taken_by_id is not None:
|
||||
raise TelegramTakenError()
|
||||
user.telegram_id = identity.telegram_id
|
||||
session.add(user)
|
||||
session.add(
|
||||
AuthIdentity(
|
||||
user_id=user.id, # type: ignore[arg-type]
|
||||
provider=identity.provider,
|
||||
external_id=identity.external_id,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
def update_nickname(session: Session, user: User, new_nickname: str) -> User:
|
||||
new_nickname = (new_nickname or "").strip()
|
||||
if not nickname_format_ok(new_nickname):
|
||||
|
||||
Reference in New Issue
Block a user