1
0
forked from baron/baron-sso

adminfront 로그인 해결

This commit is contained in:
2026-02-19 18:02:47 +09:00
parent ba7cca3c60
commit e40dd8120e
22 changed files with 317 additions and 214 deletions

View File

@@ -110,7 +110,7 @@ HYDRA_PUBLIC_URL=${OATHKEEPER_PUBLIC_URL}/oidc
# OIDC 클라이언트 callback (콤마 구분) # OIDC 클라이언트 callback (콤마 구분)
ADMINFRONT_CALLBACK_URLS=http://localhost:5173/auth/callback,https://sso.hmac.kr/auth/callback ADMINFRONT_CALLBACK_URLS=http://localhost:5173/auth/callback,https://sso.hmac.kr/auth/callback
DEVFRONT_CALLBACK_URLS=http://localhost:5174/callback,https://sso.hmac.kr/devfront/callback DEVFRONT_CALLBACK_URLS=http://localhost:5174/auth/callback,https://sso.hmac.kr/devfront/auth/callback
# Kratos allowed_return_urls 확장 목록 (콤마 구분, 선택) # Kratos allowed_return_urls 확장 목록 (콤마 구분, 선택)
# 기본값은 KRATOS_UI_URL, USERFRONT_URL, 각 callback URL을 자동 포함합니다. # 기본값은 KRATOS_UI_URL, USERFRONT_URL, 각 callback URL을 자동 포함합니다.
@@ -134,9 +134,9 @@ CSRF_COOKIE_NAME=__HOST-baronSSO_csrf
CSRF_COOKIE_SECRET=localcsrf123 CSRF_COOKIE_SECRET=localcsrf123
# AdminFront OIDC 설정 # AdminFront OIDC 설정
ADMINFRONT_CALLBACK_URLS=http://localhost:5000/callback,https://sso.hmac.kr/devfront/callback ADMINFRONT_CALLBACK_URLS=http://localhost:5173/auth/callback,https://sso.hmac.kr/auth/callback
# DevFront OIDC 설정 # DevFront OIDC 설정
VITE_OIDC_CLIENT_ID=devfront VITE_OIDC_CLIENT_ID=devfront
VITE_OIDC_AUTHORITY=https://sso.hmac.kr/oidc VITE_OIDC_AUTHORITY=https://sso.hmac.kr/oidc
DEVFRONT_CALLBACK_URLS=http://localhost:5174/callback,https://sso.hmac.kr/devfront/callback DEVFRONT_CALLBACK_URLS=http://localhost:5174/auth/callback,https://sso.hmac.kr/devfront/auth/callback

View File

@@ -20,9 +20,11 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"oidc-client-ts": "^3.4.1",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hook-form": "^7.71.1", "react-hook-form": "^7.71.1",
"react-oidc-context": "^3.3.0",
"react-router-dom": "^6.28.2", "react-router-dom": "^6.28.2",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
@@ -3163,6 +3165,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.31.1", "version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
@@ -3605,6 +3616,18 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/oidc-client-ts": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz",
"integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==",
"license": "Apache-2.0",
"dependencies": {
"jwt-decode": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -3926,6 +3949,19 @@
"react": "^16.8.0 || ^17 || ^18 || ^19" "react": "^16.8.0 || ^17 || ^18 || ^19"
} }
}, },
"node_modules/react-oidc-context": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.3.0.tgz",
"integrity": "sha512-302T/ma4AOVAxrHdYctDSKXjCq9KNHT564XEO2yOPxRfxEP58xa4nz+GQinNl8x7CnEXECSM5JEjQJk3Cr5BvA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"oidc-client-ts": "^3.1.0",
"react": ">=16.14.0"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",

View File

@@ -26,9 +26,11 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"oidc-client-ts": "^3.4.1",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hook-form": "^7.71.1", "react-hook-form": "^7.71.1",
"react-oidc-context": "^3.3.0",
"react-router-dom": "^6.28.2", "react-router-dom": "^6.28.2",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",

View File

@@ -44,12 +44,8 @@ function LanguageSelector() {
className="rounded-full border border-border bg-transparent px-3 py-2 text-sm text-muted-foreground transition hover:bg-muted/20" className="rounded-full border border-border bg-transparent px-3 py-2 text-sm text-muted-foreground transition hover:bg-muted/20"
aria-label={t("ui.common.language", "언어")} aria-label={t("ui.common.language", "언어")}
> >
<option value="ko"> <option value="ko">{t("ui.common.language_ko", "한국어")}</option>
{t("ui.common.language_ko", "한국어")} <option value="en">{t("ui.common.language_en", "English")}</option>
</option>
<option value="en">
{t("ui.common.language_en", "English")}
</option>
</select> </select>
); );
} }

View File

@@ -12,6 +12,7 @@ import {
Users, Users,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useAuth } from "react-oidc-context";
import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { NavLink, Outlet, useNavigate } from "react-router-dom";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import LanguageSelector from "../common/LanguageSelector"; import LanguageSelector from "../common/LanguageSelector";
@@ -40,6 +41,7 @@ const navItems = [
{ label: "ui.admin.nav.auth_guard", to: "/auth", icon: KeyRound }, { label: "ui.admin.nav.auth_guard", to: "/auth", icon: KeyRound },
]; ];
function AppLayout() { function AppLayout() {
const auth = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [theme, setTheme] = useState<"light" | "dark">(() => { const [theme, setTheme] = useState<"light" | "dark">(() => {
const stored = window.localStorage.getItem("admin_theme"); const stored = window.localStorage.getItem("admin_theme");
@@ -51,16 +53,16 @@ function AppLayout() {
window.confirm(t("msg.admin.logout_confirm", "로그아웃 하시겠습니까?")) window.confirm(t("msg.admin.logout_confirm", "로그아웃 하시겠습니까?"))
) { ) {
window.localStorage.removeItem("admin_session"); window.localStorage.removeItem("admin_session");
auth.removeUser();
navigate("/login"); navigate("/login");
} }
}; };
useEffect(() => { useEffect(() => {
const session = window.localStorage.getItem("admin_session"); if (!auth.isLoading && !auth.isAuthenticated) {
if (!session) {
navigate("/login"); navigate("/login");
} }
}, [navigate]); }, [auth.isLoading, auth.isAuthenticated, navigate]);
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
@@ -77,6 +79,14 @@ function AppLayout() {
setTheme((prev) => (prev === "light" ? "dark" : "light")); setTheme((prev) => (prev === "light" ? "dark" : "light"));
}; };
if (auth.isLoading) {
return (
<div className="flex h-screen items-center justify-center bg-background">
<div className="h-8 w-8 border-4 border-primary/30 border-t-primary rounded-full animate-spin" />
</div>
);
}
return ( return (
<div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]"> <div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]">
<aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur"> <aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur">

View File

@@ -26,8 +26,7 @@ const badgeVariants = cva(
); );
export interface BadgeProps export interface BadgeProps
extends extends React.HTMLAttributes<HTMLDivElement>,
React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {

View File

@@ -34,8 +34,7 @@ const buttonVariants = cva(
); );
export interface ButtonProps export interface ButtonProps
extends extends React.ButtonHTMLAttributes<HTMLButtonElement>,
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean;
} }

View File

@@ -1,16 +1,16 @@
import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as DialogPrimitive from "@radix-ui/react-dialog" import { X } from "lucide-react";
import { X } from "lucide-react" import * as React from "react";
import { cn } from "../../lib/utils" import { cn } from "../../lib/utils";
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -20,12 +20,12 @@ const DialogOverlay = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@@ -37,7 +37,7 @@ const DialogContent = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className,
)} )}
{...props} {...props}
> >
@@ -48,8 +48,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ const DialogHeader = ({
className, className,
@@ -58,12 +58,12 @@ const DialogHeader = ({
<div <div
className={cn( className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left", "flex flex-col space-y-1.5 text-center sm:text-left",
className className,
)} )}
{...props} {...props}
/> />
) );
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ const DialogFooter = ({
className, className,
@@ -72,12 +72,12 @@ const DialogFooter = ({
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className className,
)} )}
{...props} {...props}
/> />
) );
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@@ -87,12 +87,12 @@ const DialogTitle = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", "text-lg font-semibold leading-none tracking-tight",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@@ -103,8 +103,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, Dialog,
@@ -117,4 +117,4 @@ export {
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
} };

View File

@@ -1,7 +1,8 @@
import * as React from "react"; import * as React from "react";
import { cn } from "../../lib/utils"; import { cn } from "../../lib/utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {

View File

@@ -1,14 +1,14 @@
import * as React from "react" import * as SelectPrimitive from "@radix-ui/react-select";
import * as SelectPrimitive from "@radix-ui/react-select" import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { Check, ChevronDown, ChevronUp } from "lucide-react" import * as React from "react";
import { cn } from "../../lib/utils" import { cn } from "../../lib/utils";
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className className,
)} )}
{...props} {...props}
> >
@@ -27,8 +27,8 @@ const SelectTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)) ));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@@ -38,14 +38,14 @@ const SelectScrollUpButton = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...props}
> >
<ChevronUp className="h-4 w-4" /> <ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
)) ));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@@ -55,15 +55,15 @@ const SelectScrollDownButton = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...props}
> >
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
)) ));
SelectScrollDownButton.displayName = SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
@@ -76,7 +76,7 @@ const SelectContent = React.forwardRef<
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=top]:slide-in-from-bottom-2 data-[state=bottom]:slide-in-from-top-2", "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=top]:slide-in-from-bottom-2 data-[state=bottom]:slide-in-from-top-2",
position === "popper" && position === "popper" &&
"data-[state=open]:slide-in-from-top-2 data-[state=bottom]:translate-y-1 data-[state=left]:-translate-x-1 data-[state=right]:translate-x-1 data-[state=top]:-translate-y-1", "data-[state=open]:slide-in-from-top-2 data-[state=bottom]:translate-y-1 data-[state=left]:-translate-x-1 data-[state=right]:translate-x-1 data-[state=top]:-translate-y-1",
className className,
)} )}
position={position} position={position}
{...props} {...props}
@@ -86,7 +86,7 @@ const SelectContent = React.forwardRef<
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-[var(--radix-select-content-available-height)] w-full min-w-[var(--radix-select-trigger-width)]" "h-[var(--radix-select-content-available-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)} )}
> >
{children} {children}
@@ -94,8 +94,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
)) ));
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef< const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, React.ElementRef<typeof SelectPrimitive.Label>,
@@ -106,8 +106,8 @@ const SelectLabel = React.forwardRef<
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props} {...props}
/> />
)) ));
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ElementRef<typeof SelectPrimitive.Item>,
@@ -117,7 +117,7 @@ const SelectItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -129,8 +129,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
)) ));
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
@@ -141,8 +141,8 @@ const SelectSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)) ));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export { export {
Select, Select,
@@ -155,4 +155,4 @@ export {
SelectSeparator, SelectSeparator,
SelectScrollUpButton, SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
} };

View File

@@ -1,7 +1,8 @@
import * as React from "react"; import * as React from "react";
import { cn } from "../../lib/utils"; import { cn } from "../../lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {

View File

@@ -1,29 +1,25 @@
import { ShieldHalf } from "lucide-react"; import { ShieldHalf } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useAuth } from "react-oidc-context";
import { useNavigate } from "react-router-dom";
function AuthCallbackPage() { function AuthCallbackPage() {
const auth = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams();
useEffect(() => { useEffect(() => {
const token = searchParams.get("token"); if (auth.isAuthenticated) {
if (token) { // Save token to localStorage for existing API clients that might still use it
window.localStorage.setItem("admin_session", token); const user = auth.user;
if (user?.access_token) {
// 만약 팝업창에서 실행 중이라면 부모 창에 알리고 닫기 window.localStorage.setItem("admin_session", user.access_token);
if (window.opener) {
window.opener.postMessage({ type: "LOGIN_SUCCESS", token }, "*");
window.close();
} else {
// 일반 리다이렉트 방식인 경우 홈으로 이동
navigate("/", { replace: true });
} }
} else { navigate("/", { replace: true });
console.error("No token found in callback URL"); } else if (auth.error) {
console.error("Auth Error:", auth.error);
navigate("/login", { replace: true }); navigate("/login", { replace: true });
} }
}, [navigate, searchParams]); }, [auth.isAuthenticated, auth.error, navigate, auth.user]);
return ( return (
<div className="flex min-h-screen items-center justify-center bg-background"> <div className="flex min-h-screen items-center justify-center bg-background">

View File

@@ -1,6 +1,5 @@
import { ExternalLink, LogIn, ShieldHalf } from "lucide-react"; import { ExternalLink, LogIn, ShieldHalf } from "lucide-react";
import { useEffect, useState } from "react"; import { useAuth } from "react-oidc-context";
import { useNavigate } from "react-router-dom";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { import {
Card, Card,
@@ -11,53 +10,11 @@ import {
} from "../../components/ui/card"; } from "../../components/ui/card";
function LoginPage() { function LoginPage() {
const navigate = useNavigate(); const auth = useAuth();
const [isLoggingIn, setIsLoggingIn] = useState(false);
useEffect(() => {
// Listen for login success message from the popup
const handleMessage = (event: MessageEvent) => {
// Security check: In production, verify event.origin
if (event.data?.type === "LOGIN_SUCCESS" && event.data?.token) {
window.localStorage.setItem("admin_session", event.data.token);
setIsLoggingIn(false);
navigate("/");
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [navigate]);
const handleSSOLogin = () => { const handleSSOLogin = () => {
const userfrontUrl = import.meta.env.USERFRONT_URL || "https://sso.hmac.kr"; // OIDC client-side authentication flow started here
const callbackUrl = `${window.location.origin}/auth/callback`; auth.signinRedirect();
// 항상 redirect_uri를 포함하여 로그인이 성공하면 콜백 페이지로 오도록 함
const loginUrl = `${userfrontUrl}/signin?source=adminfront&redirect_uri=${encodeURIComponent(callbackUrl)}`;
const width = 500;
const height = 700;
const left = window.screen.width / 2 - width / 2;
const top = window.screen.height / 2 - height / 2;
const popup = window.open(
loginUrl,
"BaronSSOLogin",
`width=${width},height=${height},top=${top},left=${left},status=no,menubar=no,toolbar=no`,
);
if (popup) {
setIsLoggingIn(true);
const timer = setInterval(() => {
if (popup.closed) {
clearInterval(timer);
setIsLoggingIn(false);
}
}, 1000);
} else {
alert("팝업 차단이 설정되어 있습니다. 팝업 허용 후 다시 시도해 주세요.");
}
}; };
return ( return (
@@ -89,9 +46,9 @@ function LoginPage() {
<Button <Button
onClick={handleSSOLogin} onClick={handleSSOLogin}
className="w-full h-14 text-lg font-semibold flex gap-3 shadow-lg" className="w-full h-14 text-lg font-semibold flex gap-3 shadow-lg"
disabled={isLoggingIn} disabled={auth.isLoading}
> >
{isLoggingIn ? ( {auth.isLoading ? (
<> <>
<div className="h-5 w-5 border-2 border-white/30 border-t-white rounded-full animate-spin" /> <div className="h-5 w-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
... ...

View File

@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Building2, Plus, Users } from "lucide-react"; import { Building2, Plus, Users } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Badge } from "../../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button"; import { Button } from "../../../components/ui/button";
@@ -18,8 +19,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../../components/ui/table"; } from "../../../components/ui/table";
import { fetchTenants, fetchGroups } from "../../../lib/adminApi"; import { fetchGroups, fetchTenants } from "../../../lib/adminApi";
import { useState } from "react";
export default function GlobalUserGroupListPage() { export default function GlobalUserGroupListPage() {
const { data: tenantList, isLoading: isTenantsLoading } = useQuery({ const { data: tenantList, isLoading: isTenantsLoading } = useQuery({
@@ -27,7 +27,8 @@ export default function GlobalUserGroupListPage() {
queryFn: () => fetchTenants(100, 0), queryFn: () => fetchTenants(100, 0),
}); });
if (isTenantsLoading) return <div className="p-8">Loading tenants and groups...</div>; if (isTenantsLoading)
return <div className="p-8">Loading tenants and groups...</div>;
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@@ -35,7 +36,8 @@ export default function GlobalUserGroupListPage() {
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-3xl font-bold tracking-tight">User Groups</h2> <h2 className="text-3xl font-bold tracking-tight">User Groups</h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
. . .
.
</p> </p>
</div> </div>
</header> </header>
@@ -62,7 +64,9 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
<CardTitle className="text-xl flex items-center gap-2"> <CardTitle className="text-xl flex items-center gap-2">
<Building2 size={20} className="text-muted-foreground" /> <Building2 size={20} className="text-muted-foreground" />
{tenant.name} {tenant.name}
<Badge variant="outline" className="ml-2">{tenant.slug}</Badge> <Badge variant="outline" className="ml-2">
{tenant.slug}
</Badge>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
. .
@@ -88,11 +92,16 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
<TableBody> <TableBody>
{isLoading ? ( {isLoading ? (
<TableRow> <TableRow>
<TableCell colSpan={4} className="text-center">Loading...</TableCell> <TableCell colSpan={4} className="text-center">
Loading...
</TableCell>
</TableRow> </TableRow>
) : groups?.length === 0 ? ( ) : groups?.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground py-4"> <TableCell
colSpan={4}
className="text-center text-muted-foreground py-4"
>
. .
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -102,7 +111,10 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
<TableCell className="font-medium"> <TableCell className="font-medium">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users size={14} className="text-primary" /> <Users size={14} className="text-primary" />
<Link to={`/tenants/${tenant.id}/user-groups/${group.id}`} className="hover:underline"> <Link
to={`/tenants/${tenant.id}/user-groups/${group.id}`}
className="hover:underline"
>
{group.name} {group.name}
</Link> </Link>
</div> </div>
@@ -111,7 +123,11 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
<TableCell>{group.members?.length || 0} </TableCell> <TableCell>{group.members?.length || 0} </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" asChild>
<Link to={`/tenants/${tenant.id}/user-groups/${group.id}`}></Link> <Link
to={`/tenants/${tenant.id}/user-groups/${group.id}`}
>
</Link>
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -61,7 +61,11 @@ export function UserGroupDetailPage() {
const [selectedRelation, setSelectedRelation] = useState("view"); const [selectedRelation, setSelectedRelation] = useState("view");
// Fetch specific group details // Fetch specific group details
const { data: currentGroup, isLoading: isGroupLoading, error } = useQuery({ const {
data: currentGroup,
isLoading: isGroupLoading,
error,
} = useQuery({
queryKey: ["user-group-detail", id], queryKey: ["user-group-detail", id],
queryFn: () => fetchGroup(tenantId!, id!), queryFn: () => fetchGroup(tenantId!, id!),
enabled: !!id && !!tenantId, enabled: !!id && !!tenantId,
@@ -112,12 +116,7 @@ export function UserGroupDetailPage() {
const assignRoleMutation = useMutation({ const assignRoleMutation = useMutation({
mutationFn: () => mutationFn: () =>
assignGroupRole( assignGroupRole(tenantId!, id!, selectedTargetTenantId, selectedRelation),
tenantId!,
id!,
selectedTargetTenantId,
selectedRelation,
),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] }); queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
setIsAddRoleOpen(false); setIsAddRoleOpen(false);
@@ -137,24 +136,45 @@ export function UserGroupDetailPage() {
}, },
}); });
if (isGroupLoading) return ( if (isGroupLoading)
return (
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<span className="ml-3 text-muted-foreground">Loading group details...</span> <span className="ml-3 text-muted-foreground">
Loading group details...
</span>
</div> </div>
); );
if (error || !currentGroup) return ( if (error || !currentGroup)
return (
<div className="p-8 text-center space-y-4"> <div className="p-8 text-center space-y-4">
<h3 className="text-xl font-semibold text-destructive">Could not load group</h3> <h3 className="text-xl font-semibold text-destructive">
Could not load group
</h3>
<div className="p-4 bg-red-50 text-red-700 rounded-md text-left text-sm font-mono overflow-auto max-w-xl mx-auto border border-red-100"> <div className="p-4 bg-red-50 text-red-700 rounded-md text-left text-sm font-mono overflow-auto max-w-xl mx-auto border border-red-100">
<p>Error: {(error as any)?.response?.data?.error || (error as any)?.message || "Not found"}</p> <p>
<p className="mt-2 text-red-500 opacity-70">Path: /admin/tenants/{tenantId}/user-groups/{id}</p> Error:{" "}
{(error as any)?.response?.data?.error ||
(error as any)?.message ||
"Not found"}
</p>
<p className="mt-2 text-red-500 opacity-70">
Path: /admin/tenants/{tenantId}/user-groups/{id}
</p>
</div> </div>
<p className="text-muted-foreground pt-2">The group ID might be invalid or you don't have sufficient permissions.</p> <p className="text-muted-foreground pt-2">
<Button variant="outline" onClick={() => window.location.reload()}>Retry</Button> The group ID might be invalid or you don't have sufficient
permissions.
</p>
<Button variant="outline" onClick={() => window.location.reload()}>
Retry
</Button>
<div className="pt-4 border-t"> <div className="pt-4 border-t">
<Link to={`/tenants/${tenantId}/user-groups`} className="text-primary hover:underline text-sm"> <Link
to={`/tenants/${tenantId}/user-groups`}
className="text-primary hover:underline text-sm"
>
Return to Group List Return to Group List
</Link> </Link>
</div> </div>
@@ -188,7 +208,7 @@ export function UserGroupDetailPage() {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Badge variant="outline">User Group</Badge> <Badge variant="outline">User Group</Badge>
<Badge variant="muted">Tenant: {tenantId?.split('-')[0]}...</Badge> <Badge variant="muted">Tenant: {tenantId?.split("-")[0]}...</Badge>
</div> </div>
</header> </header>
@@ -225,7 +245,10 @@ export function UserGroupDetailPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Select User</Label> <Label>Select User</Label>
<Select value={selectedUserId} onValueChange={setSelectedUserId}> <Select
value={selectedUserId}
onValueChange={setSelectedUserId}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Choose a user" /> <SelectValue placeholder="Choose a user" />
</SelectTrigger> </SelectTrigger>
@@ -240,7 +263,10 @@ export function UserGroupDetailPage() {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsAddMemberOpen(false)}> <Button
variant="outline"
onClick={() => setIsAddMemberOpen(false)}
>
Cancel Cancel
</Button> </Button>
<Button <Button
@@ -264,7 +290,10 @@ export function UserGroupDetailPage() {
<TableBody> <TableBody>
{!currentGroup.members || currentGroup.members.length === 0 ? ( {!currentGroup.members || currentGroup.members.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={2} className="text-center py-4 text-muted-foreground"> <TableCell
colSpan={2}
className="text-center py-4 text-muted-foreground"
>
No members in this group. No members in this group.
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -274,7 +303,9 @@ export function UserGroupDetailPage() {
<TableCell> <TableCell>
<div> <div>
<p className="font-medium">{member.name}</p> <p className="font-medium">{member.name}</p>
<p className="text-xs text-muted-foreground">{member.email}</p> <p className="text-xs text-muted-foreground">
{member.email}
</p>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
@@ -300,7 +331,9 @@ export function UserGroupDetailPage() {
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<div> <div>
<CardTitle>Permissions</CardTitle> <CardTitle>Permissions</CardTitle>
<CardDescription>Tenant roles assigned to this group.</CardDescription> <CardDescription>
Tenant roles assigned to this group.
</CardDescription>
</div> </div>
<Dialog open={isAddRoleOpen} onOpenChange={setIsAddRoleOpen}> <Dialog open={isAddRoleOpen} onOpenChange={setIsAddRoleOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -313,13 +346,17 @@ export function UserGroupDetailPage() {
<DialogHeader> <DialogHeader>
<DialogTitle>Assign Tenant Role</DialogTitle> <DialogTitle>Assign Tenant Role</DialogTitle>
<DialogDescription> <DialogDescription>
Members of this group will inherit this role on the target tenant. Members of this group will inherit this role on the target
tenant.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Target Tenant</Label> <Label>Target Tenant</Label>
<Select value={selectedTargetTenantId} onValueChange={setSelectedTargetTenantId}> <Select
value={selectedTargetTenantId}
onValueChange={setSelectedTargetTenantId}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select target tenant" /> <SelectValue placeholder="Select target tenant" />
</SelectTrigger> </SelectTrigger>
@@ -334,25 +371,37 @@ export function UserGroupDetailPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Role (Relation)</Label> <Label>Role (Relation)</Label>
<Select value={selectedRelation} onValueChange={setSelectedRelation}> <Select
value={selectedRelation}
onValueChange={setSelectedRelation}
>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="view">View (Read-only)</SelectItem> <SelectItem value="view">View (Read-only)</SelectItem>
<SelectItem value="manage">Manage (Read/Write)</SelectItem> <SelectItem value="manage">
<SelectItem value="admins">Admin (Full Control)</SelectItem> Manage (Read/Write)
</SelectItem>
<SelectItem value="admins">
Admin (Full Control)
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setIsAddRoleOpen(false)}> <Button
variant="outline"
onClick={() => setIsAddRoleOpen(false)}
>
Cancel Cancel
</Button> </Button>
<Button <Button
onClick={() => assignRoleMutation.mutate()} onClick={() => assignRoleMutation.mutate()}
disabled={!selectedTargetTenantId || assignRoleMutation.isPending} disabled={
!selectedTargetTenantId || assignRoleMutation.isPending
}
> >
Assign Assign
</Button> </Button>
@@ -371,10 +420,17 @@ export function UserGroupDetailPage() {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{isRolesLoading ? ( {isRolesLoading ? (
<TableRow><TableCell colSpan={3} className="text-center">Loading...</TableCell></TableRow> <TableRow>
<TableCell colSpan={3} className="text-center">
Loading...
</TableCell>
</TableRow>
) : !groupRoles || groupRoles.length === 0 ? ( ) : !groupRoles || groupRoles.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={3} className="text-center py-4 text-muted-foreground"> <TableCell
colSpan={3}
className="text-center py-4 text-muted-foreground"
>
No roles assigned. No roles assigned.
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -382,17 +438,26 @@ export function UserGroupDetailPage() {
groupRoles.map((role, idx) => ( groupRoles.map((role, idx) => (
<TableRow key={`${role.tenantId}-${role.relation}-${idx}`}> <TableRow key={`${role.tenantId}-${role.relation}-${idx}`}>
<TableCell> <TableCell>
<div className="font-medium">{role.tenantName || role.tenantId}</div> <div className="font-medium">
{role.tenantName || role.tenantId}
</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="capitalize">{role.relation}</Badge> <Badge variant="outline" className="capitalize">
{role.relation}
</Badge>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="text-destructive" className="text-destructive"
onClick={() => removeRoleMutation.mutate({ targetTenantId: role.tenantId, relation: role.relation })} onClick={() =>
removeRoleMutation.mutate({
targetTenantId: role.tenantId,
relation: role.relation,
})
}
> >
<Trash2 size={14} /> <Trash2 size={14} />
</Button> </Button>

View File

@@ -54,8 +54,7 @@
body { body {
@apply min-h-screen bg-background font-sans text-foreground antialiased; @apply min-h-screen bg-background font-sans text-foreground antialiased;
background-image: background-image: radial-gradient(
radial-gradient(
circle at 10% 18%, circle at 10% 18%,
rgba(54, 211, 153, 0.16), rgba(54, 211, 153, 0.16),
transparent 28% transparent 28%

View File

@@ -208,7 +208,9 @@ export async function createGroup(
} }
export async function deleteGroup(tenantId: string, groupId: string) { export async function deleteGroup(tenantId: string, groupId: string) {
await apiClient.delete(`/v1/admin/tenants/${tenantId}/user-groups/${groupId}`); await apiClient.delete(
`/v1/admin/tenants/${tenantId}/user-groups/${groupId}`,
);
} }
export async function addGroupMember( export async function addGroupMember(

View File

@@ -0,0 +1,20 @@
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
import type { AuthProviderProps } from "react-oidc-context";
export const oidcConfig: AuthProviderProps = {
authority: import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "adminfront",
redirect_uri: `${window.location.origin}/auth/callback`,
response_type: "code",
scope: "openid offline_access profile email", // offline_access for refresh token
post_logout_redirect_uri: window.location.origin,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: true,
};
export const userManager = new UserManager({
...oidcConfig,
authority: oidcConfig.authority || "",
client_id: oidcConfig.client_id || "",
redirect_uri: oidcConfig.redirect_uri || "",
});

View File

@@ -1,9 +1,11 @@
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { AuthProvider } from "react-oidc-context";
import { RouterProvider } from "react-router-dom"; import { RouterProvider } from "react-router-dom";
import { queryClient } from "./app/queryClient"; import { queryClient } from "./app/queryClient";
import { router } from "./app/routes"; import { router } from "./app/routes";
import { oidcConfig } from "./lib/auth";
import "./index.css"; import "./index.css";
const rootElement = document.getElementById("root"); const rootElement = document.getElementById("root");
@@ -14,8 +16,10 @@ if (!rootElement) {
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<AuthProvider {...oidcConfig}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</QueryClientProvider> </QueryClientProvider>
</AuthProvider>
</StrictMode>, </StrictMode>,
); );

View File

@@ -211,10 +211,10 @@ services:
hydra create oauth2-client \ hydra create oauth2-client \
--endpoint http://hydra:4445 \ --endpoint http://hydra:4445 \
--id adminfront \ --id adminfront \
--secret admin-secret \
--grant-type authorization_code,refresh_token \ --grant-type authorization_code,refresh_token \
--response-type code \ --response-type code \
--scope openid,offline_access,profile,email \ --scope openid,offline_access,profile,email \
--token-endpoint-auth-method none \
--redirect-uri ${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback} --redirect-uri ${ADMINFRONT_CALLBACK_URLS:-http://localhost:5173/auth/callback}
hydra create oauth2-client \ hydra create oauth2-client \
@@ -224,7 +224,7 @@ services:
--response-type code \ --response-type code \
--scope openid,offline_access,profile,email \ --scope openid,offline_access,profile,email \
--token-endpoint-auth-method none \ --token-endpoint-auth-method none \
--redirect-uri ${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/callback} --redirect-uri ${DEVFRONT_CALLBACK_URLS:-http://localhost:5174/auth/callback}
hydra create oauth2-client \ hydra create oauth2-client \
--endpoint http://hydra:4445 \ --endpoint http://hydra:4445 \

View File

@@ -15,7 +15,7 @@ export const router = createBrowserRouter(
element: <LoginPage />, element: <LoginPage />,
}, },
{ {
path: "/callback", path: "/auth/callback",
element: <AuthCallbackPage />, element: <AuthCallbackPage />,
}, },
{ {

View File

@@ -4,7 +4,7 @@ import type { AuthProviderProps } from "react-oidc-context";
export const oidcConfig: AuthProviderProps = { export const oidcConfig: AuthProviderProps = {
authority: import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL authority: import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "devfront", client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "devfront",
redirect_uri: `${window.location.origin}/callback`, redirect_uri: `${window.location.origin}/auth/callback`,
response_type: "code", response_type: "code",
scope: "openid offline_access profile email", // offline_access for refresh token scope: "openid offline_access profile email", // offline_access for refresh token
post_logout_redirect_uri: window.location.origin, post_logout_redirect_uri: window.location.origin,