forked from baron/baron-sso
Baron 통합로그인 -> Baron 로그인 명칭 변경
This commit is contained in:
@@ -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차 인증 강화수단 검토 필요
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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": "인증번호가 전송되었습니다."})
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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차 인증 강화수단 검토 필요
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 코드 새로고침")
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
Reference in New Issue
Block a user