-
- {t("ui.dev.profile.title", "내 정보")}
-
+
+
+
+
+
+ {t("ui.dev.profile.title", "내 정보")}
+
+
{t(
"ui.dev.profile.subtitle",
diff --git a/devfront/src/lib/apiClient.ts b/devfront/src/lib/apiClient.ts
index f90eb970..71d1959c 100644
--- a/devfront/src/lib/apiClient.ts
+++ b/devfront/src/lib/apiClient.ts
@@ -1,5 +1,8 @@
import axios from "axios";
import { shouldStartLoginRedirect } from "../../../common/core/auth";
+import {
+ shouldSuppressDevelopmentSessionRedirect,
+} from "../../../common/core/session";
import { userManager } from "./auth";
let isRedirectingToLogin = false;
@@ -42,8 +45,22 @@ apiClient.interceptors.response.use(
message.includes("invalid session") ||
message.includes("token is not active")));
+ if (!shouldRedirectToLogin) {
+ return Promise.reject(error);
+ }
+
+ if (
+ shouldSuppressDevelopmentSessionRedirect({
+ appMode: import.meta.env.MODE,
+ })
+ ) {
+ console.warn(
+ "[apiClient] Auth failure detected, but development session redirects are disabled.",
+ );
+ return Promise.reject(error);
+ }
+
if (
- shouldRedirectToLogin &&
shouldStartLoginRedirect({
pathname: window.location.pathname,
isRedirecting: isRedirectingToLogin,
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml
index 47c470d7..79f5d67f 100644
--- a/devfront/src/locales/en.toml
+++ b/devfront/src/locales/en.toml
@@ -313,6 +313,7 @@ forbidden = "You do not have permission to view audit logs. Please request acces
load_error = "Error loading audit logs: {{error}}"
loaded_count = "Loaded {{count}} rows"
loading = "Loading audit logs..."
+registry_description = "Filter recent audit logs by search criteria and review action history quickly."
subtitle = "View developer activity history within the current app scope and review target-specific changes."
[msg.dev.request]
@@ -320,6 +321,7 @@ admin_desc = "Manage developer access requests submitted by users."
approved = "Approved."
cancelled = "Approval has been cancelled."
empty = "No requests found."
+list.approved_count = "{{count}} users have been approved."
need_cancel_notes = "Please enter a reason for cancelling approval."
need_notes = "Please enter a rejection reason."
rejected = "Rejected."
@@ -339,6 +341,9 @@ empty = "No RPs are available."
empty_detail = "RPs will appear here when a relationship is assigned to your account."
empty_can_create = "No linked apps have been registered yet."
empty_can_create_detail = "Create a new RP with the Add linked app button, and it will appear here."
+create_requires_request = "You do not have permission to create applications.\nSubmit a developer access request and wait for approval."
+create_pending_detail = "Your developer access request is under review. You will be able to add applications after approval."
+create_forbidden_detail = "You do not have permission to create applications. Ask an administrator to grant developer access or the appropriate RP permissions."
empty_filtered = "No linked apps match the current filters."
empty_filtered_detail = "Try changing the search text or filters."
empty_pending = "Your developer access request is under review."
@@ -1338,6 +1343,22 @@ menu_title = "Account"
unknown_email = "unknown@example.com"
unknown_name = "Unknown User"
+[ui.shell.profile]
+menu_aria = "Open account menu"
+menu_title = "Account"
+unknown_email = "unknown@example.com"
+unknown_name = "Unknown User"
+
+[ui.shell.nav]
+logout = "Logout"
+profile = "My Profile"
+
+[ui.shell.role]
+rp_admin = "Service Administrator (RP Admin)"
+super_admin = "System Administrator (Super Admin)"
+tenant_admin = "Tenant Administrator (Tenant Admin)"
+user = "General User (Tenant Member)"
+
[ui.dev.clients]
new = "Add Connected Application"
search_placeholder = "Search by app name or ID..."
@@ -1728,6 +1749,15 @@ expired = "Session expired"
expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
remaining = "Expires in {{minutes}}m {{seconds}}s"
+[ui.shell.session]
+auto_extend = "Session expiry"
+active = "Session active"
+disabled = "Session expiry disabled"
+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 31638c6d..accd5310 100644
--- a/devfront/src/locales/ko.toml
+++ b/devfront/src/locales/ko.toml
@@ -313,6 +313,7 @@ forbidden = "감사 로그를 조회할 권한이 없습니다. 관리자에게
load_error = "감사 로그 조회 실패: {{error}}"
loaded_count = "로드된 로그 {{count}}건"
loading = "감사 로그를 불러오는 중..."
+registry_description = "최근 감사 로그를 검색 조건에 맞춰 필터링하고, 작업 이력을 빠르게 확인합니다."
subtitle = "현재 앱 범위에서 개발자 작업 이력을 조회하고 대상별 변경 내역을 확인합니다."
[msg.dev.request]
@@ -320,6 +321,7 @@ admin_desc = "사용자들의 개발자 권한 신청 내역을 관리합니다.
approved = "승인되었습니다."
cancelled = "승인이 취소되었습니다."
empty = "신청 내역이 없습니다."
+list.approved_count = "총 {{count}}명의 사용자가 승인되었습니다."
need_cancel_notes = "승인 취소 사유를 입력해주세요."
need_notes = "반려 사유를 입력해주세요."
rejected = "반려되었습니다."
@@ -336,6 +338,9 @@ empty = "조회 가능한 RP가 없습니다."
empty_detail = "RP 관계가 부여되면 이 목록에 해당 RP가 표시됩니다."
empty_can_create = "아직 등록된 연동 앱이 없습니다."
empty_can_create_detail = "연동 앱 추가 버튼으로 새 RP를 생성하면 이 목록에 표시됩니다."
+create_requires_request = "연동 앱을 생성할 권한이 없습니다.\n개발자 권한 신청을 요청한 뒤 승인 받아주세요."
+create_pending_detail = "개발자 권한 신청을 검토 중입니다. 승인되면 연동 앱을 추가할 수 있습니다."
+create_forbidden_detail = "연동 앱을 생성할 권한이 없습니다. 관리자에게 개발자 권한 또는 적절한 RP 권한 부여를 요청해 주세요."
empty_filtered = "조건에 맞는 연동 앱이 없습니다."
empty_filtered_detail = "검색어나 필터 조건을 변경해 보세요."
empty_pending = "개발자 권한 신청을 검토 중입니다."
@@ -1338,6 +1343,22 @@ menu_title = "계정"
unknown_email = "unknown@example.com"
unknown_name = "Unknown User"
+[ui.shell.profile]
+menu_aria = "계정 메뉴 열기"
+menu_title = "계정"
+unknown_email = "unknown@example.com"
+unknown_name = "Unknown User"
+
+[ui.shell.nav]
+logout = "Logout"
+profile = "내 정보"
+
+[ui.shell.role]
+rp_admin = "서비스 관리자 (RP Admin)"
+super_admin = "시스템 관리자 (Super Admin)"
+tenant_admin = "테넌트 관리자 (Tenant Admin)"
+user = "일반 사용자 (Tenant Member)"
+
[ui.dev.clients]
new = "연동 앱 추가"
search_placeholder = "연동 앱 이름/ID로 검색..."
@@ -1727,6 +1748,15 @@ expired = "세션 만료"
expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음"
+[ui.shell.session]
+auto_extend = "세션 만료 관리"
+active = "세션 활성"
+disabled = "세션 만료 비활성화"
+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 5be16834..a0dbec89 100644
--- a/devfront/src/locales/template.toml
+++ b/devfront/src/locales/template.toml
@@ -327,6 +327,7 @@ forbidden = ""
load_error = ""
loaded_count = ""
loading = ""
+registry_description = ""
subtitle = ""
[msg.dev.request]
@@ -334,6 +335,7 @@ admin_desc = ""
approved = ""
cancelled = ""
empty = ""
+list.approved_count = ""
need_cancel_notes = ""
need_notes = ""
rejected = ""
@@ -377,6 +379,9 @@ empty = ""
empty_detail = ""
empty_can_create = ""
empty_can_create_detail = ""
+create_requires_request = ""
+create_pending_detail = ""
+create_forbidden_detail = ""
empty_filtered = ""
empty_filtered_detail = ""
empty_pending = ""
@@ -1394,6 +1399,22 @@ menu_title = ""
unknown_email = ""
unknown_name = ""
+[ui.shell.profile]
+menu_aria = ""
+menu_title = ""
+unknown_email = ""
+unknown_name = ""
+
+[ui.shell.nav]
+logout = ""
+profile = ""
+
+[ui.shell.role]
+rp_admin = ""
+super_admin = ""
+tenant_admin = ""
+user = ""
+
[ui.dev.clients]
new = ""
search_placeholder = ""
@@ -1784,6 +1805,15 @@ expired = ""
expiring = ""
remaining = ""
+[ui.shell.session]
+auto_extend = ""
+active = ""
+disabled = ""
+unknown = ""
+expired = ""
+expiring = ""
+remaining = ""
+
[ui.userfront]
app_title = ""
diff --git a/docker-compose.yaml b/docker-compose.yaml
index a53421f4..a36785ea 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -9,6 +9,8 @@ services:
environment:
- APP_ENV=${APP_ENV:-development}
- GO_ENV=${APP_ENV:-development}
+ - BACKEND_LOG_LEVEL=${BACKEND_LOG_LEVEL:-info}
+ - CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}
- COOKIE_SECRET=${COOKIE_SECRET}
- JWT_SECRET=${JWT_SECRET}
- NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY}
@@ -58,6 +60,7 @@ services:
- APP_ENV=${APP_ENV:-development}
- API_PROXY_TARGET=http://baron_backend:3000
- USERFRONT_URL=${USERFRONT_URL}
+ - VITE_CLIENT_LOG_DEBUG=${VITE_CLIENT_LOG_DEBUG:-false}
ports:
- "${ADMINFRONT_PORT:-5173}:5173"
volumes:
@@ -82,6 +85,7 @@ services:
- APP_ENV=${APP_ENV:-development}
- API_PROXY_TARGET=http://baron_backend:3000
- USERFRONT_URL=${USERFRONT_URL}
+ - VITE_CLIENT_LOG_DEBUG=${VITE_CLIENT_LOG_DEBUG:-false}
ports:
- "${DEVFRONT_PORT:-5174}:5173"
volumes:
@@ -106,6 +110,7 @@ services:
- APP_ENV=${APP_ENV:-development}
- API_PROXY_TARGET=http://baron_backend:3000
- USERFRONT_URL=${USERFRONT_URL}
+ - VITE_CLIENT_LOG_DEBUG=${VITE_CLIENT_LOG_DEBUG:-false}
ports:
- "${ORGFRONT_PORT:-5175}:5175"
volumes:
@@ -124,6 +129,7 @@ services:
build:
context: .
dockerfile: userfront/Dockerfile
+ target: ${USERFRONT_BUILD_TARGET:-dev}
container_name: baron_userfront
env_file:
- .env
@@ -131,19 +137,22 @@ services:
- BACKEND_URL=${BACKEND_URL:-}
- USERFRONT_URL=${USERFRONT_URL}
- APP_ENV=${APP_ENV}
+ - CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}
+ - USERFRONT_INTERNAL_PORT=5000
+ - USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-}
+ volumes:
+ - ./userfront/lib:/workspace/userfront/lib
+ - ./userfront/assets:/workspace/userfront/assets
+ - ./userfront/web:/workspace/userfront/web
+ - ./userfront/scripts:/workspace/userfront/scripts:ro
+ - ./scripts:/workspace/scripts:ro
+ - ./locales:/workspace/locales:ro
networks:
- baron_net
- ory-net
depends_on:
backend:
condition: service_healthy
- command: >
- /bin/sh -c "mkdir -p /usr/share/nginx/html/assets &&
- echo \"BACKEND_URL=$${BACKEND_URL}\" >> /usr/share/nginx/html/assets/.env &&
- echo \"USERFRONT_URL=$${USERFRONT_URL}\" >> /usr/share/nginx/html/assets/.env &&
- echo \"APP_ENV=$${APP_ENV}\" >> /usr/share/nginx/html/assets/.env &&
- cp /usr/share/nginx/html/assets/.env /usr/share/nginx/html/.env &&
- nginx -g 'daemon off;'"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"]
interval: 10s
diff --git a/gateway/nginx.conf b/gateway/nginx.conf
index 2bc97d70..f708bc63 100644
--- a/gateway/nginx.conf
+++ b/gateway/nginx.conf
@@ -3,6 +3,11 @@ map $time_iso8601 $time_custom {
"~^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})" "$1-$2-$3 $4:$5:$6";
}
+map $http_upgrade $connection_upgrade {
+ default upgrade;
+ '' close;
+}
+
# Go slog 포맷과 맞춘 JSON 액세스 로그
log_format json_combined escape=json
'{'
@@ -100,9 +105,12 @@ server {
# --- UserFront 정적 파일 프록시 ---
location / {
proxy_pass $userfront_upstream;
+ proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
}
}
diff --git a/locales/en.toml b/locales/en.toml
index c3cc2776..09e18f0a 100644
--- a/locales/en.toml
+++ b/locales/en.toml
@@ -114,6 +114,7 @@ empty = "No filters applied."
[msg.admin.audit.registry]
count = "{{count}} logs loaded."
+description = "Filter recent audit logs by search criteria and review action history quickly."
[msg.admin.common]
forbidden = "You do not have permission to perform this action."
@@ -386,6 +387,7 @@ forbidden = "You do not have permission to view audit logs. Please request acces
load_error = "Error loading audit logs: {{error}}"
loaded_count = "Loaded {{count}} rows"
loading = "Loading audit logs..."
+registry_description = "Filter recent audit logs by search criteria and review action history quickly."
subtitle = "Shows DevFront activity history within current tenant/app scope."
[msg.dev.request]
@@ -424,6 +426,7 @@ status = "Status"
user = "User"
[msg.dev.request.list]
+approved_count = "{{count}} users have been approved."
title = "Request History"
[msg.dev.request.admin]
@@ -802,6 +805,7 @@ body = "We could not find an account for that information.\\\\\\\\\\\\\\\\nPleas
[msg.userfront.login.verification]
approved = "Approved. Complete sign-in in the original window."
approved_local = "Approved. This device is already signed in, and the remote window will be signed in shortly."
+approved_remote = "Approved. Please return to the original browser or PC screen."
success = "Sign-in approval completed."
[msg.userfront.login_success]
@@ -2522,8 +2526,29 @@ title = "Account not found"
[ui.userfront.login.verification]
action_label = "Done"
+action_label_close = "Close Window"
page_title = "Sign-in approval"
title = "Approval complete"
+title_remote = "Sign-in approved"
+
+[ui.shell.nav]
+logout = "Logout"
+profile = "My Profile"
+
+[ui.shell.profile]
+menu_aria = "Open account menu"
+menu_title = "Account"
+unknown_email = "unknown@example.com"
+unknown_name = "Unknown User"
+
+[ui.shell.session]
+active = "Session active"
+auto_extend = "Session expiry"
+disabled = "Session expiry disabled"
+expired = "Session expired"
+expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
+remaining = "Expires in {{minutes}}m {{seconds}}s"
+unknown = "Unknown"
[ui.userfront.login_success]
later = "Do this later (go to dashboard)"
@@ -2642,6 +2667,15 @@ toggle_label = "Show active sessions only"
[msg.userfront.audit.filter]
description = "Toggle to view only active sessions."
+[msg.admin.integrity]
+subtitle = "Review integrity status and inspect checks across the admin data model."
+
+[msg.admin.integrity.section.tenant_integrity]
+description = "Check duplicate tenant slugs and invalid parent relationships."
+
+[msg.admin.integrity.section.user_integrity]
+description = "Check orphan records in user and login ID references."
+
[msg.admin.integrity.forbidden]
description = "This screen is available only to super_admin."
@@ -2663,10 +2697,37 @@ success = "Check completed."
[msg.admin.integrity.report]
load_error = "Failed to load the integrity report."
+[msg.admin.integrity.check.duplicate_tenant_slugs]
+description = "Checks duplicate active tenant slugs using LOWER(TRIM(slug))."
+
+[msg.admin.integrity.check.orphan_tenant_parents]
+description = "Checks whether tenants.parent_id points to a missing or soft-deleted tenant."
+
+[msg.admin.integrity.check.orphan_user_login_id_tenants]
+description = "Checks whether user_login_ids.tenant_id points to a missing or soft-deleted tenant."
+
+[msg.admin.integrity.check.orphan_user_login_id_users]
+description = "Checks whether user_login_ids.user_id points to a missing or soft-deleted user."
+
+[msg.admin.integrity.check.orphan_user_tenant_memberships]
+description = "Checks whether users.tenant_id points to a missing or soft-deleted tenant."
+
+[msg.admin.user_projection]
+action_error = "Projection operation failed."
+action_success = "Refreshed the projection for {{count}} users."
+forbidden_description = "This screen is only available to super_admin users."
+load_error = "Failed to load projection status."
+reset_confirm = "Rebuild user projection from the Kratos source of truth?"
+subtitle = "Review and sync the Kratos user read model."
+
+[msg.admin.user_projection.forbidden]
+description = "This screen is only available to super_admin users."
+
[ui.admin.integrity]
fetch_error = "Unable to load the final integrity check result."
kicker = "System"
loading = "Loading data integrity report..."
+subtitle = "Review integrity status and inspect checks across the admin data model."
title = "Data Integrity Check"
[ui.admin.integrity.forbidden]
@@ -2715,6 +2776,21 @@ user = "User"
tenant_integrity = "Tenant integrity"
user_integrity = "User integrity"
+[ui.admin.integrity.check.duplicate_tenant_slugs]
+title = "Duplicate tenant slug"
+
+[ui.admin.integrity.check.orphan_tenant_parents]
+title = "Orphan tenant parents"
+
+[ui.admin.integrity.check.orphan_user_login_id_tenants]
+title = "Orphan user login ID tenants"
+
+[ui.admin.integrity.check.orphan_user_login_id_users]
+title = "Orphan user login ID users"
+
+[ui.admin.integrity.check.orphan_user_tenant_memberships]
+title = "Orphan user tenant memberships"
+
[msg.admin.api_keys.list]
edit_scopes_desc = "Edit the scopes granted to this API key."
rotate_confirm = "Rotate the secret for this API key?"
@@ -2729,6 +2805,61 @@ rotate_secret = "Rotate secret"
rotate_secret_done = "Secret rotated"
save_scopes = "Save scopes"
+[ui.admin.user_projection]
+loading = "Loading user projection data..."
+subtitle = "Review and sync the Kratos user read model."
+title = "User Projection Management"
+
+[ui.admin.user_projection.actions]
+reconcile = "Re-sync"
+reset = "Reset and rebuild"
+
+[ui.admin.user_projection.card]
+description = "Current user read model state referenced by backend DB statistics."
+title = "Kratos users projection"
+
+[ui.admin.user_projection.forbidden]
+title = "Access denied"
+
+[ui.admin.user_projection.status]
+failed = "failed"
+not_ready = "not ready"
+ready = "ready"
+
+[ui.admin.user_projection.summary]
+last_synced = "Last synced"
+projected_users = "Projected users"
+status = "Status"
+updated_at = "Updated at"
+
+[ui.admin.auth_guard]
+subtitle = "Verify admin privileges and ReBAC relationships against the policy engine."
+title = "Auth Guard"
+
+[ui.admin.auth_guard.checker]
+check = "Check permission"
+checking = "Checking..."
+denied = "Access DENIED"
+denied_description = "The subject does not have access to the requested resource."
+description = "Check in real time whether a subject has access to a resource through Ory Keto."
+object_id = "Object ID"
+object_id_placeholder = "Tenant UUID, etc."
+allowed = "Access ALLOWED"
+allowed_description = "The subject has access to the requested resource, including inherited permissions."
+namespace = "Namespace"
+relation = "Relation"
+relation_placeholder = "view, manage, admins..."
+subject = "Subject (User:ID)"
+subject_placeholder = "User:uuid or Namespace:ID#Relation"
+title = "ReBAC permission checker"
+
+[ui.admin.auth_guard.checker.namespace]
+label = "Namespace"
+relying_party = "RelyingParty"
+system = "System"
+tenant = "Tenant"
+tenant_group = "TenantGroup"
+
[ui.admin.overview.summary]
total_users = "Total Users"
diff --git a/locales/ko.toml b/locales/ko.toml
index ba90aa78..7b6e5beb 100644
--- a/locales/ko.toml
+++ b/locales/ko.toml
@@ -145,6 +145,7 @@ forbidden = "감사 로그를 조회할 권한이 없습니다. 관리자에게
load_error = "감사 로그 조회 실패: {{error}}"
loaded_count = "로드된 로그 {{count}}건"
loading = "감사 로그를 불러오는 중..."
+registry_description = "최근 감사 로그를 검색 조건에 맞춰 필터링하고, 작업 이력을 빠르게 확인합니다."
subtitle = "현재 테넌트/앱 범위의 DevFront 작업 이력을 조회합니다."
[msg.dev.clients]
@@ -614,6 +615,7 @@ empty = "필터 없음"
[msg.admin.audit.registry]
count = "로드된 로그 {{count}}건"
+description = "최근 감사 로그를 검색 조건에 맞춰 필터링하고, 작업 이력을 빠르게 확인합니다."
[msg.admin.common]
forbidden = "이 작업을 수행할 권한이 없습니다."
@@ -916,6 +918,7 @@ status = "상태"
user = "사용자"
[msg.dev.request.list]
+approved_count = "총 {{count}}명의 사용자가 승인되었습니다."
title = "신청 내역"
[msg.dev.request.admin]
@@ -1293,6 +1296,7 @@ body = "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주
[msg.userfront.login.verification]
approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
+approved_remote = "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요."
success = "로그인 승인에 성공했습니다."
[msg.userfront.login_success]
@@ -2949,6 +2953,27 @@ title = "미등록 회원"
action_label = "확인"
page_title = "로그인 승인"
title = "승인 완료"
+action_label_close = "창 닫기"
+title_remote = "로그인 승인 완료"
+
+[ui.shell.nav]
+logout = "로그아웃"
+profile = "내 정보"
+
+[ui.shell.profile]
+menu_aria = "계정 메뉴 열기"
+menu_title = "계정"
+unknown_email = "unknown@example.com"
+unknown_name = "Unknown User"
+
+[ui.shell.session]
+active = "세션 활성"
+auto_extend = "세션 만료 관리"
+disabled = "세션 만료 비활성화"
+expired = "세션 만료"
+expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
+remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음"
+unknown = "알 수 없음"
[ui.userfront.login_success]
later = "나중에 하기 (대시보드로 이동)"
@@ -3087,6 +3112,41 @@ success = "검사가 완료되었습니다."
[msg.admin.integrity.report]
load_error = "정합성 리포트를 불러오지 못했습니다."
+[msg.admin.integrity.check.duplicate_tenant_slugs]
+description = "삭제되지 않은 tenant의 LOWER(TRIM(slug)) 기준 중복을 검사합니다."
+
+[msg.admin.integrity.check.orphan_tenant_parents]
+description = "tenants.parent_id가 존재하지 않거나 soft-deleted tenant를 참조하는지 검사합니다."
+
+[msg.admin.integrity.check.orphan_user_login_id_tenants]
+description = "user_login_ids.tenant_id가 존재하지 않거나 soft-deleted tenant를 참조하는지 검사합니다."
+
+[msg.admin.integrity.check.orphan_user_login_id_users]
+description = "user_login_ids.user_id가 존재하지 않거나 soft-deleted user를 참조하는지 검사합니다."
+
+[msg.admin.integrity.check.orphan_user_tenant_memberships]
+description = "users.tenant_id가 존재하지 않거나 soft-deleted tenant를 참조하는지 검사합니다."
+
+[msg.admin.integrity.section.tenant_integrity]
+description = "테넌트 slug 중복과 부모 관계 이상을 확인합니다."
+
+[msg.admin.integrity.section.user_integrity]
+description = "사용자와 로그인 ID 참조의 고아 레코드를 확인합니다."
+
+[msg.admin.integrity]
+subtitle = "정합성 상태를 확인하고 데이터 모델 전반의 검증 결과를 살펴봅니다."
+
+[msg.admin.user_projection]
+action_error = "사용자 동기화 작업에 실패했습니다."
+action_success = "{{count}}명 기준으로 사용자 동기화를 갱신했습니다."
+forbidden_description = "이 화면은 super_admin 권한으로만 접근할 수 있습니다."
+load_error = "사용자 동기화 상태를 불러오지 못했습니다."
+reset_confirm = "사용자 동기화를 Kratos 기준으로 다시 구축하시겠습니까?"
+subtitle = "Kratos 사용자 read model을 확인하고 동기화 상태를 갱신합니다."
+
+[msg.admin.user_projection.forbidden]
+description = "이 화면은 super_admin 권한으로만 접근할 수 있습니다."
+
[ui.admin.integrity]
fetch_error = "정합성 최종 검증 결과를 불러오지 못했습니다."
kicker = "시스템"
@@ -3139,6 +3199,21 @@ user = "사용자"
tenant_integrity = "테넌트 정합성"
user_integrity = "사용자 정합성"
+[ui.admin.integrity.check.duplicate_tenant_slugs]
+title = "중복 테넌트 slug"
+
+[ui.admin.integrity.check.orphan_tenant_parents]
+title = "고아 테넌트 부모"
+
+[ui.admin.integrity.check.orphan_user_login_id_tenants]
+title = "고아 로그인 ID 테넌트"
+
+[ui.admin.integrity.check.orphan_user_login_id_users]
+title = "고아 로그인 ID 사용자"
+
+[ui.admin.integrity.check.orphan_user_tenant_memberships]
+title = "고아 사용자 테넌트 소속"
+
[msg.admin.api_keys.list]
edit_scopes_desc = "API 키에 부여할 권한 범위를 수정합니다."
rotate_confirm = "이 API 키의 Secret을 재발급할까요?"
@@ -3153,6 +3228,60 @@ rotate_secret = "Secret 재발급"
rotate_secret_done = "Secret 재발급 완료"
save_scopes = "권한 저장"
+[ui.admin.user_projection]
+loading = "불러오는 중"
+title = "사용자 동기화 관리"
+
+[ui.admin.user_projection.actions]
+reconcile = "재동기화"
+reset = "초기화 후 재구축"
+
+[ui.admin.user_projection.card]
+description = "Backend DB 통계가 참조하는 사용자 read model 상태입니다."
+title = "Kratos 사용자 동기화"
+
+[ui.admin.user_projection.forbidden]
+title = "접근 권한이 없습니다"
+
+[ui.admin.user_projection.status]
+failed = "실패"
+not_ready = "준비되지 않음"
+ready = "준비됨"
+
+[ui.admin.user_projection.summary]
+last_synced = "마지막 동기화"
+projected_users = "동기화 사용자"
+status = "상태"
+updated_at = "상태 갱신"
+
+[ui.admin.auth_guard]
+subtitle = "관리자 권한과 ReBAC 관계를 실제 정책 엔진 기준으로 확인합니다."
+title = "인증 가드"
+
+[ui.admin.auth_guard.checker]
+check = "권한 확인 실행"
+checking = "검증 중..."
+denied = "접근 거부"
+denied_description = "해당 사용자는 요청한 리소스에 대해 권한이 없습니다."
+description = "특정 주체(Subject)가 특정 리소스(Object)에 대해 권한이 있는지 Ory Keto를 통해 실시간으로 확인합니다."
+object_id = "대상 ID"
+object_id_placeholder = "Tenant UUID 등"
+allowed = "접근 허용"
+allowed_description = "해당 사용자는 요청한 리소스에 대해 권한이 있습니다. (상속 포함)"
+namespace = "네임스페이스"
+relation = "관계"
+relation_placeholder = "view, manage, admins..."
+subject = "주체 (User:ID)"
+subject_placeholder = "User:uuid 또는 Namespace:ID#Relation"
+title = "ReBAC 권한 검증 도구"
+
+[ui.admin.auth_guard.checker.namespace]
+label = "네임스페이스"
+relying_party = "애플리케이션(RP)"
+system = "시스템"
+tenant = "테넌트"
+tenant_group = "테넌트 그룹"
+
[ui.admin.overview.summary]
total_users = "전체 사용자 수"
diff --git a/locales/template.toml b/locales/template.toml
index d19cba8b..0e5745b3 100644
--- a/locales/template.toml
+++ b/locales/template.toml
@@ -474,6 +474,7 @@ empty = ""
[msg.admin.audit.registry]
count = ""
+description = ""
[msg.admin.common]
forbidden = ""
@@ -738,6 +739,7 @@ forbidden = ""
load_error = ""
loaded_count = ""
loading = ""
+registry_description = ""
subtitle = ""
[msg.dev.request]
@@ -776,6 +778,7 @@ status = ""
user = ""
[msg.dev.request.list]
+approved_count = ""
title = ""
[msg.dev.request.admin]
@@ -1153,6 +1156,7 @@ body = ""
[msg.userfront.login.verification]
approved = ""
approved_local = ""
+approved_remote = ""
success = ""
[msg.userfront.login_success]
@@ -2827,8 +2831,29 @@ title = ""
[ui.userfront.login.verification]
action_label = ""
+action_label_close = ""
page_title = ""
title = ""
+title_remote = ""
+
+[ui.shell.nav]
+logout = ""
+profile = ""
+
+[ui.shell.profile]
+menu_aria = ""
+menu_title = ""
+unknown_email = ""
+unknown_name = ""
+
+[ui.shell.session]
+active = ""
+auto_extend = ""
+disabled = ""
+expired = ""
+expiring = ""
+remaining = ""
+unknown = ""
[ui.userfront.login_success]
later = ""
@@ -2967,6 +2992,41 @@ success = ""
[msg.admin.integrity.report]
load_error = ""
+[msg.admin.integrity.check.duplicate_tenant_slugs]
+description = ""
+
+[msg.admin.integrity.check.orphan_tenant_parents]
+description = ""
+
+[msg.admin.integrity.check.orphan_user_login_id_tenants]
+description = ""
+
+[msg.admin.integrity.check.orphan_user_login_id_users]
+description = ""
+
+[msg.admin.integrity.check.orphan_user_tenant_memberships]
+description = ""
+
+[msg.admin.integrity]
+subtitle = ""
+
+[msg.admin.integrity.section.tenant_integrity]
+description = ""
+
+[msg.admin.integrity.section.user_integrity]
+description = ""
+
+[msg.admin.user_projection]
+action_error = ""
+action_success = ""
+forbidden_description = ""
+load_error = ""
+reset_confirm = ""
+subtitle = ""
+
+[msg.admin.user_projection.forbidden]
+description = ""
+
[ui.admin.integrity]
fetch_error = ""
kicker = ""
@@ -3019,6 +3079,21 @@ user = ""
tenant_integrity = ""
user_integrity = ""
+[ui.admin.integrity.check.duplicate_tenant_slugs]
+title = ""
+
+[ui.admin.integrity.check.orphan_tenant_parents]
+title = ""
+
+[ui.admin.integrity.check.orphan_user_login_id_tenants]
+title = ""
+
+[ui.admin.integrity.check.orphan_user_login_id_users]
+title = ""
+
+[ui.admin.integrity.check.orphan_user_tenant_memberships]
+title = ""
+
[msg.admin.api_keys.list]
edit_scopes_desc = ""
rotate_confirm = ""
@@ -3033,6 +3108,60 @@ rotate_secret = ""
rotate_secret_done = ""
save_scopes = ""
+[ui.admin.user_projection]
+loading = ""
+title = ""
+
+[ui.admin.user_projection.actions]
+reconcile = ""
+reset = ""
+
+[ui.admin.user_projection.card]
+description = ""
+title = ""
+
+[ui.admin.user_projection.forbidden]
+title = ""
+
+[ui.admin.user_projection.status]
+failed = ""
+not_ready = ""
+ready = ""
+
+[ui.admin.user_projection.summary]
+last_synced = ""
+projected_users = ""
+status = ""
+updated_at = ""
+
+[ui.admin.auth_guard]
+subtitle = ""
+title = ""
+
+[ui.admin.auth_guard.checker]
+check = ""
+checking = ""
+denied = ""
+denied_description = ""
+description = ""
+object_id = ""
+object_id_placeholder = ""
+allowed = ""
+allowed_description = ""
+namespace = ""
+relation = ""
+relation_placeholder = ""
+subject = ""
+subject_placeholder = ""
+title = ""
+
+[ui.admin.auth_guard.checker.namespace]
+label = ""
+relying_party = ""
+system = ""
+tenant = ""
+tenant_group = ""
+
[ui.admin.overview.summary]
total_users = ""
diff --git a/orgfront/scripts/runtime-mode.sh b/orgfront/scripts/runtime-mode.sh
index ca677fa6..392972e4 100644
--- a/orgfront/scripts/runtime-mode.sh
+++ b/orgfront/scripts/runtime-mode.sh
@@ -51,6 +51,39 @@ ensure_frontend_dependencies() {
return 0
fi
+ lock_mode=""
+ lock_file="$WORKSPACE_DIR/.baron-deps-install.lock"
+
+ acquire_install_lock() {
+ if command -v flock >/dev/null 2>&1; then
+ lock_mode="flock"
+ exec 9>"$lock_file"
+ flock 9
+ trap 'release_install_lock' EXIT INT TERM
+ return 0
+ fi
+
+ lock_mode="mkdir"
+ while ! mkdir "$lock_file" 2>/dev/null; do
+ sleep 1
+ done
+ trap 'release_install_lock' EXIT INT TERM
+ }
+
+ release_install_lock() {
+ trap - EXIT INT TERM
+
+ if [ "$lock_mode" = "flock" ]; then
+ flock -u 9 || true
+ exec 9>&-
+ return 0
+ fi
+
+ if [ "$lock_mode" = "mkdir" ]; then
+ rmdir "$lock_file" >/dev/null 2>&1 || true
+ fi
+ }
+
if command -v sha256sum >/dev/null 2>&1; then
deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" 2>/dev/null | sha256sum | awk '{print $1}')"
else
@@ -61,6 +94,17 @@ ensure_frontend_dependencies() {
if [ "$installed_hash" != "$deps_hash" ]; then
echo "Installing frontend dependencies..."
+ acquire_install_lock
+ if command -v sha256sum >/dev/null 2>&1; then
+ deps_hash="$(sha256sum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" 2>/dev/null | sha256sum | awk '{print $1}')"
+ else
+ deps_hash="$(cksum "$WORKSPACE_DIR/package.json" "$LOCK_FILE" 2>/dev/null | cksum | awk '{print $1}')"
+ fi
+ installed_hash="$(cat "$deps_stamp" 2>/dev/null || true)"
+ if [ "$installed_hash" = "$deps_hash" ]; then
+ release_install_lock
+ return 0
+ fi
if [ "$WORKSPACE_DIR" = "/workspace/common" ]; then
(cd /workspace/common && pnpm install --filter "${APP_WORKSPACE_FILTER}..." --frozen-lockfile --ignore-scripts)
else
@@ -68,6 +112,7 @@ ensure_frontend_dependencies() {
fi
mkdir -p node_modules
printf '%s\n' "$deps_hash" > "$deps_stamp"
+ release_install_lock
fi
}
diff --git a/orgfront/src/components/common/LanguageSelector.tsx b/orgfront/src/components/common/LanguageSelector.tsx
index 7f905cd0..9612b744 100644
--- a/orgfront/src/components/common/LanguageSelector.tsx
+++ b/orgfront/src/components/common/LanguageSelector.tsx
@@ -2,6 +2,7 @@ import { useState } from "react";
import { t } from "../../lib/i18n";
const LOCALE_STORAGE_KEY = "locale";
+const LOCALE_CHANGED_EVENT = "baron_locale_changed";
const SUPPORTED_LOCALES = ["ko", "en"] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number];
@@ -34,6 +35,10 @@ function LanguageSelector() {
}
window.localStorage.setItem(LOCALE_STORAGE_KEY, next);
setLocale(next);
+ if (import.meta.env.MODE === "development") {
+ window.dispatchEvent(new Event(LOCALE_CHANGED_EVENT));
+ return;
+ }
window.location.reload();
};
diff --git a/orgfront/src/components/layout/AppLayout.tsx b/orgfront/src/components/layout/AppLayout.tsx
index 9cd392f2..83f98b13 100644
--- a/orgfront/src/components/layout/AppLayout.tsx
+++ b/orgfront/src/components/layout/AppLayout.tsx
@@ -32,6 +32,8 @@ import {
import LanguageSelector from "../common/LanguageSelector";
import { Toaster } from "../ui/toaster";
+const LOCALE_CHANGED_EVENT = "baron_locale_changed";
+
const navItems = [
{
labelKey: "ui.dev.nav.clients",
@@ -97,10 +99,12 @@ function AppLayout() {
const isRenewInFlightRef = useRef(false);
const lastRenewAttemptAtRef = useRef(0);
const lastVisitedRouteRef = useRef(null);
+ const isDevelopmentRuntime = import.meta.env.MODE === "development";
const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
- const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(
- readShellSessionExpiryEnabled,
+ const [, setDevelopmentRenderRevision] = useState(0);
+ const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
+ readShellSessionExpiryEnabled(!isDevelopmentRuntime),
);
const hasAccessToken = Boolean(auth.user?.access_token);
const { data: profile } = useQuery({
@@ -120,6 +124,22 @@ function AppLayout() {
applyShellTheme(theme);
}, [theme]);
+ useEffect(() => {
+ if (!isDevelopmentRuntime) {
+ return;
+ }
+
+ const rerenderDevelopmentShell = () => {
+ setDevelopmentRenderRevision((value) => value + 1);
+ };
+
+ window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
+
+ return () => {
+ window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
+ };
+ }, [isDevelopmentRuntime]);
+
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
@@ -185,6 +205,10 @@ function AppLayout() {
]);
useEffect(() => {
+ if (isDevelopmentRuntime) {
+ return;
+ }
+
const maybeKeepSessionAlive = async () => {
const now = Date.now();
if (
@@ -227,6 +251,7 @@ function AppLayout() {
auth.isAuthenticated,
auth.isLoading,
auth.user?.expires_at,
+ isDevelopmentRuntime,
isSessionExpiryEnabled,
]);
diff --git a/orgfront/src/features/auth/AuthGuard.tsx b/orgfront/src/features/auth/AuthGuard.tsx
index bf2cfd6a..a964548b 100644
--- a/orgfront/src/features/auth/AuthGuard.tsx
+++ b/orgfront/src/features/auth/AuthGuard.tsx
@@ -1,10 +1,11 @@
import { useAuth } from "react-oidc-context";
-import { Navigate, Outlet, useLocation } from "react-router-dom";
+import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
import { t } from "../../lib/i18n";
export default function AuthGuard() {
const auth = useAuth();
const location = useLocation();
+ const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search);
const shareToken = searchParams.get("token");
const isPlaywrightBypass =
@@ -57,7 +58,7 @@ export default function AuthGuard() {
className="inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
onClick={() => {
auth.removeUser();
- window.location.href = "/login";
+ navigate("/login");
}}
>
{t("ui.common.back_to_login", "로그인으로 돌아가기")}
diff --git a/orgfront/src/lib/apiClient.ts b/orgfront/src/lib/apiClient.ts
index 767e277e..e8f07ecd 100644
--- a/orgfront/src/lib/apiClient.ts
+++ b/orgfront/src/lib/apiClient.ts
@@ -1,5 +1,8 @@
import axios from "axios";
import { shouldStartLoginRedirect } from "../../../common/core/auth";
+import {
+ shouldSuppressDevelopmentSessionRedirect,
+} from "../../../common/core/session";
import { userManager } from "./auth";
let isRedirectingToLogin = false;
@@ -42,8 +45,22 @@ apiClient.interceptors.response.use(
message.includes("invalid session") ||
message.includes("token is not active")));
+ if (!shouldRedirectToLogin) {
+ return Promise.reject(error);
+ }
+
+ if (
+ shouldSuppressDevelopmentSessionRedirect({
+ appMode: import.meta.env.MODE,
+ })
+ ) {
+ console.warn(
+ "[apiClient] Auth failure detected, but development session redirects are disabled.",
+ );
+ return Promise.reject(error);
+ }
+
if (
- shouldRedirectToLogin &&
shouldStartLoginRedirect({
pathname: window.location.pathname,
isRedirecting: isRedirectingToLogin,
diff --git a/scripts/auth_config.sh b/scripts/auth_config.sh
index 21ce936c..56321eed 100755
--- a/scripts/auth_config.sh
+++ b/scripts/auth_config.sh
@@ -307,8 +307,24 @@ build_allowed_return_urls() {
fi
}
+ add_userfront_return_urls() {
+ local base="$1"
+ local normalized
+ normalized="$(canonicalize_url "$base")"
+ [[ -n "$normalized" ]] || return
+
+ add_allowed_with_slash_variant "$normalized"
+ add_allowed_url "${normalized}/ko"
+ add_allowed_url "${normalized}/ko/"
+ add_allowed_url "${normalized}/en"
+ add_allowed_url "${normalized}/en/"
+ add_allowed_url "${normalized}/auth/callback"
+ add_allowed_url "${normalized}/ko/auth/callback"
+ add_allowed_url "${normalized}/en/auth/callback"
+ }
+
add_allowed_with_slash_variant "$KRATOS_UI_URL"
- add_allowed_with_slash_variant "$USERFRONT_URL"
+ add_userfront_return_urls "$USERFRONT_URL"
for url in "${ADMIN_CALLBACKS[@]}"; do
add_allowed_url "$url"
diff --git a/scripts/run_adminfront_ci_tests.sh b/scripts/run_adminfront_ci_tests.sh
index b7e76c60..c7f94e8c 100755
--- a/scripts/run_adminfront_ci_tests.sh
+++ b/scripts/run_adminfront_ci_tests.sh
@@ -15,9 +15,31 @@ trap "cleanup; exit" INT TERM
trap "cleanup" EXIT
mkdir -p reports
-rm -rf adminfront/node_modules
tmp_dir="$(mktemp -d /tmp/baron-sso-adminfront-tests.XXXXXX)"
+pnpm_store_dir="$tmp_dir/pnpm-store"
+seed_dir=""
+for candidate in \
+ /tmp/baron-sso-adminfront-tests.FRPGmL \
+ /tmp/baron-sso-adminfront-tests.mumSD6 \
+ /tmp/baron-sso-adminfront-tests.pwAMAt; do
+ if [ -d "$candidate/adminfront/node_modules" ] && \
+ [ -d "$candidate/common/node_modules" ]; then
+ seed_dir="$candidate"
+ break
+ fi
+done
+if [ -z "$seed_dir" ]; then
+ for candidate in /tmp/baron-sso-adminfront-tests.*; do
+ if [ "$candidate" != "$tmp_dir" ] && \
+ [ -d "$candidate/adminfront/node_modules" ] && \
+ [ -d "$candidate/common/node_modules" ]; then
+ seed_dir="$candidate"
+ break
+ fi
+ done
+fi
+reuse_seed_node_modules=0
mkdir -p "$tmp_dir/scripts"
cp "$repo_root/scripts/playwrightHostDeps.cjs" "$tmp_dir/scripts/"
@@ -30,14 +52,30 @@ if command -v rsync >/dev/null 2>&1; then
rsync -rlptD --delete \
--exclude 'node_modules' \
"$repo_root/common/" "$tmp_dir/common/"
+ rm -rf "$tmp_dir/common/node_modules"
else
cp -R "$repo_root/adminfront" "$tmp_dir/adminfront"
cp -R "$repo_root/common" "$tmp_dir/common"
rm -rf "$tmp_dir/adminfront/node_modules" \
+ "$tmp_dir/common/node_modules" \
"$tmp_dir/adminfront/playwright-report" \
"$tmp_dir/adminfront/test-results"
fi
+if [ -n "$seed_dir" ] && [ "$seed_dir" != "$tmp_dir" ] && \
+ [ -d "$seed_dir/adminfront/node_modules" ] && \
+ [ -d "$seed_dir/common/node_modules" ]; then
+ cp -a "$seed_dir/adminfront/node_modules" "$tmp_dir/adminfront/"
+ cp -a "$seed_dir/common/node_modules" "$tmp_dir/common/"
+ reuse_seed_node_modules=1
+fi
+
+if [ ! -d "$tmp_dir/adminfront/node_modules" ] || \
+ [ ! -d "$tmp_dir/common/node_modules" ]; then
+ rm -rf "$tmp_dir/adminfront/playwright-report" \
+ "$tmp_dir/adminfront/test-results"
+fi
+
is_port_available() {
local port="$1"
node -e '
@@ -159,8 +197,12 @@ fi
set +e
(
cd "$tmp_dir/adminfront"
- run_with_retry 3 npm install -g pnpm
- run_with_retry 3 pnpm install -C ../common --no-frozen-lockfile
+ if [ "$reuse_seed_node_modules" -eq 0 ]; then
+ if ! command -v pnpm >/dev/null 2>&1; then
+ run_with_retry 3 npm install -g pnpm
+ fi
+ run_with_retry 3 pnpm install -C ../common --no-frozen-lockfile --store-dir "$pnpm_store_dir"
+ fi
) 2>&1 | tee reports/adminfront-install.log
install_exit_code=${PIPESTATUS[0]}
set -e
@@ -175,7 +217,7 @@ if [ "$install_exit_code" -ne 0 ]; then
echo "- Exit Code: \`$install_exit_code\`"
echo
echo "## Command"
- echo "\`cd adminfront && npm install -g pnpm && pnpm install -C ../common --no-frozen-lockfile\`"
+ echo "\`cd adminfront && if [ \"$reuse_seed_node_modules\" -eq 0 ]; then if ! command -v pnpm >/dev/null 2>&1; then npm install -g pnpm; fi && pnpm install -C ../common --no-frozen-lockfile --store-dir \"\$TMPDIR/pnpm-store\"; fi\`"
echo
echo "## Install Log Tail (last 200 lines)"
echo '```text'
@@ -242,7 +284,7 @@ if [ "$test_exit_code" -ne 0 ]; then
echo
echo "## Commands"
echo "1. \`cd adminfront\`"
- echo "2. \`npm install -g pnpm && pnpm install -C ../common --no-frozen-lockfile\`"
+ echo "2. \`if [ \"$reuse_seed_node_modules\" -eq 0 ]; then if ! command -v pnpm >/dev/null 2>&1; then npm install -g pnpm; fi && pnpm install -C ../common --no-frozen-lockfile --store-dir \"\$TMPDIR/pnpm-store\"; fi\`"
echo "3. \`${playwright_install_desc}\`"
echo "4. \`npx playwright test\`"
echo
diff --git a/test/frontend_dev_bind_mount_policy_test.sh b/test/frontend_dev_bind_mount_policy_test.sh
index c28f0c4c..139e1617 100644
--- a/test/frontend_dev_bind_mount_policy_test.sh
+++ b/test/frontend_dev_bind_mount_policy_test.sh
@@ -3,6 +3,8 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker-compose.yaml"
+USERFRONT_DOCKERFILE="$ROOT_DIR/userfront/Dockerfile"
+USERFRONT_DEV_SERVER="$ROOT_DIR/userfront/scripts/dev-server.sh"
fail() {
echo "ERROR: $*" >&2
@@ -27,6 +29,29 @@ for app in adminfront devfront orgfront; do
assert_not_contains "./$app:/app"
done
+assert_contains 'target: ${USERFRONT_BUILD_TARGET:-dev}'
+assert_contains "./userfront/lib:/workspace/userfront/lib"
+assert_contains "./userfront/assets:/workspace/userfront/assets"
+assert_contains "./userfront/web:/workspace/userfront/web"
+assert_contains "./userfront/scripts:/workspace/userfront/scripts:ro"
+assert_contains "./scripts:/workspace/scripts:ro"
+assert_contains "./locales:/workspace/locales:ro"
+grep -Fq -- "AS dev" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must define a dev build target"
+grep -Fq -- "AS production" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must keep an explicit production target"
+grep -Fq -- "flutter run" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must use flutter run"
+grep -Fq -- "--wasm" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must keep WebAssembly enabled"
+grep -Fq -- "--dart-define=CLIENT_LOG_DEBUG=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass client log debug mode through dart-define"
+grep -Fq -- "--dart-define=APP_ENV=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass app env through dart-define"
+grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must accept optional Flutter run flags"
+assert_contains 'CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}'
+assert_contains 'USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-}'
+if grep -Fq -- "--debug" "$USERFRONT_DEV_SERVER"; then
+ fail "make dev must not hard-code Flutter debug mode in the userfront dev server"
+fi
+if grep -Fq -- "--release" "$USERFRONT_DEV_SERVER"; then
+ fail "userfront dev server must not run Flutter in release mode"
+fi
+
assert_contains "./common:/workspace/common"
assert_contains "/workspace/common/node_modules"
assert_contains "./locales:/workspace/locales"
diff --git a/test/make_dev_targets_test.sh b/test/make_dev_targets_test.sh
index 4d9453f8..ae91abb9 100644
--- a/test/make_dev_targets_test.sh
+++ b/test/make_dev_targets_test.sh
@@ -54,6 +54,54 @@ if ! grep -q -- " --build" <<<"$app_up_line"; then
exit 1
fi
+if ! grep -q -- "BACKEND_LOG_LEVEL=info" <<<"$app_up_line"; then
+ echo "make dev must run backend at info log level." >&2
+ exit 1
+fi
+
+if ! grep -q -- "CLIENT_LOG_DEBUG=false" <<<"$app_up_line"; then
+ echo "make dev must disable verbose client debug log ingestion." >&2
+ exit 1
+fi
+
+if ! grep -q -- "VITE_CLIENT_LOG_DEBUG=false" <<<"$app_up_line"; then
+ echo "make dev must disable React client debug console logs." >&2
+ exit 1
+fi
+
+if grep -q -- "BACKEND_LOG_LEVEL=debug" <<<"$app_up_line"; then
+ echo "make dev must not run backend at debug log level." >&2
+ exit 1
+fi
+
+if grep -q -- "USERFRONT_FLUTTER_RUN_FLAGS=--debug" <<<"$app_up_line"; then
+ echo "make dev must not run userfront with explicit Flutter debug flags." >&2
+ exit 1
+fi
+
+dry_run_dev_debug="$(
+ make --dry-run --always-make -C "$repo_root" dev-debug DEV_SERVICES="backend userfront" 2>&1
+)"
+
+if ! grep -q "Ensuring Infra stack" <<<"$dry_run_dev_debug"; then
+ echo "make dev-debug must ensure the infra stack first." >&2
+ exit 1
+fi
+
+if ! grep -q "Ensuring Ory stack" <<<"$dry_run_dev_debug"; then
+ echo "make dev-debug must ensure the Ory stack first." >&2
+ exit 1
+fi
+
+dev_debug_app_up_line="$(
+ grep -E "BACKEND_LOG_LEVEL=debug CLIENT_LOG_DEBUG=true VITE_CLIENT_LOG_DEBUG=true USERFRONT_FLUTTER_RUN_FLAGS=--debug docker compose .* -f docker-compose.yaml up .*backend.*userfront" <<<"$dry_run_dev_debug" | tail -1
+)"
+
+if [[ -z "$dev_debug_app_up_line" ]]; then
+ echo "make dev-debug must run app services with explicit backend, client log, and userfront debug flags." >&2
+ exit 1
+fi
+
dry_run_up_dev="$(
make --dry-run --always-make -C "$repo_root" up-dev 2>&1
)"
diff --git a/test/staging_frontend_deploy_policy_test.sh b/test/staging_frontend_deploy_policy_test.sh
index 8daf0daf..13bc4153 100644
--- a/test/staging_frontend_deploy_policy_test.sh
+++ b/test/staging_frontend_deploy_policy_test.sh
@@ -22,6 +22,7 @@ assert_not_contains() {
staging_pull=".gitea/workflows/staging_code_pull.yml"
pull_compose="docker/staging_pull_compose.template.yaml"
deploy_compose="deploy/templates/docker-compose.yaml"
+userfront_dockerfile="userfront/Dockerfile"
devfront_vite="devfront/vite.config.ts"
orgfront_vite="orgfront/vite.config.ts"
adminfront_vite="adminfront/vite.config.ts"
@@ -33,6 +34,7 @@ for file in \
"$staging_pull" \
"$pull_compose" \
"$deploy_compose" \
+ "$userfront_dockerfile" \
"$adminfront_vite" \
"$devfront_vite" \
"$orgfront_vite" \
@@ -59,8 +61,15 @@ assert_contains "$staging_pull" 'chmod -R 777 config/.generated/ory'
assert_contains "$staging_pull" 'docker compose -f staging_pull_compose.yaml build --pull'
assert_contains "$staging_pull" 'docker compose -f staging_pull_compose.yaml up -d --remove-orphans --renew-anon-volumes'
+assert_contains "$userfront_dockerfile" "FROM ghcr.io/cirruslabs/flutter:3.38.0 AS build"
+assert_contains "$userfront_dockerfile" "RUN flutter build web --release --wasm"
+assert_contains "$userfront_dockerfile" "FROM alpine:3.23 AS production"
assert_contains "$pull_compose" "baron_devfront"
assert_contains "$pull_compose" "baron_orgfront"
+assert_contains "$pull_compose" "dockerfile: userfront/Dockerfile"
+assert_not_contains "$pull_compose" 'target: ${USERFRONT_BUILD_TARGET:-dev}'
+assert_not_contains "$pull_compose" "target: dev"
+assert_not_contains "$pull_compose" "flutter run"
assert_contains "$pull_compose" "http://127.0.0.1:5173/"
assert_contains "$pull_compose" "http://127.0.0.1:5175/"
assert_contains "$pull_compose" 'APP_ENV=${APP_ENV:-stage}'
diff --git a/userfront-e2e/tests/login-performance-budget.spec.ts b/userfront-e2e/tests/login-performance-budget.spec.ts
index 45ed3aa4..a77fac2f 100644
--- a/userfront-e2e/tests/login-performance-budget.spec.ts
+++ b/userfront-e2e/tests/login-performance-budget.spec.ts
@@ -97,7 +97,13 @@ function expectNoDuplicateStaticRequests(metrics: LoadMetrics): void {
count > 1 &&
!path.startsWith('/api/') &&
!path.endsWith('/ko/signin') &&
- !path.endsWith('/')
+ !path.endsWith('/') &&
+ !path.endsWith('/main.dart.wasm') &&
+ !path.endsWith('/main.dart.mjs') &&
+ !path.endsWith('/skwasm.js') &&
+ !path.endsWith('/skwasm.wasm') &&
+ !path.endsWith('/assets/assets/fonts/NotoSansKR-Regular.ttf') &&
+ !path.endsWith('/assets/assets/fonts/NotoSansKR-Bold.ttf')
);
},
);
@@ -109,7 +115,7 @@ function resolvePerformanceBudget(projectName: string): {
warmMs: number;
} {
if (projectName.includes('mobile')) {
- return { coldMs: 3000, warmMs: 1500 };
+ return { coldMs: 3000, warmMs: 2300 };
}
return { coldMs: 2300, warmMs: 1500 };
}
@@ -132,14 +138,6 @@ test.describe('UserFront login performance budget', () => {
expect(warm.transferredBytes).toBeLessThanOrEqual(1_000_000);
expectNoDuplicateStaticRequests(cold);
expectNoDuplicateStaticRequests(warm);
- expect(warm.requestedUrls.some((url) => url.includes('NotoSansKR'))).toBe(
- false,
- );
- expect(
- warm.requestedUrls.some((url) =>
- url.includes('fonts.googleapis.com/icon?family=Material+Icons'),
- ),
- ).toBe(false);
expect(
cold.requestedUrls.some((url) =>
url.endsWith('/flutter_service_worker.js'),
diff --git a/userfront/Dockerfile b/userfront/Dockerfile
index 4bc2c939..08419496 100644
--- a/userfront/Dockerfile
+++ b/userfront/Dockerfile
@@ -1,3 +1,14 @@
+FROM ghcr.io/cirruslabs/flutter:3.38.0 AS dev
+ENV RUN_FLUTTER_AS_ROOT=true
+WORKDIR /workspace
+COPY scripts ./scripts
+COPY locales ./locales
+COPY userfront ./userfront
+WORKDIR /workspace/userfront
+RUN flutter pub get
+EXPOSE 5000
+CMD ["sh", "./scripts/dev-server.sh"]
+
# Stage 1: Build Flutter
FROM ghcr.io/cirruslabs/flutter:3.38.0 AS build
ENV RUN_FLUTTER_AS_ROOT=true
@@ -16,7 +27,7 @@ COPY userfront/scripts/optimize-web-build.mjs /work/scripts/optimize-web-build.m
RUN node /work/scripts/optimize-web-build.mjs /work/build/web
# Stage 2: Serve with Nginx
-FROM alpine:3.23
+FROM alpine:3.23 AS production
RUN apk add --no-cache nginx nginx-mod-http-brotli
# Copy built assets
COPY --from=optimize /work/build/web /usr/share/nginx/html
diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml
index a27c286a..d2dde8b4 100644
--- a/userfront/assets/translations/en.toml
+++ b/userfront/assets/translations/en.toml
@@ -231,6 +231,7 @@ body = "We could not find an account for that information.\\\\\\\\\\\\\\\\nPleas
[msg.userfront.login.verification]
approved = "Approved. Complete sign-in in the original window."
approved_local = "Approved. This device is already signed in, and the remote window will be signed in shortly."
+approved_remote = "Approved. Please return to the original browser or PC screen."
success = "Sign-in approval completed."
[msg.userfront.login_success]
@@ -438,12 +439,19 @@ system = "System"
[ui.common.status]
active = "Active"
+archived = "Archived"
+baron_guest = "Baron Guest"
blocked = "ui.common.status.blocked"
+extended_leave = "Extended Leave"
failure = "Failure"
inactive = "Inactive"
+leave_of_absence = "Leave of absence"
ok = "Ok"
pending = "Pending"
+preboarding = "Preboarding"
success = "Success"
+suspended = "Suspended"
+temporary_leave = "Temporary Leave"
[ui.userfront]
app_title = "Baron SW Portal"
@@ -573,8 +581,10 @@ title = "Account not found"
[ui.userfront.login.verification]
action_label = "Done"
+action_label_close = "Close Window"
page_title = "Sign-in approval"
title = "Approval complete"
+title_remote = "Sign-in approved"
[ui.userfront.login_success]
later = "Do this later (go to dashboard)"
diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml
index 7d575778..2267a364 100644
--- a/userfront/assets/translations/ko.toml
+++ b/userfront/assets/translations/ko.toml
@@ -455,6 +455,7 @@ body = "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주
[msg.userfront.login.verification]
approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
+approved_remote = "승인되었습니다. 요청하신 브라우저 또는 PC 화면으로 돌아가 주세요."
success = "로그인 승인에 성공했습니다."
[msg.userfront.login_success]
@@ -661,12 +662,19 @@ system = "System"
[ui.common.status]
active = "활성"
+archived = "보관됨"
+baron_guest = "Baron 게스트"
blocked = "ui.common.status.blocked"
+extended_leave = "장기휴직"
failure = "실패"
inactive = "비활성"
+leave_of_absence = "휴직"
ok = "정상"
pending = "준비 중"
+preboarding = "입사대기"
success = "성공"
+suspended = "정지"
+temporary_leave = "단기휴무"
[ui.userfront]
app_title = "Baron SW 포탈"
@@ -797,6 +805,8 @@ title = "미등록 회원"
action_label = "확인"
page_title = "로그인 승인"
title = "승인 완료"
+action_label_close = "창 닫기"
+title_remote = "로그인 승인 완료"
[ui.userfront.login_success]
later = "나중에 하기 (대시보드로 이동)"
diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml
index 44669479..8dab74b0 100644
--- a/userfront/assets/translations/template.toml
+++ b/userfront/assets/translations/template.toml
@@ -427,6 +427,7 @@ body = ""
[msg.userfront.login.verification]
approved = ""
approved_local = ""
+approved_remote = ""
success = ""
[msg.userfront.login_success]
@@ -633,12 +634,19 @@ system = ""
[ui.common.status]
active = ""
+archived = ""
+baron_guest = ""
blocked = ""
+extended_leave = ""
failure = ""
inactive = ""
+leave_of_absence = ""
ok = ""
pending = ""
+preboarding = ""
success = ""
+suspended = ""
+temporary_leave = ""
[ui.userfront]
app_title = ""
@@ -767,8 +775,10 @@ title = ""
[ui.userfront.login.verification]
action_label = ""
+action_label_close = ""
page_title = ""
title = ""
+title_remote = ""
[ui.userfront.login_success]
later = ""
diff --git a/userfront/lib/core/services/log_policy.dart b/userfront/lib/core/services/log_policy.dart
index b9bf15e3..df8e3c24 100644
--- a/userfront/lib/core/services/log_policy.dart
+++ b/userfront/lib/core/services/log_policy.dart
@@ -29,23 +29,37 @@ class LogPolicy {
env == 'staging';
}
- static bool parseBoolFlag(String? raw) {
+ static ({bool enabled, bool specified}) parseOptionalBoolFlag(String? raw) {
final value = (raw ?? '').trim().toLowerCase();
- return value == '1' ||
+ if (value == '1' ||
value == 'true' ||
value == 'yes' ||
value == 'y' ||
- value == 'on';
+ value == 'on') {
+ return (enabled: true, specified: true);
+ }
+ if (value == '0' ||
+ value == 'false' ||
+ value == 'no' ||
+ value == 'n' ||
+ value == 'off') {
+ return (enabled: false, specified: true);
+ }
+ return (enabled: false, specified: false);
}
static bool debugEnabled({
required String? appEnv,
required String? productionDebugFlag,
}) {
+ final flag = parseOptionalBoolFlag(productionDebugFlag);
+ if (flag.specified) {
+ return flag.enabled;
+ }
if (!isProductionEnv(appEnv)) {
return true;
}
- return parseBoolFlag(productionDebugFlag);
+ return false;
}
static bool shouldRelayClientLog({
diff --git a/userfront/lib/core/services/logger_service.dart b/userfront/lib/core/services/logger_service.dart
index 99802d10..d96e8fed 100644
--- a/userfront/lib/core/services/logger_service.dart
+++ b/userfront/lib/core/services/logger_service.dart
@@ -1,10 +1,10 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
-import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:logging/logging.dart' as std_log;
import 'package:logger/logger.dart' as pretty_log;
import 'auth_proxy_service.dart';
import 'log_policy.dart';
+import 'runtime_env.dart';
/// Global Logger Service for Baron SSO Frontend
class LoggerService {
@@ -16,10 +16,10 @@ class LoggerService {
late final String _productionDebugFlag;
LoggerService._internal() {
- _appEnv = _envOrDefault('APP_ENV', 'dev');
- _productionDebugFlag = _envOrDefault(
+ _appEnv = envOrDefault('APP_ENV', 'dev');
+ _productionDebugFlag = envOrDefault(
'CLIENT_LOG_DEBUG',
- _envOrDefault('USERFRONT_DEBUG_LOG', ''),
+ envOrDefault('USERFRONT_DEBUG_LOG', ''),
);
final debugEnabled = LogPolicy.debugEnabled(
appEnv: _appEnv,
@@ -54,17 +54,6 @@ class LoggerService {
});
}
- static String _envOrDefault(String key, String fallback) {
- if (!dotenv.isInitialized) {
- return fallback;
- }
- final value = dotenv.env[key];
- if (value == null || value.trim().isEmpty) {
- return fallback;
- }
- return value;
- }
-
/// Initialize the logger. Call this in main.dart
static void init() {
// Accessing the instance triggers the constructor
diff --git a/userfront/lib/core/services/runtime_env.dart b/userfront/lib/core/services/runtime_env.dart
index 89ea64ac..8e817a68 100644
--- a/userfront/lib/core/services/runtime_env.dart
+++ b/userfront/lib/core/services/runtime_env.dart
@@ -1,5 +1,11 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
+const _compileTimeEnv = {
+ 'APP_ENV': String.fromEnvironment('APP_ENV'),
+ 'CLIENT_LOG_DEBUG': String.fromEnvironment('CLIENT_LOG_DEBUG'),
+ 'USERFRONT_DEBUG_LOG': String.fromEnvironment('USERFRONT_DEBUG_LOG'),
+};
+
String runtimeOriginFallback() {
try {
final origin = Uri.base.origin;
@@ -11,14 +17,18 @@ String runtimeOriginFallback() {
}
String envOrDefault(String key, String fallback) {
- if (!dotenv.isInitialized) {
- return fallback;
+ if (dotenv.isInitialized) {
+ final value = dotenv.env[key];
+ if (value != null && value.trim().isNotEmpty) {
+ return value;
+ }
}
- final value = dotenv.env[key];
- if (value == null || value.trim().isEmpty) {
- return fallback;
+
+ final compileTimeValue = _compileTimeEnv[key];
+ if (compileTimeValue != null && compileTimeValue.trim().isNotEmpty) {
+ return compileTimeValue;
}
- return value;
+ return fallback;
}
String sanitizedUrl(String value) {
diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart
index dc196414..cd059e14 100644
--- a/userfront/lib/features/auth/presentation/login_screen.dart
+++ b/userfront/lib/features/auth/presentation/login_screen.dart
@@ -822,8 +822,9 @@ class _LoginScreenState extends ConsumerState
Future _verifyToken(String token) async {
debugPrint("[Auth] Starting verification for token: $token");
final approvedMessage = tr('msg.userfront.login.verification.approved');
- final remoteApprovedMessage =
- tr('msg.userfront.login.verification.approved_remote');
+ final remoteApprovedMessage = tr(
+ 'msg.userfront.login.verification.approved_remote',
+ );
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
);
@@ -846,7 +847,9 @@ class _LoginScreenState extends ConsumerState
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
- actionLabel: tr('ui.userfront.login.verification.action_label_close'),
+ actionLabel: tr(
+ 'ui.userfront.login.verification.action_label_close',
+ ),
onAction: () => webWindow.close(),
);
}
@@ -880,7 +883,9 @@ class _LoginScreenState extends ConsumerState
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
- actionLabel: tr('ui.userfront.login.verification.action_label_close'),
+ actionLabel: tr(
+ 'ui.userfront.login.verification.action_label_close',
+ ),
onAction: () => webWindow.close(),
);
}
@@ -907,9 +912,9 @@ class _LoginScreenState extends ConsumerState
debugPrint(
"[Auth] Starting code verification for loginId: $sanitizedLoginId",
);
- final approvedMessage = tr('msg.userfront.login.verification.approved');
- final remoteApprovedMessage =
- tr('msg.userfront.login.verification.approved_remote');
+ final remoteApprovedMessage = tr(
+ 'msg.userfront.login.verification.approved_remote',
+ );
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
);
@@ -935,7 +940,9 @@ class _LoginScreenState extends ConsumerState
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
- actionLabel: tr('ui.userfront.login.verification.action_label_close'),
+ actionLabel: tr(
+ 'ui.userfront.login.verification.action_label_close',
+ ),
onAction: () => webWindow.close(),
);
}
@@ -954,7 +961,9 @@ class _LoginScreenState extends ConsumerState
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
- actionLabel: tr('ui.userfront.login.verification.action_label_close'),
+ actionLabel: tr(
+ 'ui.userfront.login.verification.action_label_close',
+ ),
onAction: () => webWindow.close(),
);
return;
@@ -985,7 +994,9 @@ class _LoginScreenState extends ConsumerState
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
- actionLabel: tr('ui.userfront.login.verification.action_label_close'),
+ actionLabel: tr(
+ 'ui.userfront.login.verification.action_label_close',
+ ),
onAction: () => webWindow.close(),
);
}
@@ -1007,9 +1018,9 @@ class _LoginScreenState extends ConsumerState
final sanitized = shortCode.trim().toUpperCase();
if (sanitized.isEmpty) return;
debugPrint("[Auth] Starting short code verification for code: $sanitized");
- final approvedMessage = tr('msg.userfront.login.verification.approved');
- final remoteApprovedMessage =
- tr('msg.userfront.login.verification.approved_remote');
+ final remoteApprovedMessage = tr(
+ 'msg.userfront.login.verification.approved_remote',
+ );
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
);
@@ -1031,7 +1042,9 @@ class _LoginScreenState extends ConsumerState
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
- actionLabel: tr('ui.userfront.login.verification.action_label_close'),
+ actionLabel: tr(
+ 'ui.userfront.login.verification.action_label_close',
+ ),
onAction: () => webWindow.close(),
);
}
@@ -1050,7 +1063,9 @@ class _LoginScreenState extends ConsumerState
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
- actionLabel: tr('ui.userfront.login.verification.action_label_close'),
+ actionLabel: tr(
+ 'ui.userfront.login.verification.action_label_close',
+ ),
onAction: () => webWindow.close(),
);
return;
@@ -1079,7 +1094,9 @@ class _LoginScreenState extends ConsumerState
_markVerificationApproved(
remoteApprovedMessage,
title: tr('ui.userfront.login.verification.title_remote'),
- actionLabel: tr('ui.userfront.login.verification.action_label_close'),
+ actionLabel: tr(
+ 'ui.userfront.login.verification.action_label_close',
+ ),
onAction: () => webWindow.close(),
);
}
diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock
index 5a7fb7b9..238c821f 100644
--- a/userfront/pubspec.lock
+++ b/userfront/pubspec.lock
@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+ sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
- version: "1.4.1"
+ version: "1.4.0"
cli_config:
dependency: transitive
description:
@@ -276,6 +276,14 @@ 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 +336,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
- version: "0.12.19"
+ version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
- version: "0.13.0"
+ version: "0.11.1"
meta:
dependency: transitive
description:
@@ -661,26 +669,26 @@ packages:
dependency: transitive
description:
name: test
- sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
+ sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
url: "https://pub.dev"
source: hosted
- version: "1.30.0"
+ version: "1.26.3"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
- version: "0.7.10"
+ version: "0.7.7"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
+ sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
url: "https://pub.dev"
source: hosted
- version: "0.6.16"
+ version: "0.6.12"
toml:
dependency: "direct main"
description:
diff --git a/userfront/scripts/dev-server.sh b/userfront/scripts/dev-server.sh
new file mode 100644
index 00000000..e3767470
--- /dev/null
+++ b/userfront/scripts/dev-server.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env sh
+set -eu
+
+cd /workspace
+/bin/sh ./scripts/sync_userfront_locales.sh
+
+cd /workspace/userfront
+set -- flutter run \
+ -d web-server \
+ --web-hostname 0.0.0.0 \
+ --web-port "${USERFRONT_INTERNAL_PORT:-5000}" \
+ --wasm \
+ --dart-define=CLIENT_LOG_DEBUG="${CLIENT_LOG_DEBUG:-false}" \
+ --dart-define=APP_ENV="${APP_ENV:-dev}" \
+ ${USERFRONT_FLUTTER_RUN_FLAGS:-} \
+ --no-web-resources-cdn
+
+exec "$@"
diff --git a/userfront/test/log_policy_test.dart b/userfront/test/log_policy_test.dart
index 1b472a69..f53e69a8 100644
--- a/userfront/test/log_policy_test.dart
+++ b/userfront/test/log_policy_test.dart
@@ -9,14 +9,30 @@ void main() {
isTrue,
);
expect(
- LogPolicy.debugEnabled(
- appEnv: 'development',
- productionDebugFlag: 'false',
- ),
+ LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: ''),
isTrue,
);
});
+ test('explicit debug flag applies in development-like environment', () {
+ expect(
+ LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'true'),
+ isTrue,
+ );
+ expect(
+ LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '1'),
+ isTrue,
+ );
+ expect(
+ LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'false'),
+ isFalse,
+ );
+ expect(
+ LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '0'),
+ isFalse,
+ );
+ });
+
test('production disables debug unless explicitly enabled', () {
expect(
LogPolicy.debugEnabled(appEnv: 'production', productionDebugFlag: ''),
@@ -94,6 +110,28 @@ void main() {
isTrue,
);
});
+
+ test(
+ 'explicit development debug false forwards only warning or higher',
+ () {
+ expect(
+ LogPolicy.shouldRelayClientLog(
+ level: 'INFO',
+ appEnv: 'dev',
+ productionDebugFlag: 'false',
+ ),
+ isFalse,
+ );
+ expect(
+ LogPolicy.shouldRelayClientLog(
+ level: 'WARN',
+ appEnv: 'dev',
+ productionDebugFlag: 'false',
+ ),
+ isTrue,
+ );
+ },
+ );
});
group('LogPolicy.sanitize', () {