1
0
forked from baron/baron-sso

Baron 통합로그인 -> Baron 로그인 명칭 변경

This commit is contained in:
Lectom C Han
2026-02-05 18:36:28 +09:00
parent d3d9f7bea6
commit 62b5bdba76
13 changed files with 357 additions and 340 deletions

View File

@@ -1,6 +1,6 @@
# Baron SSO # Baron SSO
**Baron 통합로그인**은 화이트 라벨링된 가족사의 모든 소프트웨어 Auth를 총괄하는 사용자 인증/인가 허브입니다. **Baron 로그인**은 화이트 라벨링된 가족사의 모든 소프트웨어 Auth를 총괄하는 사용자 인증/인가 허브입니다.
* Ory Stack으로 모든 구성요소를 self-hosting 합니다. * Ory Stack으로 모든 구성요소를 self-hosting 합니다.
* Backend는 Go (Fiber)로 구성된 Ory Stack의 유일한 Command 전송 포인트입니다. 모든 Command는 ClickHouse로 강제 전송되며 Audit Log 시스템을 구성합니다. * Backend는 Go (Fiber)로 구성된 Ory Stack의 유일한 Command 전송 포인트입니다. 모든 Command는 ClickHouse로 강제 전송되며 Audit Log 시스템을 구성합니다.
@@ -75,7 +75,7 @@ flowchart
### 4. 주요 시나리오 (Core Scenarios) ### 4. 주요 시나리오 (Core Scenarios)
1. **Same Browser SSO**: Baron 통합로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인). 1. **Same Browser SSO**: Baron 로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인).
1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동 1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동
2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화 2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화
2.1 향후 App Push 등 2차 인증 강화수단 검토 필요 2.1 향후 App Push 등 2차 인증 강화수단 검토 필요

View File

@@ -1,142 +1,148 @@
import { import {
BadgeCheck, BadgeCheck,
Building2, Building2,
Key, Key,
KeyRound, KeyRound,
LayoutDashboard, LayoutDashboard,
Moon, Moon,
NotebookTabs, NotebookTabs,
ShieldHalf, ShieldHalf,
Sun, Sun,
Users, Users,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { NavLink, Outlet } from "react-router-dom"; import { NavLink, Outlet } from "react-router-dom";
import RoleSwitcher from "./RoleSwitcher"; import RoleSwitcher from "./RoleSwitcher";
const navItems = [ const navItems = [
{ label: "Overview", to: "/", icon: LayoutDashboard }, { label: "Overview", to: "/", icon: LayoutDashboard },
{ label: "Tenant Dashboard", to: "/dashboard", icon: ShieldHalf }, { label: "Tenant Dashboard", to: "/dashboard", icon: ShieldHalf },
{ label: "Tenants", to: "/tenants", icon: Building2 }, { label: "Tenants", to: "/tenants", icon: Building2 },
{ label: "Users", to: "/users", icon: Users }, { label: "Users", to: "/users", icon: Users },
{ label: "API Keys (M2M)", to: "/api-keys", icon: Key }, { label: "API Keys (M2M)", to: "/api-keys", icon: Key },
{ label: "Audit Logs", to: "/audit-logs", icon: NotebookTabs }, { label: "Audit Logs", to: "/audit-logs", icon: NotebookTabs },
{ label: "Auth Guard", to: "/auth", icon: KeyRound }, { label: "Auth Guard", to: "/auth", icon: KeyRound },
]; ];
function AppLayout() { function AppLayout() {
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");
return stored === "dark" ? "dark" : "light"; return stored === "dark" ? "dark" : "light";
}); });
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
root.classList.remove("light", "dark"); root.classList.remove("light", "dark");
if (theme === "light") { if (theme === "light") {
root.classList.add("light"); root.classList.add("light");
} else { } else {
root.classList.add("dark"); root.classList.add("dark");
} }
window.localStorage.setItem("admin_theme", theme); window.localStorage.setItem("admin_theme", theme);
}, [theme]); }, [theme]);
const toggleTheme = () => { const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light")); setTheme((prev) => (prev === "light" ? "dark" : "light"));
}; };
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">
<div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6"> <div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6">
<div className="flex items-center gap-3 md:flex-col md:items-start"> <div className="flex items-center gap-3 md:flex-col md:items-start">
<div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]"> <div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]">
<ShieldHalf size={20} /> <ShieldHalf size={20} />
</div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Baron
</p>
<h1 className="text-lg font-semibold">
Admin Control
</h1>
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
Scoped to /admin
</div>
</div>
<nav className="px-2 pb-4 md:px-3 md:pb-8">
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
IDP env: prod
</span>
<span className="rounded-full border border-border px-3 py-1">
Tenant-aware headers
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{label}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p> /admin .</p>
<p>
IDP API로만 , ·
.
</p>
</div>
</aside>
<div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
Admin Plane
</p>
<span className="text-lg font-semibold">
Tenant isolation & least privilege by default
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
aria-label="테마 전환"
>
{theme === "light" ? (
<Sun size={16} />
) : (
<Moon size={16} />
)}
{theme === "light" ? "Light" : "Dark"}
</button>
<span className="rounded-full border border-border px-3 py-2 text-muted-foreground">
Session TTL: 15m admin
</span>
</div>
</div>
</header>
<main className="px-5 py-6 md:px-10 md:py-10">
<Outlet />
</main>
<RoleSwitcher />
</div> </div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Baron
</p>
<h1 className="text-lg font-semibold">Admin Control</h1>
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
Scoped to /admin
</div>
</div> </div>
<nav className="px-2 pb-4 md:px-3 md:pb-8"> );
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
IDP env: prod
</span>
<span className="rounded-full border border-border px-3 py-1">
Tenant-aware headers
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{label}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p> /admin .</p>
<p>
IDP API로만 , ·
.
</p>
</div>
</aside>
<div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
Admin Plane
</p>
<span className="text-lg font-semibold">
Tenant isolation & least privilege by default
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
aria-label="테마 전환"
>
{theme === "light" ? <Sun size={16} /> : <Moon size={16} />}
{theme === "light" ? "Light" : "Dark"}
</button>
<span className="rounded-full border border-border px-3 py-2 text-muted-foreground">
Session TTL: 15m admin
</span>
</div>
</div>
</header>
<main className="px-5 py-6 md:px-10 md:py-10">
<Outlet />
</main>
<RoleSwitcher />
</div>
</div>
);
} }
export default AppLayout; export default AppLayout;

View File

@@ -237,7 +237,7 @@ func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
} }
subject := "[Baron 통합로그인] 회원가입 인증코드" subject := "[Baron 로그인] 회원가입 인증코드"
body := fmt.Sprintf(` body := fmt.Sprintf(`
<div style="padding: 20px; font-family: sans-serif;"> <div style="padding: 20px; font-family: sans-serif;">
<h2>이메일 인증</h2> <h2>이메일 인증</h2>
@@ -287,7 +287,7 @@ func (h *AuthHandler) SendSignupSmsCode(c *fiber.Ctx) error {
h.saveSignupState(key, newState, signupStateExpiration) h.saveSignupState(key, newState, signupStateExpiration)
// 4. Send SMS // 4. Send SMS
content := fmt.Sprintf("[Baron 통합로그인] 인증번호 [%s]를 입력해주세요.", code) content := fmt.Sprintf("[Baron 로그인] 인증번호 [%s]를 입력해주세요.", code)
go h.SmsService.SendSms(phone, content) go h.SmsService.SendSms(phone, content)
return c.JSON(fiber.Map{"message": "Verification code sent"}) return c.JSON(fiber.Map{"message": "Verification code sent"})
@@ -808,7 +808,7 @@ func (h *AuthHandler) SendSms(c *fiber.Ctx) error {
sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "") sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "")
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000)) code := fmt.Sprintf("%06d", rand.Intn(1000000))
content := fmt.Sprintf("[Baron 통합로그인] 인증번호: %s", code) content := fmt.Sprintf("[Baron 로그인] 인증번호: %s", code)
h.RedisService.StoreVerificationCode(sanitizedPhone, code) h.RedisService.StoreVerificationCode(sanitizedPhone, code)
if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil { if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil {
@@ -991,10 +991,10 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
} }
subject := "[Baron 통합로그인] 링크" subject := "[Baron 로그인] 링크"
body := fmt.Sprintf(` body := fmt.Sprintf(`
<div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;"> <div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;">
<h2 style="color: #1A1F2C;">Baron SSO 로그인</h2> <h2 style="color: #1A1F2C;">Baron 로그인</h2>
<p>안녕하세요,</p> <p>안녕하세요,</p>
<p>아래 버튼을 클릭하여 로그인을 완료해 주세요. 이 링크는 5분 동안 유효합니다.</p> <p>아래 버튼을 클릭하여 로그인을 완료해 주세요. 이 링크는 5분 동안 유효합니다.</p>
<div style="margin: 30px 0;"> <div style="margin: 30px 0;">
@@ -1016,7 +1016,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
} }
} else { } else {
// Send SMS // Send SMS
content := fmt.Sprintf("[Baron 통합로그인] 로그인 링크: %s | 코드: %s", link, userCode) content := fmt.Sprintf("[Baron 로그인] 로그인 링크: %s | 코드: %s", link, userCode)
if drySend { if drySend {
slog.Info("[Enchanted][DrySend] SMS send skipped", "loginID", loginID, "content", content) slog.Info("[Enchanted][DrySend] SMS send skipped", "loginID", loginID, "content", content)
} else { } else {
@@ -1677,10 +1677,10 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
ale.Log(slog.LevelError, "Email service not configured") ale.Log(slog.LevelError, "Email service not configured")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
} }
subject := "[Baron 통합로그인] 비밀번호 재설정" subject := "[Baron 로그인] 비밀번호 재설정"
body := fmt.Sprintf(` body := fmt.Sprintf(`
<div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;"> <div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;">
<h2 style="color: #1A1F2C;">Baron SSO 비밀번호 재설정</h2> <h2 style="color: #1A1F2C;">Baron 로그인 비밀번호 재설정</h2>
<p>아래 버튼을 클릭해 비밀번호 재설정을 진행해 주세요. 링크는 15분간 유효합니다.</p> <p>아래 버튼을 클릭해 비밀번호 재설정을 진행해 주세요. 링크는 15분간 유효합니다.</p>
<div style="margin: 30px 0;"> <div style="margin: 30px 0;">
<a href="%s" style="background-color: #1A1F2C; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">비밀번호 재설정</a> <a href="%s" style="background-color: #1A1F2C; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">비밀번호 재설정</a>
@@ -1700,7 +1700,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
} }
} }
} else { } else {
resetSms := fmt.Sprintf("[Baron 통합로그인] 비밀번호 재설정 링크: %s", resetLink) resetSms := fmt.Sprintf("[Baron 로그인] 비밀번호 재설정 링크: %s", resetLink)
if drySend { if drySend {
ale.Log(slog.LevelInfo, "SMS send skipped (dry-send)", slog.String("loginId", loginID), slog.String("content", resetSms)) ale.Log(slog.LevelInfo, "SMS send skipped (dry-send)", slog.String("loginId", loginID), slog.String("content", resetSms))
} else { } else {
@@ -1736,7 +1736,7 @@ func (h *AuthHandler) VerifyPasswordResetPage(c *fiber.Ctx) error {
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Baron SSO - 비밀번호 재설정</title> <title>Baron 로그인 - 비밀번호 재설정</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> <style>
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f5f5f5; } body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f5f5f5; }
@@ -2227,7 +2227,7 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
body := strings.TrimSpace(req.Body) body := strings.TrimSpace(req.Body)
if body != "" || subject != "" { if body != "" || subject != "" {
if subject == "" { if subject == "" {
subject = "[Baron 통합로그인] 알림" subject = "[Baron 로그인] 알림"
} }
return subject, body return subject, body
} }
@@ -2251,9 +2251,9 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
if subject == "" { if subject == "" {
if label == "알림" { if label == "알림" {
subject = "[Baron 통합로그인] 알림" subject = "[Baron 로그인] 알림"
} else { } else {
subject = fmt.Sprintf("[Baron 통합로그인] %s 코드", label) subject = fmt.Sprintf("[Baron 로그인] %s 코드", label)
} }
} }
@@ -2272,10 +2272,10 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
} }
if code == "" { if code == "" {
return subject, fmt.Sprintf("[Baron 통합로그인] %s 요청이 도착했습니다", label) return subject, fmt.Sprintf("[Baron 로그인] %s 요청이 도착했습니다", label)
} }
message := fmt.Sprintf("[Baron 통합로그인] %s 코드: %s", label, code) message := fmt.Sprintf("[Baron 로그인] %s 코드: %s", label, code)
if label == "로그인" { if label == "로그인" {
baseURL := os.Getenv("USERFRONT_URL") baseURL := os.Getenv("USERFRONT_URL")
if baseURL == "" { if baseURL == "" {
@@ -2320,9 +2320,9 @@ func (h *AuthHandler) buildKratosShortSmsBody(req *kratosCourierRequest, loginID
return "" return ""
} }
if h.isSmsCodeOnly(loginID) { if h.isSmsCodeOnly(loginID) {
return fmt.Sprintf("[Baron 통합로그인] 로그인 코드: %s", shortCode) return fmt.Sprintf("[Baron 로그인] 로그인 코드: %s", shortCode)
} }
return fmt.Sprintf("[Baron 통합로그인] %s", link) return fmt.Sprintf("[Baron 로그인] %s", link)
} }
func (h *AuthHandler) buildKratosShortEmailBody(req *kratosCourierRequest, loginID string) (string, string) { func (h *AuthHandler) buildKratosShortEmailBody(req *kratosCourierRequest, loginID string) (string, string) {
@@ -2330,10 +2330,10 @@ func (h *AuthHandler) buildKratosShortEmailBody(req *kratosCourierRequest, login
if !ok { if !ok {
return "", "" return "", ""
} }
subject := "[Baron 통합로그인] 로그인 링크" subject := "[Baron 로그인] 로그인 링크"
body := fmt.Sprintf(` body := fmt.Sprintf(`
<div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;"> <div style="font-family: sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px; max-width: 500px;">
<h2 style="color: #1A1F2C;">Baron SSO 로그인</h2> <h2 style="color: #1A1F2C;">Baron 로그인</h2>
<p>아래 버튼을 클릭하여 로그인을 완료해 주세요.</p> <p>아래 버튼을 클릭하여 로그인을 완료해 주세요.</p>
<div style="margin: 24px 0;"> <div style="margin: 24px 0;">
<a href="%s" style="background-color: #1A1F2C; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">로그인 완료하기</a> <a href="%s" style="background-color: #1A1F2C; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">로그인 완료하기</a>
@@ -3074,7 +3074,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs)) items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs))
for i := range authLogs { for i := range authLogs {
log := authLogs[i] log := authLogs[i]
appName := "Baron 통합로그인" appName := "Baron 로그인"
clientID := "" clientID := ""
path := strings.ToLower(extractAuditPath(log)) path := strings.ToLower(extractAuditPath(log))
if strings.Contains(path, "/api/v1/auth/oidc/login/accept") { if strings.Contains(path, "/api/v1/auth/oidc/login/accept") {
@@ -3221,9 +3221,8 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
} }
var sessions []domain.HydraConsentSession var sessions []domain.HydraConsentSession
var lastErr error var lastErr error
hasSuccess := false hasSuccess := false
for _, subject := range subjects { for _, subject := range subjects {
@@ -3361,14 +3360,14 @@ func (h *AuthHandler) RevokeLinkedRp(c *fiber.Ctx) error {
detailsBytes, _ := json.Marshal(detailsMap) detailsBytes, _ := json.Marshal(detailsMap)
_ = h.AuditRepo.Create(&domain.AuditLog{ _ = h.AuditRepo.Create(&domain.AuditLog{
EventID: GenerateSecureToken(16), EventID: GenerateSecureToken(16),
Timestamp: time.Now(), Timestamp: time.Now(),
UserID: subject, UserID: subject,
EventType: "consent.revoked", EventType: "consent.revoked",
Status: "success", Status: "success",
IPAddress: c.IP(), IPAddress: c.IP(),
UserAgent: string(c.Request().Header.UserAgent()), UserAgent: string(c.Request().Header.UserAgent()),
Details: string(detailsBytes), Details: string(detailsBytes),
}) })
} }
@@ -3534,14 +3533,14 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
detailsBytes, _ := json.Marshal(detailsMap) detailsBytes, _ := json.Marshal(detailsMap)
_ = h.AuditRepo.Create(&domain.AuditLog{ _ = h.AuditRepo.Create(&domain.AuditLog{
EventID: GenerateSecureToken(16), EventID: GenerateSecureToken(16),
Timestamp: time.Now(), Timestamp: time.Now(),
UserID: consentRequest.Subject, UserID: consentRequest.Subject,
EventType: "consent.granted", EventType: "consent.granted",
Status: "success", Status: "success",
IPAddress: c.IP(), IPAddress: c.IP(),
UserAgent: string(c.Request().Header.UserAgent()), UserAgent: string(c.Request().Header.UserAgent()),
Details: string(detailsBytes), Details: string(detailsBytes),
}) })
} }
@@ -3572,7 +3571,6 @@ func (h *AuthHandler) RejectConsentRequest(c *fiber.Ctx) error {
return c.JSON(rejectResp) return c.JSON(rejectResp)
} }
func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
var req struct { var req struct {
LoginChallenge string `json:"login_challenge"` LoginChallenge string `json:"login_challenge"`
@@ -3646,8 +3644,8 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
} }
func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) { func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
slog.Info("🚨 [FATAL_DEBUG] ENVIRONMENT CHECK", slog.Info("🚨 [FATAL_DEBUG] ENVIRONMENT CHECK",
"APP_ENV", os.Getenv("APP_ENV"), "APP_ENV", os.Getenv("APP_ENV"),
"GO_ENV", os.Getenv("GO_ENV"), "GO_ENV", os.Getenv("GO_ENV"),
"X-Test-Role", c.Get("X-Test-Role"), "X-Test-Role", c.Get("X-Test-Role"),
) )
@@ -3661,9 +3659,9 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
// Always log in development to see what's happening // Always log in development to see what's happening
if appEnv == "dev" || appEnv == "development" || appEnv == "" { if appEnv == "dev" || appEnv == "development" || appEnv == "" {
slog.Info("🔍 [AUTH_DEBUG] Checking mock role", slog.Info("🔍 [AUTH_DEBUG] Checking mock role",
"env", appEnv, "env", appEnv,
"mockRole", mockRole, "mockRole", mockRole,
"X-Test-Role", c.Get("X-Test-Role"), "X-Test-Role", c.Get("X-Test-Role"),
"X-Mock-Role", c.Get("X-Mock-Role"), "X-Mock-Role", c.Get("X-Mock-Role"),
) )
@@ -3686,9 +3684,9 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
// Mock bypass failed - log headers for debugging if in dev // Mock bypass failed - log headers for debugging if in dev
if appEnv == "dev" || appEnv == "development" || appEnv == "" { if appEnv == "dev" || appEnv == "development" || appEnv == "" {
slog.Warn("⚠️ [DEBUG] Mock auth bypass failed", slog.Warn("⚠️ [DEBUG] Mock auth bypass failed",
"appEnv", appEnv, "appEnv", appEnv,
"X-Test-Role", c.Get("X-Test-Role"), "X-Test-Role", c.Get("X-Test-Role"),
"X-Mock-Role", c.Get("X-Mock-Role"), "X-Mock-Role", c.Get("X-Mock-Role"),
"path", c.Path()) "path", c.Path())
} }
@@ -4973,7 +4971,7 @@ func (h *AuthHandler) SendUpdateCode(c *fiber.Ctx) error {
h.RedisService.Set(key, code, 5*time.Minute) h.RedisService.Set(key, code, 5*time.Minute)
// Send SMS // Send SMS
content := fmt.Sprintf("[Baron 통합로그인] 정보 수정 인증번호: [%s]", code) content := fmt.Sprintf("[Baron 로그인] 정보 수정 인증번호: [%s]", code)
go h.SmsService.SendSms(phone, content) go h.SmsService.SendSms(phone, content)
return c.JSON(fiber.Map{"message": "인증번호가 전송되었습니다."}) return c.JSON(fiber.Map{"message": "인증번호가 전송되었습니다."})

View File

@@ -6,109 +6,118 @@ import { Toaster } from "../ui/toaster";
const navItems = [{ label: "Clients", to: "/clients", icon: ShieldHalf }]; const navItems = [{ label: "Clients", to: "/clients", icon: ShieldHalf }];
function AppLayout() { function AppLayout() {
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");
return stored === "dark" ? "dark" : "light"; return stored === "dark" ? "dark" : "light";
}); });
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
root.classList.remove("light", "dark"); root.classList.remove("light", "dark");
if (theme === "light") { if (theme === "light") {
root.classList.add("light"); root.classList.add("light");
} else { } else {
root.classList.add("dark"); root.classList.add("dark");
} }
window.localStorage.setItem("admin_theme", theme); window.localStorage.setItem("admin_theme", theme);
}, [theme]); }, [theme]);
const toggleTheme = () => { const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light")); setTheme((prev) => (prev === "light" ? "dark" : "light"));
}; };
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">
<div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6"> <div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6">
<div className="flex items-center gap-3 md:flex-col md:items-start"> <div className="flex items-center gap-3 md:flex-col md:items-start">
<div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]"> <div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]">
<ShieldHalf size={20} /> <ShieldHalf size={20} />
</div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Baron
</p>
<h1 className="text-lg font-semibold">
Developer Console
</h1>
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
Scoped to /dev
</div>
</div>
<nav className="px-2 pb-4 md:px-3 md:pb-8">
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
Env: dev
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{label}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p> .</p>
<p>
.
</p>
</div>
</aside>
<div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
Dev Plane
</p>
<span className="text-lg font-semibold">
Manage your applications
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
aria-label="테마 전환"
>
{theme === "light" ? (
<Sun size={16} />
) : (
<Moon size={16} />
)}
{theme === "light" ? "Light" : "Dark"}
</button>
</div>
</div>
</header>
<main className="px-5 py-6 md:px-10 md:py-10">
<Outlet />
</main>
</div> </div>
<div> <Toaster />
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Baron
</p>
<h1 className="text-lg font-semibold">Developer Console</h1>
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
Scoped to /dev
</div>
</div> </div>
<nav className="px-2 pb-4 md:px-3 md:pb-8"> );
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
Env: dev
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{label}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p> .</p>
<p> .</p>
</div>
</aside>
<div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
Dev Plane
</p>
<span className="text-lg font-semibold">
Manage your applications
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20"
aria-label="테마 전환"
>
{theme === "light" ? <Sun size={16} /> : <Moon size={16} />}
{theme === "light" ? "Light" : "Dark"}
</button>
</div>
</div>
</header>
<main className="px-5 py-6 md:px-10 md:py-10">
<Outlet />
</main>
</div>
<Toaster />
</div>
);
} }
export default AppLayout; export default AppLayout;

View File

@@ -1,4 +1,4 @@
[Baron 통합로그인] 로그인 링크 [Baron 로그인] 로그인 링크
# 운영 링크: https://app.brsw.kr/verify?loginId={{ .To }}&code={{ .LoginCode }} # 운영 링크: https://app.brsw.kr/verify?loginId={{ .To }}&code={{ .LoginCode }}
http://localhost:5000/verify?loginId={{ .To }}&code={{ .LoginCode }} http://localhost:5000/verify?loginId={{ .To }}&code={{ .LoginCode }}
코드: {{ .LoginCode }} 코드: {{ .LoginCode }}

View File

@@ -118,7 +118,7 @@ flowchart
- RP별 Consent 관리 - RP별 Consent 관리
### 5. 주요 시나리오 (Core Scenarios) ### 5. 주요 시나리오 (Core Scenarios)
1. **Same Browser SSO**: Baron 통합로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인). 1. **Same Browser SSO**: Baron 로그인 서비스에 로그인된 상태에서 런처를 통해 타 앱/서비스로 이동 (자동 로그인).
1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동 1.1. 단 약관동의(Consent) 이력이 없으면 Consent 단계로 이동
2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화 2. **Cross-Device Auth**: 이메일 SMS 등의 수단으로 링크를 전달받고 해당 링크를 사용자가 클릭하면 최초 로그인 요청한 세션이 활성화
2.1 향후 App Push 등 2차 인증 강화수단 검토 필요 2.1 향후 App Push 등 2차 인증 강화수단 검토 필요

View File

@@ -9,17 +9,19 @@ import json
import sys import sys
from dotenv import load_dotenv from dotenv import load_dotenv
def get_env_variable(key, env_file): def get_env_variable(key, env_file):
"""Reads an environment variable from a given .env file.""" """Reads an environment variable from a given .env file."""
with open(env_file, 'r') as f: with open(env_file, "r") as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if line and not line.startswith('#'): if line and not line.startswith("#"):
k, v = line.split('=', 1) k, v = line.split("=", 1)
if k == key: if k == key:
return v.strip() return v.strip()
return None return None
def main(): def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Usage: python test/test_sms.py <recipient_phone_number>") print("Usage: python test/test_sms.py <recipient_phone_number>")
@@ -28,10 +30,10 @@ def main():
recipient_phone = sys.argv[1] recipient_phone = sys.argv[1]
# Load environment variables from .env or .env.sample # Load environment variables from .env or .env.sample
env_path = os.path.join(os.getcwd(), '.env') env_path = os.path.join(os.getcwd(), ".env")
if not os.path.exists(env_path): if not os.path.exists(env_path):
print("Info: .env file not found. Using .env.sample as a fallback.") print("Info: .env file not found. Using .env.sample as a fallback.")
env_path = os.path.join(os.getcwd(), '.env.sample') env_path = os.path.join(os.getcwd(), ".env.sample")
if not os.path.exists(env_path): if not os.path.exists(env_path):
print("Error: No configuration file found (.env or .env.sample).") print("Error: No configuration file found (.env or .env.sample).")
@@ -43,7 +45,9 @@ def main():
sender_phone = get_env_variable("NAVER_SENDER_PHONE_NUMBER", env_path) sender_phone = get_env_variable("NAVER_SENDER_PHONE_NUMBER", env_path)
if not all([access_key, secret_key, service_id, sender_phone]): if not all([access_key, secret_key, service_id, sender_phone]):
print(f"Error: One or more required environment variables are missing in {env_path}.") print(
f"Error: One or more required environment variables are missing in {env_path}."
)
sys.exit(1) sys.exit(1)
timestamp = str(int(time.time() * 1000)) timestamp = str(int(time.time() * 1000))
@@ -52,8 +56,8 @@ def main():
# Create the signature for the API request # Create the signature for the API request
message = f"POST {api_path}\n{timestamp}\n{access_key}" message = f"POST {api_path}\n{timestamp}\n{access_key}"
h = hmac.new(bytes(secret_key, 'UTF-8'), bytes(message, 'UTF-8'), hashlib.sha256) h = hmac.new(bytes(secret_key, "UTF-8"), bytes(message, "UTF-8"), hashlib.sha256)
signature = base64.b64encode(h.digest()).decode('UTF-8') signature = base64.b64encode(h.digest()).decode("UTF-8")
# Construct the JSON request body # Construct the JSON request body
json_body = { json_body = {
@@ -61,19 +65,15 @@ def main():
"contentType": "COMM", "contentType": "COMM",
"countryCode": "82", "countryCode": "82",
"from": sender_phone, "from": sender_phone,
"content": "[Baron 통합로그인] Test message from Python script.", "content": "[Baron 로그인] Test message from Python script.",
"messages": [ "messages": [{"to": recipient_phone}],
{
"to": recipient_phone
}
]
} }
headers = { headers = {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
"x-ncp-apigw-timestamp": timestamp, "x-ncp-apigw-timestamp": timestamp,
"x-ncp-iam-access-key": access_key, "x-ncp-iam-access-key": access_key,
"x-ncp-apigw-signature-v2": signature "x-ncp-apigw-signature-v2": signature,
} }
print("========================================") print("========================================")
@@ -87,12 +87,12 @@ def main():
try: try:
response = requests.post(api_url, headers=headers, json=json_body) response = requests.post(api_url, headers=headers, json=json_body)
response.raise_for_status() # Raise an exception for HTTP errors response.raise_for_status() # Raise an exception for HTTP errors
print("API Response:") print("API Response:")
print(json.dumps(response.json(), indent=2, ensure_ascii=False)) print(json.dumps(response.json(), indent=2, ensure_ascii=False))
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"Request failed: {e}") print(f"Request failed: {e}")
if hasattr(e, 'response') and e.response is not None: if hasattr(e, "response") and e.response is not None:
print("API Error Response:") print("API Error Response:")
try: try:
print(json.dumps(e.response.json(), indent=2, ensure_ascii=False)) print(json.dumps(e.response.json(), indent=2, ensure_ascii=False))
@@ -106,5 +106,6 @@ def main():
print(" Request complete.") print(" Request complete.")
print("========================================") print("========================================")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -249,7 +249,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
try { try {
final parts = jwt.split('.'); final parts = jwt.split('.');
if (parts.length != 3) return 'User'; if (parts.length != 3) return 'User';
final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))); final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1])));
final data = json.decode(payload); final data = json.decode(payload);
return data['name'] ?? data['email'] ?? data['sub'] ?? 'User'; return data['name'] ?? data['email'] ?? data['sub'] ?? 'User';
@@ -737,9 +737,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
loginId = '+82${loginId.substring(1)}'; loginId = '+82${loginId.substring(1)}';
} }
} }
debugPrint("[Auth] Initiating Enchanted Link for: $loginId"); debugPrint("[Auth] Initiating Enchanted Link for: $loginId");
try { try {
await _startEnchantedFlow(loginId, isEmail: input.contains('@')); await _startEnchantedFlow(loginId, isEmail: input.contains('@'));
} catch (e) { } catch (e) {
@@ -826,7 +826,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
try { try {
final result = await AuthProxyService.pollEnchantedLink(pendingRef); final result = await AuthProxyService.pollEnchantedLink(pendingRef);
if (result['error'] == 'slow_down') { if (result['error'] == 'slow_down') {
final interval = result['interval']; final interval = result['interval'];
if (interval is int && interval > 0) { if (interval is int && interval > 0) {
@@ -904,7 +904,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final decodedPayload = base64Url.decode(base64Url.normalize(parts[1])); final decodedPayload = base64Url.decode(base64Url.normalize(parts[1]));
final payloadJson = utf8.decode(decodedPayload); final payloadJson = utf8.decode(decodedPayload);
final data = json.decode(payloadJson) as Map<String, dynamic>; final data = json.decode(payloadJson) as Map<String, dynamic>;
final accessExpValue = data['exp'] as num?; final accessExpValue = data['exp'] as num?;
final accessExp = accessExpValue != null final accessExp = accessExpValue != null
? DateTime.fromMillisecondsSinceEpoch(accessExpValue.toInt() * 1000) ? DateTime.fromMillisecondsSinceEpoch(accessExpValue.toInt() * 1000)
@@ -923,7 +923,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
void _onLoginSuccess(String token, {String? provider}) async { void _onLoginSuccess(String token, {String? provider}) async {
if (!mounted) return; if (!mounted) return;
_logTokenDetails(token); _logTokenDetails(token);
final providerName = provider ?? AuthTokenStore.getProvider(); final providerName = provider ?? AuthTokenStore.getProvider();
@@ -1028,7 +1028,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
"Baron 통합로그인", "Baron 로그인",
style: TextStyle( style: TextStyle(
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -1110,7 +1110,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
], ],
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 16.0), padding: const EdgeInsets.only(top: 16.0),
child: Column( child: Column(
@@ -1275,7 +1275,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
_qrRemainingSeconds > 0 _qrRemainingSeconds > 0
? "남은 시간: ${_formatTime(_qrRemainingSeconds)}" ? "남은 시간: ${_formatTime(_qrRemainingSeconds)}"
: "QR 코드 만료됨", : "QR 코드 만료됨",
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -1291,7 +1291,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
style: TextStyle(color: Colors.grey, fontSize: 12), style: TextStyle(color: Colors.grey, fontSize: 12),
), ),
TextButton( TextButton(
onPressed: _startQrFlow, onPressed: _startQrFlow,
child: const Text("QR 코드 새로고침") child: const Text("QR 코드 새로고침")
), ),
], ],

View File

@@ -801,10 +801,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
String _appLabelForPath(String path) { String _appLabelForPath(String path) {
if (path.startsWith('/api/v1/auth')) { if (path.startsWith('/api/v1/auth')) {
return 'Baron 통합로그인'; return 'Baron 로그인';
} }
if (path.startsWith('/api/v1/user')) { if (path.startsWith('/api/v1/user')) {
return 'Baron 통합로그인'; return 'Baron 로그인';
} }
if (path.startsWith('/api/v1/dev')) { if (path.startsWith('/api/v1/dev')) {
return 'Dev Console'; return 'Dev Console';
@@ -812,7 +812,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
if (path.startsWith('/api/v1/admin')) { if (path.startsWith('/api/v1/admin')) {
return 'Admin Console'; return 'Admin Console';
} }
return 'Baron 통합로그인'; return 'Baron 로그인';
} }
@override @override
@@ -831,7 +831,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
backgroundColor: _subtle, backgroundColor: _subtle,
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Baron 통합로그인', 'Baron 로그인',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
elevation: 0, elevation: 0,
@@ -890,7 +890,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildPastRps(isMobile), _buildPastRps(isMobile),
const SizedBox(height: 28), const SizedBox(height: 28),
_buildSectionTitle('접속이력', 'Baron 통합로그인 기준의 최근 접근 기록입니다.'), _buildSectionTitle('접속이력', 'Baron 로그인 기준의 최근 접근 기록입니다.'),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAccessHistory(timelineWide), _buildAccessHistory(timelineWide),
], ],
@@ -1041,7 +1041,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(height: 40, child: Center(child: CircularProgressIndicator())); return const SizedBox(height: 40, child: Center(child: CircularProgressIndicator()));
} }
final pastItems = (snapshot.data ?? []).where((h) => h.status != 'active').toList(); final pastItems = (snapshot.data ?? []).where((h) => h.status != 'active').toList();
if (pastItems.isEmpty) { if (pastItems.isEmpty) {
return Column( return Column(
@@ -1078,7 +1078,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final lastAuthLabel = rp.lastAuthenticatedAt != null final lastAuthLabel = rp.lastAuthenticatedAt != null
? _formatDateTime(rp.lastAuthenticatedAt!) ? _formatDateTime(rp.lastAuthenticatedAt!)
: '연동됨'; : '연동됨';
final normalizedStatus = rp.status.toLowerCase(); final normalizedStatus = rp.status.toLowerCase();
final statusLabel = isRevoked ? '비활성' : (normalizedStatus.isEmpty || normalizedStatus == 'active' ? '활성' : '비활성'); final statusLabel = isRevoked ? '비활성' : (normalizedStatus.isEmpty || normalizedStatus == 'active' ? '활성' : '비활성');
final name = rp.name.isNotEmpty ? rp.name : rp.id; final name = rp.name.isNotEmpty ? rp.name : rp.id;
@@ -1150,7 +1150,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final statusColor = isActive ? Colors.green : Colors.grey; final statusColor = isActive ? Colors.green : Colors.grey;
final borderColor = isActive ? Colors.green.withOpacity(0.5) : _border; final borderColor = isActive ? Colors.green.withOpacity(0.5) : _border;
final borderWidth = isActive ? 1.5 : 1.0; final borderWidth = isActive ? 1.5 : 1.0;
// 활성 상태면 클릭 가능 (URL 유무와 관계없이) // 활성 상태면 클릭 가능 (URL 유무와 관계없이)
final isClickable = isActive; final isClickable = isActive;

View File

@@ -971,7 +971,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
backgroundColor: _subtle, backgroundColor: _subtle,
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Baron 통합로그인', 'Baron 로그인',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
elevation: 0, elevation: 0,

View File

@@ -39,7 +39,7 @@ Future<void> _loadBundledFonts() async {
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
usePathUrlStrategy(); usePathUrlStrategy();
// 1. Global Error Handling // 1. Global Error Handling
FlutterError.onError = (details) { FlutterError.onError = (details) {
FlutterError.presentError(details); FlutterError.presentError(details);
@@ -90,7 +90,7 @@ final _router = GoRouter(
builder: (context, state) => const ProfilePage(), builder: (context, state) => const ProfilePage(),
), ),
GoRoute( GoRoute(
path: '/signin', path: '/signin',
builder: (context, state) { builder: (context, state) {
final loginChallenge = state.uri.queryParameters['login_challenge']; final loginChallenge = state.uri.queryParameters['login_challenge'];
_routerLogger.info("Navigating to /signin with login_challenge: $loginChallenge"); _routerLogger.info("Navigating to /signin with login_challenge: $loginChallenge");
@@ -179,7 +179,7 @@ final _router = GoRouter(
path: '/reset-password', path: '/reset-password',
builder: (context, state) { builder: (context, state) {
// For deep linking, you might pass the token in the path, e.g., /reset-password/:token // For deep linking, you might pass the token in the path, e.g., /reset-password/:token
// final token = state.pathParameters['token']; // final token = state.pathParameters['token'];
_routerLogger.info("Navigating to /reset-password"); _routerLogger.info("Navigating to /reset-password");
return const ResetPasswordScreen(); return const ResetPasswordScreen();
}, },
@@ -244,13 +244,13 @@ final _router = GoRouter(
final path = state.uri.path; final path = state.uri.path;
// Public paths that don't require login // Public paths that don't require login
final isPublicPath = path == '/signin' || final isPublicPath = path == '/signin' ||
path == '/signup' || path == '/signup' ||
path == '/login' || path == '/login' ||
path == '/registration' || path == '/registration' ||
path == '/verify' || path == '/verify' ||
path == '/verification' || path == '/verification' ||
path.startsWith('/verify/') || path.startsWith('/verify/') ||
path == '/approve' || path == '/approve' ||
path.startsWith('/ql/') || path.startsWith('/ql/') ||
path == '/forgot-password' || path == '/forgot-password' ||
@@ -295,7 +295,7 @@ class BaronSSOApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp.router( return MaterialApp.router(
title: 'Baron 통합로그인', title: 'Baron 로그인',
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
@@ -14,26 +14,29 @@
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="/"> <base href="/" />
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project."> <meta name="description" content="A new Flutter project." />
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Baron 통합로그인"> <meta name="apple-mobile-web-app-title" content="Baron 로그인" />
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png" />
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png" />
<title>Baron 통합로그인</title> <title>Baron 로그인</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link
</head> href="https://fonts.googleapis.com/icon?family=Material+Icons"
<body> rel="stylesheet"
<script src="flutter_bootstrap.js" async></script> />
</body> </head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html> </html>

View File

@@ -1,11 +1,11 @@
{ {
"name": "Baron 통합로그인", "name": "Baron 로그인",
"short_name": "Baron 통합로그인", "short_name": "Baron 로그인",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#0175C2", "background_color": "#0175C2",
"theme_color": "#0175C2", "theme_color": "#0175C2",
"description": "Baron 통합로그인 사용자 포털.", "description": "Baron 로그인 사용자 포털.",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"prefer_related_applications": false, "prefer_related_applications": false,
"icons": [ "icons": [