Todos os componentes

Settings Security

Settings Security é um componente para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.

HextaUI MIT other

O que é

Settings Security é um componente para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Não precisa de pacotes além do que o projeto já tem

Licença

Este componente vem de HextaUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Settings Security

registry/new-york/blocks/settings/settings-security.tsx
"use client";

import {
  AlertTriangle,
  Check,
  Clock,
  Eye,
  EyeOff,
  Key,
  Loader2,
  Lock,
  MapPin,
  Monitor,
  Shield,
  Smartphone,
  Trash2,
} from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/registry/new-york/ui/alert-dialog";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/registry/new-york/ui/dialog";
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/registry/new-york/ui/field";
import {
  InputGroup,
  InputGroupInput,
} from "@/registry/new-york/ui/input-group";
import { Separator } from "@/registry/new-york/ui/separator";

export interface SecuritySession {
  id: string;
  device: string;
  location: string;
  ipAddress: string;
  lastActive: Date;
  current: boolean;
}

export interface SecurityEvent {
  id: string;
  type: "login" | "password_change" | "2fa_enabled" | "2fa_disabled" | "logout";
  description: string;
  ipAddress: string;
  location: string;
  timestamp: Date;
  status: "success" | "failed" | "suspicious";
}

export interface SettingsSecurityProps {
  twoFactorEnabled?: boolean;
  sessions?: SecuritySession[];
  securityHistory?: SecurityEvent[];
  onPasswordChange?: (
    currentPassword: string,
    newPassword: string
  ) => Promise<void>;
  onEnable2FA?: () => Promise<void>;
  onDisable2FA?: () => Promise<void>;
  onRevokeSession?: (sessionId: string) => Promise<void>;
  onRevokeAllSessions?: () => Promise<void>;
  onGenerateBackupCodes?: () => Promise<string[]>;
  className?: string;
}

function formatDate(date: Date): string {
  return new Intl.DateTimeFormat("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
    hour: "numeric",
    minute: "2-digit",
  }).format(date);
}

function formatRelativeTime(date: Date): string {
  const now = Date.now();
  const diff = now - date.getTime();
  const minutes = Math.floor(diff / 60_000);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  if (minutes < 1) return "Just now";
  if (minutes < 60) return `${minutes}m ago`;
  if (hours < 24) return `${hours}h ago`;
  if (days < 7) return `${days}d ago`;
  return formatDate(date);
}

export default function SettingsSecurity({
  twoFactorEnabled = false,
  sessions = [],
  securityHistory = [],
  onPasswordChange,
  onEnable2FA,
  onDisable2FA,
  onRevokeSession,
  onRevokeAllSessions,
  onGenerateBackupCodes,
  className,
}: SettingsSecurityProps) {
  const [isChangingPassword, setIsChangingPassword] = useState(false);
  const [isRevoking, setIsRevoking] = useState<string | null>(null);
  const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
  const [backupCodesDialogOpen, setBackupCodesDialogOpen] = useState(false);
  const [showBackupCodes, setShowBackupCodes] = useState(false);
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [backupCodes, setBackupCodes] = useState<string[]>([]);

  const [passwordData, setPasswordData] = useState({
    current: "",
    new: "",
    confirm: "",
  });

  const handlePasswordChange = async () => {
    setErrors({});

    if (!passwordData.current.trim()) {
      setErrors({ current: "Current password is required" });
      return;
    }

    if (!passwordData.new.trim()) {
      setErrors({ new: "New password is required" });
      return;
    }

    if (passwordData.new.length < 8) {
      setErrors({ new: "Password must be at least 8 characters" });
      return;
    }

    if (passwordData.new !== passwordData.confirm) {
      setErrors({ confirm: "Passwords do not match" });
      return;
    }

    setIsChangingPassword(true);
    try {
      await onPasswordChange?.(passwordData.current, passwordData.new);
      setPasswordData({ current: "", new: "", confirm: "" });
      setPasswordDialogOpen(false);
    } catch (error) {
      setErrors({
        _general:
          error instanceof Error ? error.message : "Failed to change password",
      });
    } finally {
      setIsChangingPassword(false);
    }
  };

  const handleGenerateBackupCodes = async () => {
    try {
      const codes = await onGenerateBackupCodes?.();
      if (codes) {
        setBackupCodes(codes);
        setBackupCodesDialogOpen(true);
      }
    } catch (error) {
      setErrors({
        backupCodes:
          error instanceof Error
            ? error.message
            : "Failed to generate backup codes",
      });
    }
  };

  const getEventIcon = (type: SecurityEvent["type"]) => {
    switch (type) {
      case "login":
        return Lock;
      case "password_change":
        return Key;
      case "2fa_enabled":
      case "2fa_disabled":
        return Shield;
      default:
        return Clock;
    }
  };

  const getStatusBadge = (status: SecurityEvent["status"]) => {
    switch (status) {
      case "success":
        return (
          <Badge className="text-xs" variant="default">
            Success
          </Badge>
        );
      case "failed":
        return (
          <Badge className="text-xs" variant="destructive">
            Failed
          </Badge>
        );
      case "suspicious":
        return (
          <Badge
            className="flex items-center gap-1 text-xs"
            variant="destructive"
          >
            <AlertTriangle className="size-3" />
            <span>Suspicious</span>
          </Badge>
        );
    }
  };

  return (
    <Card className={cn("w-full shadow-xs", className)}>
      <CardHeader>
        <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
          <div className="flex min-w-0 flex-1 flex-col gap-2">
            <CardTitle className="wrap-break-word">Security</CardTitle>
            <CardDescription className="wrap-break-word">
              Manage your account security and authentication
            </CardDescription>
          </div>
        </div>
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-6">
          {/* Password & 2FA */}
          <div className="grid gap-4 md:grid-cols-2">
            {/* Password Change */}
            <div className="flex flex-col gap-4 rounded-lg border p-4">
              <div className="flex items-center gap-2">
                <div className="flex size-8 items-center justify-center rounded-lg bg-muted">
                  <Key className="size-4" />
                </div>
                <FieldLabel className="mb-0">Password</FieldLabel>
              </div>
              <Dialog
                onOpenChange={setPasswordDialogOpen}
                open={passwordDialogOpen}
              >
                <DialogTrigger asChild>
                  <Button className="w-full" type="button" variant="outline">
                    Change Password
                  </Button>
                </DialogTrigger>
                <DialogContent className="sm:max-w-md">
                  <DialogHeader>
                    <DialogTitle>Change Password</DialogTitle>
                    <DialogDescription>
                      Enter your current password and choose a new one
                    </DialogDescription>
                  </DialogHeader>
                  <div className="flex flex-col gap-4">
                    {errors._general && (
                      <div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3">
                        <p className="text-destructive text-sm">
                          {errors._general}
                        </p>
                      </div>
                    )}

                    <Field>
                      <FieldLabel htmlFor="current-password">
                        Current Password{" "}
                        <span className="text-destructive">*</span>
                      </FieldLabel>
                      <FieldContent>
                        <InputGroup>
                          <InputGroupInput
                            id="current-password"
                            onChange={(e) =>
                              setPasswordData((prev) => ({
                                ...prev,
                                current: e.target.value,
                              }))
                            }
                            type="password"
                            value={passwordData.current}
                          />
                        </InputGroup>
                        {errors.current && (
                          <FieldError>{errors.current}</FieldError>
                        )}
                      </FieldContent>
                    </Field>

                    <Field>
                      <FieldLabel htmlFor="new-password">
                        New Password <span className="text-destructive">*</span>
                      </FieldLabel>
                      <FieldContent>
                        <InputGroup>
                          <InputGroupInput
                            id="new-password"
                            onChange={(e) =>
                              setPasswordData((prev) => ({
                                ...prev,
                                new: e.target.value,
                              }))
                            }
                            type="password"
                            value={passwordData.new}
                          />
                        </InputGroup>
                        {errors.new && <FieldError>{errors.new}</FieldError>}
                        <FieldDescription>
                          Must be at least 8 characters long
                        </FieldDescription>
                      </FieldContent>
                    </Field>

                    <Field>
                      <FieldLabel htmlFor="confirm-password">
                        Confirm Password{" "}
                        <span className="text-destructive">*</span>
                      </FieldLabel>
                      <FieldContent>
                        <InputGroup>
                          <InputGroupInput
                            id="confirm-password"
                            onChange={(e) =>
                              setPasswordData((prev) => ({
                                ...prev,
                                confirm: e.target.value,
                              }))
                            }
                            type="password"
                            value={passwordData.confirm}
                          />
                        </InputGroup>
                        {errors.confirm && (
                          <FieldError>{errors.confirm}</FieldError>
                        )}
                      </FieldContent>
                    </Field>
                  </div>
                  <DialogFooter>
                    <Button
                      onClick={() => setPasswordDialogOpen(false)}
                      type="button"
                      variant="outline"
                    >
                      Cancel
                    </Button>
                    <Button
                      disabled={isChangingPassword}
                      onClick={handlePasswordChange}
                      type="button"
                    >
                      {isChangingPassword ? (
                        <>
                          <Loader2 className="size-4 animate-spin" />
                          Changing…
                        </>
                      ) : (
                        "Change Password"
                      )}
                    </Button>
                  </DialogFooter>
                </DialogContent>
              </Dialog>
            </div>

            {/* Two-Factor Authentication */}
            <div className="flex flex-col gap-4 rounded-lg border p-4">
              <div className="flex items-center gap-2">
                <div className="flex size-8 items-center justify-center rounded-lg bg-muted">
                  <Shield className="size-4" />
                </div>
                <FieldLabel className="mb-0">
                  Two-Factor Authentication
                </FieldLabel>
              </div>
              <div className="flex flex-col gap-3">
                {twoFactorEnabled ? (
                  <>
                    <div className="flex items-center gap-2">
                      <Badge
                        className="flex items-center gap-1 text-xs"
                        variant="default"
                      >
                        <Check className="size-3" />
                        <span>Enabled</span>
                      </Badge>
                    </div>
                    <div className="flex flex-col gap-2">
                      <Button
                        className="w-full"
                        onClick={handleGenerateBackupCodes}
                        type="button"
                        variant="outline"
                      >
                        View Backup Codes
                      </Button>
                      <AlertDialog>
                        <AlertDialogTrigger asChild>
                          <Button
                            className="w-full"
                            type="button"
                            variant="destructive"
                          >
                            Disable 2FA
                          </Button>
                        </AlertDialogTrigger>
                        <AlertDialogContent>
                          <AlertDialogHeader>
                            <AlertDialogTitle>
                              Disable Two-Factor Authentication?
                            </AlertDialogTitle>
                            <AlertDialogDescription>
                              This will reduce the security of your account. You
                              can re-enable it anytime.
                            </AlertDialogDescription>
                          </AlertDialogHeader>
                          <AlertDialogFooter>
                            <AlertDialogCancel>Cancel</AlertDialogCancel>
                            <AlertDialogAction onClick={onDisable2FA}>
                              Disable 2FA
                            </AlertDialogAction>
                          </AlertDialogFooter>
                        </AlertDialogContent>
                      </AlertDialog>
                    </div>
                  </>
                ) : (
                  <Button
                    className="w-full"
                    onClick={onEnable2FA}
                    type="button"
                  >
                    <div className="flex items-center gap-2">
                      <Shield className="size-4" />
                      <span>Enable 2FA</span>
                    </div>
                  </Button>
                )}
              </div>
            </div>
          </div>

          <Separator />

          {/* Active Sessions */}
          <div className="flex flex-col gap-4">
            <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <div className="flex items-center gap-2">
                <Monitor className="size-5 text-muted-foreground" />
                <h3 className="font-semibold text-base">Active Sessions</h3>
              </div>
              {sessions.length > 1 && (
                <AlertDialog>
                  <AlertDialogTrigger asChild>
                    <Button
                      className="w-full sm:w-auto"
                      type="button"
                      variant="outline"
                    >
                      Revoke All
                    </Button>
                  </AlertDialogTrigger>
                  <AlertDialogContent>
                    <AlertDialogHeader>
                      <AlertDialogTitle>Revoke All Sessions?</AlertDialogTitle>
                      <AlertDialogDescription>
                        This will sign you out from all devices except this one.
                      </AlertDialogDescription>
                    </AlertDialogHeader>
                    <AlertDialogFooter>
                      <AlertDialogCancel>Cancel</AlertDialogCancel>
                      <AlertDialogAction onClick={onRevokeAllSessions}>
                        Revoke All
                      </AlertDialogAction>
                    </AlertDialogFooter>
                  </AlertDialogContent>
                </AlertDialog>
              )}
            </div>

            {sessions.length === 0 ? (
              <p className="text-muted-foreground text-sm">
                No active sessions
              </p>
            ) : (
              <div className="flex flex-col gap-3">
                {sessions.map((session) => (
                  <div
                    className="flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center"
                    key={session.id}
                  >
                    <div className="flex min-w-0 flex-1 items-center gap-3">
                      <div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted">
                        {session.device.includes("Mobile") ? (
                          <Smartphone className="size-5" />
                        ) : (
                          <Monitor className="size-5" />
                        )}
                      </div>
                      <div className="flex min-w-0 flex-1 flex-col gap-2">
                        <div className="flex flex-wrap items-center gap-2">
                          <span className="font-medium text-sm">
                            {session.device}
                          </span>
                          {session.current && (
                            <Badge className="text-xs" variant="default">
                              Current
                            </Badge>
                          )}
                        </div>
                        <div className="flex flex-wrap items-center gap-2 text-muted-foreground text-xs">
                          <span className="flex items-center gap-1">
                            <MapPin className="size-3" />
                            {session.location}
                          </span>
                          <span>•</span>
                          <span>{session.ipAddress}</span>
                          <span>•</span>
                          <span>
                            Active {formatRelativeTime(session.lastActive)}
                          </span>
                        </div>
                      </div>
                    </div>
                    {!session.current && (
                      <Button
                        className="w-full sm:w-auto"
                        disabled={isRevoking === session.id}
                        onClick={() => {
                          setIsRevoking(session.id);
                          onRevokeSession?.(session.id).finally(() =>
                            setIsRevoking(null)
                          );
                        }}
                        type="button"
                        variant="outline"
                      >
                        {isRevoking === session.id ? (
                          <>
                            <Loader2 className="size-4 animate-spin" />
                            Revoking…
                          </>
                        ) : (
                          <>
                            <Trash2 className="size-4" />
                            Revoke
                          </>
                        )}
                      </Button>
                    )}
                  </div>
                ))}
              </div>
            )}
          </div>

          <Separator />

          {/* Security History */}
          <div className="flex flex-col gap-4">
            <div className="flex items-center gap-2">
              <Clock className="size-5 text-muted-foreground" />
              <h3 className="font-semibold text-base">Security History</h3>
            </div>

            {securityHistory.length === 0 ? (
              <p className="text-muted-foreground text-sm">
                No security events
              </p>
            ) : (
              <div className="flex flex-col gap-3">
                {securityHistory.map((event) => {
                  const Icon = getEventIcon(event.type);
                  return (
                    <div
                      className="flex items-start gap-3 rounded-lg border p-4"
                      key={event.id}
                    >
                      <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted">
                        <Icon className="size-4" />
                      </div>
                      <div className="flex min-w-0 flex-1 flex-col gap-2">
                        <div className="flex flex-wrap items-center gap-2">
                          <span className="font-medium text-sm">
                            {event.description}
                          </span>
                          {getStatusBadge(event.status)}
                        </div>
                        <div className="flex flex-wrap items-center gap-2 text-muted-foreground text-xs">
                          <span className="flex items-center gap-1">
                            <MapPin className="size-3" />
                            {event.location}
                          </span>
                          <span>•</span>
                          <span>{event.ipAddress}</span>
                          <span>•</span>
                          <span>{formatDate(event.timestamp)}</span>
                        </div>
                      </div>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        </div>
      </CardContent>

      {/* Backup Codes Dialog */}
      <Dialog
        onOpenChange={setBackupCodesDialogOpen}
        open={backupCodesDialogOpen}
      >
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>Backup Codes</DialogTitle>
            <DialogDescription>
              Save these codes in a safe place. You can use them to access your
              account if you lose access to your authenticator app.
            </DialogDescription>
          </DialogHeader>
          <div className="flex flex-col gap-4">
            <div className="grid grid-cols-2 gap-2 rounded-lg border bg-muted/30 p-4">
              {backupCodes.map((code, index) => (
                <code className="font-medium font-mono text-sm" key={index}>
                  {showBackupCodes ? code : "•".repeat(8)}
                </code>
              ))}
            </div>
            <Button
              onClick={() => setShowBackupCodes(!showBackupCodes)}
              type="button"
              variant="outline"
            >
              {showBackupCodes ? (
                <div className="flex items-center gap-2">
                  <EyeOff className="size-4" />
                  <span>Hide Codes</span>
                </div>
              ) : (
                <div className="flex items-center gap-2">
                  <Eye className="size-4" />
                  <span>Show Codes</span>
                </div>
              )}
            </Button>
          </div>
          <DialogFooter>
            <Button
              onClick={() => setBackupCodesDialogOpen(false)}
              type="button"
            >
              Done
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </Card>
  );
}

Componentes parecidos