Skip to content

Commit

Permalink
account login details
Browse files Browse the repository at this point in the history
  • Loading branch information
JorrinKievit committed Sep 21, 2024
1 parent a8ea716 commit f32aa38
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 38 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@hookform/resolvers": "^3.9.0",
"@marsidev/react-turnstile": "^1.0.2",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
Expand All @@ -33,6 +34,7 @@
"clsx": "^2.1.1",
"epubjs": "^0.3.93",
"input-otp": "^1.2.4",
"jose": "^5.9.2",
"lucide-react": "^0.441.0",
"ofetch": "^1.3.4",
"react": "^18.3.1",
Expand Down
36 changes: 36 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/api/backend/auth/signin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ export const login = (body: { code: string; ttkn: string }) => {
body,
});
};

export const refresh = (refreshToken: string) => {
return client<LoginResponse>("/_secure/signin/refresh", {
method: "POST",
headers: {
Authorization: `Bearer ${refreshToken}`,
},
});
};
13 changes: 12 additions & 1 deletion src/api/backend/auth/sync.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { authClient } from "../base";

export const syncUserData = async ({ bookmarks, preferences, readingLists }: { bookmarks: string[]; preferences?: Record<string, unknown>; readingLists?: Record<string, unknown> }) => {
export const syncUserData = async ({
username,
bookmarks,
preferences,
readingLists,
}: {
username?: string;
bookmarks?: string[];
preferences?: Record<string, unknown>;
readingLists?: Record<string, unknown>;
}) => {
await authClient<{ message: string }>("/_secure/sync", {
method: "POST",
body: {
username,
bookmarks,
preferences,
reading_lists: readingLists,
Expand Down
50 changes: 43 additions & 7 deletions src/api/backend/base.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { router } from "@/main";
import { useAuthStore } from "@/stores/auth";
import { useSettingsStore } from "@/stores/settings";
import { redirect } from "@tanstack/react-router";
import { ofetch } from "ofetch";
import { refresh } from "./auth/signin";

export type BaseResponse<T> = {
results: T[];
Expand All @@ -15,21 +16,56 @@ export const client = ofetch.create({

export const authClient = ofetch.create({
baseURL: backendURL + `/api`,
onRequest(context) {
const token = useAuthStore.getState().accessToken;
if (!token) {
async onRequest(context) {
function handleLogout() {
useAuthStore.getState().reset();
throw redirect({ to: "/login" });
router.navigate({
to: "/login",
});
}

const { accessToken, refreshToken, tokenInfo } = useAuthStore.getState();

let accessTokenToSend = accessToken;

if (!tokenInfo) {
handleLogout();
return;
}

const currentTime = Math.floor(Date.now() / 1000);
const expirationTime = tokenInfo.exp;
const timeLeft = expirationTime - currentTime;

if (timeLeft <= 10) {
try {
const response = await refresh(refreshToken);
if (response.access_token && response.refresh_token) {
const valid = useAuthStore.getState().setTokens(response.access_token, response.refresh_token);

if (!valid) {
handleLogout();
return;
}
}
accessTokenToSend = response.access_token;
} catch {
handleLogout();
return;
}
}

context.options.headers = {
...context.options.headers,
Authorization: `Bearer ${token}`,
Authorization: `Bearer ${accessTokenToSend}`,
};
},
onResponseError(context) {
if (context.response?.status === 401) {
useAuthStore.getState().reset();
throw redirect({ to: "/login" });
router.navigate({
to: "/login",
});
}
},
});
2 changes: 1 addition & 1 deletion src/components/layout/clipboard-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function ClipBoardButton(props: ClipBoardButtonProps) {
navigator.clipboard.writeText(props.content);
setClickedOnClipBoard(true);
props.onClick?.();
toast.success("Link copied to clipboard");
toast.success("Copied to clipboard");

setTimeout(() => {
setClickedOnClipBoard(false);
Expand Down
28 changes: 25 additions & 3 deletions src/components/layout/menu.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from "react";
import { Ellipsis } from "lucide-react";
import { Ellipsis, LogOut } from "lucide-react";

import { cn } from "@/lib/utils";
import { useLayout } from "@/hooks/use-layout";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@/components/ui/tooltip";
import { Link } from "@tanstack/react-router";
import { Link, useRouteContext } from "@tanstack/react-router";
import { CollapseMenuButton } from "./collapse-menu-button";
import { useAuth } from "@/hooks/auth/use-auth";

interface MenuProps {
isOpen: boolean | undefined;
Expand All @@ -16,6 +16,10 @@ interface MenuProps {

export function Menu({ isOpen, closeSheetMenu }: MenuProps) {
const { menuList } = useLayout();
const { handleLogout } = useAuth();
const routeContext = useRouteContext({
from: "__root__",
});

return (
<ScrollArea className="[&>div>div[style]]:!block">
Expand Down Expand Up @@ -78,6 +82,24 @@ export function Menu({ isOpen, closeSheetMenu }: MenuProps) {
)}
</li>
))}

{routeContext.auth.isLoggedIn ? (
<li className="flex w-full grow items-end">
<TooltipProvider disableHoverableContent>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button onClick={handleLogout} variant="outline" className="mt-5 h-10 w-full justify-center">
<span className={cn(isOpen === false ? "" : "mr-4")}>
<LogOut size={18} />
</span>
<p className={cn("whitespace-nowrap", isOpen === false ? "hidden opacity-0" : "opacity-100")}>Log out</p>
</Button>
</TooltipTrigger>
{isOpen === false && <TooltipContent side="right">Log out</TooltipContent>}
</Tooltip>
</TooltipProvider>
</li>
) : null}
</ul>
</nav>
</ScrollArea>
Expand Down
2 changes: 2 additions & 0 deletions src/components/layout/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SheetMenu } from "./sheet-menu";
import { useLayout } from "@/hooks/use-layout";
import { useLayoutStore } from "@/stores/layout";
import { ThemeToggle } from "./theme-toggle";
import { UserNav } from "./user-nav";

export function Navbar() {
const { pageTitle } = useLayout();
Expand All @@ -21,6 +22,7 @@ export function Navbar() {
<h1 className="font-bold">{pageTitle ?? pageTitleFromStore}</h1>
</div>
<div className="flex flex-1 items-center justify-end space-x-2">
<UserNav />
<ThemeToggle />
</div>
</div>
Expand Down
69 changes: 69 additions & 0 deletions src/components/layout/user-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { LogOut } from "lucide-react";
import { useEffect } from "react";
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from "../ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { Button } from "../ui/button";
import { useAuth } from "@/hooks/auth/use-auth";
import { useAuthStore } from "@/stores/auth";
import Logo from "@/assets/logo.svg";
import { useRouteContext } from "@tanstack/react-router";

export function UserNav() {
const { handleLogout } = useAuth();
const displayName = useAuthStore((state) => state.displayName);

const routeContext = useRouteContext({
from: "__root__",
});

useEffect(() => {
window.addEventListener("keydown", (e) => {
if (e.key === "q" && (e.ctrlKey || e.metaKey)) {
handleLogout();
}
});

return () => {
window.removeEventListener("keydown", () => {});
};
});

if (!routeContext.auth.isLoggedIn) return null;

return (
<DropdownMenu>
<TooltipProvider disableHoverableContent>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src={Logo} />
<AvatarFallback>BR</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">Profile</TooltipContent>
</Tooltip>
</TooltipProvider>

<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{displayName}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="hover:cursor-pointer" onClick={handleLogout}>
<LogOut className="mr-3 h-4 w-4 text-muted-foreground" />
<span>Log out</span>
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
48 changes: 48 additions & 0 deletions src/components/ui/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"

import { cn } from "@/lib/utils"

const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName

const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName

const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName

export { Avatar, AvatarImage, AvatarFallback }
Loading

0 comments on commit f32aa38

Please sign in to comment.