feat: session and user management docs

This commit is contained in:
Bereket Engida
2024-09-22 22:56:03 +03:00
parent 70d94d394b
commit f3c7de2c40
4 changed files with 855 additions and 618 deletions

View File

@@ -10,52 +10,78 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input"; import { PasswordInput } from "@/components/ui/password-input";
import { client, signOut, user, useSession } from "@/lib/auth-client"; import { client, signOut, user, useSession } from "@/lib/auth-client";
import { Session } from "@/lib/auth-types"; import { Session } from "@/lib/auth-types";
import { MobileIcon } from "@radix-ui/react-icons"; import { MobileIcon } from "@radix-ui/react-icons";
import { Edit, Fingerprint, Laptop, Loader2, LogOut, Plus, QrCode, ShieldCheck, ShieldOff, Trash, X } from "lucide-react"; import {
Edit,
Fingerprint,
Laptop,
Loader2,
LogOut,
Plus,
QrCode,
ShieldCheck,
ShieldOff,
Trash,
X,
} from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import QRCode from "react-qr-code"; import QRCode from "react-qr-code";
import CopyButton from "@/components/ui/copy-button"; import CopyButton from "@/components/ui/copy-button";
export default function UserCard(props: { export default function UserCard(props: {
session: Session | null; session: Session | null;
activeSessions: Session["session"][] activeSessions: Session["session"][];
}) { }) {
const router = useRouter(); const router = useRouter();
const { data, isPending, error } = useSession(props.session); const { data, isPending, error } = useSession(props.session);
const [ua, setUa] = useState<UAParser.UAParserInstance>() const [ua, setUa] = useState<UAParser.UAParserInstance>();
const session = data || props.session const session = data || props.session;
useEffect(() => { useEffect(() => {
setUa(new UAParser(session?.session.userAgent)) setUa(new UAParser(session?.session.userAgent));
}, [session?.session.userAgent]) }, [session?.session.userAgent]);
const [isTerminating, setIsTerminating] = useState<string>(); const [isTerminating, setIsTerminating] = useState<string>();
const { data: qr } = useQuery({ const { data: qr } = useQuery({
queryKey: ["two-factor-qr"], queryKey: ["two-factor-qr"],
queryFn: async () => { queryFn: async () => {
const res = await client.twoFactor.getTotpUri() const res = await client.twoFactor.getTotpUri();
if (res.error) { if (res.error) {
throw res.error throw res.error;
} }
return res.data return res.data;
}, },
enabled: !!session?.user.twoFactorSecret enabled: !!session?.user.twoFactorSecret,
}) });
const [isPendingTwoFa, setIsPendingTwoFa] = useState<boolean>(false); const [isPendingTwoFa, setIsPendingTwoFa] = useState<boolean>(false);
const [twoFaPassword, setTwoFaPassword] = useState<string>(""); const [twoFaPassword, setTwoFaPassword] = useState<string>("");
@@ -71,87 +97,90 @@ export default function UserCard(props: {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Avatar className="hidden h-9 w-9 sm:flex "> <Avatar className="hidden h-9 w-9 sm:flex ">
<AvatarImage src={session?.user.image || "#"} alt="Avatar" className="object-cover" /> <AvatarImage
src={session?.user.image || "#"}
alt="Avatar"
className="object-cover"
/>
<AvatarFallback>{session?.user.name.charAt(0)}</AvatarFallback> <AvatarFallback>{session?.user.name.charAt(0)}</AvatarFallback>
</Avatar> </Avatar>
<div className="grid gap-1"> <div className="grid gap-1">
<p className="text-sm font-medium leading-none"> <p className="text-sm font-medium leading-none">
{session?.user.name} {session?.user.name}
</p> </p>
<p className="text-sm"> <p className="text-sm">{session?.user.email}</p>
{session?.user.email}
</p>
</div> </div>
</div> </div>
<EditUserDialog session={session} /> <EditUserDialog session={session} />
</div> </div>
<div className="border-l-2 px-2 w-max gap-1 flex flex-col"> <div className="border-l-2 px-2 w-max gap-1 flex flex-col">
<p className="text-xs font-medium "> <p className="text-xs font-medium ">Active Sessions</p>
Active Sessions {props.activeSessions
</p> .filter((session) => session.userAgent)
{ .map((session) => {
props.activeSessions.filter((session) => session.userAgent).map((session) => {
return ( return (
<div key={session.id}> <div key={session.id}>
<div className="flex items-center gap-2 text-sm text-black font-medium dark:text-white"> <div className="flex items-center gap-2 text-sm text-black font-medium dark:text-white">
{ {new UAParser(session.userAgent).getDevice().type ===
new UAParser(session.userAgent).getDevice().type === "mobile" ? <MobileIcon /> : <Laptop size={16} /> "mobile" ? (
} <MobileIcon />
{new UAParser(session.userAgent).getOS().name}, {new UAParser(session.userAgent).getBrowser().name} ) : (
<button className="text-red-500 opacity-80 cursor-pointer text-xs border-muted-foreground border-red-600 underline " onClick={async () => { <Laptop size={16} />
setIsTerminating(session.id) )}
{new UAParser(session.userAgent).getOS().name},{" "}
{new UAParser(session.userAgent).getBrowser().name}
<button
className="text-red-500 opacity-80 cursor-pointer text-xs border-muted-foreground border-red-600 underline "
onClick={async () => {
setIsTerminating(session.id);
const res = await client.user.revokeSession({ const res = await client.user.revokeSession({
id: session.id, id: session.id,
}) });
if (res.error) { if (res.error) {
toast.error(res.error.message) toast.error(res.error.message);
} else { } else {
toast.success("Session terminated successfully") toast.success("Session terminated successfully");
}
router.refresh()
setIsTerminating(undefined)
}}>
{
isTerminating === session.id ? <Loader2 size={15} className="animate-spin" /> : session.id === props.session?.session.id ? "Sign Out" : "Terminate"
} }
router.refresh();
setIsTerminating(undefined);
}}
>
{isTerminating === session.id ? (
<Loader2 size={15} className="animate-spin" />
) : session.id === props.session?.session.id ? (
"Sign Out"
) : (
"Terminate"
)}
</button> </button>
</div> </div>
</div> </div>
) );
}) })}
}
</div> </div>
<div className="border-y py-4 flex items-center flex-wrap justify-between gap-2"> <div className="border-y py-4 flex items-center flex-wrap justify-between gap-2">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<p className="text-sm"> <p className="text-sm">Passkeys</p>
Passkeys
</p>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<AddPasskey /> <AddPasskey />
<ListPasskeys /> <ListPasskeys />
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<p className="text-sm"> <p className="text-sm">Two Factor</p>
Two Factor
</p>
<div className="flex gap-2"> <div className="flex gap-2">
{ {session?.user.twoFactorEnabled && (
session?.user.twoFactorEnabled && ( <Dialog>
<Dialog >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="gap-2"> <Button variant="outline" className="gap-2">
<QrCode size={16} /> <QrCode size={16} />
<span className="md:text-sm text-xs"> <span className="md:text-sm text-xs">Scan QR Code</span>
Scan QR Code
</span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px] w-11/12"> <DialogContent className="sm:max-w-[425px] w-11/12">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>Scan QR Code</DialogTitle>
Scan QR Code
</DialogTitle>
<DialogDescription> <DialogDescription>
Scan the QR code with your TOTP app Scan the QR code with your TOTP app
</DialogDescription> </DialogDescription>
@@ -167,8 +196,7 @@ export default function UserCard(props: {
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) )}
}
<Dialog open={twoFactorDialog} onOpenChange={setTwoFactorDialog}> <Dialog open={twoFactorDialog} onOpenChange={setTwoFactorDialog}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
@@ -177,35 +205,41 @@ export default function UserCard(props: {
} }
className="gap-2" className="gap-2"
> >
{ {session?.user.twoFactorEnabled ? (
session?.user.twoFactorEnabled ? <ShieldOff size={16} /> : <ShieldCheck size={16} /> <ShieldOff size={16} />
} ) : (
<ShieldCheck size={16} />
)}
<span className="md:text-sm text-xs"> <span className="md:text-sm text-xs">
{ {session?.user.twoFactorEnabled
session?.user.twoFactorEnabled ? "Disable 2FA" : "Enable 2FA" ? "Disable 2FA"
} : "Enable 2FA"}
</span> </span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px] w-11/12"> <DialogContent className="sm:max-w-[425px] w-11/12">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{ {session?.user.twoFactorEnabled
session?.user.twoFactorEnabled ? "Disable 2FA" : "Enable 2FA" ? "Disable 2FA"
} : "Enable 2FA"}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{ {session?.user.twoFactorEnabled
session?.user.twoFactorEnabled ? "Disable the second factor authentication from your account" : "Enable 2FA to secure your account" ? "Disable the second factor authentication from your account"
} : "Enable 2FA to secure your account"}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
<PasswordInput id="password" placeholder="Password" value={twoFaPassword} onChange={(e) => setTwoFaPassword(e.target.value)} /> <PasswordInput
id="password"
placeholder="Password"
value={twoFaPassword}
onChange={(e) => setTwoFaPassword(e.target.value)}
/>
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button
disabled={isPendingTwoFa} disabled={isPendingTwoFa}
onClick={async () => { onClick={async () => {
@@ -225,9 +259,9 @@ export default function UserCard(props: {
onSuccess() { onSuccess() {
toast("2FA disabled successfully"); toast("2FA disabled successfully");
setTwoFactorDialog(false); setTwoFactorDialog(false);
} },
} },
}) });
} else { } else {
const res = await client.twoFactor.enable({ const res = await client.twoFactor.enable({
password: twoFaPassword, password: twoFaPassword,
@@ -238,50 +272,56 @@ export default function UserCard(props: {
onSuccess() { onSuccess() {
toast.success("2FA enabled successfully"); toast.success("2FA enabled successfully");
setTwoFactorDialog(false); setTwoFactorDialog(false);
} },
} },
}) });
} }
setIsPendingTwoFa(false); setIsPendingTwoFa(false);
setTwoFaPassword(""); setTwoFaPassword("");
}}> }}
{ >
isPendingTwoFa ? <Loader2 size={15} className="animate-spin" /> : session?.user.twoFactorEnabled ? "Disable 2FA" : "Enable 2FA" {isPendingTwoFa ? (
} <Loader2 size={15} className="animate-spin" />
) : session?.user.twoFactorEnabled ? (
"Disable 2FA"
) : (
"Enable 2FA"
)}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
</div> </div>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="gap-2 justify-between items-center"> <CardFooter className="gap-2 justify-between items-center">
<ChangePassword /> <ChangePassword />
<Button className="gap-2 z-10" variant="secondary" onClick={async () => { <Button
setIsSignOut(true) className="gap-2 z-10"
variant="secondary"
onClick={async () => {
setIsSignOut(true);
await signOut({ await signOut({
options: { options: {
body: { body: {
callbackURL: "/", callbackURL: "/",
} },
} },
}) });
setIsSignOut(false) setIsSignOut(false);
}} disabled={isSignOut}> }}
disabled={isSignOut}
<span
className="text-sm"
> >
{ <span className="text-sm">
isSignOut ? <Loader2 size={15} className="animate-spin" /> : <div className="flex items-center gap-2"> {isSignOut ? (
<Loader2 size={15} className="animate-spin" />
) : (
<div className="flex items-center gap-2">
<LogOut size={16} /> <LogOut size={16} />
Sign Out Sign Out
</div> </div>
} )}
</span> </span>
</Button> </Button>
</CardFooter> </CardFooter>
@@ -289,8 +329,6 @@ export default function UserCard(props: {
); );
} }
async function convertImageToBase64(file: File): Promise<string> { async function convertImageToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
@@ -300,7 +338,6 @@ async function convertImageToBase64(file: File): Promise<string> {
}); });
} }
function ChangePassword() { function ChangePassword() {
const [currentPassword, setCurrentPassword] = useState<string>(""); const [currentPassword, setCurrentPassword] = useState<string>("");
const [newPassword, setNewPassword] = useState<string>(""); const [newPassword, setNewPassword] = useState<string>("");
@@ -312,51 +349,60 @@ function ChangePassword() {
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="gap-2 z-10" variant="outline" size="sm"> <Button className="gap-2 z-10" variant="outline" size="sm">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M2.5 18.5v-1h19v1zm.535-5.973l-.762-.442l.965-1.693h-1.93v-.884h1.93l-.965-1.642l.762-.443L4 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L4 10.835zm8 0l-.762-.442l.966-1.693H9.308v-.884h1.93l-.965-1.642l.762-.443L12 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L12 10.835zm8 0l-.762-.442l.966-1.693h-1.931v-.884h1.93l-.965-1.642l.762-.443L20 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L20 10.835z"></path></svg> <svg
<span xmlns="http://www.w3.org/2000/svg"
className="text-sm text-muted-foreground" width="1em"
height="1em"
viewBox="0 0 24 24"
> >
Change Password <path
</span> fill="currentColor"
d="M2.5 18.5v-1h19v1zm.535-5.973l-.762-.442l.965-1.693h-1.93v-.884h1.93l-.965-1.642l.762-.443L4 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L4 10.835zm8 0l-.762-.442l.966-1.693H9.308v-.884h1.93l-.965-1.642l.762-.443L12 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L12 10.835zm8 0l-.762-.442l.966-1.693h-1.931v-.884h1.93l-.965-1.642l.762-.443L20 9.066l.966-1.643l.761.443l-.965 1.642h1.93v.884h-1.93l.965 1.693l-.762.442L20 10.835z"
></path>
</svg>
<span className="text-sm text-muted-foreground">Change Password</span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px] w-11/12"> <DialogContent className="sm:max-w-[425px] w-11/12">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>Change Password</DialogTitle>
Change Password <DialogDescription>Change your password</DialogDescription>
</DialogTitle>
<DialogDescription>
Change your password
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="current-password">Current Password</Label> <Label htmlFor="current-password">Current Password</Label>
<PasswordInput id="current-password" <PasswordInput
id="current-password"
value={currentPassword} value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)} onChange={(e) => setCurrentPassword(e.target.value)}
autoComplete="new-password" autoComplete="new-password"
placeholder="Password" /> placeholder="Password"
/>
<Label htmlFor="new-password">New Password</Label> <Label htmlFor="new-password">New Password</Label>
<PasswordInput <PasswordInput
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
autoComplete="new-password" autoComplete="new-password"
placeholder="New Password" /> placeholder="New Password"
/>
<Label htmlFor="password">Confirm Password</Label> <Label htmlFor="password">Confirm Password</Label>
<PasswordInput <PasswordInput
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
autoComplete="new-password" autoComplete="new-password"
placeholder="Confirm Password" /> placeholder="Confirm Password"
/>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Checkbox onCheckedChange={(checked) => checked ? setSignOutDevices(true) : setSignOutDevices(false)} /> <Checkbox
<p className="text-sm"> onCheckedChange={(checked) =>
Sign out from other devices checked ? setSignOutDevices(true) : setSignOutDevices(false)
</p> }
/>
<p className="text-sm">Sign out from other devices</p>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button onClick={async () => { <Button
onClick={async () => {
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
toast.error("Passwords do not match"); toast.error("Passwords do not match");
return; return;
@@ -370,10 +416,13 @@ function ChangePassword() {
newPassword: newPassword, newPassword: newPassword,
currentPassword: currentPassword, currentPassword: currentPassword,
revokeOtherSessions: signOutDevices, revokeOtherSessions: signOutDevices,
}) });
setLoading(false); setLoading(false);
if (res.error) { if (res.error) {
toast.error(res.error.message || "Couldn't change your password! Make sure it's correct"); toast.error(
res.error.message ||
"Couldn't change your password! Make sure it's correct"
);
} else { } else {
setOpen(false); setOpen(false);
toast.success("Password changed successfully"); toast.success("Password changed successfully");
@@ -381,19 +430,23 @@ function ChangePassword() {
setNewPassword(""); setNewPassword("");
setConfirmPassword(""); setConfirmPassword("");
} }
}}> }}
{loading ? <Loader2 size={15} className="animate-spin" /> : "Change Password"} >
{loading ? (
<Loader2 size={15} className="animate-spin" />
) : (
"Change Password"
)}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }
function EditUserDialog(props: { session: Session | null }) { function EditUserDialog(props: { session: Session | null }) {
const [name, setName] = useState<string>(); const [name, setName] = useState<string>();
const router = useRouter() const router = useRouter();
const [image, setImage] = useState<File | null>(null); const [image, setImage] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null); const [imagePreview, setImagePreview] = useState<string | null>(null);
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -419,12 +472,8 @@ function EditUserDialog(props: { session: Session | null }) {
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px] w-11/12"> <DialogContent className="sm:max-w-[425px] w-11/12">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>Edit User</DialogTitle>
Edit User <DialogDescription>Edit user information</DialogDescription>
</DialogTitle>
<DialogDescription>
Edit user information
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Full Name</Label> <Label htmlFor="name">Full Name</Label>
@@ -434,7 +483,7 @@ function EditUserDialog(props: { session: Session | null }) {
placeholder={props.session?.user.name} placeholder={props.session?.user.name}
required required
onChange={(e) => { onChange={(e) => {
setName(e.target.value) setName(e.target.value);
}} }}
/> />
<div className="grid gap-2"> <div className="grid gap-2">
@@ -458,16 +507,23 @@ function EditUserDialog(props: { session: Session | null }) {
onChange={handleImageChange} onChange={handleImageChange}
className="w-full text-muted-foreground" className="w-full text-muted-foreground"
/> />
{imagePreview && <X className="cursor-pointer" onClick={() => { {imagePreview && (
<X
className="cursor-pointer"
onClick={() => {
setImage(null); setImage(null);
setImagePreview(null); setImagePreview(null);
}} />} }}
/>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<DialogFooter > <DialogFooter>
<Button disabled={isLoading} onClick={async () => { <Button
disabled={isLoading}
onClick={async () => {
setIsLoading(true); setIsLoading(true);
await user.update({ await user.update({
image: image ? await convertImageToBase64(image) : undefined, image: image ? await convertImageToBase64(image) : undefined,
@@ -475,32 +531,36 @@ function EditUserDialog(props: { session: Session | null }) {
options: { options: {
onSuccess: () => { onSuccess: () => {
toast.success("User updated successfully"); toast.success("User updated successfully");
}, },
onError: (error) => { onError: (error) => {
toast.error(error.error.message); toast.error(error.error.message);
} },
} },
}) });
setName("") setName("");
router.refresh() router.refresh();
setImage(null) setImage(null);
setImagePreview(null) setImagePreview(null);
setIsLoading(false); setIsLoading(false);
setOpen(false); setOpen(false);
}}> }}
{isLoading ? <Loader2 size={15} className="animate-spin" /> : "Update"} >
{isLoading ? (
<Loader2 size={15} className="animate-spin" />
) : (
"Update"
)}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }
function AddPasskey() { function AddPasskey() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [passkeyName, setPasskeyName] = useState(""); const [passkeyName, setPasskeyName] = useState("");
const queryClient = useQueryClient() const queryClient = useQueryClient();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleAddPasskey = async () => { const handleAddPasskey = async () => {
@@ -538,15 +598,27 @@ function AddPasskey() {
</DialogHeader> </DialogHeader>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="passkey-name">Passkey Name</Label> <Label htmlFor="passkey-name">Passkey Name</Label>
<Input id="passkey-name" value={passkeyName} onChange={(e) => setPasskeyName(e.target.value)} /> <Input
id="passkey-name"
value={passkeyName}
onChange={(e) => setPasskeyName(e.target.value)}
/>
</div> </div>
<DialogFooter> <DialogFooter>
<Button disabled={isLoading} type="submit" onClick={handleAddPasskey} className="w-full"> <Button
{isLoading ? <Loader2 size={15} className="animate-spin" /> : <> disabled={isLoading}
type="submit"
onClick={handleAddPasskey}
className="w-full"
>
{isLoading ? (
<Loader2 size={15} className="animate-spin" />
) : (
<>
<Fingerprint className="mr-2 h-4 w-4" /> <Fingerprint className="mr-2 h-4 w-4" />
Create Passkey Create Passkey
</>} </>
)}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@@ -554,9 +626,8 @@ function AddPasskey() {
); );
} }
function ListPasskeys() { function ListPasskeys() {
const { data, error } = client.useListPasskeys() const { data, error } = client.useListPasskeys();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [passkeyName, setPasskeyName] = useState(""); const [passkeyName, setPasskeyName] = useState("");
@@ -583,19 +654,13 @@ function ListPasskeys() {
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="text-xs md:text-sm"> <Button variant="outline" className="text-xs md:text-sm">
<Fingerprint className="mr-2 h-4 w-4" /> <Fingerprint className="mr-2 h-4 w-4" />
<span> <span>Passkeys {data?.length ? `[${data?.length}]` : ""}</span>
Passkeys {
data?.length ? `[${data?.length}]` : ""
}
</span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px] w-11/12"> <DialogContent className="sm:max-w-[425px] w-11/12">
<DialogHeader> <DialogHeader>
<DialogTitle>Passkeys</DialogTitle> <DialogTitle>Passkeys</DialogTitle>
<DialogDescription> <DialogDescription>List of passkeys</DialogDescription>
List of passkeys
</DialogDescription>
</DialogHeader> </DialogHeader>
{data?.length ? ( {data?.length ? (
<Table> <Table>
@@ -606,10 +671,14 @@ function ListPasskeys() {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{data.map((passkey) => ( {data.map((passkey) => (
<TableRow key={passkey.id} className="flex justify-between items-center"> <TableRow
key={passkey.id}
className="flex justify-between items-center"
>
<TableCell>{passkey.name || "My Passkey"}</TableCell> <TableCell>{passkey.name || "My Passkey"}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<button onClick={async () => { <button
onClick={async () => {
const res = await client.passkey.deletePasskey({ const res = await client.passkey.deletePasskey({
id: passkey.id, id: passkey.id,
options: { options: {
@@ -617,17 +686,25 @@ function ListPasskeys() {
setIsDeletePasskey(true); setIsDeletePasskey(true);
}, },
onSuccess: () => { onSuccess: () => {
toast("Passkey deleted successfully"); setIsDeletePasskey(false); toast("Passkey deleted successfully");
setIsDeletePasskey(false);
}, },
onError: (error) => { onError: (error) => {
toast.error(error.error.message); toast.error(error.error.message);
setIsDeletePasskey(false); setIsDeletePasskey(false);
} },
} },
}) });
}} > }}
{isDeletePasskey ? <Loader2 size={15} className="animate-spin" /> : <Trash size={15} className="cursor-pointer text-red-600" />} >
{isDeletePasskey ? (
<Loader2 size={15} className="animate-spin" />
) : (
<Trash
size={15}
className="cursor-pointer text-red-600"
/>
)}
</button> </button>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -635,36 +712,37 @@ function ListPasskeys() {
</TableBody> </TableBody>
</Table> </Table>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">No passkeys found</p>
No passkeys found
</p>
)} )}
{ {!data?.length && (
!data?.length && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="passkey-name" className="text-sm"> <Label htmlFor="passkey-name" className="text-sm">
New Passkey New Passkey
</Label> </Label>
<Input id="passkey-name" value={passkeyName} onChange={(e) => setPasskeyName(e.target.value)} placeholder="My Passkey" /> <Input
id="passkey-name"
value={passkeyName}
onChange={(e) => setPasskeyName(e.target.value)}
placeholder="My Passkey"
/>
</div> </div>
<Button type="submit" onClick={handleAddPasskey} className="w-full"> <Button type="submit" onClick={handleAddPasskey} className="w-full">
{isLoading ? <Loader2 size={15} className="animate-spin" /> : <> {isLoading ? (
<Loader2 size={15} className="animate-spin" />
) : (
<>
<Fingerprint className="mr-2 h-4 w-4" /> <Fingerprint className="mr-2 h-4 w-4" />
Create Passkey Create Passkey
</>} </>
)}
</Button> </Button>
</div> </div>
) )}
}
<DialogFooter> <DialogFooter>
<Button onClick={() => setIsOpen(false)}> <Button onClick={() => setIsOpen(false)}>Close</Button>
Close
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }

View File

@@ -196,6 +196,40 @@ export const contents: Content[] = [
</svg> </svg>
), ),
}, },
{
title: "Session Management",
href: "/docs/concepts/session-management",
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 20 20"
>
<path
className="fill-foreground"
d="M16 5c0 1.657-2.686 3-6 3S4 6.657 4 5s2.686-3 6-3s6 1.343 6 3m-1.31 3.016a6 6 0 0 0 .81-.485c0 .811-.696 1.439-1.412 1.821a3 3 0 0 0-.815 4.658A2.5 2.5 0 0 0 11 16.5c0 .485.106.974.33 1.426Q10.687 18 10 18c-3.314 0-6-1.343-6-3V7.12c.383.362.84.661 1.31.896C6.562 8.642 8.222 9 10 9s3.438-.358 4.69-.984M17.5 12a2 2 0 1 1-4 0a2 2 0 0 1 4 0m1.5 4.5c0 1.245-1 2.5-3.5 2.5S12 17.75 12 16.5a1.5 1.5 0 0 1 1.5-1.5h4a1.5 1.5 0 0 1 1.5 1.5"
></path>
</svg>
),
},
{
title: "User Management",
href: "/docs/concepts/user-management",
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
className="fill-foreground"
d="M17 15q-1.05 0-1.775-.725T14.5 12.5t.725-1.775T17 10t1.775.725t.725 1.775t-.725 1.775T17 15m-4 5q-.425 0-.712-.288T12 19v-.4q0-.6.313-1.112t.887-.738q.9-.375 1.863-.562T17 16t1.938.188t1.862.562q.575.225.888.738T22 18.6v.4q0 .425-.288.713T21 20zm-3-8q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 5.2q0-.85.425-1.562T3.6 14.55q1.5-.75 3.113-1.15T10 13q.875 0 1.75.15t1.75.35l-1.7 1.7q-.625.625-1.213 1.275T10 18v.975q0 .3.113.563t.362.462H4q-.825 0-1.412-.587T2 18z"
></path>
</svg>
),
},
], ],
Icon: () => ( Icon: () => (
<svg <svg

View File

@@ -0,0 +1,81 @@
---
title: Session Management
description: Better Auth Session Management
---
Better auth manages session using a traditional cookie-based session management. The session is stored in a cookie and is sent to the server on every request. The server then verifies the session and returns the user data if the session is valid.
## Session table
The session table stores the session data. The session table has the following fields:
- `id`: The session id. Which is also used as the session cookie.
- `userId`: The user id of the user.
- `expiresAt`: The expiration date of the session.
- `ipAddress`: The IP address of the user.
- `userAgent`: The user agent of the user. It stores the user agent header from the request.
## Session Expiration
The session expires after 7 days by default. But whenever the session is used, and the `updateAge` is reached the session expiration is updated to the current time plus the `expiresIn` value.
You can change both the `expiresIn` and `updateAge` values by passing the `session` object to the `auth` configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = await betterAuth({
//... other config options
session: {
expiresIn: 1000 * 60 * 60 * 24 * 7 // 7 days,
updateAge: 1000 * 60 * 60 * 24 // 1 day (every 1 day the session expiration is updated)
}
})
```
## Session Management
Better Auth provides a set of functions to manage sessions.
### List Sessions
The `listSessions` function returns a list of sessions that are active for the user.
```ts title="client.ts"
import { client } from "@/lib/client"
const sessions = await client.user.listSessions()
```
### Revoke Session
When a user signs out of a device, the session is automatically ended. However, you can also end a session manually from any device the user is signed into.
To end a session, use the `revokeSession` function. Just pass the session ID as a parameter.
```ts title="client.ts"
await client.user.revokeSession({
id: session.id,
})
```
### Revoke All Sessions
To revoke all sessions, you can use the `revokeSessions` function.
```ts title="client.ts"
await client.user.revokeSessions()
```
### Revoking Sessions on Password Change
You can revoke all sessions when the user changes their password by passing `revokeOtherSessions` true on `changePAssword` function.
```ts title="auth.ts"
await user.changePassword({
newPassword: newPassword,
currentPassword: currentPassword,
revokeOtherSessions: signOutDevices,
})
```

View File

@@ -0,0 +1,44 @@
---
title: User Management
description: User management concepts
---
Beyond authenticating users, Better auth also provides a set of functions to manage users. This includes, updating user information, changing passwords, and more.
## User table
The user table stores the user data. The user table has the following fields:
- `id`: The user id.
- `email`: The email of the user.
- `name`: The name of the user.
- `image`: The image of the user.
- `createdAt`: The creation date of the user.
- `updatedAt`: The last update date of the user.
The user table can be extended by plugins to store additional data. When a plugin extends a user table it's infered by the type system and can be used in the client.
## Update User
### Update User Information
To update user information, you can use the `updateUser` function provided by the client. The `updateUser` function takes an object with the following properties:
```ts
await user.update({
image: "https://example.com/image.jpg",
name: "John Doe",
})
```
### Change Password
Password of a user isn't stored in the user table. Instead, it's stored in the account table. To change the password of a user, you can use the `changePassword` function provided by the client. The `changePassword` function takes an object with the following properties:
```ts
await user.changePassword({
newPassword: "newPassword123",
currentPassword: "oldPassword123",
revokeOtherSessions: true, // revoke all other sessions the user is signed into
});
```