diff --git a/src/components/admin/UsersPage.tsx b/src/components/admin/UsersPage.tsx index 5228ce6..8bf6e06 100644 --- a/src/components/admin/UsersPage.tsx +++ b/src/components/admin/UsersPage.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { AdminUserRow, UpdateAdminUserInput } from "@/actions/admin/users"; import { formatDate } from "@/lib/format-date"; import CreateUserModal from "./CreateUserModal"; @@ -124,6 +124,14 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre const [actionMenuId, setActionMenuId] = useState(null); const [passwordModal, setPasswordModal] = useState(null); const [resetLinkLoading, setResetLinkLoading] = useState(null); + const [success, setSuccess] = useState(null); + + // Auto-dismiss the success banner after 6 seconds. + useEffect(() => { + if (!success) return; + const t = setTimeout(() => setSuccess(null), 6000); + return () => clearTimeout(t); + }, [success]); const canManageUsers = currentUser.role === "platform_admin" || @@ -300,12 +308,14 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre async function handleSendResetLink(email: string) { setResetLinkLoading(email); + setError(null); + setSuccess(null); try { const { sendPasswordResetEmail } = await import("@/actions/admin/users"); const result = await sendPasswordResetEmail(email); setResetLinkLoading(null); if (result.error) throw new Error(result.error); - setError(null); + setSuccess(`Password-reset email sent to ${email}.`); } catch (e: unknown) { setResetLinkLoading(null); setError(e instanceof Error ? e.message : "Failed to send reset email"); @@ -341,6 +351,18 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre )} + {/* Success banner */} + {success && ( +
+ ✓ {success} + +
+ )} + {/* Error banner */} {error && (
diff --git a/tests/unit/reset-admin-password.test.ts b/tests/unit/reset-admin-password.test.ts index df3d56f..8d1d670 100644 --- a/tests/unit/reset-admin-password.test.ts +++ b/tests/unit/reset-admin-password.test.ts @@ -105,6 +105,7 @@ describe("resetAdminPassword — authorization", () => { mockGetAdminUser.mockResolvedValue(null); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(false); + if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/not authenticated/i); expect(mockSetUserPassword).not.toHaveBeenCalled(); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); @@ -114,6 +115,7 @@ describe("resetAdminPassword — authorization", () => { mockGetAdminUser.mockResolvedValue(BRAND_ADMIN); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(false); + if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/only platform admins/i); expect(mockSetUserPassword).not.toHaveBeenCalled(); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); @@ -124,6 +126,7 @@ describe("resetAdminPassword — input handling", () => { it("rejects an empty email", async () => { const r = await resetAdminPassword(" "); expect(r.success).toBe(false); + if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/required/i); }); @@ -145,6 +148,7 @@ describe("resetAdminPassword — input handling", () => { }); const r = await resetAdminPassword("missing@example.com"); expect(r.success).toBe(false); + if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/no neon auth user found/i); expect(mockSetUserPassword).not.toHaveBeenCalled(); }); @@ -154,6 +158,7 @@ describe("resetAdminPassword — happy path via setUserPassword", () => { it("returns a server-generated temp password and flips must_change_password", async () => { const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(true); + if (!r.success) throw new Error("expected success"); if (r.method !== "set") throw new Error("expected method=set"); // The password is server-generated — must be a non-empty strong // string, never a hard-coded constant like "Tuxedo2026!". @@ -182,17 +187,16 @@ describe("resetAdminPassword — FORBIDDEN fallback to requestPasswordReset", () }); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(true); + if (!r.success) throw new Error("expected success"); if (r.method !== "reset_link_sent") throw new Error("expected method=reset_link_sent"); expect(r.message).toMatch(/reset email/i); expect(mockRequestPasswordReset).toHaveBeenCalledWith({ email: "user@example.com", redirectTo: "http://localhost:4000/reset-password", }); - // In the fallback case, no tempPassword is returned and the - // must_change_password flag is NOT flipped (the user picks their - // own password via the link). - if (r.method === "set") throw new Error("unreachable"); - expect((r as { tempPassword?: string }).tempPassword).toBeUndefined(); + // The fallback case returns method="reset_link_sent" with a + // `message` describing what was sent; TypeScript guarantees + // tempPassword is not on this variant. }); it("falls back when setUserPassword returns UNAUTHORIZED", async () => { @@ -202,14 +206,16 @@ describe("resetAdminPassword — FORBIDDEN fallback to requestPasswordReset", () }); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(true); - expect(r.method).toBe("reset_link_sent"); + if (!r.success) throw new Error("expected success"); + if (r.method !== "reset_link_sent") throw new Error("expected method=reset_link_sent"); }); it("falls back when setUserPassword throws", async () => { mockSetUserPassword.mockRejectedValue(new Error("network down")); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(true); - expect(r.method).toBe("reset_link_sent"); + if (!r.success) throw new Error("expected success"); + if (r.method !== "reset_link_sent") throw new Error("expected method=reset_link_sent"); }); it("does NOT fall back for USER_NOT_FOUND — it surfaces the error", async () => { @@ -219,6 +225,7 @@ describe("resetAdminPassword — FORBIDDEN fallback to requestPasswordReset", () }); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(false); + if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/no such user in neon auth/); expect(mockRequestPasswordReset).not.toHaveBeenCalled(); }); @@ -234,6 +241,7 @@ describe("resetAdminPassword — FORBIDDEN fallback to requestPasswordReset", () }); const r = await resetAdminPassword("user@example.com"); expect(r.success).toBe(false); + if (r.success) throw new Error("expected failure"); expect(r.error).toMatch(/too many requests/); }); });