1
0
forked from baron/baron-sso

feat: SSO 로그인 리다이렉션 흐름 구현 (userfront & adminfront 연동) #243

This commit is contained in:
2026-02-11 15:20:17 +09:00
parent 3163514227
commit 9d7c28b1c1
5 changed files with 83 additions and 6 deletions

View File

@@ -7,6 +7,7 @@ import AuthPage from "../features/auth/AuthPage";
import DashboardPage from "../features/dashboard/DashboardPage";
import GlobalOverviewPage from "../features/overview/GlobalOverviewPage";
import LoginPage from "../features/auth/LoginPage";
import AuthCallbackPage from "../features/auth/AuthCallbackPage";
import TenantGroupCreatePage from "../features/tenant-groups/routes/TenantGroupCreatePage";
import TenantGroupDetailPage from "../features/tenant-groups/routes/TenantGroupDetailPage";
import TenantGroupListPage from "../features/tenant-groups/routes/TenantGroupListPage";
@@ -29,6 +30,10 @@ export const router = createBrowserRouter(
path: "/login",
element: <LoginPage />,
},
{
path: "/auth/callback",
element: <AuthCallbackPage />,
},
{
path: "/",
element: <AppLayout />,

View 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;

View File

@@ -29,10 +29,16 @@ function LoginPage() {
return () => window.removeEventListener("message", handleMessage);
}, [navigate]);
const handleSSOLogin = () => {
const handleSSOLogin = (mode: "popup" | "redirect" = "popup") => {
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 height = 700;
const left = window.screen.width / 2 - width / 2;
@@ -54,7 +60,8 @@ function LoginPage() {
}
}, 1000);
} else {
alert("팝업 차단이 설정되어 있습니다. 팝업 허용 후 다시 시도해 주세요.");
// If popup blocked, fallback to redirect
window.location.href = loginUrl;
}
};
@@ -83,9 +90,9 @@ function LoginPage() {
Baron (SSO) .
</CardDescription>
</CardHeader>
<CardContent className="pt-4 pb-8">
<CardContent className="pt-4 pb-8 space-y-3">
<Button
onClick={handleSSOLogin}
onClick={() => handleSSOLogin("popup")}
className="w-full h-14 text-lg font-semibold flex gap-3 shadow-lg"
disabled={isLoggingIn}
>
@@ -97,11 +104,20 @@ function LoginPage() {
) : (
<>
<ShieldHalf size={22} />
SSO
<ExternalLink size={16} className="opacity-50" />
</>
)}
</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">
15 .<br />

View File

@@ -5,6 +5,21 @@ import 'dart:html' as html;
import 'package:flutter/foundation.dart';
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};
if (html.window.opener != null) {

View File

@@ -98,6 +98,13 @@ final _router = GoRouter(
return LoginScreen(key: state.pageKey, loginChallenge: loginChallenge);
},
),
GoRoute(
path: '/ssologin',
builder: (context, state) {
_routerLogger.info("Navigating to /ssologin");
return LoginScreen(key: state.pageKey);
},
),
GoRoute(
path: '/login',
builder: (context, state) {