All components

Settings Security

Settings Security is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.

HextaUI MIT other

What it is

Settings Security is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

No packages beyond the project baseline

Licence

This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

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>
  );
}

Similar components