| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- "use client";
- import React from "react";
- import {
- Check,
- Copy,
- Eye,
- EyeOff,
- KeyRound,
- Loader2,
- } from "lucide-react";
- import { Button } from "@/components/ui/button";
- import { Input } from "@/components/ui/input";
- import {
- adminResetUserPassword,
- ApiClientError,
- } from "@/lib/frontend/apiClient";
- import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
- import {
- getDisplayedTemporaryPassword,
- hasTemporaryPassword,
- } from "@/lib/frontend/admin/users/userManagementUx";
- import {
- notifySuccess,
- notifyError,
- notifyApiError,
- } from "@/lib/frontend/ui/toast";
- function useCopySuccessTimeout(isActive, onReset) {
- React.useEffect(() => {
- if (!isActive) return undefined;
- const timer = window.setTimeout(() => onReset?.(), 1200);
- return () => window.clearTimeout(timer);
- }, [isActive, onReset]);
- }
- export default function UserTemporaryPasswordField({
- user,
- temporaryPassword,
- onTemporaryPasswordChange,
- onPasswordReset,
- disabled = false,
- compact = false,
- }) {
- const [isVisible, setIsVisible] = React.useState(false);
- const [isResetting, setIsResetting] = React.useState(false);
- const [copySuccess, setCopySuccess] = React.useState(false);
- const hasTempPassword = hasTemporaryPassword(temporaryPassword);
- const isDisabled = Boolean(disabled || isResetting || !user?.id);
- useCopySuccessTimeout(copySuccess, () => setCopySuccess(false));
- React.useEffect(() => {
- if (hasTempPassword) return;
- setIsVisible(false);
- setCopySuccess(false);
- }, [hasTempPassword]);
- const redirectToLoginExpired = React.useCallback(() => {
- const next =
- typeof window !== "undefined"
- ? `${window.location.pathname}${window.location.search}`
- : "/admin/users";
- window.location.replace(
- buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next }),
- );
- }, []);
- const handleResetPassword = React.useCallback(async () => {
- if (!user?.id || isDisabled) return;
- setIsResetting(true);
- setCopySuccess(false);
- try {
- const result = await adminResetUserPassword(String(user.id));
- const nextPassword =
- typeof result?.temporaryPassword === "string"
- ? result.temporaryPassword
- : "";
- if (!nextPassword) {
- throw new Error("Missing temporaryPassword in reset response");
- }
- onTemporaryPasswordChange?.(nextPassword);
- setIsVisible(false);
- notifySuccess({
- title: "Temporäres Passwort gesetzt",
- description: `Für "${user.username}" wurde ein neues Startpasswort erstellt.`,
- });
- onPasswordReset?.();
- } catch (err) {
- if (err instanceof ApiClientError) {
- if (err.code === "AUTH_UNAUTHENTICATED") {
- notifyApiError(err);
- redirectToLoginExpired();
- return;
- }
- if (
- err.code === "VALIDATION_INVALID_FIELD" &&
- err.details?.reason === "SELF_PASSWORD_RESET_FORBIDDEN"
- ) {
- notifyError({
- title: "Nicht möglich",
- description:
- "Sie können Ihr eigenes Passwort hier nicht zurücksetzen.",
- });
- return;
- }
- if (err.code === "USER_NOT_FOUND") {
- notifyError({
- title: "Benutzer nicht gefunden.",
- description:
- "Der Benutzer existiert nicht (mehr). Bitte aktualisieren Sie die Liste.",
- });
- return;
- }
- notifyApiError(err, {
- fallbackTitle: "Passwort konnte nicht zurückgesetzt werden.",
- fallbackDescription: "Bitte versuchen Sie es erneut.",
- });
- return;
- }
- notifyError({
- title: "Passwort konnte nicht zurückgesetzt werden.",
- description: "Bitte versuchen Sie es erneut.",
- });
- } finally {
- setIsResetting(false);
- }
- }, [
- user?.id,
- user?.username,
- isDisabled,
- onTemporaryPasswordChange,
- onPasswordReset,
- redirectToLoginExpired,
- ]);
- const handleToggleVisible = React.useCallback(() => {
- if (!hasTempPassword || isDisabled) return;
- setIsVisible((prev) => !prev);
- }, [hasTempPassword, isDisabled]);
- const handleCopyPassword = React.useCallback(async () => {
- if (!hasTempPassword || isDisabled) return;
- if (!navigator?.clipboard?.writeText) {
- notifyError({
- title: "Kopieren nicht verfügbar",
- description: "Die Zwischenablage ist in diesem Browser nicht verfügbar.",
- });
- return;
- }
- try {
- await navigator.clipboard.writeText(temporaryPassword);
- setCopySuccess(true);
- } catch {
- notifyError({
- title: "Passwort konnte nicht kopiert werden.",
- description: "Bitte erneut versuchen.",
- });
- }
- }, [hasTempPassword, isDisabled, temporaryPassword]);
- const displayValue = getDisplayedTemporaryPassword({
- temporaryPassword,
- isVisible,
- });
- const controls = (
- <div className="flex items-center gap-1">
- <Button
- type="button"
- variant="outline"
- size="icon-sm"
- disabled={isDisabled}
- onClick={handleResetPassword}
- title="Temporäres Passwort setzen"
- aria-label="Temporäres Passwort setzen"
- >
- {isResetting ? (
- <Loader2 className="h-4 w-4 animate-spin" />
- ) : (
- <KeyRound className="h-4 w-4" />
- )}
- </Button>
- <Button
- type="button"
- variant="outline"
- size="icon-sm"
- disabled={isDisabled || !hasTempPassword}
- onClick={handleToggleVisible}
- title={isVisible ? "Passwort verbergen" : "Passwort anzeigen"}
- aria-label={isVisible ? "Passwort verbergen" : "Passwort anzeigen"}
- >
- {isVisible ? (
- <EyeOff className="h-4 w-4" />
- ) : (
- <Eye className="h-4 w-4" />
- )}
- </Button>
- <Button
- type="button"
- variant="outline"
- size="icon-sm"
- disabled={isDisabled || !hasTempPassword}
- onClick={handleCopyPassword}
- title="Passwort kopieren"
- aria-label="Passwort kopieren"
- >
- {copySuccess ? (
- <Check className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
- ) : (
- <Copy className="h-4 w-4" />
- )}
- </Button>
- </div>
- );
- if (compact) {
- return (
- <div className="flex items-center justify-between gap-2">
- <span className="truncate font-mono text-xs tracking-wide text-foreground">
- {displayValue}
- </span>
- {controls}
- </div>
- );
- }
- return (
- <div className="grid gap-2">
- <div className="flex items-center gap-2">
- <Input
- value={displayValue}
- readOnly
- disabled
- className="font-mono tracking-wide"
- />
- {controls}
- </div>
- <p className="text-xs text-muted-foreground">
- {hasTempPassword
- ? "Das temporäre Passwort ist nur in dieser Ansicht verfügbar."
- : "Noch kein temporäres Passwort gesetzt. Bitte zuerst zurücksetzen."}
- </p>
- </div>
- );
- }
|