forked from baron/baron-sso
feat: SSO 로그인 리다이렉션 흐름 구현 (userfront & adminfront 연동) #243
This commit is contained in:
@@ -7,6 +7,7 @@ import AuthPage from "../features/auth/AuthPage";
|
|||||||
import DashboardPage from "../features/dashboard/DashboardPage";
|
import DashboardPage from "../features/dashboard/DashboardPage";
|
||||||
import GlobalOverviewPage from "../features/overview/GlobalOverviewPage";
|
import GlobalOverviewPage from "../features/overview/GlobalOverviewPage";
|
||||||
import LoginPage from "../features/auth/LoginPage";
|
import LoginPage from "../features/auth/LoginPage";
|
||||||
|
import AuthCallbackPage from "../features/auth/AuthCallbackPage";
|
||||||
import TenantGroupCreatePage from "../features/tenant-groups/routes/TenantGroupCreatePage";
|
import TenantGroupCreatePage from "../features/tenant-groups/routes/TenantGroupCreatePage";
|
||||||
import TenantGroupDetailPage from "../features/tenant-groups/routes/TenantGroupDetailPage";
|
import TenantGroupDetailPage from "../features/tenant-groups/routes/TenantGroupDetailPage";
|
||||||
import TenantGroupListPage from "../features/tenant-groups/routes/TenantGroupListPage";
|
import TenantGroupListPage from "../features/tenant-groups/routes/TenantGroupListPage";
|
||||||
@@ -29,6 +30,10 @@ export const router = createBrowserRouter(
|
|||||||
path: "/login",
|
path: "/login",
|
||||||
element: <LoginPage />,
|
element: <LoginPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/auth/callback",
|
||||||
|
element: <AuthCallbackPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <AppLayout />,
|
element: <AppLayout />,
|
||||||
|
|||||||
34
adminfront/src/features/auth/AuthCallbackPage.tsx
Normal file
34
adminfront/src/features/auth/AuthCallbackPage.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
import { ShieldHalf } from "lucide-react";
|
||||||
|
|
||||||
|
function AuthCallbackPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
if (token) {
|
||||||
|
window.localStorage.setItem("admin_session", token);
|
||||||
|
// Redirect to home after a short delay or immediately
|
||||||
|
navigate("/", { replace: true });
|
||||||
|
} else {
|
||||||
|
console.error("No token found in callback URL");
|
||||||
|
navigate("/login", { replace: true });
|
||||||
|
}
|
||||||
|
}, [navigate, searchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/15 text-primary shadow-lg animate-pulse">
|
||||||
|
<ShieldHalf size={32} />
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-semibold">인증 완료 중...</div>
|
||||||
|
<p className="text-sm text-muted-foreground">세션을 동기화하고 있습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthCallbackPage;
|
||||||
@@ -29,10 +29,16 @@ function LoginPage() {
|
|||||||
return () => window.removeEventListener("message", handleMessage);
|
return () => window.removeEventListener("message", handleMessage);
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const handleSSOLogin = () => {
|
const handleSSOLogin = (mode: "popup" | "redirect" = "popup") => {
|
||||||
const userfrontUrl = import.meta.env.USERFRONT_URL || "https://sso.hmac.kr";
|
const userfrontUrl = import.meta.env.USERFRONT_URL || "https://sso.hmac.kr";
|
||||||
const loginUrl = `${userfrontUrl}/ssologin?source=adminfront`;
|
const callbackUrl = `${window.location.origin}/auth/callback`;
|
||||||
|
const loginUrl = `${userfrontUrl}/ssologin?source=adminfront&redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||||||
|
|
||||||
|
if (mode === "redirect") {
|
||||||
|
window.location.href = loginUrl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const width = 500;
|
const width = 500;
|
||||||
const height = 700;
|
const height = 700;
|
||||||
const left = window.screen.width / 2 - width / 2;
|
const left = window.screen.width / 2 - width / 2;
|
||||||
@@ -54,7 +60,8 @@ function LoginPage() {
|
|||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
alert("팝업 차단이 설정되어 있습니다. 팝업 허용 후 다시 시도해 주세요.");
|
// If popup blocked, fallback to redirect
|
||||||
|
window.location.href = loginUrl;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,9 +90,9 @@ function LoginPage() {
|
|||||||
Baron 통합 인증(SSO)을 통해 관리자 페이지에 접속합니다.
|
Baron 통합 인증(SSO)을 통해 관리자 페이지에 접속합니다.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-4 pb-8">
|
<CardContent className="pt-4 pb-8 space-y-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSSOLogin}
|
onClick={() => handleSSOLogin("popup")}
|
||||||
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={isLoggingIn}
|
||||||
>
|
>
|
||||||
@@ -97,11 +104,20 @@ function LoginPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ShieldHalf size={22} />
|
<ShieldHalf size={22} />
|
||||||
SSO 계정으로 로그인
|
팝업창으로 로그인
|
||||||
<ExternalLink size={16} className="opacity-50" />
|
<ExternalLink size={16} className="opacity-50" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleSSOLogin("redirect")}
|
||||||
|
className="w-full h-12 text-base font-medium flex gap-3"
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
>
|
||||||
|
현재 창에서 로그인 (리다이렉트)
|
||||||
|
</Button>
|
||||||
|
|
||||||
<p className="mt-6 text-xs text-center text-muted-foreground leading-relaxed">
|
<p className="mt-6 text-xs text-center text-muted-foreground leading-relaxed">
|
||||||
관리자 전역 세션은 보안을 위해 15분간 유지됩니다.<br />
|
관리자 전역 세션은 보안을 위해 15분간 유지됩니다.<br />
|
||||||
|
|||||||
@@ -5,6 +5,21 @@ import 'dart:html' as html;
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
void implSendLoginSuccess(String token) {
|
void implSendLoginSuccess(String token) {
|
||||||
|
final uri = Uri.parse(html.window.location.href);
|
||||||
|
final redirectUri = uri.queryParameters['redirect_uri'];
|
||||||
|
|
||||||
|
if (redirectUri != null && redirectUri.isNotEmpty) {
|
||||||
|
// Redirection flow
|
||||||
|
final target = Uri.parse(redirectUri);
|
||||||
|
final query = Map<String, String>.from(target.queryParameters);
|
||||||
|
query['token'] = token;
|
||||||
|
final finalUri = target.replace(queryParameters: query);
|
||||||
|
|
||||||
|
debugPrint('Redirecting to: ${finalUri.toString()}');
|
||||||
|
html.window.location.href = finalUri.toString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final message = {'type': 'LOGIN_SUCCESS', 'token': token};
|
final message = {'type': 'LOGIN_SUCCESS', 'token': token};
|
||||||
|
|
||||||
if (html.window.opener != null) {
|
if (html.window.opener != null) {
|
||||||
|
|||||||
@@ -98,6 +98,13 @@ final _router = GoRouter(
|
|||||||
return LoginScreen(key: state.pageKey, loginChallenge: loginChallenge);
|
return LoginScreen(key: state.pageKey, loginChallenge: loginChallenge);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/ssologin',
|
||||||
|
builder: (context, state) {
|
||||||
|
_routerLogger.info("Navigating to /ssologin");
|
||||||
|
return LoginScreen(key: state.pageKey);
|
||||||
|
},
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/login',
|
path: '/login',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
|||||||
Reference in New Issue
Block a user