Skip to content

Commit a0e74cd

Browse files
steveiliop56claude
andauthored
refactor: move oidc handling to backend and add support for oidc post (#923)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 49105ce commit a0e74cd

33 files changed

Lines changed: 1485 additions & 1175 deletions

frontend/src/components/language/language.tsx

Lines changed: 0 additions & 36 deletions
This file was deleted.

frontend/src/components/layout/layout.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { useAppContext } from "@/context/app-context";
2-
import { LanguageSelector } from "../language/language";
32
import { Outlet } from "react-router";
43
import { useCallback, useEffect, useState } from "react";
54
import { DomainWarning } from "../domain-warning/domain-warning";
6-
import { ThemeToggle } from "../theme-toggle/theme-toggle";
5+
import { QuickActions } from "../quick-actions/quick-actions";
76

87
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
98
const { ui } = useAppContext();
@@ -21,9 +20,8 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
2120
backgroundPosition: "center",
2221
}}
2322
>
24-
<div className="absolute top-4 right-4 flex flex-row gap-2">
25-
<ThemeToggle />
26-
<LanguageSelector />
23+
<div className="absolute top-4 right-4">
24+
<QuickActions />
2725
</div>
2826
<div className="max-w-sm md:min-w-sm min-w-xs">{children}</div>
2927
</div>
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { languages, SupportedLanguage } from "@/lib/i18n/locales";
2+
import {
3+
DropdownMenu,
4+
DropdownMenuContent,
5+
DropdownMenuItem,
6+
DropdownMenuLabel,
7+
DropdownMenuPortal,
8+
DropdownMenuSeparator,
9+
DropdownMenuSub,
10+
DropdownMenuSubContent,
11+
DropdownMenuSubTrigger,
12+
DropdownMenuTrigger,
13+
} from "../ui/dropdown-menu";
14+
import { useState } from "react";
15+
import i18n from "@/lib/i18n/i18n";
16+
import { useUserContext } from "@/context/user-context";
17+
import { ScrollArea } from "../ui/scroll-area";
18+
import { useTheme } from "../providers/theme-provider";
19+
import {
20+
Check,
21+
DoorOpenIcon,
22+
Languages,
23+
Monitor,
24+
Moon,
25+
Palette,
26+
Settings,
27+
Sun,
28+
} from "lucide-react";
29+
import { useTranslation } from "react-i18next";
30+
import { useLocation } from "react-router";
31+
import { useRef } from "react";
32+
import {
33+
useScreenParams,
34+
recompileScreenParams,
35+
} from "@/lib/hooks/screen-params";
36+
import { useMutation } from "@tanstack/react-query";
37+
import axios from "axios";
38+
import { toast } from "sonner";
39+
import { useEffect } from "react";
40+
41+
function Avatar({ initial }: { initial: string }) {
42+
return (
43+
<span className="group relative grid size-10 place-items-center rounded-full">
44+
<span className="absolute inset-0 overflow-hidden rounded-full bg-linear-to-b from-neutral-50 to-neutral-100 dark:from-neutral-700 dark:to-neutral-950 shadow-lg"></span>
45+
<span className="relative text-sm font-semibold text-primary">
46+
{initial}
47+
</span>
48+
</span>
49+
);
50+
}
51+
52+
export const QuickActions = () => {
53+
const { auth } = useUserContext();
54+
const { theme, setTheme } = useTheme();
55+
const { t } = useTranslation();
56+
const { search } = useLocation();
57+
58+
const [language, setLanguage] = useState<SupportedLanguage>(
59+
i18n.language as SupportedLanguage,
60+
);
61+
62+
const redirectTimer = useRef<number | null>(null);
63+
const searchParams = new URLSearchParams(search);
64+
const screenParams = useScreenParams(searchParams);
65+
const compiledParams = recompileScreenParams(screenParams);
66+
67+
const logoutMutation = useMutation({
68+
mutationFn: () => axios.post("/api/user/logout"),
69+
mutationKey: ["logout"],
70+
onSuccess: () => {
71+
toast.success(t("logoutSuccessTitle"), {
72+
description: t("logoutSuccessSubtitle"),
73+
});
74+
75+
redirectTimer.current = window.setTimeout(() => {
76+
window.location.replace(`/login${compiledParams}`);
77+
}, 500);
78+
},
79+
onError: () => {
80+
toast.error(t("logoutFailTitle"), {
81+
description: t("logoutFailSubtitle"),
82+
});
83+
},
84+
});
85+
86+
useEffect(() => {
87+
return () => {
88+
if (redirectTimer.current) {
89+
clearTimeout(redirectTimer.current);
90+
}
91+
};
92+
}, [redirectTimer]);
93+
94+
const initial = auth.authenticated
95+
? (auth.name[0] || "U").toUpperCase()
96+
: null;
97+
98+
const handleSelect = (option: string) => {
99+
setLanguage(option as SupportedLanguage);
100+
i18n.changeLanguage(option as SupportedLanguage);
101+
};
102+
103+
const themes = [
104+
{ key: "light", label: t("quickActionsThemeLight"), icon: Sun },
105+
{ key: "dark", label: t("quickActionsThemeDark"), icon: Moon },
106+
{ key: "system", label: t("quickActionsThemeSystem"), icon: Monitor },
107+
] as const;
108+
109+
return (
110+
<DropdownMenu>
111+
<DropdownMenuTrigger asChild>
112+
<button
113+
aria-label={t("quickActionsTitle")}
114+
className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50"
115+
>
116+
{auth.authenticated ? (
117+
<Avatar initial={initial!} />
118+
) : (
119+
<span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg">
120+
<Settings className="size-4" />
121+
</span>
122+
)}
123+
</button>
124+
</DropdownMenuTrigger>
125+
126+
<DropdownMenuContent
127+
align="end"
128+
sideOffset={8}
129+
className="rounded-xl p-1"
130+
>
131+
{auth.authenticated && (
132+
<>
133+
<DropdownMenuLabel className="flex items-center gap-3 p-2">
134+
<div className="bg-foreground text-background flex size-9 shrink-0 items-center justify-center rounded-full text-sm font-medium">
135+
{initial}
136+
</div>
137+
<div className="flex min-w-0 flex-col">
138+
<span className="truncate text-sm font-medium">
139+
{auth.name}
140+
</span>
141+
<span className="text-muted-foreground truncate text-xs font-normal">
142+
{auth.email}
143+
</span>
144+
</div>
145+
</DropdownMenuLabel>
146+
147+
<DropdownMenuSeparator />
148+
</>
149+
)}
150+
151+
<DropdownMenuSub>
152+
<DropdownMenuSubTrigger>
153+
<Languages className="size-4" />
154+
{t("quickActionsLanguage")}
155+
</DropdownMenuSubTrigger>
156+
<DropdownMenuPortal>
157+
<DropdownMenuSubContent sideOffset={8} className="rounded-xl p-1">
158+
<ScrollArea className="h-80">
159+
{Object.entries(languages).map(([key, value]) => (
160+
<DropdownMenuItem
161+
key={key}
162+
onSelect={() => handleSelect(key)}
163+
>
164+
{value}
165+
{language === key && <Check className="size-4" />}
166+
</DropdownMenuItem>
167+
))}
168+
</ScrollArea>
169+
</DropdownMenuSubContent>
170+
</DropdownMenuPortal>
171+
</DropdownMenuSub>
172+
173+
<DropdownMenuSub>
174+
<DropdownMenuSubTrigger>
175+
<Palette className="size-4" />
176+
{t("quickActionsTheme")}
177+
</DropdownMenuSubTrigger>
178+
<DropdownMenuPortal>
179+
<DropdownMenuSubContent className="rounded-xl p-1" sideOffset={8}>
180+
{themes.map(({ key, label, icon: Icon }) => (
181+
<DropdownMenuItem key={key} onClick={() => setTheme(key)}>
182+
<span className="flex items-center gap-2">
183+
<Icon className="size-4" />
184+
{label}
185+
</span>
186+
{theme === key && <Check className="size-4" />}
187+
</DropdownMenuItem>
188+
))}
189+
</DropdownMenuSubContent>
190+
</DropdownMenuPortal>
191+
</DropdownMenuSub>
192+
193+
{auth.authenticated && (
194+
<>
195+
<DropdownMenuSeparator />
196+
<DropdownMenuItem
197+
onSelect={() => logoutMutation.mutate()}
198+
className="text-destructive"
199+
>
200+
<DoorOpenIcon className="size-4" />
201+
{t("quickActionsLogout")}
202+
</DropdownMenuItem>
203+
</>
204+
)}
205+
</DropdownMenuContent>
206+
</DropdownMenu>
207+
);
208+
};

frontend/src/components/theme-toggle/theme-toggle.tsx

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as React from "react"
2+
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
function ScrollArea({
7+
className,
8+
children,
9+
...props
10+
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
11+
return (
12+
<ScrollAreaPrimitive.Root
13+
data-slot="scroll-area"
14+
className={cn("relative", className)}
15+
{...props}
16+
>
17+
<ScrollAreaPrimitive.Viewport
18+
data-slot="scroll-area-viewport"
19+
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
20+
>
21+
{children}
22+
</ScrollAreaPrimitive.Viewport>
23+
<ScrollBar />
24+
<ScrollAreaPrimitive.Corner />
25+
</ScrollAreaPrimitive.Root>
26+
)
27+
}
28+
29+
function ScrollBar({
30+
className,
31+
orientation = "vertical",
32+
...props
33+
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
34+
return (
35+
<ScrollAreaPrimitive.ScrollAreaScrollbar
36+
data-slot="scroll-area-scrollbar"
37+
orientation={orientation}
38+
className={cn(
39+
"flex touch-none p-px transition-colors select-none",
40+
orientation === "vertical" &&
41+
"h-full w-2.5 border-l border-l-transparent",
42+
orientation === "horizontal" &&
43+
"h-2.5 flex-col border-t border-t-transparent",
44+
className
45+
)}
46+
{...props}
47+
>
48+
<ScrollAreaPrimitive.ScrollAreaThumb
49+
data-slot="scroll-area-thumb"
50+
className="relative flex-1 rounded-full bg-border"
51+
/>
52+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
53+
)
54+
}
55+
56+
export { ScrollArea, ScrollBar }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type UseLoginForProps = {
2+
login_for?: "oidc" | "app";
3+
compiledParams: string;
4+
};
5+
6+
export const useLoginFor = (props: UseLoginForProps): string => {
7+
const { login_for, compiledParams } = props;
8+
9+
switch (login_for) {
10+
case "oidc":
11+
return "/oidc/authorize" + compiledParams;
12+
case "app":
13+
return "/continue" + compiledParams;
14+
default:
15+
return "/logout";
16+
}
17+
};

0 commit comments

Comments
 (0)