forked from baron/baron-sso
feat: 어드민 로그인 방식을 SSO 팝업 연동 방식으로 변경 #243
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { ShieldHalf, LogIn, ExternalLink } from "lucide-react";
|
||||||
import type { AxiosError } from "axios";
|
import { useState, useEffect } from "react";
|
||||||
import { KeyRound, Lock, Mail, ShieldHalf } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -11,32 +9,55 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "../../components/ui/card";
|
} from "../../components/ui/card";
|
||||||
import { Input } from "../../components/ui/input";
|
|
||||||
import { Label } from "../../components/ui/label";
|
|
||||||
import { login } from "../../lib/adminApi";
|
|
||||||
import { t } from "../../lib/i18n";
|
|
||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [loginId, setLoginId] = useState("");
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
useEffect(() => {
|
||||||
mutationFn: () => login({ loginId, password }),
|
// Listen for login success message from the popup
|
||||||
onSuccess: (data) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
window.localStorage.setItem("admin_session", data.sessionToken);
|
// Security check: In production, verify event.origin
|
||||||
navigate("/");
|
if (event.data?.type === "LOGIN_SUCCESS" && event.data?.token) {
|
||||||
},
|
window.localStorage.setItem("admin_session", event.data.token);
|
||||||
});
|
setIsLoggingIn(false);
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
window.addEventListener("message", handleMessage);
|
||||||
e.preventDefault();
|
return () => window.removeEventListener("message", handleMessage);
|
||||||
loginMutation.mutate();
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleSSOLogin = () => {
|
||||||
|
const userfrontUrl = import.meta.env.VITE_USERFRONT_URL || "https://sso.hmac.kr";
|
||||||
|
const loginUrl = `${userfrontUrl}/login?source=adminfront`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
// Optional: Polling to detect if popup was closed without login
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (popup.closed) {
|
||||||
|
clearInterval(timer);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
alert("팝업 차단이 설정되어 있습니다. 팝업 허용 후 다시 시도해 주세요.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const errorMsg = (loginMutation.error as AxiosError<{ error?: string }>)?.response
|
|
||||||
?.data?.error;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-background to-background">
|
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-background to-background">
|
||||||
<div className="w-full max-w-md space-y-8">
|
<div className="w-full max-w-md space-y-8">
|
||||||
@@ -55,62 +76,46 @@ function LoginPage() {
|
|||||||
<Card className="border-primary/20 bg-card/50 backdrop-blur-xl shadow-2xl">
|
<Card className="border-primary/20 bg-card/50 backdrop-blur-xl shadow-2xl">
|
||||||
<CardHeader className="space-y-1">
|
<CardHeader className="space-y-1">
|
||||||
<CardTitle className="text-2xl flex items-center gap-2">
|
<CardTitle className="text-2xl flex items-center gap-2">
|
||||||
<KeyRound size={20} className="text-primary" />
|
<LogIn size={20} className="text-primary" />
|
||||||
Sign in
|
관리자 로그인
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
관리자 계정 정보를 입력하여 로그인하세요.
|
Baron 통합 인증(SSO)을 통해 관리자 페이지에 접속합니다.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="pt-4 pb-8">
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<Button
|
||||||
<div className="space-y-2">
|
onClick={handleSSOLogin}
|
||||||
<Label htmlFor="loginId">ID (Email)</Label>
|
className="w-full h-14 text-lg font-semibold flex gap-3 shadow-lg"
|
||||||
<div className="relative">
|
disabled={isLoggingIn}
|
||||||
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
>
|
||||||
<Input
|
{isLoggingIn ? (
|
||||||
id="loginId"
|
<>
|
||||||
type="email"
|
<div className="h-5 w-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
placeholder="admin@baron.co.kr"
|
로그인 진행 중...
|
||||||
className="pl-10"
|
</>
|
||||||
value={loginId}
|
) : (
|
||||||
onChange={(e) => setLoginId(e.target.value)}
|
<>
|
||||||
required
|
<ShieldHalf size={22} />
|
||||||
/>
|
SSO 계정으로 로그인
|
||||||
</div>
|
<ExternalLink size={16} className="opacity-50" />
|
||||||
</div>
|
</>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Password</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
className="pl-10"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{errorMsg && (
|
|
||||||
<div className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive animate-in fade-in zoom-in duration-200">
|
|
||||||
{errorMsg}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</Button>
|
||||||
<Button
|
|
||||||
type="submit"
|
<p className="mt-6 text-xs text-center text-muted-foreground leading-relaxed">
|
||||||
className="w-full h-11 text-base font-semibold"
|
관리자 전역 세션은 보안을 위해 15분간 유지됩니다.<br />
|
||||||
disabled={loginMutation.isPending}
|
민감한 작업 시 재인증을 요구할 수 있습니다.
|
||||||
>
|
</p>
|
||||||
{loginMutation.isPending ? "Signing in..." : "Login to Dashboard"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
||||||
|
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
||||||
|
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="px-8 text-center text-sm text-muted-foreground">
|
<p className="px-8 text-center text-sm text-muted-foreground">
|
||||||
인증 정보가 없거나 로그인이 되지 않는 경우<br />
|
인증 정보가 없거나 로그인이 되지 않는 경우<br />
|
||||||
시스템 관리자에게 문의하세요.
|
시스템 관리자에게 문의하세요.
|
||||||
@@ -120,4 +125,4 @@ function LoginPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoginPage;
|
export default LoginPage;
|
||||||
@@ -29,7 +29,10 @@ apiClient.interceptors.request.use((config) => {
|
|||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
// TODO: 401/403 응답 시 로그인/재인증 플로우로 리다이렉션한다.
|
if (error.response?.status === 401) {
|
||||||
|
window.localStorage.removeItem("admin_session");
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user