Profile (fixes Safari/SSO no-password redirect): - User lookup falls back to email when JWT id is stale (SSO users) - generateDownloadUrl wrapped in try/catch so storage never crashes the page - Signature gate now uses approve_po permission (approvers only) - SSO/no-password users see a Set Password form (current-password field hidden) Vendors: - New create_vendor permission for all PO roles incl. submitters - Submitters create UNVERIFIED vendors (no Vendor ID); simple form mode - verifyVendor action + Verify menu item (manage_vendors) - Vendors auto-verify when a PO closes with them (receipt confirm + import) - Add Vendor button on /inventory/vendors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import { redirect } from "next/navigation";
|
|
import { hasPermission } from "@/lib/permissions";
|
|
import { generateDownloadUrl } from "@/lib/storage";
|
|
import { ChangePasswordForm } from "./change-password-form";
|
|
import { SignatureUploader } from "./signature-uploader";
|
|
import { SuperUserRequestForm } from "./superuser-request-form";
|
|
import type { Metadata } from "next";
|
|
|
|
export const metadata: Metadata = { title: "My Profile" };
|
|
|
|
const ROLE_LABELS: Record<string, string> = {
|
|
TECHNICAL: "Technical",
|
|
MANNING: "Manning",
|
|
ACCOUNTS: "Accounts",
|
|
MANAGER: "Manager",
|
|
SUPERUSER: "SuperUser",
|
|
AUDITOR: "Auditor",
|
|
ADMIN: "Admin",
|
|
};
|
|
|
|
export default async function ProfilePage() {
|
|
const session = await auth();
|
|
if (!session?.user) redirect("/login");
|
|
|
|
const userSelect = {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
employeeId: true,
|
|
role: true,
|
|
signatureKey: true,
|
|
passwordHash: true,
|
|
superUserRequests: {
|
|
orderBy: { createdAt: "desc" as const },
|
|
take: 1,
|
|
select: { status: true, createdAt: true },
|
|
},
|
|
};
|
|
|
|
// Look up by id, falling back to email. SSO/no-password users can carry a JWT
|
|
// whose `id` differs from the DB row; the email fallback keeps the page reachable.
|
|
let user = await db.user.findUnique({ where: { id: session.user.id }, select: userSelect });
|
|
if (!user && session.user.email) {
|
|
user = await db.user.findUnique({ where: { email: session.user.email }, select: userSelect });
|
|
}
|
|
if (!user) redirect("/login");
|
|
|
|
// Only approvers (those who can approve POs) may upload a signature.
|
|
const canHaveSignature = hasPermission(user.role, "approve_po");
|
|
const canRequestSuperUser = user.role !== "SUPERUSER" && user.role !== "ADMIN";
|
|
// SSO-only users have no password yet; the form lets them set one.
|
|
const hasPassword = !!user.passwordHash;
|
|
|
|
// Never let a storage hiccup (missing key, R2 misconfig) crash the profile page.
|
|
let signatureUrl: string | null = null;
|
|
if (user.signatureKey) {
|
|
try {
|
|
signatureUrl = await generateDownloadUrl(user.signatureKey);
|
|
} catch {
|
|
signatureUrl = null;
|
|
}
|
|
}
|
|
|
|
const latestRequest = user.superUserRequests[0] ?? null;
|
|
|
|
return (
|
|
<div className="max-w-2xl space-y-8">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold text-neutral-900">My Profile</h1>
|
|
<p className="mt-1 text-sm text-neutral-500">Manage your account settings</p>
|
|
</div>
|
|
|
|
{/* Account Info */}
|
|
<section className="rounded-lg border border-neutral-200 bg-white p-6">
|
|
<h2 className="text-base font-semibold text-neutral-900 mb-4">Account Information</h2>
|
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
|
<div>
|
|
<dt className="text-neutral-500">Name</dt>
|
|
<dd className="font-medium text-neutral-900">{user.name}</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="text-neutral-500">Email</dt>
|
|
<dd className="font-medium text-neutral-900">{user.email}</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="text-neutral-500">Employee ID</dt>
|
|
<dd className="font-mono text-sm font-medium text-neutral-900">{user.employeeId}</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="text-neutral-500">Role</dt>
|
|
<dd>
|
|
<span className="inline-flex items-center rounded-full bg-primary-50 px-2.5 py-0.5 text-xs font-medium text-primary-700">
|
|
{ROLE_LABELS[user.role] ?? user.role}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</section>
|
|
|
|
{/* Change / Set Password */}
|
|
<section className="rounded-lg border border-neutral-200 bg-white p-6">
|
|
<h2 className="text-base font-semibold text-neutral-900 mb-1">
|
|
{hasPassword ? "Change Password" : "Set Password"}
|
|
</h2>
|
|
{!hasPassword && (
|
|
<p className="mb-4 text-sm text-neutral-500">
|
|
You sign in with single sign-on. Optionally set a password to also sign in with email.
|
|
</p>
|
|
)}
|
|
<ChangePasswordForm hasPassword={hasPassword} />
|
|
</section>
|
|
|
|
{/* Signature (managers & superusers) */}
|
|
{canHaveSignature && (
|
|
<section className="rounded-lg border border-neutral-200 bg-white p-6">
|
|
<div className="mb-4">
|
|
<h2 className="text-base font-semibold text-neutral-900">Approval Signature</h2>
|
|
<p className="mt-1 text-sm text-neutral-500">
|
|
Your signature is embedded in approved PO documents (PDF and XLSX).
|
|
{!user.signatureKey && (
|
|
<span className="ml-1 font-medium text-warning-700">
|
|
A signature is required to approve purchase orders.
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
<SignatureUploader currentSignatureUrl={signatureUrl} />
|
|
</section>
|
|
)}
|
|
|
|
{/* SuperUser access request */}
|
|
{canRequestSuperUser && (
|
|
<section className="rounded-lg border border-neutral-200 bg-white p-6">
|
|
<h2 className="text-base font-semibold text-neutral-900 mb-2">SuperUser Access</h2>
|
|
<SuperUserRequestForm pendingRequest={latestRequest} />
|
|
</section>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|