1
0
forked from baron/baron-sso

Merge pull request 'feature/devfront-refactor' (#330) from feature/devfront-refactor into dev

Reviewed-on: baron/baron-sso#330
This commit is contained in:
2026-02-24 17:44:05 +09:00
19 changed files with 457 additions and 342 deletions

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@
*.log
*.out
*.exe
.npm-cache/
reports
reports/*

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
import { BadgeCheck, LogOut, Moon, ShieldHalf, Sun } from "lucide-react";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useAuth } from "react-oidc-context";
import { NavLink, Outlet, useNavigate } from "react-router-dom";
import { t } from "../../lib/i18n";
@@ -18,10 +18,14 @@ const navItems = [
function AppLayout() {
const auth = useAuth();
const navigate = useNavigate();
const profileMenuRef = useRef<HTMLDivElement>(null);
const [theme, setTheme] = useState<"light" | "dark">(() => {
const stored = window.localStorage.getItem("admin_theme");
return stored === "dark" ? "dark" : "light";
});
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
const [isRefreshingSession, setIsRefreshingSession] = useState(false);
const [nowMs, setNowMs] = useState(() => Date.now());
const handleLogout = () => {
if (window.confirm(t("msg.dev.logout_confirm", "로그아웃 하시겠습니까?"))) {
@@ -41,10 +45,109 @@ function AppLayout() {
window.localStorage.setItem("admin_theme", theme);
}, [theme]);
useEffect(() => {
const timer = window.setInterval(() => {
setNowMs(Date.now());
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
profileMenuRef.current &&
!profileMenuRef.current.contains(event.target as Node)
) {
setIsProfileMenuOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
const profileName =
auth.user?.profile?.name?.toString().trim() ||
auth.user?.profile?.preferred_username?.toString().trim() ||
auth.user?.profile?.nickname?.toString().trim() ||
t("ui.dev.profile.unknown_name", "Unknown User");
const profileEmail =
auth.user?.profile?.email?.toString().trim() ||
t("ui.dev.profile.unknown_email", "unknown@example.com");
const profileInitial = profileName.charAt(0).toUpperCase();
const expiresAtSec = auth.user?.expires_at;
const remainingMs =
typeof expiresAtSec === "number" ? expiresAtSec * 1000 - nowMs : null;
const remainingTotalSec =
remainingMs !== null ? Math.max(0, Math.floor(remainingMs / 1000)) : null;
const remainingMinutes =
remainingTotalSec !== null ? Math.floor(remainingTotalSec / 60) : null;
const remainingSeconds =
remainingTotalSec !== null ? remainingTotalSec % 60 : null;
let sessionToneClass =
"border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
let sessionText = t("ui.dev.session.active", "세션 만료 시간 확인 중");
if (remainingMs === null) {
sessionToneClass = "border-border bg-card text-muted-foreground";
sessionText = t("ui.dev.session.unknown", "세션 만료 시간 확인 불가");
} else if (remainingMs <= 0) {
sessionToneClass =
"border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300";
sessionText = t("ui.dev.session.expired", "세션 만료됨");
} else {
if (
remainingMinutes !== null &&
remainingSeconds !== null &&
remainingMinutes <= 5
) {
sessionToneClass =
"border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300";
sessionText = t(
"ui.dev.session.expiring",
"만료 임박: {{minutes}}분 {{seconds}}초 남음",
{
minutes: remainingMinutes,
seconds: remainingSeconds,
},
);
} else {
sessionText = t(
"ui.dev.session.remaining",
"만료까지 {{minutes}}분 {{seconds}}초",
{
minutes: remainingMinutes ?? 0,
seconds: remainingSeconds ?? 0,
},
);
}
}
const handleRefreshSessionExpiry = async () => {
if (isRefreshingSession) {
return;
}
setIsRefreshingSession(true);
try {
await auth.signinSilent();
setNowMs(Date.now());
setIsProfileMenuOpen(false);
} catch (error) {
console.error("Failed to refresh session expiry:", error);
} finally {
setIsRefreshingSession(false);
}
};
return (
<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 flex flex-col justify-between">
@@ -141,6 +244,68 @@ function AppLayout() {
? t("ui.common.theme_light", "Light")
: t("ui.common.theme_dark", "Dark")}
</button>
<span
className={[
"hidden rounded-full border px-3 py-2 text-xs font-medium md:inline-flex",
sessionToneClass,
].join(" ")}
>
{sessionText}
</span>
<div className="relative" ref={profileMenuRef}>
<button
type="button"
onClick={() => setIsProfileMenuOpen((prev) => !prev)}
className="inline-flex items-center gap-3 rounded-full border border-border bg-card px-3 py-2 transition hover:bg-muted/20"
aria-haspopup="menu"
aria-expanded={isProfileMenuOpen}
aria-label={t("ui.dev.profile.menu_aria", "계정 메뉴 열기")}
>
<div className="grid h-8 w-8 place-items-center rounded-full bg-primary/15 text-xs font-semibold text-primary">
{profileInitial}
</div>
<div className="hidden min-w-0 text-left md:block">
<p className="truncate text-xs font-medium text-foreground">
{profileName}
</p>
<p className="truncate text-[11px] text-muted-foreground">
{profileEmail}
</p>
</div>
</button>
{isProfileMenuOpen ? (
<div
role="menu"
className="absolute right-0 z-30 mt-2 w-72 rounded-xl border border-border bg-card p-3 shadow-xl"
>
<p className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
{t("ui.dev.profile.menu_title", "Account")}
</p>
<div className="mt-2 rounded-lg border border-border px-3 py-2">
<p className="truncate text-sm font-semibold text-foreground">
{profileName}
</p>
<p className="truncate text-xs text-muted-foreground">
{profileEmail}
</p>
</div>
<button
type="button"
role="menuitem"
className="mt-2 w-full rounded-lg border border-border px-3 py-2 text-left text-sm text-foreground transition hover:bg-muted/20 disabled:cursor-not-allowed disabled:opacity-60"
onClick={handleRefreshSessionExpiry}
disabled={isRefreshingSession}
>
{isRefreshingSession
? t(
"ui.dev.session.refreshing",
"세션 만료 시간 갱신 중...",
)
: t("ui.dev.session.refresh", "세션 만료 시간 갱신")}
</button>
</div>
) : null}
</div>
</div>
</div>
</header>

View File

@@ -117,7 +117,7 @@ function ClientConsentsPage() {
to={`/clients/${clientId}`}
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
>
{t("ui.dev.clients.details.tab.connection", "Connection")}
{t("ui.dev.clients.details.tab.connection", "Federation")}
</Link>
<span className="whitespace-nowrap border-b-2 border-primary pb-1 text-primary">
{t("ui.dev.clients.details.tab.consents", "Consent & Users")}

View File

@@ -218,7 +218,7 @@ function ClientDetailsPage() {
to={`/clients/${clientId}`}
className="border-b-2 border-primary pb-3 text-sm font-bold text-primary"
>
{t("ui.dev.clients.details.tab.connection", "Connection")}
{t("ui.dev.clients.details.tab.connection", "Federation")}
</Link>
<Link
to={`/clients/${clientId}/consents`}

View File

@@ -16,7 +16,12 @@ import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import { Textarea } from "../../components/ui/textarea";
import { createClient, fetchClient, updateClient } from "../../lib/devApi";
import {
createClient,
deleteClient,
fetchClient,
updateClient,
} from "../../lib/devApi";
import type {
ClientStatus,
ClientType,
@@ -174,6 +179,39 @@ function ClientGeneralPage() {
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteClient(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["clients"] });
alert(t("msg.dev.clients.deleted", "앱이 삭제되었습니다."));
navigate("/clients");
},
onError: (err) => {
const errorMessage =
(err as AxiosError<{ error?: string }>).response?.data?.error ??
(err as Error)?.message;
alert(
t("msg.dev.clients.delete_error", "삭제 실패: {{error}}", {
error: errorMessage,
}),
);
},
});
const handleDelete = () => {
if (
clientId &&
window.confirm(
t(
"msg.dev.clients.delete_confirm",
"정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
),
)
) {
deleteMutation.mutate(clientId);
}
};
if (!isCreate && isLoading) {
return (
<div className="p-8 text-center">
@@ -220,14 +258,16 @@ function ClientGeneralPage() {
: t("ui.dev.clients.general.title_edit", "Client Settings")}
</h1>
</div>
<Badge
variant={status === "active" ? "success" : "muted"}
className="px-3 py-1 text-xs uppercase"
>
{status === "active"
? t("ui.common.status.active", "Active")
: t("ui.common.status.inactive", "Inactive")}
</Badge>
{!isCreate && (
<Badge
variant={status === "active" ? "success" : "muted"}
className="px-3 py-1 text-xs uppercase"
>
{status === "active"
? t("ui.common.status.active", "Active")
: t("ui.common.status.inactive", "Inactive")}
</Badge>
)}
</div>
<div className="flex gap-6 overflow-x-auto border-b border-border pb-3 text-sm font-bold">
{!isCreate && (
@@ -254,15 +294,49 @@ function ClientGeneralPage() {
{/* 1. Application Identity */}
<div className="glass-panel p-6">
<CardTitle className="text-xl font-bold mb-2">
{t("ui.dev.clients.general.identity.title", "Application Identity")}
</CardTitle>
<CardDescription className="mb-6">
{t(
"msg.dev.clients.general.identity.subtitle",
"앱 이름과 설명, 로고를 설정합니다.",
<div className="flex items-center justify-between mb-6">
<div>
<CardTitle className="text-xl font-bold mb-2">
{t(
"ui.dev.clients.general.identity.title",
"Application Identity",
)}
</CardTitle>
<CardDescription>
{t(
"msg.dev.clients.general.identity.subtitle",
"앱 이름과 설명, 로고를 설정합니다.",
)}
</CardDescription>
</div>
{!isCreate && (
<div className="flex flex-col items-end gap-2">
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{t("ui.dev.clients.table.status", "상태")}
</Label>
<div className="flex items-center gap-3">
<Switch
checked={status === "active"}
onCheckedChange={(checked) =>
setStatus(checked ? "active" : "inactive")
}
/>
<span
className={cn(
"text-sm font-medium",
status === "active"
? "text-emerald-400"
: "text-muted-foreground",
)}
>
{status === "active"
? t("ui.common.status.active", "활성")
: t("ui.common.status.inactive", "비활성")}
</span>
</div>
</div>
)}
</CardDescription>
</div>
<div className="grid gap-8 md:grid-cols-2">
<div className="space-y-5">
<div className="space-y-2">
@@ -519,7 +593,10 @@ function ClientGeneralPage() {
/>
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
<Shield className="h-4 w-4 text-primary" />
{t("ui.dev.clients.general.security.private", "Private")}
{t(
"ui.dev.clients.general.security.private",
"Server side App",
)}
</span>
<span className="text-xs text-muted-foreground">
{t(
@@ -565,43 +642,39 @@ function ClientGeneralPage() {
</CardContent>
</Card>
<div className="flex items-center justify-end gap-3 border-t border-border pt-4">
<Button variant="outline" onClick={() => navigate("/clients")}>
{t("ui.common.cancel", "취소")}
</Button>
<Button
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
className="px-8 shadow-lg shadow-primary/20"
>
{mutation.isPending
? t("msg.common.saving", "저장 중...")
: isCreate
? t("ui.dev.clients.general.create", "클라이언트 생성")
: t("ui.dev.clients.general.save", "설정 저장")}
</Button>
</div>
{!isCreate && (
<div className="glass-panel flex flex-wrap gap-x-12 gap-y-4 p-4 opacity-70">
<div className="space-y-1">
<span className="text-xs font-semibold uppercase text-muted-foreground">
{t("ui.dev.clients.general.footer.client_id", "Client ID")}
</span>
<span className="font-mono text-sm block">{data?.client?.id}</span>
</div>
<div className="space-y-1">
<span className="text-xs font-semibold uppercase text-muted-foreground">
{t("ui.dev.clients.general.footer.created_on", "Created On")}
</span>
<span className="text-sm text-muted-foreground block">
{data?.client?.createdAt
? new Date(data.client.createdAt).toLocaleString()
: "-"}
</span>
</div>
<div className="flex items-center justify-between border-t border-border pt-4">
<div>
{!isCreate && (
<Button
variant="destructive"
className="gap-2"
onClick={handleDelete}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-4 w-4" />
{deleteMutation.isPending
? t("msg.common.requesting", "요청 중...")
: t("ui.common.delete", "삭제")}
</Button>
)}
</div>
)}
<div className="flex items-center gap-3">
<Button variant="outline" onClick={() => navigate("/clients")}>
{t("ui.common.cancel", "취소")}
</Button>
<Button
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
className="px-8 shadow-lg shadow-primary/20"
>
{mutation.isPending
? t("msg.common.saving", "저장 중...")
: isCreate
? t("ui.dev.clients.general.create", "클라이언트 생성")
: t("ui.dev.clients.general.save", "설정 저장")}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
BookOpenText,
@@ -22,10 +22,8 @@ import {
CardHeader,
CardTitle,
} from "../../components/ui/card";
import { CopyButton } from "../../components/ui/copy-button";
import { Input } from "../../components/ui/input";
import { Separator } from "../../components/ui/separator";
import { Switch } from "../../components/ui/switch";
import {
Table,
TableBody,
@@ -34,56 +32,16 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { toast } from "../../components/ui/use-toast";
import {
deleteClient,
fetchClients,
updateClientStatus,
} from "../../lib/devApi";
import { fetchClients } from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
function ClientsPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ["clients"],
queryFn: fetchClients,
});
const updateStatusMutation = useMutation({
mutationFn: (payload: { id: string; status: "active" | "inactive" }) =>
updateClientStatus(payload.id, payload.status),
onSuccess: (_, variables) => {
const statusText =
variables.status === "active"
? t("ui.common.status.active", "활성화")
: t("ui.common.status.inactive", "비활성화");
toast(
t(
"msg.dev.clients.status_updated",
"클라이언트가 {{status}}되었습니다.",
{
status: statusText,
},
),
);
queryClient.invalidateQueries({ queryKey: ["clients"] });
},
onError: (error: AxiosError<{ error?: string }>) => {
const errMsg =
error.response?.data?.error ??
error.message ??
t(
"msg.dev.clients.status_update_error",
"Failed to update client status",
);
toast(errMsg, "error");
},
});
const deleteMutation = useMutation({
mutationFn: (clientId: string) => deleteClient(clientId),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["clients"] }),
});
const clients = data?.items || [];
const totalClients = clients.length;
@@ -159,7 +117,7 @@ function ClientsPage() {
{t("ui.dev.clients.registry.title", "RP registry")}
</p>
<CardTitle className="text-3xl font-black tracking-tight">
{t("ui.dev.clients.registry.subtitle", "Relying Parties")}
{t("ui.dev.clients.registry.subtitle", "연동 앱")}
</CardTitle>
<CardDescription>
{t(
@@ -267,7 +225,10 @@ function ClientsPage() {
{clients.map((client) => (
<TableRow key={client.id} className="bg-card/40">
<TableCell>
<div className="flex items-center gap-3">
<Link
to={`/clients/${client.id}`}
className="flex items-center gap-3 transition-colors hover:text-primary"
>
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
{client.type === "private" ? (
<ServerCog className="h-4 w-4" />
@@ -284,30 +245,13 @@ function ClientsPage() {
{t("ui.dev.clients.tenant_scoped", "Tenant-scoped")}
</p>
</div>
</div>
</Link>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<code className="rounded-md bg-secondary/60 px-2 py-1 font-mono text-xs text-muted-foreground">
{client.id}
</code>
<CopyButton
value={client.id}
variant="ghost"
className="h-8 w-8 text-muted-foreground hover:text-primary"
aria-label={t(
"ui.dev.clients.copy_client_id",
"Copy client id",
)}
onCopy={() =>
toast(
t(
"msg.dev.clients.copy_client_id",
"클라이언트 ID가 복사되었습니다.",
),
)
}
/>
</div>
</TableCell>
<TableCell>
@@ -315,38 +259,19 @@ function ClientsPage() {
variant={client.type === "private" ? "success" : "muted"}
>
{client.type === "private"
? t("ui.dev.clients.type.private", "Private")
? t("ui.dev.clients.type.private", "Server side App")
: t("ui.dev.clients.type.pkce", "PKCE")}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-3">
<Switch
disabled={
updateStatusMutation.isPending &&
updateStatusMutation.variables?.id === client.id
}
checked={client.status === "active"}
onCheckedChange={(checked) =>
updateStatusMutation.mutate({
id: client.id,
status: checked ? "active" : "inactive",
})
}
/>
<span
className={cn(
"text-sm font-medium",
client.status === "active"
? "text-emerald-400"
: "text-muted-foreground",
)}
>
{client.status === "active"
? t("ui.common.status.active", "활성")
: t("ui.common.status.inactive", "비활성")}
</span>
</div>
<Badge
variant={client.status === "active" ? "success" : "muted"}
className="px-3 py-1 text-xs uppercase"
>
{client.status === "active"
? t("ui.common.status.active", "Active")
: t("ui.common.status.inactive", "Inactive")}
</Badge>
</TableCell>
<TableCell className="text-muted-foreground">
{client.createdAt
@@ -357,17 +282,9 @@ function ClientsPage() {
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" size="sm" asChild>
<Link to={`/clients/${client.id}`}>
{t("ui.common.edit", "Edit")}
{t("ui.common.view", "View")}
</Link>
</Button>
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-destructive"
onClick={() => deleteMutation.mutate(client.id)}
>
{t("ui.common.delete", "Delete")}
</Button>
</div>
</TableCell>
</TableRow>

View File

@@ -214,6 +214,9 @@ loading = "Loading apps..."
showing = "Showing {{shown}} of {{total}} apps"
status_update_error = "Failed to update client status"
status_updated = "The app has been {{status}}."
deleted = "App deleted."
delete_error = "Failed to delete: {{error}}"
delete_confirm = "Are you sure you want to delete this app? This action cannot be undone."
[msg.dev.clients.consents]
empty = "No consents found."
@@ -248,20 +251,21 @@ note = "Note"
load_error = "Error loading client: {{error}}"
loading = "Loading client..."
saved = "Saved"
save_error = "Failed to save: {{error}}"
[msg.dev.clients.general.identity]
logo_help = "Logo Help"
subtitle = "Subtitle"
[msg.dev.clients.general.redirect]
help = "Help"
help = "Enter the redirect URIs. You can modify them in the Federation tab after creation."
[msg.dev.clients.general.scopes]
empty = "Empty"
subtitle = "Subtitle"
[msg.dev.clients.general.security]
private_help = "Private App (Server-side): For apps that can safely store a client secret, such as Node.js or Java servers."
private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers."
pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory."
subtitle = "Select application type. Security level determines authentication method."
@@ -315,6 +319,7 @@ approved_device = "Approved Device"
approved_ip = "Approve IP: {{ip}}"
audit_empty = "Audit Empty"
audit_load_error = "Audit Load Error"
render_error = "Dashboard render error: {{error}}"
auth_method = "Auth Method"
client_id = "Client ID: {{id}}"
client_id_missing = "Client Id Missing"
@@ -903,6 +908,7 @@ theme_dark = "Dark"
theme_light = "Light"
theme_toggle = "Theme Toggle"
unknown = "Unknown"
view = "View"
[ui.common.badge]
admin_only = "Admin only"
@@ -979,7 +985,7 @@ user = "User"
[ui.dev.clients.details.breadcrumb]
current = "Current"
section = "Relying Parties"
section = "Applications"
[ui.dev.clients.details.credentials]
client_id = "Client ID"
@@ -1006,7 +1012,7 @@ show = "Show"
title = "Title"
[ui.dev.clients.details.tab]
connection = "Connection"
connection = "Federation"
consents = "Consent & Users"
settings = "Settings"
@@ -1051,7 +1057,7 @@ name = "Scope Name"
delete = "Delete"
[ui.dev.clients.general.security]
private = "Private"
private = "Server Side App"
pkce = "PKCE"
title = "Security Settings"
@@ -1073,7 +1079,7 @@ subtitle = "Tenant admin on-call"
title = "Owner"
[ui.dev.clients.registry]
subtitle = "Relying Parties"
subtitle = "Applications"
title = "RP registry"
[ui.dev.clients.table]
@@ -1085,7 +1091,7 @@ status = "Status"
type = "Type"
[ui.dev.clients.type]
private = "Private"
private = "Server side App"
pkce = "PKCE"
[ui.dev.dashboard]
@@ -1122,6 +1128,12 @@ title = "Stack readiness"
plane = "Dev Plane"
subtitle = "Manage your applications"
[ui.dev.session]
active = "Checking expiration..."
unknown = "Unknown"
expired = "Session expired"
expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
remaining = "Expires in: {{minutes}}m {{seconds}}s"
[ui.userfront]
app_title = "Baron SW Portal"

View File

@@ -214,6 +214,9 @@ loading = "Loading apps..."
showing = "Showing {{shown}} of {{total}} apps"
status_update_error = "Failed to update client status"
status_updated = "앱이 {{status}}되었습니다."
deleted = "앱이 삭제되었습니다."
delete_error = "삭제 실패: {{error}}"
delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
[msg.dev.clients.consents]
empty = "No consents found."
@@ -248,20 +251,21 @@ note = "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행
load_error = "Error loading client: {{error}}"
loading = "Loading client..."
saved = "설정이 저장되었습니다."
save_error = "저장 실패: {{error}}"
[msg.dev.clients.general.identity]
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
subtitle = "앱 이름과 설명, 로고를 설정합니다."
[msg.dev.clients.general.redirect]
help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 Connection 탭에서 수정 가능합니다."
help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다."
[msg.dev.clients.general.scopes]
empty = "등록된 스코프가 없습니다."
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
[msg.dev.clients.general.security]
private_help = "Private 앱 (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
private_help = "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다."
subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
@@ -315,6 +319,7 @@ approved_device = "승인 기기: {{device}}"
approved_ip = "승인 IP: {{ip}}"
audit_empty = "최근 접속 이력이 없습니다."
audit_load_error = "접속이력을 불러오지 못했습니다."
render_error = "대시보드 렌더링 오류: {{error}}"
auth_method = "인증수단: {{method}}"
client_id = "Client ID: {{id}}"
client_id_missing = "Client ID 없음"
@@ -903,6 +908,7 @@ theme_dark = "Dark"
theme_light = "Light"
theme_toggle = "테마 전환"
unknown = "Unknown"
view = "보기"
[ui.common.badge]
admin_only = "Admin only"
@@ -979,7 +985,7 @@ user = "User"
[ui.dev.clients.details.breadcrumb]
current = "연동 앱 상세"
section = "Relying Parties"
section = "연동 앱"
[ui.dev.clients.details.credentials]
client_id = "Client ID"
@@ -1006,7 +1012,7 @@ show = "비밀키 보기"
title = "보안 메모"
[ui.dev.clients.details.tab]
connection = "Connection"
connection = "연동 설정"
consents = "Consent & Users"
settings = "Settings"
@@ -1051,7 +1057,7 @@ name = "Scope Name"
delete = "Delete"
[ui.dev.clients.general.security]
private = "Private"
private = "Server side App"
pkce = "PKCE"
title = "보안 설정"
@@ -1073,7 +1079,7 @@ subtitle = "Tenant admin on-call"
title = "Owner"
[ui.dev.clients.registry]
subtitle = "Relying Parties"
subtitle = "연동 앱"
title = "RP registry"
[ui.dev.clients.table]
@@ -1085,7 +1091,7 @@ status = "상태"
type = "유형"
[ui.dev.clients.type]
private = "Private"
private = "Server side App"
pkce = "PKCE"
[ui.dev.dashboard]
@@ -1122,6 +1128,12 @@ title = "Stack readiness"
plane = "Dev Plane"
subtitle = "Manage your applications"
[ui.dev.session]
active = "만료 시간 확인 중..."
unknown = "확인 불가"
expired = "세션 만료"
expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음"
[ui.userfront]
app_title = "Baron SW 포탈"

View File

@@ -214,6 +214,9 @@ loading = ""
showing = ""
status_update_error = ""
status_updated = ""
deleted = ""
delete_error = ""
delete_confirm = ""
[msg.dev.clients.consents]
empty = ""
@@ -248,6 +251,7 @@ note = ""
load_error = ""
loading = ""
saved = ""
save_error = ""
[msg.dev.clients.general.identity]
logo_help = ""
@@ -315,6 +319,7 @@ approved_device = ""
approved_ip = ""
audit_empty = ""
audit_load_error = ""
render_error = ""
auth_method = ""
client_id = ""
client_id_missing = ""
@@ -915,6 +920,7 @@ theme_dark = ""
theme_light = ""
theme_toggle = ""
unknown = ""
view = ""
[ui.common.badge]
admin_only = ""
@@ -1134,6 +1140,12 @@ title = ""
plane = ""
subtitle = ""
[ui.dev.session]
active = ""
unknown = ""
expired = ""
expiring = ""
remaining = ""
[ui.userfront]
app_title = ""

View File

@@ -92,3 +92,7 @@ oidc:
salt: youReallyNeedToChangeThis
dynamic_client_registration:
enabled: true
ttl:
access_token: 15m
id_token: 15m

View File

@@ -258,6 +258,7 @@ error = "Error"
loading = "Loading..."
no_description = "No Description."
saving = "Saving..."
requesting = "Requesting..."
unknown_error = "unknown error"
[msg.dev]
@@ -270,6 +271,9 @@ loading = "Loading apps..."
showing = "Showing {{shown}} of {{total}} apps"
status_update_error = "Failed to update client status"
status_updated = "The app has been {{status}}."
deleted = "App deleted."
delete_error = "Failed to delete: {{error}}"
delete_confirm = "Are you sure you want to delete this app? This action cannot be undone."
[msg.dev.clients.consents]
empty = "No consents found."
@@ -311,14 +315,14 @@ logo_help = "Logo Help"
subtitle = "Subtitle"
[msg.dev.clients.general.redirect]
help = "Help"
help = "Enter the redirect URIs. You can modify them in the Federation tab after creation."
[msg.dev.clients.general.scopes]
empty = "Empty"
subtitle = "Subtitle"
[msg.dev.clients.general.security]
private_help = "Private App (Server-side): For apps that can safely store a client secret, such as Node.js or Java servers."
private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers."
pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory."
subtitle = "Select application type. Security level determines authentication method."
@@ -1047,6 +1051,7 @@ theme_dark = "Dark"
theme_light = "Light"
theme_toggle = "Theme Toggle"
unknown = "Unknown"
view = "View"
[ui.common.badge]
admin_only = "Admin only"
@@ -1076,6 +1081,12 @@ scope_badge = "Scoped to /dev"
clients = "Connected Application"
logout = "Logout"
[ui.dev.profile]
menu_aria = "Open account menu"
menu_title = "Account"
unknown_email = "unknown@example.com"
unknown_name = "Unknown User"
[ui.dev.clients]
copy_client_id = "Copy client id"
new = "Add Connected Application"
@@ -1123,7 +1134,7 @@ user = "User"
[ui.dev.clients.details.breadcrumb]
current = "Current"
section = "Relying Parties"
section = "Applications"
[ui.dev.clients.details.credentials]
client_id = "Client ID"
@@ -1150,7 +1161,7 @@ show = "Show"
title = "Title"
[ui.dev.clients.details.tab]
connection = "Connection"
connection = "Federation"
consents = "Consent & Users"
settings = "Settings"
@@ -1195,7 +1206,7 @@ name = "Scope Name"
delete = "Delete"
[ui.dev.clients.general.security]
private = "Private"
private = "Server Side App"
pkce = "PKCE"
title = "Security Settings"
@@ -1217,7 +1228,7 @@ subtitle = "Tenant admin on-call"
title = "Owner"
[ui.dev.clients.registry]
subtitle = "Relying Parties"
subtitle = "Applications"
title = "RP registry"
[ui.dev.clients.table]
@@ -1229,7 +1240,7 @@ status = "Status"
type = "Type"
[ui.dev.clients.type]
private = "Private"
private = "Server side App"
pkce = "PKCE"
[ui.dev.dashboard]
@@ -1266,6 +1277,15 @@ title = "Stack readiness"
plane = "Dev Plane"
subtitle = "Manage your applications"
[ui.dev.session]
active = "Checking expiration..."
unknown = "Unknown"
expired = "Session expired"
expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
remaining = "Expires in: {{minutes}}m {{seconds}}s"
refresh = "Refresh session expiry"
refreshing = "Refreshing session expiry..."
[ui.userfront]
app_title = "Baron SW Portal"

View File

@@ -258,6 +258,7 @@ error = "오류가 발생했습니다."
loading = "로딩 중..."
no_description = "설명이 없습니다."
saving = "저장 중..."
requesting = "요청 중..."
unknown_error = "unknown error"
[msg.dev]
@@ -270,6 +271,9 @@ loading = "Loading apps..."
showing = "Showing {{shown}} of {{total}} apps"
status_update_error = "Failed to update client status"
status_updated = "앱이 {{status}}되었습니다."
deleted = "앱이 삭제되었습니다."
delete_error = "삭제 실패: {{error}}"
delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
[msg.dev.clients.consents]
empty = "No consents found."
@@ -311,14 +315,14 @@ logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
subtitle = "앱 이름과 설명, 로고를 설정합니다."
[msg.dev.clients.general.redirect]
help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 Connection 탭에서 수정 가능합니다."
help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다."
[msg.dev.clients.general.scopes]
empty = "등록된 스코프가 없습니다."
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
[msg.dev.clients.general.security]
private_help = "Private 앱 (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
private_help = "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다."
subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
@@ -1047,6 +1051,7 @@ theme_dark = "Dark"
theme_light = "Light"
theme_toggle = "테마 전환"
unknown = "Unknown"
view = "보기"
[ui.common.badge]
admin_only = "Admin only"
@@ -1076,6 +1081,12 @@ scope_badge = "Scoped to /dev"
clients = "연동 앱"
logout = "로그아웃"
[ui.dev.profile]
menu_aria = "계정 메뉴 열기"
menu_title = "계정"
unknown_email = "unknown@example.com"
unknown_name = "Unknown User"
[ui.dev.clients]
copy_client_id = "Copy client id"
new = "연동 앱 추가"
@@ -1123,7 +1134,7 @@ user = "User"
[ui.dev.clients.details.breadcrumb]
current = "연동 앱 상세"
section = "Relying Parties"
section = "연동 앱"
[ui.dev.clients.details.credentials]
client_id = "Client ID"
@@ -1150,7 +1161,7 @@ show = "비밀키 보기"
title = "보안 메모"
[ui.dev.clients.details.tab]
connection = "Connection"
connection = "연동 설정"
consents = "Consent & Users"
settings = "Settings"
@@ -1195,7 +1206,7 @@ name = "Scope Name"
delete = "Delete"
[ui.dev.clients.general.security]
private = "Private"
private = "Server side App"
pkce = "PKCE"
title = "보안 설정"
@@ -1217,7 +1228,7 @@ subtitle = "Tenant admin on-call"
title = "Owner"
[ui.dev.clients.registry]
subtitle = "Relying Parties"
subtitle = "연동 앱"
title = "RP registry"
[ui.dev.clients.table]
@@ -1229,7 +1240,7 @@ status = "상태"
type = "유형"
[ui.dev.clients.type]
private = "Private"
private = "Server side App"
pkce = "PKCE"
[ui.dev.dashboard]
@@ -1266,6 +1277,15 @@ title = "Stack readiness"
plane = "Dev Plane"
subtitle = "Manage your applications"
[ui.dev.session]
active = "만료 시간 확인 중..."
unknown = "확인 불가"
expired = "세션 만료"
expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음"
refresh = "세션 만료 시간 갱신"
refreshing = "세션 만료 시간 갱신 중..."
[ui.userfront]
app_title = "Baron SW 포탈"

View File

@@ -18,24 +18,6 @@ saman = ""
[err.common]
unknown = ""
[err.backend]
authorization_pending = ""
bad_request = ""
conflict = ""
expired_token = ""
forbidden = ""
internal_error = ""
invalid_code = ""
invalid_or_expired_code = ""
invalid_session = ""
invalid_session_reference = ""
not_found = ""
not_supported = ""
password_or_email_mismatch = ""
rate_limited = ""
service_unavailable = ""
slow_down = ""
[err.userfront]
[err.userfront.auth_proxy]
@@ -220,18 +202,19 @@ count = ""
[msg.common]
loading = ""
saving = ""
requesting = ""
unknown_error = ""
[msg.dev]
logout_confirm = ""
[msg.dev.clients]
copy_client_id = ""
load_error = ""
loading = ""
showing = ""
status_update_error = ""
status_updated = ""
deleted = ""
delete_error = ""
delete_confirm = ""
[msg.dev.clients.consents]
empty = ""
@@ -427,7 +410,6 @@ token_missing = ""
verification_failed = ""
[msg.userfront.login.link]
approved = ""
helper = ""
missing_login_id = ""
missing_phone = ""
@@ -490,8 +472,6 @@ organization = ""
security = ""
[msg.userfront.qr]
approve_error = ""
approve_success = ""
camera_error = ""
permission_error = ""
permission_required = ""
@@ -912,6 +892,7 @@ create = ""
delete = ""
details = ""
edit = ""
view = ""
hyphen = ""
na = ""
never = ""
@@ -922,7 +903,6 @@ previous = ""
qr = ""
read_only = ""
refresh = ""
requesting = ""
resend = ""
retry = ""
save = ""
@@ -964,8 +944,13 @@ scope_badge = ""
clients = ""
logout = ""
[ui.dev.profile]
menu_aria = ""
menu_title = ""
unknown_email = ""
unknown_name = ""
[ui.dev.clients]
copy_client_id = ""
new = ""
search_placeholder = ""
tenant_scoped = ""
@@ -1052,10 +1037,6 @@ title_edit = ""
[ui.dev.clients.general.breadcrumb]
section = ""
[ui.dev.clients.general.footer]
client_id = ""
created_on = ""
[ui.dev.clients.general.identity]
description = ""
description_placeholder = ""
@@ -1154,6 +1135,14 @@ title = ""
plane = ""
subtitle = ""
[ui.dev.session]
active = ""
unknown = ""
expired = ""
expiring = ""
remaining = ""
refresh = ""
refreshing = ""
[ui.userfront]
app_title = ""
@@ -1230,12 +1219,9 @@ login_id = ""
password = ""
[ui.userfront.login.link]
action_label = ""
code_only = ""
page_title = ""
resend_with_time = ""
send = ""
title = ""
[ui.userfront.login.qr]
expired = ""
@@ -1305,9 +1291,7 @@ organization = ""
security = ""
[ui.userfront.qr]
request_permission = ""
rescan = ""
result_failure = ""
result_success = ""
title = ""
@@ -1384,8 +1368,6 @@ delete_confirm = ""
delete_error = ""
delete_success = ""
empty = ""
import_error = ""
import_success = ""
loading = ""
[msg.admin.groups.members]
@@ -1419,7 +1401,6 @@ no_description = ""
[ui.admin.groups]
add_unit = ""
import_csv = ""
[ui.admin.groups.create]
description = ""
@@ -1439,10 +1420,6 @@ parent_none = ""
unit_level_label = ""
unit_level_placeholder = ""
[ui.admin.groups.table]
created_at = ""
level = ""
[ui.admin.tenants.admins]
add_button = ""
already_admin = ""

View File

@@ -14,6 +14,7 @@ t("ui.admin.nav.audit_logs");
t("ui.admin.nav.auth_guard");
t("ui.admin.nav.logout");
t("ui.admin.nav.relying_parties");
t("ui.dev.nav.clients");
// Common & Info
t("err.common.unknown");

View File

@@ -334,6 +334,7 @@ theme_dark = "Dark"
theme_light = "Light"
theme_toggle = "Theme Toggle"
unknown = "Unknown"
view = "View"
[ui.common.badge]
admin_only = "Admin only"

View File

@@ -334,6 +334,7 @@ theme_dark = "Dark"
theme_light = "Light"
theme_toggle = "테마 전환"
unknown = "Unknown"
view = "보기"
[ui.common.badge]
admin_only = "Admin only"

View File

@@ -131,7 +131,6 @@ token_missing = ""
verification_failed = ""
[msg.userfront.login.link]
approved = ""
helper = ""
missing_login_id = ""
missing_phone = ""
@@ -194,8 +193,6 @@ organization = ""
security = ""
[msg.userfront.qr]
approve_error = ""
approve_success = ""
camera_error = ""
permission_error = ""
permission_required = ""
@@ -306,6 +303,7 @@ create = ""
delete = ""
details = ""
edit = ""
view = ""
hyphen = ""
na = ""
never = ""
@@ -316,7 +314,6 @@ previous = ""
qr = ""
read_only = ""
refresh = ""
requesting = ""
resend = ""
retry = ""
save = ""
@@ -423,12 +420,9 @@ login_id = ""
password = ""
[ui.userfront.login.link]
action_label = ""
code_only = ""
page_title = ""
resend_with_time = ""
send = ""
title = ""
[ui.userfront.login.qr]
expired = ""
@@ -498,9 +492,7 @@ organization = ""
security = ""
[ui.userfront.qr]
request_permission = ""
rescan = ""
result_failure = ""
result_success = ""
title = ""

View File

@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
cli_config:
dependency: transitive
description:
@@ -268,14 +268,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
leak_tracker:
dependency: transitive
description:
@@ -328,18 +320,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
@@ -653,26 +645,26 @@ packages:
dependency: transitive
description:
name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
url: "https://pub.dev"
source: hosted
version: "1.26.3"
version: "1.29.0"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.9"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
url: "https://pub.dev"
source: hosted
version: "0.6.12"
version: "0.6.15"
toml:
dependency: "direct main"
description: