Merge pull request 'feat/logout' (#6) from feat/logout into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #6
This commit is contained in:
@ -30,6 +30,7 @@ export const backendFetch = async (path: string, options: RequestInit = {}) => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-client-info": JSON.stringify(clientInfo),
|
"x-client-info": JSON.stringify(clientInfo),
|
||||||
Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
|
Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
|
||||||
|
cookie: (await headers()).get("cookie") || "",
|
||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
},
|
||||||
cache: "default",
|
cache: "default",
|
||||||
@ -37,7 +38,7 @@ export const backendFetch = async (path: string, options: RequestInit = {}) => {
|
|||||||
|
|
||||||
const resJson = (await res.json()) as BackendResponse;
|
const resJson = (await res.json()) as BackendResponse;
|
||||||
|
|
||||||
if (!res.ok || !resJson.success) {
|
if (!res.ok) {
|
||||||
throw new Error(`Elysia error: ${resJson.error}`);
|
throw new Error(`Elysia error: ${resJson.error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
184
shared/libs/shadcn/ui/alert-dialog.tsx
Normal file
184
shared/libs/shadcn/ui/alert-dialog.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/shared/libs/shadcn/lib/utils"
|
||||||
|
import { Button } from "@/shared/libs/shadcn/ui/button"
|
||||||
|
|
||||||
|
function AlertDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||||
|
size?: "default" | "sm"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-6 rounded-xl p-6 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
className={cn("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogMedia({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-media"
|
||||||
|
className={cn("bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
className={cn("text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
className={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogAction({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
||||||
|
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||||
|
return (
|
||||||
|
<Button variant={variant} size={size} asChild>
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
data-slot="alert-dialog-action"
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogCancel({
|
||||||
|
className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
||||||
|
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||||
|
return (
|
||||||
|
<Button variant={variant} size={size} asChild>
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
data-slot="alert-dialog-cancel"
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogMedia,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
}
|
||||||
23
shared/models/auth/logout.ts
Normal file
23
shared/models/auth/logout.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { backendFetch } from "@/shared/helpers/backendFetch";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export const logout = async () => {
|
||||||
|
const res = await backendFetch("auth/logout", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
(await cookies()).delete("auth_token");
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Logged out successfully",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Logout failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
66
shared/widgets/navbar/components/LogoutAlert.tsx
Normal file
66
shared/widgets/navbar/components/LogoutAlert.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/shared/libs/shadcn/ui/alert-dialog";
|
||||||
|
import { Spinner } from "@/shared/libs/shadcn/ui/spinner";
|
||||||
|
import { logout } from "@/shared/models/auth/logout";
|
||||||
|
import { Button } from "@base-ui/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const LogoutAlert = ({
|
||||||
|
openState,
|
||||||
|
setOpenState,
|
||||||
|
}: {
|
||||||
|
openState: boolean;
|
||||||
|
setOpenState: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) => {
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const continueLogout = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await logout().then((res) =>
|
||||||
|
res.success ? window.location.reload() : setIsLoading(false),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={openState}>
|
||||||
|
<AlertDialogContent size="sm" onEscapeKeyDown={() => setOpenState(false)}>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action will log you out of your account. You can log back in at
|
||||||
|
any time. Do you want to proceed?
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel
|
||||||
|
disabled={isLoading}
|
||||||
|
className="hover:cursor-pointer"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpenState(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction variant="destructive" asChild>
|
||||||
|
<Button
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full hover:cursor-pointer"
|
||||||
|
onClick={continueLogout}
|
||||||
|
>
|
||||||
|
{isLoading && <Spinner />}
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogoutAlert;
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/libs/shadcn/ui/dropdown-menu";
|
} from "@/shared/libs/shadcn/ui/dropdown-menu";
|
||||||
|
import { Button } from "@base-ui/react";
|
||||||
import {
|
import {
|
||||||
Bookmark,
|
Bookmark,
|
||||||
CircleUserRound,
|
CircleUserRound,
|
||||||
@ -19,9 +20,16 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Webhook,
|
Webhook,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import LogoutAlert from "./LogoutAlert";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
const UserProfile = () => {
|
const UserProfile = () => {
|
||||||
const { session } = useAuth();
|
const { session } = useAuth();
|
||||||
|
const [openState, setOpenState] = React.useState(false);
|
||||||
|
const triggerLogoutPopup = () => {
|
||||||
|
setOpenState(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex items-center">
|
<div className="h-full flex items-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -71,13 +79,18 @@ const UserProfile = () => {
|
|||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem variant="destructive">
|
<DropdownMenuItem variant="destructive" asChild>
|
||||||
<LogOut />
|
<Button
|
||||||
Log Out
|
onClick={triggerLogoutPopup}
|
||||||
|
className="w-full hover:cursor-pointer"
|
||||||
|
>
|
||||||
|
<LogOut /> Logout
|
||||||
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
<LogoutAlert openState={openState} setOpenState={setOpenState} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user