Add reset password to admin panel

This commit is contained in:
2025-12-20 00:34:22 +07:00
parent 2d281d1c8c
commit a77a757317
7 changed files with 171 additions and 2 deletions

View File

@@ -8,10 +8,11 @@ from app.api.deps import DbSession, CurrentUser, require_admin_with_2fa
from app.models import User, UserRole, Marathon, MarathonStatus, Participant, Game, AdminLog, AdminActionType, StaticContent
from app.schemas import (
UserPublic, MessageResponse,
AdminUserResponse, BanUserRequest, AdminLogResponse, AdminLogsListResponse,
AdminUserResponse, BanUserRequest, AdminResetPasswordRequest, AdminLogResponse, AdminLogsListResponse,
BroadcastRequest, BroadcastResponse, StaticContentResponse, StaticContentUpdate,
StaticContentCreate, DashboardStats
)
from app.core.security import get_password_hash
from app.services.telegram_notifier import telegram_notifier
from app.core.rate_limit import limiter
@@ -431,6 +432,66 @@ async def unban_user(
)
# ============ Reset Password ============
@router.post("/users/{user_id}/reset-password", response_model=AdminUserResponse)
async def reset_user_password(
request: Request,
user_id: int,
data: AdminResetPasswordRequest,
current_user: CurrentUser,
db: DbSession,
):
"""Reset user password. Admin only."""
require_admin_with_2fa(current_user)
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Hash and save new password
user.password_hash = get_password_hash(data.new_password)
await db.commit()
await db.refresh(user)
# Log action
await log_admin_action(
db, current_user.id, AdminActionType.USER_PASSWORD_RESET.value,
"user", user_id,
{"nickname": user.nickname},
request.client.host if request.client else None
)
# Notify user via Telegram if linked
if user.telegram_id:
await telegram_notifier.send_message(
user.telegram_id,
"🔐 <b>Ваш пароль был сброшен</b>\n\n"
"Администратор установил вам новый пароль. "
"Если это были не вы, свяжитесь с поддержкой."
)
marathons_count = await db.scalar(
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
)
return AdminUserResponse(
id=user.id,
login=user.login,
nickname=user.nickname,
role=user.role,
avatar_url=user.avatar_url,
telegram_id=user.telegram_id,
telegram_username=user.telegram_username,
marathons_count=marathons_count,
created_at=user.created_at.isoformat(),
is_banned=user.is_banned,
banned_at=user.banned_at.isoformat() if user.banned_at else None,
banned_until=user.banned_until.isoformat() if user.banned_until else None,
ban_reason=user.ban_reason,
)
# ============ Force Finish Marathon ============
@router.post("/marathons/{marathon_id}/force-finish", response_model=MessageResponse)
async def force_finish_marathon(

View File

@@ -12,6 +12,7 @@ class AdminActionType(str, Enum):
USER_UNBAN = "user_unban"
USER_AUTO_UNBAN = "user_auto_unban" # System automatic unban
USER_ROLE_CHANGE = "user_role_change"
USER_PASSWORD_RESET = "user_password_reset"
# Marathon actions
MARATHON_FORCE_FINISH = "marathon_force_finish"

View File

@@ -83,6 +83,7 @@ from app.schemas.dispute import (
)
from app.schemas.admin import (
BanUserRequest,
AdminResetPasswordRequest,
AdminUserResponse,
AdminLogResponse,
AdminLogsListResponse,
@@ -175,6 +176,7 @@ __all__ = [
"ReturnedAssignmentResponse",
# Admin
"BanUserRequest",
"AdminResetPasswordRequest",
"AdminUserResponse",
"AdminLogResponse",
"AdminLogsListResponse",

View File

@@ -9,6 +9,10 @@ class BanUserRequest(BaseModel):
banned_until: datetime | None = None # None = permanent ban
class AdminResetPasswordRequest(BaseModel):
new_password: str = Field(..., min_length=6, max_length=100)
class AdminUserResponse(BaseModel):
id: int
login: str