Skip to content
File

Blob: src/client/pages/account/password.tsx

typescript145 lines
1import { useState } from "react";
2 
3import { BackLink } from "@/client/components/ui/back-link";
4import { Button } from "@/client/components/ui/button";
5import { Card } from "@/client/components/ui/card";
6import { Input } from "@/client/components/ui/input";
7import { PageError } from "@/client/components/ui/page-error";
8import { PageHeader } from "@/client/components/ui/page-header";
9import { Skeleton } from "@/client/components/ui/skeleton";
10import { useRequireSession } from "@/client/hooks/use-auth-guard";
11import { authClient } from "@/client/lib/auth-client";
12 
13const AccountPasswordPageSkeleton = () => (
14 <div className="mx-auto max-w-md py-12" aria-busy="true">
15 <Skeleton className="mb-6 h-4 w-24" />
16 <div className="mb-8">
17 <Skeleton className="mb-3 h-8 w-52" />
18 <Skeleton className="h-4 w-full max-w-sm" />
19 </div>
20 <div className="rounded-2xl border border-zinc-800/60 bg-zinc-900/40 p-5 sm:p-6">
21 <div className="flex flex-col gap-4">
22 <div className="flex flex-col gap-1.5">
23 <Skeleton className="h-4 w-32" />
24 <Skeleton className="h-11 w-full rounded-xl" />
25 </div>
26 <div className="flex flex-col gap-1.5">
27 <Skeleton className="h-4 w-28" />
28 <Skeleton className="h-11 w-full rounded-xl" />
29 <Skeleton className="h-3 w-40" />
30 </div>
31 <div className="flex flex-col gap-1.5">
32 <Skeleton className="h-4 w-44" />
33 <Skeleton className="h-11 w-full rounded-xl" />
34 </div>
35 <Skeleton className="mt-3 h-11 w-40 rounded-xl" />
36 </div>
37 </div>
38 </div>
39);
40 
41export const AccountPasswordPage = () => {
42 const session = useRequireSession("/account/password");
43 
44 const [current, setCurrent] = useState("");
45 const [next, setNext] = useState("");
46 const [confirm, setConfirm] = useState("");
47 const [pending, setPending] = useState(false);
48 const [err, setErr] = useState<string | null>(null);
49 const [success, setSuccess] = useState(false);
50 
51 if (session.isPending || !session.data) {
52 return <AccountPasswordPageSkeleton />;
53 }
54 
55 const submit = async (e: React.FormEvent) => {
56 e.preventDefault();
57 setErr(null);
58 setSuccess(false);
59 if (next !== confirm) {
60 setErr("New passwords don't match.");
61 return;
62 }
63 if (next.length < 12) {
64 setErr("New password must be at least 12 characters.");
65 return;
66 }
67 setPending(true);
68 try {
69 const res = await authClient.changePassword({
70 currentPassword: current,
71 newPassword: next,
72 revokeOtherSessions: true,
73 });
74 if (res.error) {
75 setErr(res.error.message ?? "Could not change password.");
76 return;
77 }
78 setCurrent("");
79 setNext("");
80 setConfirm("");
81 setSuccess(true);
82 } finally {
83 setPending(false);
84 }
85 };
86 
87 return (
88 <div className="mx-auto max-w-md py-12">
89 <BackLink to="/account">Account</BackLink>
90
91 <PageHeader
92 title="Change password"
93 description="Replaces the password on your email + password credential. Other active sessions are revoked."
94 />
95
96 <PageError message={err} onDismiss={() => setErr(null)} />
97
98 <Card>
99 <form onSubmit={submit} className="flex flex-col gap-4">
100 <Input
101 label="Current password"
102 type="password"
103 autoComplete="current-password"
104 required
105 value={current}
106 onChange={(e) => setCurrent(e.target.value)}
107 />
108 <Input
109 label="New password"
110 type="password"
111 autoComplete="new-password"
112 required
113 minLength={12}
114 helper="At least 12 characters."
115 value={next}
116 onChange={(e) => setNext(e.target.value)}
117 />
118 <Input
119 label="Confirm new password"
120 type="password"
121 autoComplete="new-password"
122 required
123 minLength={12}
124 value={confirm}
125 onChange={(e) => setConfirm(e.target.value)}
126 onBlur={() => {
127 // Catch the mismatch before submit so the user notices while
128 // their attention is still on the field.
129 if (confirm && next && confirm !== next) {
130 setErr("New passwords don't match.");
131 }
132 }}
133 />
134 {success ? (
135 <p className="mt-2 text-sm text-accent-400">Password updated. Other sessions have been signed out.</p>
136 ) : null}
137 <Button type="submit" loading={pending} className="mt-3">
138 Change password
139 </Button>
140 </form>
141 </Card>
142 </div>
143 );
144};