SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
3.5 KB · 75 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { authClient } from "@/lib/auth-client";4import { recordAuditFromClient } from "@/actions/account";5import { PasswordInput, PasswordRequirements, passwordStrong } from "@/components/auth/password-input";6import { Button } from "@/components/ui/button";7import { Field, FieldError, Hint, Label } from "@/components/ui/label";8import { Alert } from "@/components/ui/alert";910export function ChangePasswordForm() {11  const [current, setCurrent] = React.useState("");12  const [next, setNext] = React.useState("");13  const [confirm, setConfirm] = React.useState("");14  const [revokeOthers, setRevokeOthers] = React.useState(true);15  const [error, setError] = React.useState<string | null>(null);16  const [done, setDone] = React.useState(false);17  const [loading, setLoading] = React.useState(false);1819  const mismatch = confirm.length > 0 && confirm !== next;20  const canSubmit = current.length > 0 && passwordStrong(next) && next === confirm && next !== current;2122  async function submit(e: React.FormEvent) {23    e.preventDefault();24    setError(null);25    setDone(false);26    setLoading(true);27    const { error } = await authClient.changePassword({ currentPassword: current, newPassword: next, revokeOtherSessions: revokeOthers });28    if (error) {29      setLoading(false);30      setError(error.status === 400 || error.status === 401 ? "Your current password is incorrect." : error.message ?? "Could not change the password. Try again.");31      return;32    }33    await recordAuditFromClient("password.changed", { revokedOtherSessions: revokeOthers }).catch(() => {});34    setLoading(false);35    setDone(true);36    setCurrent("");37    setNext("");38    setConfirm("");39  }4041  return (42    <form onSubmit={submit} className="grid gap-4 sm:max-w-md" noValidate>43      {error ? <Alert variant="danger">{error}</Alert> : null}44      {done ? <Alert variant="success">Password changed.{revokeOthers ? " Other devices have been signed out." : ""}</Alert> : null}45      <Field>46        <Label htmlFor="pw-current">Current password</Label>47        <PasswordInput id="pw-current" autoComplete="current-password" value={current} onChange={(e) => setCurrent(e.target.value)} required />48      </Field>49      <Field>50        <Label htmlFor="pw-new">New password</Label>51        <PasswordInput id="pw-new" autoComplete="new-password" value={next} onChange={(e) => setNext(e.target.value)} required />52        <PasswordRequirements password={next} />53        {next && current && next === current ? <FieldError>Choose a password different from the current one.</FieldError> : null}54      </Field>55      <Field>56        <Label htmlFor="pw-confirm">Confirm new password</Label>57        <PasswordInput id="pw-confirm" autoComplete="new-password" value={confirm} onChange={(e) => setConfirm(e.target.value)} required aria-invalid={mismatch} />58        <FieldError>{mismatch ? "Passwords do not match." : null}</FieldError>59      </Field>60      <label className="flex items-start gap-2 text-[13px] text-fg-muted">61        <input type="checkbox" checked={revokeOthers} onChange={(e) => setRevokeOthers(e.target.checked)} className="mt-0.5 size-4 rounded border-border accent-[var(--accent)]" />62        <span>63          Sign out all other devices64          <Hint>Recommended if you suspect your password leaked. This session stays active.</Hint>65        </span>66      </label>67      <div>68        <Button type="submit" variant="primary" loading={loading} disabled={!canSubmit}>69          Update password70        </Button>71      </div>72    </form>73  );74}75