mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-06 12:27:44 +00:00
feat: session and user management docs
This commit is contained in:
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
81
docs/content/docs/concepts/session-management.mdx
Normal file
81
docs/content/docs/concepts/session-management.mdx
Normal 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,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
44
docs/content/docs/concepts/user-management.mdx
Normal file
44
docs/content/docs/concepts/user-management.mdx
Normal 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
|
||||||
|
});
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user