diff --git a/.gitignore b/.gitignore index 9c6d4a32..ba9819d2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.log *.out *.exe +.npm-cache/ reports reports/* diff --git a/adminfront/playwright-report/index.html b/adminfront/playwright-report/index.html deleted file mode 100644 index a369469d..00000000 --- a/adminfront/playwright-report/index.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - Playwright Test Report - - - - -
- - - \ No newline at end of file diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx index e0a1d7bb..9e798031 100644 --- a/devfront/src/components/layout/AppLayout.tsx +++ b/devfront/src/components/layout/AppLayout.tsx @@ -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(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 (
diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index 7bacc080..e82277ca 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -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")} {t("ui.dev.clients.details.tab.consents", "Consent & Users")} diff --git a/devfront/src/features/clients/ClientDetailsPage.tsx b/devfront/src/features/clients/ClientDetailsPage.tsx index 1882c04a..a372fef0 100644 --- a/devfront/src/features/clients/ClientDetailsPage.tsx +++ b/devfront/src/features/clients/ClientDetailsPage.tsx @@ -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")} 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 (
@@ -220,14 +258,16 @@ function ClientGeneralPage() { : t("ui.dev.clients.general.title_edit", "Client Settings")}
- - {status === "active" - ? t("ui.common.status.active", "Active") - : t("ui.common.status.inactive", "Inactive")} - + {!isCreate && ( + + {status === "active" + ? t("ui.common.status.active", "Active") + : t("ui.common.status.inactive", "Inactive")} + + )}
{!isCreate && ( @@ -254,15 +294,49 @@ function ClientGeneralPage() { {/* 1. Application Identity */}
- - {t("ui.dev.clients.general.identity.title", "Application Identity")} - - - {t( - "msg.dev.clients.general.identity.subtitle", - "앱 이름과 설명, 로고를 설정합니다.", +
+
+ + {t( + "ui.dev.clients.general.identity.title", + "Application Identity", + )} + + + {t( + "msg.dev.clients.general.identity.subtitle", + "앱 이름과 설명, 로고를 설정합니다.", + )} + +
+ {!isCreate && ( +
+ +
+ + setStatus(checked ? "active" : "inactive") + } + /> + + {status === "active" + ? t("ui.common.status.active", "활성") + : t("ui.common.status.inactive", "비활성")} + +
+
)} - +
@@ -519,7 +593,10 @@ function ClientGeneralPage() { /> - {t("ui.dev.clients.general.security.private", "Private")} + {t( + "ui.dev.clients.general.security.private", + "Server side App", + )} {t( @@ -565,43 +642,39 @@ function ClientGeneralPage() { -
- - -
- - {!isCreate && ( -
-
- - {t("ui.dev.clients.general.footer.client_id", "Client ID")} - - {data?.client?.id} -
-
- - {t("ui.dev.clients.general.footer.created_on", "Created On")} - - - {data?.client?.createdAt - ? new Date(data.client.createdAt).toLocaleString() - : "-"} - -
+
+
+ {!isCreate && ( + + )}
- )} +
+ + +
+
); } diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 7cd7ad2a..9d9def58 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -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")}

- {t("ui.dev.clients.registry.subtitle", "Relying Parties")} + {t("ui.dev.clients.registry.subtitle", "연동 앱")} {t( @@ -267,7 +225,10 @@ function ClientsPage() { {clients.map((client) => ( -
+
{client.type === "private" ? ( @@ -284,30 +245,13 @@ function ClientsPage() { {t("ui.dev.clients.tenant_scoped", "Tenant-scoped")}

-
+
{client.id} - - toast( - t( - "msg.dev.clients.copy_client_id", - "클라이언트 ID가 복사되었습니다.", - ), - ) - } - />
@@ -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")} -
- - updateStatusMutation.mutate({ - id: client.id, - status: checked ? "active" : "inactive", - }) - } - /> - - {client.status === "active" - ? t("ui.common.status.active", "활성") - : t("ui.common.status.inactive", "비활성")} - -
+ + {client.status === "active" + ? t("ui.common.status.active", "Active") + : t("ui.common.status.inactive", "Inactive")} +
{client.createdAt @@ -357,17 +282,9 @@ function ClientsPage() {
-
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index 79dbcf4f..e01dd020 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -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" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index a8392019..ef85680a 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -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 포탈" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index fb270793..5c2934c5 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -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 = "" diff --git a/docker/ory/hydra/hydra.yml b/docker/ory/hydra/hydra.yml index 82f9fb37..7bf79b10 100644 --- a/docker/ory/hydra/hydra.yml +++ b/docker/ory/hydra/hydra.yml @@ -92,3 +92,7 @@ oidc: salt: youReallyNeedToChangeThis dynamic_client_registration: enabled: true + +ttl: + access_token: 15m + id_token: 15m diff --git a/locales/en.toml b/locales/en.toml index 1781fe1c..19966500 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -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" diff --git a/locales/ko.toml b/locales/ko.toml index 3d5e6d9a..977a9b07 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -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 포탈" diff --git a/locales/template.toml b/locales/template.toml index 4243afdb..55567ffb 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -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 = "" diff --git a/tools/i18n-scanner/manual-keys.ts b/tools/i18n-scanner/manual-keys.ts index ee374a68..f6dff763 100644 --- a/tools/i18n-scanner/manual-keys.ts +++ b/tools/i18n-scanner/manual-keys.ts @@ -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"); diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index c8e88505..1acbecba 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -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" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 0ec335ff..6442beeb 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -334,6 +334,7 @@ theme_dark = "Dark" theme_light = "Light" theme_toggle = "테마 전환" unknown = "Unknown" +view = "보기" [ui.common.badge] admin_only = "Admin only" diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index d1f5404d..3911cf30 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -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 = "" diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock index fecd33f1..ae003ec3 100644 --- a/userfront/pubspec.lock +++ b/userfront/pubspec.lock @@ -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: