Introduces a new password reset page with token validation and form handling. Removes the optional email field from the Vendor interface and its display in the VendorsCard component.
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { fetchClient } from "@/lib/api-client";
|
|
|
|
interface Vendor {
|
|
_id: string;
|
|
username: string;
|
|
isAdmin: boolean;
|
|
createdAt: string;
|
|
lastLogin?: string;
|
|
}
|
|
|
|
export default function VendorsCard() {
|
|
const [vendors, setVendors] = useState<Vendor[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [resetTokens, setResetTokens] = useState<Record<string, string>>({});
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
(async () => {
|
|
try {
|
|
const data = await fetchClient<Vendor[]>("/admin/vendors");
|
|
if (mounted) setVendors(data);
|
|
} catch (e: any) {
|
|
if (mounted) setError(e?.message || "Failed to load vendors");
|
|
} finally {
|
|
if (mounted) setLoading(false);
|
|
}
|
|
})();
|
|
return () => { mounted = false; };
|
|
}, []);
|
|
|
|
async function generateResetToken(vendorId: string) {
|
|
try {
|
|
const res = await fetchClient<{ token: string; expiresAt: string }>("/admin/password-reset-token", {
|
|
method: "POST",
|
|
body: { vendorId }
|
|
});
|
|
setResetTokens(prev => ({ ...prev, [vendorId]: res.token }));
|
|
} catch (e: any) {
|
|
setError(e?.message || "Failed to generate reset token");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
|
<h2 className="font-medium">Vendors</h2>
|
|
<p className="text-sm text-muted-foreground mt-1">Manage vendor accounts and access</p>
|
|
|
|
{loading ? (
|
|
<p className="text-sm text-muted-foreground mt-3">Loading...</p>
|
|
) : error ? (
|
|
<p className="text-sm text-muted-foreground mt-3">{error}</p>
|
|
) : vendors.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground mt-3">No vendors found</p>
|
|
) : (
|
|
<div className="mt-3 space-y-2 max-h-64 overflow-y-auto">
|
|
{vendors.map((vendor) => (
|
|
<div key={vendor._id} className="rounded border border-border/50 p-3 text-sm">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<div className="font-medium">{vendor.username}</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
Created: {new Date(vendor.createdAt).toLocaleDateString()}
|
|
{vendor.lastLogin && ` · Last login: ${new Date(vendor.lastLogin).toLocaleDateString()}`}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{vendor.isAdmin && (
|
|
<span className="text-xs px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400">
|
|
Admin
|
|
</span>
|
|
)}
|
|
<button
|
|
onClick={() => generateResetToken(vendor._id)}
|
|
className="text-xs px-2 py-1 rounded border border-border hover:bg-muted/40"
|
|
>
|
|
Reset Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{resetTokens[vendor._id] && (
|
|
<div className="mt-2 p-2 rounded bg-muted/40 text-xs">
|
|
<div className="font-mono break-all">{resetTokens[vendor._id]}</div>
|
|
<div className="text-muted-foreground mt-1">Copy this token to share with the vendor</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|