forked from baron/baron-sso
adminfront 로그인 해결
This commit is contained in:
@@ -1,29 +1,25 @@
|
||||
import { ShieldHalf } from "lucide-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() {
|
||||
const auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const token = searchParams.get("token");
|
||||
if (token) {
|
||||
window.localStorage.setItem("admin_session", token);
|
||||
|
||||
// 만약 팝업창에서 실행 중이라면 부모 창에 알리고 닫기
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: "LOGIN_SUCCESS", token }, "*");
|
||||
window.close();
|
||||
} else {
|
||||
// 일반 리다이렉트 방식인 경우 홈으로 이동
|
||||
navigate("/", { replace: true });
|
||||
if (auth.isAuthenticated) {
|
||||
// Save token to localStorage for existing API clients that might still use it
|
||||
const user = auth.user;
|
||||
if (user?.access_token) {
|
||||
window.localStorage.setItem("admin_session", user.access_token);
|
||||
}
|
||||
} else {
|
||||
console.error("No token found in callback URL");
|
||||
navigate("/", { replace: true });
|
||||
} else if (auth.error) {
|
||||
console.error("Auth Error:", auth.error);
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
}, [navigate, searchParams]);
|
||||
}, [auth.isAuthenticated, auth.error, navigate, auth.user]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ExternalLink, LogIn, ShieldHalf } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -11,53 +10,11 @@ import {
|
||||
} from "../../components/ui/card";
|
||||
|
||||
function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
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 auth = useAuth();
|
||||
|
||||
const handleSSOLogin = () => {
|
||||
const userfrontUrl = import.meta.env.USERFRONT_URL || "https://sso.hmac.kr";
|
||||
const callbackUrl = `${window.location.origin}/auth/callback`;
|
||||
|
||||
// 항상 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("팝업 차단이 설정되어 있습니다. 팝업 허용 후 다시 시도해 주세요.");
|
||||
}
|
||||
// OIDC client-side authentication flow started here
|
||||
auth.signinRedirect();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -89,9 +46,9 @@ function LoginPage() {
|
||||
<Button
|
||||
onClick={handleSSOLogin}
|
||||
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" />
|
||||
로그인 진행 중...
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Building2, Plus, Users } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
@@ -18,8 +19,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import { fetchTenants, fetchGroups } from "../../../lib/adminApi";
|
||||
import { useState } from "react";
|
||||
import { fetchGroups, fetchTenants } from "../../../lib/adminApi";
|
||||
|
||||
export default function GlobalUserGroupListPage() {
|
||||
const { data: tenantList, isLoading: isTenantsLoading } = useQuery({
|
||||
@@ -27,7 +27,8 @@ export default function GlobalUserGroupListPage() {
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
@@ -35,7 +36,8 @@ export default function GlobalUserGroupListPage() {
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-3xl font-bold tracking-tight">User Groups</h2>
|
||||
<p className="text-muted-foreground">
|
||||
모든 테넌트의 유저 그룹을 관리합니다. 권한 상속의 주체가 되는 그룹을 설정하세요.
|
||||
모든 테넌트의 유저 그룹을 관리합니다. 권한 상속의 주체가 되는 그룹을
|
||||
설정하세요.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
@@ -62,7 +64,9 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<Building2 size={20} className="text-muted-foreground" />
|
||||
{tenant.name}
|
||||
<Badge variant="outline" className="ml-2">{tenant.slug}</Badge>
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{tenant.slug}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
이 테넌트에 정의된 유저 그룹 목록입니다.
|
||||
@@ -88,11 +92,16 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center">Loading...</TableCell>
|
||||
<TableCell colSpan={4} className="text-center">
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : groups?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-4">
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
className="text-center text-muted-foreground py-4"
|
||||
>
|
||||
등록된 유저 그룹이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -102,7 +111,10 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<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}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -111,7 +123,11 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
<TableCell>{group.members?.length || 0} 명</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -61,7 +61,11 @@ export function UserGroupDetailPage() {
|
||||
const [selectedRelation, setSelectedRelation] = useState("view");
|
||||
|
||||
// Fetch specific group details
|
||||
const { data: currentGroup, isLoading: isGroupLoading, error } = useQuery({
|
||||
const {
|
||||
data: currentGroup,
|
||||
isLoading: isGroupLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["user-group-detail", id],
|
||||
queryFn: () => fetchGroup(tenantId!, id!),
|
||||
enabled: !!id && !!tenantId,
|
||||
@@ -112,12 +116,7 @@ export function UserGroupDetailPage() {
|
||||
|
||||
const assignRoleMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
assignGroupRole(
|
||||
tenantId!,
|
||||
id!,
|
||||
selectedTargetTenantId,
|
||||
selectedRelation,
|
||||
),
|
||||
assignGroupRole(tenantId!, id!, selectedTargetTenantId, selectedRelation),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
|
||||
setIsAddRoleOpen(false);
|
||||
@@ -137,29 +136,50 @@ export function UserGroupDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
if (isGroupLoading) return (
|
||||
<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>
|
||||
<span className="ml-3 text-muted-foreground">Loading group details...</span>
|
||||
</div>
|
||||
);
|
||||
if (isGroupLoading)
|
||||
return (
|
||||
<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>
|
||||
<span className="ml-3 text-muted-foreground">
|
||||
Loading group details...
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error || !currentGroup) return (
|
||||
<div className="p-8 text-center space-y-4">
|
||||
<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">
|
||||
<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>
|
||||
if (error || !currentGroup)
|
||||
return (
|
||||
<div className="p-8 text-center space-y-4">
|
||||
<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">
|
||||
<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>
|
||||
<p className="text-muted-foreground pt-2">
|
||||
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">
|
||||
<Link
|
||||
to={`/tenants/${tenantId}/user-groups`}
|
||||
className="text-primary hover:underline text-sm"
|
||||
>
|
||||
Return to Group List
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground pt-2">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">
|
||||
<Link to={`/tenants/${tenantId}/user-groups`} className="text-primary hover:underline text-sm">
|
||||
Return to Group List
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -187,8 +207,8 @@ export function UserGroupDetailPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline">User Group</Badge>
|
||||
<Badge variant="muted">Tenant: {tenantId?.split('-')[0]}...</Badge>
|
||||
<Badge variant="outline">User Group</Badge>
|
||||
<Badge variant="muted">Tenant: {tenantId?.split("-")[0]}...</Badge>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -217,15 +237,18 @@ export function UserGroupDetailPage() {
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Search User</Label>
|
||||
<Input
|
||||
placeholder="Search by email or name..."
|
||||
<Input
|
||||
placeholder="Search by email or name..."
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Select User</Label>
|
||||
<Select value={selectedUserId} onValueChange={setSelectedUserId}>
|
||||
<Select
|
||||
value={selectedUserId}
|
||||
onValueChange={setSelectedUserId}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a user" />
|
||||
</SelectTrigger>
|
||||
@@ -240,10 +263,13 @@ export function UserGroupDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddMemberOpen(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsAddMemberOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => addMemberMutation.mutate(selectedUserId)}
|
||||
disabled={!selectedUserId || addMemberMutation.isPending}
|
||||
>
|
||||
@@ -264,7 +290,10 @@ export function UserGroupDetailPage() {
|
||||
<TableBody>
|
||||
{!currentGroup.members || currentGroup.members.length === 0 ? (
|
||||
<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.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -274,13 +303,15 @@ export function UserGroupDetailPage() {
|
||||
<TableCell>
|
||||
<div>
|
||||
<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>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive"
|
||||
onClick={() => removeMemberMutation.mutate(member.id)}
|
||||
>
|
||||
@@ -300,7 +331,9 @@ export function UserGroupDetailPage() {
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Permissions</CardTitle>
|
||||
<CardDescription>Tenant roles assigned to this group.</CardDescription>
|
||||
<CardDescription>
|
||||
Tenant roles assigned to this group.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Dialog open={isAddRoleOpen} onOpenChange={setIsAddRoleOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -313,13 +346,17 @@ export function UserGroupDetailPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign Tenant Role</DialogTitle>
|
||||
<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>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Target Tenant</Label>
|
||||
<Select value={selectedTargetTenantId} onValueChange={setSelectedTargetTenantId}>
|
||||
<Select
|
||||
value={selectedTargetTenantId}
|
||||
onValueChange={setSelectedTargetTenantId}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select target tenant" />
|
||||
</SelectTrigger>
|
||||
@@ -334,25 +371,37 @@ export function UserGroupDetailPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Role (Relation)</Label>
|
||||
<Select value={selectedRelation} onValueChange={setSelectedRelation}>
|
||||
<Select
|
||||
value={selectedRelation}
|
||||
onValueChange={setSelectedRelation}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="view">View (Read-only)</SelectItem>
|
||||
<SelectItem value="manage">Manage (Read/Write)</SelectItem>
|
||||
<SelectItem value="admins">Admin (Full Control)</SelectItem>
|
||||
<SelectItem value="manage">
|
||||
Manage (Read/Write)
|
||||
</SelectItem>
|
||||
<SelectItem value="admins">
|
||||
Admin (Full Control)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddRoleOpen(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsAddRoleOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => assignRoleMutation.mutate()}
|
||||
disabled={!selectedTargetTenantId || assignRoleMutation.isPending}
|
||||
disabled={
|
||||
!selectedTargetTenantId || assignRoleMutation.isPending
|
||||
}
|
||||
>
|
||||
Assign
|
||||
</Button>
|
||||
@@ -371,10 +420,17 @@ export function UserGroupDetailPage() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{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 ? (
|
||||
<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.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -382,17 +438,26 @@ export function UserGroupDetailPage() {
|
||||
groupRoles.map((role, idx) => (
|
||||
<TableRow key={`${role.tenantId}-${role.relation}-${idx}`}>
|
||||
<TableCell>
|
||||
<div className="font-medium">{role.tenantName || role.tenantId}</div>
|
||||
<div className="font-medium">
|
||||
{role.tenantName || role.tenantId}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="capitalize">{role.relation}</Badge>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{role.relation}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive"
|
||||
onClick={() => removeRoleMutation.mutate({ targetTenantId: role.tenantId, relation: role.relation })}
|
||||
onClick={() =>
|
||||
removeRoleMutation.mutate({
|
||||
targetTenantId: role.tenantId,
|
||||
relation: role.relation,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user