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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -52,6 +52,13 @@ export const adminApi = {
return response.data
},
resetUserPassword: async (id: number, newPassword: string): Promise<AdminUser> => {
const response = await client.post<AdminUser>(`/admin/users/${id}/reset-password`, {
new_password: newPassword,
})
return response.data
},
// Marathons
listMarathons: async (skip = 0, limit = 50, search?: string): Promise<AdminMarathon[]> => {
const params: Record<string, unknown> = { skip, limit }

View File

@@ -4,7 +4,7 @@ import type { AdminUser, UserRole } from '@/types'
import { useToast } from '@/store/toast'
import { useConfirm } from '@/store/confirm'
import { NeonButton } from '@/components/ui'
import { Search, Ban, UserCheck, Shield, ShieldOff, ChevronLeft, ChevronRight, Users, X } from 'lucide-react'
import { Search, Ban, UserCheck, Shield, ShieldOff, ChevronLeft, ChevronRight, Users, X, KeyRound } from 'lucide-react'
export function AdminUsersPage() {
const [users, setUsers] = useState<AdminUser[]>([])
@@ -17,6 +17,9 @@ export function AdminUsersPage() {
const [banDuration, setBanDuration] = useState<string>('permanent')
const [banCustomDate, setBanCustomDate] = useState('')
const [banning, setBanning] = useState(false)
const [resetPasswordUser, setResetPasswordUser] = useState<AdminUser | null>(null)
const [newPassword, setNewPassword] = useState('')
const [resettingPassword, setResettingPassword] = useState(false)
const toast = useToast()
const confirm = useConfirm()
@@ -120,6 +123,24 @@ export function AdminUsersPage() {
}
}
const handleResetPassword = async () => {
if (!resetPasswordUser || !newPassword.trim() || newPassword.length < 6) return
setResettingPassword(true)
try {
const updated = await adminApi.resetUserPassword(resetPasswordUser.id, newPassword)
setUsers(users.map(u => u.id === updated.id ? updated : u))
toast.success(`Пароль ${updated.nickname} сброшен`)
setResetPasswordUser(null)
setNewPassword('')
} catch (err) {
console.error('Failed to reset password:', err)
toast.error('Ошибка сброса пароля')
} finally {
setResettingPassword(false)
}
}
return (
<div className="space-y-6">
{/* Header */}
@@ -265,6 +286,14 @@ export function AdminUsersPage() {
<Shield className="w-4 h-4" />
</button>
)}
<button
onClick={() => setResetPasswordUser(user)}
className="p-2 text-yellow-400 hover:bg-yellow-500/20 rounded-lg transition-colors"
title="Сбросить пароль"
>
<KeyRound className="w-4 h-4" />
</button>
</div>
</td>
</tr>
@@ -393,6 +422,71 @@ export function AdminUsersPage() {
</div>
</div>
)}
{/* Reset Password Modal */}
{resetPasswordUser && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
<div className="glass rounded-2xl p-6 max-w-md w-full mx-4 border border-dark-600 shadow-2xl">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<KeyRound className="w-5 h-5 text-yellow-400" />
Сбросить пароль {resetPasswordUser.nickname}
</h3>
<button
onClick={() => {
setResetPasswordUser(null)
setNewPassword('')
}}
className="p-1.5 text-gray-400 hover:text-white rounded-lg hover:bg-dark-600/50 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">
Новый пароль
</label>
<input
type="text"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Минимум 6 символов"
className="w-full bg-dark-700/50 border border-dark-600 rounded-xl px-4 py-2.5 text-white placeholder:text-gray-500 focus:outline-none focus:border-accent-500/50 transition-colors"
/>
{newPassword && newPassword.length < 6 && (
<p className="mt-2 text-sm text-red-400">Пароль должен быть минимум 6 символов</p>
)}
{resetPasswordUser.telegram_id && (
<p className="mt-2 text-sm text-gray-400">
Пользователь получит уведомление в Telegram о смене пароля
</p>
)}
</div>
<div className="flex gap-3 justify-end">
<NeonButton
variant="ghost"
onClick={() => {
setResetPasswordUser(null)
setNewPassword('')
}}
>
Отмена
</NeonButton>
<NeonButton
color="neon"
onClick={handleResetPassword}
disabled={!newPassword.trim() || newPassword.length < 6 || resettingPassword}
isLoading={resettingPassword}
icon={<KeyRound className="w-4 h-4" />}
>
Сбросить
</NeonButton>
</div>
</div>
</div>
)}
</div>
)
}