Add reset password button to admin user page
This commit is contained in:
@@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma";
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
async function requireAdmin() {
|
async function requireAdmin() {
|
||||||
const session = await auth.api.getSession({ headers: await headers() });
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
@@ -78,6 +79,26 @@ export async function deleteBalanceTransaction(userId: string, txId: string) {
|
|||||||
revalidatePath(`/admin/users/${userId}`);
|
revalidatePath(`/admin/users/${userId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resetUserPassword(userId: string): Promise<{ tempPassword: string }> {
|
||||||
|
await requireAdmin();
|
||||||
|
|
||||||
|
const account = await prisma.account.findFirst({
|
||||||
|
where: { userId, providerId: "credential" },
|
||||||
|
});
|
||||||
|
if (!account) throw new Error("Аккаунт с паролем не найден");
|
||||||
|
|
||||||
|
const chars = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
|
||||||
|
const tempPassword = Array.from({ length: 10 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
||||||
|
|
||||||
|
const hash = await bcrypt.hash(tempPassword, 10);
|
||||||
|
await prisma.account.update({
|
||||||
|
where: { id: account.id },
|
||||||
|
data: { password: hash },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tempPassword };
|
||||||
|
}
|
||||||
|
|
||||||
export async function revokeUserAccess(userId: string, courseId: string) {
|
export async function revokeUserAccess(userId: string, courseId: string) {
|
||||||
const session = await requireAdmin();
|
const session = await requireAdmin();
|
||||||
await prisma.courseEnrollment.delete({
|
await prisma.courseEnrollment.delete({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { UserEnrollmentManager } from "@/components/admin/user-enrollment-manager";
|
import { UserEnrollmentManager } from "@/components/admin/user-enrollment-manager";
|
||||||
import { UserContactEditor } from "@/components/admin/user-contact-editor";
|
import { UserContactEditor } from "@/components/admin/user-contact-editor";
|
||||||
import { UserBalanceBlock } from "@/components/admin/user-balance-block";
|
import { UserBalanceBlock } from "@/components/admin/user-balance-block";
|
||||||
|
import { ResetPasswordButton } from "@/components/admin/reset-password-button";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: Promise<{ userId: string }>;
|
params: Promise<{ userId: string }>;
|
||||||
@@ -77,6 +78,14 @@ export default async function UserPage({ params }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Reset password */}
|
||||||
|
<section className="card-aubade p-6 mb-6">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest mb-4" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Пароль
|
||||||
|
</p>
|
||||||
|
<ResetPasswordButton userId={userId} />
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Balance */}
|
{/* Balance */}
|
||||||
<section className="card-aubade p-6 mb-6">
|
<section className="card-aubade p-6 mb-6">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest mb-5" style={{ color: "var(--muted-foreground)" }}>
|
<p className="text-xs font-bold uppercase tracking-widest mb-5" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { resetUserPassword } from "@/app/admin/users/[userId]/actions";
|
||||||
|
|
||||||
|
export function ResetPasswordButton({ userId }: { userId: string }) {
|
||||||
|
const [tempPassword, setTempPassword] = useState<string | null>(null);
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
async function handleReset() {
|
||||||
|
if (!confirm("Сгенерировать новый временный пароль для пользователя?")) return;
|
||||||
|
setIsPending(true);
|
||||||
|
setTempPassword(null);
|
||||||
|
try {
|
||||||
|
const { tempPassword: pw } = await resetUserPassword(userId);
|
||||||
|
setTempPassword(pw);
|
||||||
|
} catch (e) {
|
||||||
|
alert(e instanceof Error ? e.message : "Ошибка");
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopy() {
|
||||||
|
if (!tempPassword) return;
|
||||||
|
await navigator.clipboard.writeText(tempPassword);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={isPending}
|
||||||
|
className="btn-aubade text-sm"
|
||||||
|
style={isPending ? { opacity: 0.6, cursor: "not-allowed" } : undefined}
|
||||||
|
>
|
||||||
|
{isPending ? "Генерация..." : "Сбросить пароль"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{tempPassword && (
|
||||||
|
<div className="p-3 text-sm space-y-2" style={{ border: "2px solid var(--border)", backgroundColor: "var(--color-highlight)" }}>
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Временный пароль — передай пользователю
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<code className="text-base font-bold tracking-wider">{tempPassword}</code>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="text-xs underline"
|
||||||
|
style={{ color: "var(--muted-foreground)" }}
|
||||||
|
>
|
||||||
|
{copied ? "Скопировано" : "Копировать"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Пользователь сможет сменить пароль в профиле после входа.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user