since3-portal/modules/shared/ui/components/credentials-table.tsx
2026-09-29 22:56:37 +02:00

142 lines
5.6 KiB
TypeScript

"use client"
import { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { EyeIcon, EyeOffIcon, PencilIcon } from "lucide-react";
import { revealCredential } from "@/modules/shared/actions/credentials";
import { getServiceById } from "@/modules/shared/data/services-data";
import { CredentialDialog } from "./credential-dialog";
import { DeleteCredentialDialog } from "./delete-credential-dialog";
import { ServiceLogo } from "./service-logo";
type Credential = {
id: string;
service: string;
username: string;
created_at: string;
updated_at: string;
};
function RevealButton({ credentialId }: { credentialId: string }) {
const [password, setPassword] = useState<string | null>(null);
const [pending, setPending] = useState(false);
async function handleToggle() {
if (password !== null) {
setPassword(null);
return;
}
setPending(true);
const result = await revealCredential(credentialId);
setPending(false);
if ("password" in result) {
setPassword(result.password);
}
}
return (
<div className="flex items-center gap-2">
<span className="font-mono text-small break-all sm:min-w-32">
{password ?? "••••••••"}
</span>
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={handleToggle}
disabled={pending}
aria-label={password !== null ? "Passwort verbergen" : "Passwort anzeigen"}
>
{password !== null ? <EyeOffIcon className="size-4" /> : <EyeIcon className="size-4" />}
</Button>
</div>
);
}
export function CredentialsTable({
profileId,
credentials,
isAdmin,
}: {
profileId: string;
credentials: Credential[];
isAdmin: boolean;
}) {
if (credentials.length === 0) {
return (
<p className="rounded-lg border border-border bg-surface/50 p-8 text-center text-muted-foreground">
Noch keine Passwörter hinterlegt.
</p>
);
}
return (
<div className="overflow-hidden rounded-lg border border-border">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead>Dienst</TableHead>
<TableHead className="hidden md:table-cell">Benutzername</TableHead>
<TableHead>Passwort</TableHead>
<TableHead className="w-20 sm:w-24" />
</TableRow>
</TableHeader>
<TableBody>
{credentials.map((credential) => {
const service = getServiceById(credential.service);
return (
<TableRow key={credential.id}>
<TableCell>
<div className="flex items-center gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-background">
<ServiceLogo serviceId={credential.service} size={18} className="size-4.5 object-contain" />
</div>
<div className="flex min-w-0 flex-col">
<span className="font-medium">{service.label}</span>
<span className="text-small text-muted-foreground break-all md:hidden">{credential.username}</span>
</div>
</div>
</TableCell>
<TableCell className="hidden text-muted-foreground break-all md:table-cell">{credential.username}</TableCell>
<TableCell>
<RevealButton credentialId={credential.id} />
</TableCell>
<TableCell>
<div className="flex justify-end gap-1">
<CredentialDialog
profileId={profileId}
credential={credential}
trigger={
<Button variant="ghost" size="icon-sm" aria-label="Bearbeiten">
<PencilIcon className="size-4" />
</Button>
}
/>
{isAdmin && (
<DeleteCredentialDialog
profileId={profileId}
credentialId={credential.id}
service={service.label}
/>
)}
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}