/** * uds-auth browser half — sidebar.footer.action + force foot row with Settings (side by side) + settings.section. * Auth: empNo cookie + token verified server-side via user-info. * Fallback: username/password when UAC/QR unavailable. */ window.__ModuleLoader__.load({ id: 'uds-auth', factory: (require) => { const module = { exports: {} } const exports = module.exports Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }) const React = require('react') const h = React.createElement const { useCallback, useEffect, useLayoutEffect, useRef, useState } = React const name = 'uds-auth' const inject = ['slots', 'locale'] const PAGE_SIZE = 50 const LOCALE_NS = 'uds-auth' /* uds-auth-i18n-begin — keep in sync with lib/i18n.js */ const UDS_I18N_MESSAGES = { "zh": { "role.super_admin": "超级管理员", "role.admin": "管理员", "role.user": "普通用户", "role.fallback_admin": "应急管理员", "role.badge.super_admin": "超管", "role.badge.admin": "管理员", "role.badge.fallback_admin": "应急", "role.badge.user": "", "ui.settingsTitle": "UAC 认证", "ui.settingsIntro": "工号+token 双校验;UAC 挂死时用应急账号 administrator 密码登录。", "ui.loginRequiredPage": "请先登录后查看此页", "ui.roleHint": "当前角色:{role}。首位扫码登录且 roles.json 为空时会自动成为超管;普通 admin 需超管在「用户管理」提权后再扫码登录。应急账号 administrator 需超管先设密码,再在登录面板用账密登录。", "ui.deployConfig": "部署配置", "ui.userSearchUrl": "用户搜索 URL(token 校验)", "ui.workspaceRoot": "工作区根目录(空=$DSH_HOME/user-workspaces)", "ui.saveConfig": "保存配置", "ui.saving": "保存中...", "ui.configSaved": "配置已保存", "ui.saveFailed": "保存失败", "ui.fallbackTitle": "应急登录(UAC 不可用)", "ui.fallbackStatus": "状态:{status}。可在此改密或关闭。仅在扫码不可用时从登录面板切换。", "ui.enabled": "已启用", "ui.disabled": "未启用", "ui.fallbackPassword": "应急密码(至少 6 位)", "ui.saveFallbackPassword": "保存应急密码", "ui.fallbackPasswordSet": "应急密码已设置", "ui.confirmClearFallback": "确认清除应急密码?", "ui.clear": "清除", "ui.userManagement": "用户管理", "ui.searchEmpNo": "搜索工号", "ui.search": "搜索", "ui.loading": "加载中...", "ui.loadFailed": "加载失败", "ui.empNo": "工号", "ui.role": "角色", "ui.actions": "操作", "ui.noUsers": "暂无用户", "ui.delete": "删除", "ui.confirmDelete": "确认删除 {empNo}?", "ui.pager": "共 {total} 人,第 {page} / {totalPages} 页", "ui.prevPage": "上一页", "ui.nextPage": "下一页", "ui.add": "添加", "ui.department": "部门", "ui.notLoggedIn": "未登录", "ui.pleaseScan": "请使用 iCenter 扫码登录", "ui.refreshQr": "刷新二维码", "ui.fallbackLink": "UAC 不可用?应急账号登录", "ui.fallbackLogin": "应急登录", "ui.fallbackDetail": "UAC / 扫码不可用时使用", "ui.username": "用户名", "ui.password": "密码", "ui.login": "登录", "ui.loggingIn": "登录中...", "ui.backToQr": "返回扫码登录", "ui.logout": "退出登录", "ui.userPrefix": "用户", "ui.qrGenerating": "正在生成二维码...", "ui.qrScanPrompt": "请使用 iCenter 扫码登录...", "ui.userInfoFailed": "用户信息查询失败", "ui.loginSuccess": "登录成功!", "ui.missingToken": "缺少 token,无法完成校验", "ui.waitingScan": "等待扫码...", "ui.qrExpired": "二维码已失效", "ui.qrExpiredHint": "请刷新后重新扫描", "ui.loginFailed": "登录失败", "ui.networkError": "网络错误...", "ui.qrGenerateFailed": "生成二维码失败", "ui.cronLoginRequired": "登录后才能使用定时任务", "ui.workspaceCreateForbidden": "只有超级管理员可以创建工作区", "ui.workspaceLoginRequired": "登录后才能使用工作区", "err.not_logged_in": "未登录", "err.forbidden_settings": "当前账号无设置权限", "err.forbidden_manage_users": "只有超级管理员可以管理用户", "err.forbidden_list_users": "只有超级管理员可以查看用户列表", "err.forbidden_set_role": "只有超级管理员可以修改角色", "err.forbidden_add_user": "只有超级管理员可以添加用户", "err.forbidden_remove_user": "只有超级管理员可以删除用户", "err.forbidden_set_fallback": "只有超级管理员可以设置应急密码", "err.forbidden_clear_fallback": "只有超级管理员可以清除应急密码", "err.invalid_role_params": "参数错误: empNo 和 role 必填", "err.emp_no_required": "empNo 必填", "err.username_password_required": "用户名和密码必填", "err.invalid_credentials": "用户名或密码错误", "err.last_super_admin_demote": "系统至少需要 1 个超级管理员,不能降级最后一个", "err.last_super_admin_delete": "系统至少需要 1 个超级管理员,不能删除最后一个", "err.password_too_short": "密码至少 6 位", "err.config_not_ready": "配置未初始化", "err.request_failed": "请求失败", "err.method_not_allowed": "方法不允许", "err.missing_qr_params": "缺少 qrCodeKey 或 qrCodeValue", "err.missing_emp_token": "缺少 empNo 或 token", "err.user_search_failed": "用户搜索失败", "err.not_found": "未找到", "err.internal": "内部错误", "err.login_required_cron": "登录后才能使用定时任务", "err.login_required_session": "登录后才能访问会话", "err.login_required_create_session": "登录后才能创建会话", "err.login_required_workspace": "登录后才能使用工作区", "err.session_forbidden": "无权访问该会话", "err.session_workspace_only": "只能在自己的工作区创建会话", "err.workspace_path_only": "只能打开自己的工作区路径", "err.workspace_create_forbidden": "只有超级管理员可以创建工作区", "err.no_skill_credentials": "请先完成 UAC 扫码登录", "err.loopback_only_credentials": "agent-credentials 仅允许本机访问", "err.loopback_only_outbound": "outbound 仅允许本机访问", "err.url_required": "缺少 url", "err.invalid_url": "无效 url", "err.unsupported_protocol": "不支持的协议", "err.host_not_allowed": "主机不在白名单", "err.upstream_failed": "上游请求失败", "err.skill_credentials_not_ready": "skill 凭证未就绪", "err.outbound_not_ready": "outbound 未就绪", "ok.logged_out": "已退出登录", "ok.config_saved": "配置已保存", "ok.role_updated": "{empNo} 角色已更新为 {role}", "ok.user_added": "{empNo} 已添加为 {role}", "ok.user_removed": "{empNo} 已删除", "ok.fallback_password_set": "应急管理员密码已设置", "ok.fallback_password_cleared": "应急管理员密码已清除", "ok.fallback_login": "应急管理员登录成功" }, "en": { "role.super_admin": "Super admin", "role.admin": "Admin", "role.user": "User", "role.fallback_admin": "Emergency admin", "role.badge.super_admin": "Super", "role.badge.admin": "Admin", "role.badge.fallback_admin": "Emergency", "role.badge.user": "", "ui.settingsTitle": "UAC Auth", "ui.settingsIntro": "EmpNo + token verification; when UAC is down, sign in with emergency account administrator.", "ui.loginRequiredPage": "Sign in to view this page", "ui.roleHint": "Current role: {role}. The first QR login with an empty roles.json becomes super admin; grant admin in User management then re-scan. Set the emergency password before using administrator on the login panel.", "ui.deployConfig": "Deploy config", "ui.userSearchUrl": "User search URL (token verify)", "ui.workspaceRoot": "Workspace root (empty=$DSH_HOME/user-workspaces)", "ui.saveConfig": "Save config", "ui.saving": "Saving...", "ui.configSaved": "Config saved", "ui.saveFailed": "Save failed", "ui.fallbackTitle": "Emergency login (UAC unavailable)", "ui.fallbackStatus": "Status: {status}. Change or disable here. Switch from the login panel only when QR is unavailable.", "ui.enabled": "Enabled", "ui.disabled": "Disabled", "ui.fallbackPassword": "Emergency password (min 6 chars)", "ui.saveFallbackPassword": "Save emergency password", "ui.fallbackPasswordSet": "Emergency password set", "ui.confirmClearFallback": "Clear emergency password?", "ui.clear": "Clear", "ui.userManagement": "User management", "ui.searchEmpNo": "Search empNo", "ui.search": "Search", "ui.loading": "Loading...", "ui.loadFailed": "Load failed", "ui.empNo": "EmpNo", "ui.role": "Role", "ui.actions": "Actions", "ui.noUsers": "No users", "ui.delete": "Delete", "ui.confirmDelete": "Delete {empNo}?", "ui.pager": "{total} users, page {page} / {totalPages}", "ui.prevPage": "Previous", "ui.nextPage": "Next", "ui.add": "Add", "ui.department": "Department", "ui.notLoggedIn": "Not signed in", "ui.pleaseScan": "Scan with iCenter to sign in", "ui.refreshQr": "Refresh QR", "ui.fallbackLink": "UAC down? Emergency account", "ui.fallbackLogin": "Emergency login", "ui.fallbackDetail": "Use when UAC / QR is unavailable", "ui.username": "Username", "ui.password": "Password", "ui.login": "Sign in", "ui.loggingIn": "Signing in...", "ui.backToQr": "Back to QR login", "ui.logout": "Sign out", "ui.userPrefix": "User", "ui.qrGenerating": "Generating QR...", "ui.qrScanPrompt": "Scan with iCenter to sign in...", "ui.userInfoFailed": "User info lookup failed", "ui.loginSuccess": "Signed in!", "ui.missingToken": "Missing token; cannot verify", "ui.waitingScan": "Waiting for scan...", "ui.qrExpired": "QR code expired", "ui.qrExpiredHint": "Refresh and scan again", "ui.loginFailed": "Sign-in failed", "ui.networkError": "Network error...", "ui.qrGenerateFailed": "Failed to generate QR", "ui.cronLoginRequired": "Sign in to use scheduled tasks", "ui.workspaceCreateForbidden": "Only super admins can create workspaces", "ui.workspaceLoginRequired": "Sign in to use workspaces", "err.not_logged_in": "Not signed in", "err.forbidden_settings": "No settings permission", "err.forbidden_manage_users": "Only super admins can manage users", "err.forbidden_list_users": "Only super admins can list users", "err.forbidden_set_role": "Only super admins can change roles", "err.forbidden_add_user": "Only super admins can add users", "err.forbidden_remove_user": "Only super admins can remove users", "err.forbidden_set_fallback": "Only super admins can set the emergency password", "err.forbidden_clear_fallback": "Only super admins can clear the emergency password", "err.invalid_role_params": "Invalid params: empNo and role required", "err.emp_no_required": "empNo required", "err.username_password_required": "Username and password required", "err.invalid_credentials": "Invalid username or password", "err.last_super_admin_demote": "At least one super admin is required; cannot demote the last one", "err.last_super_admin_delete": "At least one super admin is required; cannot delete the last one", "err.password_too_short": "Password must be at least 6 characters", "err.config_not_ready": "Config not initialized", "err.request_failed": "Request failed", "err.method_not_allowed": "Method not allowed", "err.missing_qr_params": "Missing qrCodeKey or qrCodeValue", "err.missing_emp_token": "Missing empNo or token", "err.user_search_failed": "User search failed", "err.not_found": "Not found", "err.internal": "Internal error", "err.login_required_cron": "Sign in to use scheduled tasks", "err.login_required_session": "Sign in to access sessions", "err.login_required_create_session": "Sign in to create a session", "err.login_required_workspace": "Sign in to use workspaces", "err.session_forbidden": "No access to this session", "err.session_workspace_only": "Sessions can only be created in your own workspace", "err.workspace_path_only": "You can only open your own workspace path", "err.workspace_create_forbidden": "Only super admins can create workspaces", "err.no_skill_credentials": "Complete UAC QR sign-in first", "err.loopback_only_credentials": "agent-credentials is loopback-only", "err.loopback_only_outbound": "outbound is loopback-only", "err.url_required": "url required", "err.invalid_url": "invalid url", "err.unsupported_protocol": "unsupported protocol", "err.host_not_allowed": "host not allowed", "err.upstream_failed": "upstream failed", "err.skill_credentials_not_ready": "skill credentials not ready", "err.outbound_not_ready": "outbound not ready", "ok.logged_out": "Signed out", "ok.config_saved": "Config saved", "ok.role_updated": "{empNo} role updated to {role}", "ok.user_added": "{empNo} added as {role}", "ok.user_removed": "{empNo} removed", "ok.fallback_password_set": "Emergency admin password set", "ok.fallback_password_cleared": "Emergency admin password cleared", "ok.fallback_login": "Emergency admin signed in" } } const UDS_HOST_ARIA = { "addWorkspace": [ "添加工作区", "Add workspace" ], "chooseWorkspace": [ "选择工作区", "Choose workspace" ], "sessions": [ "会话", "Sessions" ] } /** Prefer DSH host locale over portal SSO language cookies. */ let localeHost = null let translate = null function normalizeLocale(lang) { const raw = String(lang || '').trim().toLowerCase() if (raw.startsWith('zh')) return 'zh' return 'en' } function getUiLocale() { try { const snap = typeof localeHost?.locale?.getSnapshot === 'function' ? localeHost.locale.getSnapshot() : null const raw = snap?.active || snap?.locale || snap?.preference || localeHost?.locale?.active || (document.documentElement && document.documentElement.lang) || (typeof navigator !== 'undefined' && (navigator.language || navigator.userLanguage)) || '' return normalizeLocale(raw) } catch { return 'en' } } function applyVars(text, vars) { let out = String(text) if (vars && typeof vars === 'object') { for (const k of Object.keys(vars)) { out = out.split('{' + k + '}').join(String(vars[k])) } } return out } function t(key, vars) { if (typeof translate === 'function') { try { const out = translate(key, vars) if (out != null && out !== key) return applyVars(out, vars) } catch { /* fall through */ } } const loc = getUiLocale() const table = UDS_I18N_MESSAGES[loc] || UDS_I18N_MESSAGES.zh const text = (table && table[key]) || (UDS_I18N_MESSAGES.en && UDS_I18N_MESSAGES.en[key]) || (UDS_I18N_MESSAGES.zh && UDS_I18N_MESSAGES.zh[key]) || key return applyVars(text, vars) } function makeTranslator(ctx) { if (typeof ctx?.locale?.bind === 'function') { try { const bound = ctx.locale.bind(LOCALE_NS) if (typeof bound === 'function') { return (key, params) => { try { const out = bound(key, params) if (out != null && out !== key) return out } catch { /* fall through */ } const dict = getUiLocale() === 'zh' ? UDS_I18N_MESSAGES.zh : UDS_I18N_MESSAGES.en return (dict && dict[key]) || UDS_I18N_MESSAGES.en[key] || UDS_I18N_MESSAGES.zh[key] || key } } } catch { /* fall through */ } } return (key) => { const dict = getUiLocale() === 'zh' ? UDS_I18N_MESSAGES.zh : UDS_I18N_MESSAGES.en return (dict && dict[key]) || UDS_I18N_MESSAGES.en[key] || UDS_I18N_MESSAGES.zh[key] || key } } function roleLabel(role) { return t('role.badge.' + String(role || 'user')) || t('role.' + String(role || 'user')) } function apiMessage(err) { const data = err && err.data const code = data && data.error if (code) { const key = String(code).startsWith('err.') ? code : 'err.' + code const translated = t(key) if (translated && translated !== key) return translated } return (data && (data.message || data.error)) || (err && err.message) || t('err.request_failed') } function hostAriaList(key) { return (UDS_HOST_ARIA && UDS_HOST_ARIA[key]) || [] } function hostButtonAriaSel(key) { return hostAriaList(key).map((l) => 'button[aria-label="' + l + '"]').join(',') } function hostAriaSel(key) { return hostAriaList(key).map((l) => '[aria-label="' + l + '"]').join(',') } /* uds-auth-i18n-end */ const CSS = [ '.uds-auth-host{position:relative;display:inline-flex;align-items:center;height:32px;margin:0;flex-shrink:0;pointer-events:auto}.uds-auth-host.is-rail{justify-content:center;width:100%}[data-uds-auth-foot="row"]{display:flex!important;flex-direction:row!important;align-items:center!important;gap:8px;width:100%}[data-uds-auth-foot="row"]>*:nth-child(1){order:2;flex:none!important;width:auto!important;min-width:0;margin-left:auto!important}[data-uds-auth-foot="row"]>*:nth-child(2){order:1;flex:none!important;width:auto!important;min-width:0}', 'html[data-uds-can-settings="0"] [data-uds-auth-foot="row"]>*:not(:has([data-uds-auth-host])){display:none!important}html[data-uds-can-create-ws="0"] button[aria-label="添加工作区"],html[data-uds-can-create-ws="0"] button[aria-label="Add workspace"]{display:none!important}html[data-uds-logged-in="0"] [role="tree"][aria-label="会话"],html[data-uds-logged-in="0"] [role="tree"][aria-label="Sessions"],html[data-uds-logged-in="0"] [class*="WorkspaceBrowser"],html[data-uds-logged-in="0"] [class*="workspaceBrowser"],html[data-uds-logged-in="0"] .dsh-ct-entry,html[data-uds-logged-in="0"] .dsh-ct-region,html[data-uds-logged-in="0"] .dsh-ct-main,html[data-uds-logged-in="0"] [data-dsh-ct-mode="on"] .dsh-ct-region{display:none!important}html[data-uds-can-create-ws="0"] button[aria-label="选择工作区"],html[data-uds-can-create-ws="0"] button[aria-label="Choose workspace"],html[data-uds-can-create-ws="0"] [aria-label="选择工作区"],html[data-uds-can-create-ws="0"] [aria-label="Choose workspace"]{display:none!important}html[data-uds-logged-in="0"] [class*="cardWorkspaceTrigger"],html[data-uds-logged-in="0"] [data-composer-card][class*="cardWorkspaceTrigger"]{pointer-events:none!important;opacity:.45!important;cursor:not-allowed!important}/* uds-anon-hide-workspaces *//* uds-anon-hide-conversation:removed */html[data-uds-logged-in="0"] [class*="WorkspaceBrowser"],html[data-uds-logged-in="0"] [class*="workspaceBrowser"],html[data-uds-logged-in="0"] [class*="workspaceRow"],html[data-uds-logged-in="0"] [class*="WorkspaceRow"]{display:none!important}', /* uds-session-only-sidebar */ 'html[data-uds-can-create-ws="0"][data-uds-logged-in="1"] [class*="projectRow"]:not([class*="dsh-ct-project"]),html[data-uds-can-create-ws="0"][data-uds-logged-in="1"] [class*="ProjectRow"]:not([class*="dsh-ct-project"]){display:none!important}', '.uds-auth-badge{display:inline-flex;align-items:center;justify-content:flex-start;gap:0;max-width:min(160px,42vw);min-width:0;height:32px;padding:0 8px;box-sizing:border-box;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;font-weight:400;line-height:20px;cursor:pointer;overflow:hidden}', '.uds-auth-badge:hover{background:var(--dsw-alias-interactive-bg-hover)}', '.uds-auth-host.is-rail .uds-auth-badge{width:auto;max-width:100%;height:32px;padding:0 6px;border-radius:8px}', '.uds-auth-badge-unauth{color:var(--dsw-alias-label-tertiary,#8f959e)}', '.uds-auth-avatar{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;flex:none;font-size:10px;line-height:1;color:var(--dsw-alias-label-secondary,#646a73);background:transparent;border:none}', '.uds-auth-badge-unauth .uds-auth-avatar{background:var(--dsw-alias-bg-module-platform,rgba(242,243,245,1));color:var(--dsw-alias-label-tertiary,#8f959e)}', '.uds-auth-badge-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', '.uds-auth-role{font-size:10px;padding:1px 6px;border-radius:10px;font-weight:600;flex-shrink:0}', '.uds-auth-role-super_admin{background:rgba(213,73,65,.12);color:var(--dsw-alias-state-error-primary,#d54941)}', '.uds-auth-role-admin{background:rgba(217,119,6,.12);color:var(--dsw-alias-state-warn-primary,#d97706)}', '.uds-auth-role-fallback_admin{background:rgba(51,112,255,.12);color:var(--dsw-alias-state-business-primary,#3370ff)}', '.uds-auth-panel{position:fixed;z-index:1200;width:min(300px,calc(100vw - 24px));max-height:min(70vh,560px);overflow:auto;top:auto;background:var(--dsw-alias-bg-layer-1,#fff);border-radius:8px;border:1px solid var(--dsw-alias-border-l2,#dee0e3);box-shadow:0 8px 28px rgba(0,0,0,.12)}', '.uds-auth-info{padding:14px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,#eef0f3)}', '.uds-auth-info-name{font-weight:600;margin-bottom:4px;color:var(--dsw-alias-label-primary,#1f2329);font-size:14px}', '.uds-auth-info-detail{font-size:12px;color:var(--dsw-alias-label-secondary,#646a73);margin-top:2px;display:flex;justify-content:space-between;gap:8px}', '.uds-auth-info-detail-label{color:var(--dsw-alias-label-tertiary,#8f959e)}', '.uds-auth-qr{padding:16px;text-align:center}', '.uds-auth-qr-frame{position:relative;width:160px;height:160px;margin:0 auto;background:var(--dsw-alias-bg-layer-1,#fff);box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#dee0e3);border-radius:4px;overflow:hidden}', '.uds-auth-qr-frame img{display:block;width:100%;height:100%;object-fit:contain}', '.uds-auth-qr-frame.is-expired img{opacity:.18;filter:grayscale(1)}', '.uds-auth-qr-overlay{position:absolute;inset:0;z-index:3;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:10px;box-sizing:border-box;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#fff) 82%, transparent)}', '.uds-auth-qr-overlay-title{font-size:15px;font-weight:700;line-height:1.3;color:var(--dsw-alias-label-primary,#1f2329)}', '.uds-auth-qr-overlay-hint{font-size:12px;line-height:1.3;color:var(--dsw-alias-label-secondary,#646a73);margin-bottom:2px}', '.uds-auth-qr-refresh{width:40px;height:40px;border:none;border-radius:50%;background:var(--dsw-alias-state-business-primary,#3370ff);color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0;flex:none}', '.uds-auth-qr-refresh:hover{opacity:.9}', '.uds-auth-qr-refresh:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3370ff);outline-offset:2px}', '.uds-auth-qr-status{font-size:12px;color:var(--dsw-alias-label-secondary,#646a73);margin-top:8px}', '.uds-auth-btn{display:block;width:calc(100% - 32px);margin:8px 16px 0;padding:8px 12px;border-radius:4px;cursor:pointer;font-size:13px;text-align:left;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:var(--dsw-alias-label-primary,#1f2329)}', '.uds-auth-btn:hover{background:var(--dsw-alias-interactive-bg-hover,#f7f8fa)}', '.uds-auth-btn-primary{background:var(--dsw-alias-state-business-primary,#3370ff);color:#fff;border-color:transparent;text-align:center}', '.uds-auth-btn-primary:hover{opacity:.9}', '.uds-auth-btn-danger{margin-bottom:16px;background:rgba(213,73,65,.08);color:var(--dsw-alias-state-error-primary,#d54941);border-color:rgba(213,73,65,.2)}', '.uds-auth-btn-link{background:transparent;border:none;color:var(--dsw-alias-state-business-primary,#3370ff);text-align:center;padding:4px 12px;margin:4px 16px 12px;width:calc(100% - 32px);font-size:12px;cursor:pointer}', '.uds-auth-fallback{padding:12px 16px 16px}', '.uds-auth-fallback label{display:block;font-size:12px;color:var(--dsw-alias-label-secondary,#646a73);margin:8px 0 4px}', '.uds-auth-fallback input{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:6px;font-size:13px}', '.uds-auth-fallback-hint{font-size:11px;color:var(--dsw-alias-label-tertiary,#8f959e);margin:8px 0 0;line-height:1.4}', '.uds-auth-table{width:100%;border-collapse:collapse;font-size:12px}', '.uds-auth-table th,.uds-auth-table td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--dsw-alias-border-l1,#eef0f3)}', '.uds-auth-add{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}', '.uds-auth-add input,.uds-auth-add select{padding:6px 8px;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:4px;font-size:12px}', '.uds-auth-add input{flex:1;min-width:120px}', '.uds-auth-del{padding:3px 8px;background:rgba(213,73,65,.1);color:var(--dsw-alias-state-error-primary,#d54941);border:1px solid rgba(213,73,65,.2);border-radius:3px;cursor:pointer;font-size:11px}', '.uds-auth-pager{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:12px;font-size:12px;color:var(--dsw-alias-label-secondary,#646a73);flex-wrap:wrap}', '.uds-auth-pager button{padding:4px 10px;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:4px;background:var(--dsw-alias-bg-module-platform,#f4f5f7);cursor:pointer;font-size:12px}', '.uds-auth-pager button:disabled{opacity:.4;cursor:not-allowed}', '.uds-auth-search{display:flex;gap:8px;margin-bottom:12px}', '.uds-auth-search input{flex:1;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:6px;font-size:13px}', '.uds-auth-settings{box-sizing:border-box;display:flex;flex-direction:column;gap:20px;min-height:0;padding:4px 4px 24px;color:var(--dsw-alias-label-primary,#1f2329)}', '.uds-auth-settings h2{margin:0;font-size:20px;font-weight:600;line-height:28px}', '.uds-auth-settings-intro{margin:4px 0 0;font-size:13px;color:var(--dsw-alias-label-secondary,#646a73)}', '.uds-auth-settings-card{border:1px solid var(--dsw-alias-border-l2,#dee0e3);border-radius:12px;padding:16px;background:var(--dsw-alias-bg-layer-1,transparent)}', '.uds-auth-settings-card h3{margin:0 0 12px;font-size:15px;font-weight:600}', '.uds-auth-settings-field{display:flex;flex-direction:column;gap:4px;margin-bottom:12px}', '.uds-auth-settings-field label{font-size:12px;color:var(--dsw-alias-label-secondary,#646a73)}', '.uds-auth-settings-field input{padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:6px;font-size:13px;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:inherit}', '.uds-auth-settings-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}', '.uds-auth-settings-msg{font-size:12px;color:var(--dsw-alias-label-secondary,#646a73)}', '.uds-auth-settings-msg.ok{color:var(--dsw-alias-state-success-primary,#20a162)}', '.uds-auth-settings-msg.err{color:var(--dsw-alias-state-error-primary,#d54941)}', '.uds-auth-settings-empty{padding:24px;text-align:center;color:var(--dsw-alias-label-tertiary,#8f959e);font-size:13px}', ].join('') function getCookie(cookieName) { const prefix = cookieName + '=' const parts = String(document.cookie || '').split(';') for (const part of parts) { const v = part.trim() if (v.startsWith(prefix)) return decodeURIComponent(v.slice(prefix.length)) } return null } function setCookie(cookieName, value, days) { const d = new Date() d.setTime(d.getTime() + days * 86400000) document.cookie = encodeURIComponent(cookieName) + '=' + encodeURIComponent(value) + ';expires=' + d.toUTCString() + ';path=/' } function clearAuthCookies() { const names = ['PORTALSSOUser', 'PORTALSSOCookie', 'ZTEDPGSSOUser', 'ZTEDPGSSOCookie', 'UDS_FALLBACK_USER', 'UDS_FALLBACK_UI'] for (const key of names) { // Match both Secure and non-Secure variants; HttpOnly ones need server clear. document.cookie = encodeURIComponent(key) + '=; Max-Age=0; Path=/; SameSite=Lax' document.cookie = encodeURIComponent(key) + '=; Max-Age=0; Path=/; SameSite=Lax; Secure' document.cookie = encodeURIComponent(key) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/' } } function getEmpNo() { return getCookie('PORTALSSOUser') || getCookie('ZTEDPGSSOUser') || getCookie('UDS_FALLBACK_USER') || getCookie('UDS_FALLBACK_UI') || null } /** Prefer /api/me-driven attr: fallback login cookie is HttpOnly and invisible to document.cookie. */ function isUdsAuthReady() { return document.documentElement.getAttribute('data-uds-auth-ready') === '1' } /** True only after /api/me confirmed a user — portal empNo cookie alone does not count. */ function isLoggedInInUi() { return document.documentElement.getAttribute('data-uds-logged-in') === '1' } function clearSessionIfAnonymous(sessions) { // Until /api/me confirms login, treat as anonymous (AuthBadge refreshes me on mount). if (isLoggedInInUi()) return try { if (sessions && typeof sessions.clear === 'function') sessions.clear() } catch { /* ignore */ } try { window.localStorage.removeItem('dsh.sessions.current') } catch { /* ignore */ } // Navigation may auto-reopen; clear again on next ticks while still anonymous. try { const snap = sessions && sessions.list && typeof sessions.list.getSnapshot === 'function' ? sessions.list.getSnapshot() : null if (snap && snap.current != null) { setTimeout(() => { if (isLoggedInInUi()) return try { sessions.clear() } catch { /* ignore */ } try { window.localStorage.removeItem('dsh.sessions.current') } catch { /* ignore */ } }, 0) setTimeout(() => { if (isLoggedInInUi()) return try { sessions.clear() } catch { /* ignore */ } }, 350) } } catch { /* ignore */ } } const VIEW_STORE_KEY = 'dsh.workspace.view.v5' const FORCED_FLAT_KEY = 'uds-auth-forced-flat' function readViewState() { try { const raw = window.localStorage.getItem(VIEW_STORE_KEY) const state = raw ? JSON.parse(raw) : null if (state && typeof state === 'object') return state } catch { /* ignore */ } return { groupBy: 'workspace', orderBy: 'updated', groupExpansion: {}, sessionOrderByAccount: {}, sessionUpdatedAtByAccount: {}, } } function writeViewState(state) { window.localStorage.setItem(VIEW_STORE_KEY, JSON.stringify(state)) } /** user/admin only — wait until /api/me set can-create-ws explicitly to 0. */ function ensureFlatSessionSidebar() { try { const root = document.documentElement if (root.getAttribute('data-uds-auth-ready') !== '1') return false if (root.getAttribute('data-uds-logged-in') !== '1') return false // Must be explicit 0 — missing/default must not force flat for supers. if (root.getAttribute('data-uds-can-create-ws') !== '0') return false const state = readViewState() if (state.groupBy === 'flat') { try { window.localStorage.setItem(FORCED_FLAT_KEY, '1') } catch { /* ignore */ } return false } state.groupBy = 'flat' writeViewState(state) try { window.localStorage.setItem(FORCED_FLAT_KEY, '1') } catch { /* ignore */ } return true } catch { return false } } /** super/fallback: restore workspace partitions after a forced flat. */ function ensureWorkspaceGroupedSidebar() { try { const root = document.documentElement if (root.getAttribute('data-uds-auth-ready') !== '1') return false if (root.getAttribute('data-uds-logged-in') !== '1') return false if (root.getAttribute('data-uds-can-create-ws') !== '1') return false const state = readViewState() if (state.groupBy === 'workspace') { try { window.localStorage.removeItem(FORCED_FLAT_KEY) } catch { /* ignore */ } return false } state.groupBy = 'workspace' writeViewState(state) try { window.localStorage.removeItem(FORCED_FLAT_KEY) } catch { /* ignore */ } return true } catch { return false } } function expandHiddenWorkspaceGroups() { try { if (document.documentElement.getAttribute('data-uds-auth-ready') !== '1') return if (document.documentElement.getAttribute('data-uds-logged-in') !== '1') return if (document.documentElement.getAttribute('data-uds-can-create-ws') !== '0') return document.querySelectorAll('[class*="projectRow"][aria-expanded="false"]:not([class*="dsh-ct-project"])').forEach((el) => { try { el.click() } catch { /* ignore */ } }) } catch { /* ignore */ } } function reloadAfterLogin() { // Login Set-Cookie must land before WS upgrade — hard reload is required. try { ensureFlatSessionSidebar() } catch { /* ignore */ } try { window.location.reload() } catch { /* ignore */ } } /** Soft WS reconnect (logout / auth flip). Prefer this over full reload. */ function softReconnectAuth() { try { if (typeof window.__udsAuthReconnect === 'function') { window.__udsAuthReconnect() return } } catch { /* fall through */ } try { window.location.reload() } catch { /* ignore */ } } // Back-compat alias used by older call sites in this file. function reconnectAfterLogin() { reloadAfterLogin() } function getAuthToken() { return getCookie('PORTALSSOCookie') || getCookie('ZTEDPGSSOCookie') || null } async function fetchJson(url, options) { const res = await fetch(url, options) const data = await res.json().catch(() => ({})) if (!res.ok) { const err = new Error(data.message || data.error || res.statusText || t('err.request_failed')) err.status = res.status err.data = data throw err } return data } function useLocaleTick() { const [tick, setTick] = useState(0) useEffect(() => { const bump = () => setTick((n) => n + 1) const offs = [] try { const off = localeHost?.on?.('locale/change', bump) if (typeof off === 'function') offs.push(off) } catch { /* ignore */ } try { const off = localeHost?.locale?.subscribe?.(bump) if (typeof off === 'function') offs.push(off) } catch { /* ignore */ } let last = typeof document !== 'undefined' ? document.documentElement?.lang : '' const timer = setInterval(() => { const next = document.documentElement?.lang || '' if (next !== last) { last = next bump() } }, 800) return () => { for (const off of offs) { try { off() } catch { /* ignore */ } } clearInterval(timer) } }, []) return tick } function useOutsideClose(ref, open, setOpen) { useEffect(() => { if (!open) return undefined const onPointer = (event) => { if (ref.current && !ref.current.contains(event.target)) setOpen(false) } document.addEventListener('pointerdown', onPointer, true) return () => document.removeEventListener('pointerdown', onPointer, true) }, [open, ref, setOpen]) } function UserManagementPanel() { useLocaleTick() const [users, setUsers] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [totalPages, setTotalPages] = useState(1) const [q, setQ] = useState('') const [qDraft, setQDraft] = useState('') const [newEmpNo, setNewEmpNo] = useState('') const [newRole, setNewRole] = useState('user') const [error, setError] = useState('') const [loading, setLoading] = useState(true) const reload = useCallback(async () => { setLoading(true) setError('') try { const qs = new URLSearchParams({ page: String(page), pageSize: String(PAGE_SIZE), }) if (q) qs.set('q', q) const data = await fetchJson('/uds-auth/api/users?' + qs.toString()) setUsers(data.users || []) setTotal(data.total || 0) setTotalPages(data.totalPages || 1) } catch (err) { setError(apiMessage(err) || t('ui.loadFailed')) setUsers([]) } finally { setLoading(false) } }, [page, q]) useEffect(() => { reload() }, [reload]) return h('div', { className: 'uds-auth-settings-card' }, h('h3', null, t('ui.userManagement')), h('div', { className: 'uds-auth-search' }, h('input', { value: qDraft, placeholder: t('ui.searchEmpNo'), onChange: (e) => setQDraft(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') { setPage(1); setQ(qDraft.trim()) } }, }), h('button', { type: 'button', className: 'uds-auth-btn uds-auth-btn-primary', style: { width: 'auto', margin: 0 }, onClick: () => { setPage(1); setQ(qDraft.trim()) }, }, t('ui.search')), ), error && h('div', { className: 'uds-auth-settings-msg err', role: 'alert' }, error), loading ? h('div', { className: 'uds-auth-settings-empty' }, t('ui.loading')) : h('table', { className: 'uds-auth-table' }, h('thead', null, h('tr', null, h('th', null, t('ui.empNo')), h('th', null, t('ui.role')), h('th', null, t('ui.actions')))), h('tbody', null, users.length === 0 ? h('tr', null, h('td', { colSpan: 3 }, t('ui.noUsers'))) : users.map((u) => h('tr', { key: u.empNo }, h('td', null, u.empNo), h('td', null, h('select', { value: u.role, onChange: async (e) => { try { await fetchJson('/uds-auth/api/users/role', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ empNo: u.empNo, role: e.target.value }), }) reload() } catch (err) { window.alert(apiMessage(err)) } }, }, h('option', { value: 'user' }, t('role.user')), h('option', { value: 'admin' }, t('role.admin')), h('option', { value: 'super_admin' }, t('role.super_admin')), ), ), h('td', null, h('button', { type: 'button', className: 'uds-auth-del', onClick: async () => { if (!window.confirm(t('ui.confirmDelete', { empNo: u.empNo }))) return try { await fetchJson('/uds-auth/api/users/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ empNo: u.empNo }), }) reload() } catch (err) { window.alert(apiMessage(err)) } }, }, t('ui.delete')), ), )), ), ), h('div', { className: 'uds-auth-pager' }, h('span', null, t('ui.pager', { total, page, totalPages })), h('span', null, h('button', { type: 'button', disabled: page <= 1 || loading, onClick: () => setPage((p) => Math.max(1, p - 1)), }, t('ui.prevPage')), ' ', h('button', { type: 'button', disabled: page >= totalPages || loading, onClick: () => setPage((p) => p + 1), }, t('ui.nextPage')), ), ), h('div', { className: 'uds-auth-add' }, h('input', { value: newEmpNo, placeholder: t('ui.empNo'), onChange: (e) => setNewEmpNo(e.target.value) }), h('select', { value: newRole, onChange: (e) => setNewRole(e.target.value) }, h('option', { value: 'user' }, t('role.user')), h('option', { value: 'admin' }, t('role.admin')), h('option', { value: 'super_admin' }, t('role.super_admin')), ), h('button', { type: 'button', className: 'uds-auth-btn uds-auth-btn-primary', style: { width: 'auto', margin: 0 }, onClick: async () => { const empNo = newEmpNo.trim() if (!empNo) return try { await fetchJson('/uds-auth/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ empNo, role: newRole }), }) setNewEmpNo('') reload() } catch (err) { window.alert(apiMessage(err)) } }, }, t('ui.add')), ), ) } function AuthSettingsSection() { useLocaleTick() const [me, setMe] = useState(null) const [form, setForm] = useState({ uacBaseUrl: '', userSearchUrl: '', loginSystemCode: '', originSystemCode: '', workspaceRoot: '', }) const [fallbackEnabled, setFallbackEnabled] = useState(false) const [fallbackPwd, setFallbackPwd] = useState('') const [msg, setMsg] = useState('') const [msgKind, setMsgKind] = useState('') const [busy, setBusy] = useState(false) useEffect(() => { let cancelled = false ;(async () => { try { const data = await fetchJson('/uds-auth/api/me') if (!cancelled) setMe(data?.data || null) } catch { if (!cancelled) setMe(null) } try { const cfg = await fetchJson('/uds-auth/config.get') if (!cancelled && cfg?.value) setForm((prev) => ({ ...prev, ...cfg.value })) } catch { /* ignore */ } try { const st = await fetchJson('/uds-auth/api/fallback/status') if (!cancelled) setFallbackEnabled(!!st.enabled) } catch { /* ignore */ } })() return () => { cancelled = true } }, []) const perms = me?.permissions || {} const canManage = !!perms.canManageUsers const canSettings = !!perms.canAccessSettings const saveConfig = async () => { setBusy(true) setMsg('') try { await fetchJson('/uds-auth/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }) setMsgKind('ok') setMsg(t('ui.configSaved')) } catch (err) { setMsgKind('err') setMsg(apiMessage(err) || t('ui.saveFailed')) } finally { setBusy(false) } } const field = (key, label) => h('div', { className: 'uds-auth-settings-field' }, h('label', { htmlFor: 'uds-auth-' + key }, label), h('input', { id: 'uds-auth-' + key, value: form[key] || '', disabled: !canSettings || busy, onChange: (e) => setForm((prev) => ({ ...prev, [key]: e.target.value })), }), ) return h('section', { className: 'uds-auth-settings', 'aria-label': t('ui.settingsTitle') }, h('header', null, h('h2', null, t('ui.settingsTitle')), h('p', { className: 'uds-auth-settings-intro' }, t('ui.settingsIntro')), ), !me && h('div', { className: 'uds-auth-settings-empty' }, t('ui.loginRequiredPage')), me && !canSettings && !canManage && h('div', { className: 'uds-auth-settings-empty' }, t('ui.roleHint', { role: me.role || 'user' }) ), canSettings && h('div', { className: 'uds-auth-settings-card' }, h('h3', null, t('ui.deployConfig')), field('uacBaseUrl', 'UAC Base URL'), field('userSearchUrl', t('ui.userSearchUrl')), field('loginSystemCode', 'loginSystemCode'), field('originSystemCode', 'originSystemCode'), field('workspaceRoot', t('ui.workspaceRoot')), h('div', { className: 'uds-auth-settings-actions' }, h('button', { type: 'button', className: 'uds-auth-btn uds-auth-btn-primary', style: { width: 'auto', margin: 0 }, disabled: busy, onClick: saveConfig, }, busy ? t('ui.saving') : t('ui.saveConfig')), msg && h('span', { className: 'uds-auth-settings-msg ' + msgKind }, msg), ), ), canManage && h('div', { className: 'uds-auth-settings-card' }, h('h3', null, t('ui.fallbackTitle')), h('p', { className: 'uds-auth-settings-intro' }, t('ui.fallbackStatus', { status: fallbackEnabled ? t('ui.enabled') : t('ui.disabled') })), h('div', { className: 'uds-auth-settings-field' }, h('label', { htmlFor: 'uds-auth-fallback-pwd' }, t('ui.fallbackPassword')), h('input', { id: 'uds-auth-fallback-pwd', type: 'password', value: fallbackPwd, onChange: (e) => setFallbackPwd(e.target.value), autoComplete: 'new-password', }), ), h('div', { className: 'uds-auth-settings-actions' }, h('button', { type: 'button', className: 'uds-auth-btn uds-auth-btn-primary', style: { width: 'auto', margin: 0 }, onClick: async () => { try { await fetchJson('/uds-auth/api/fallback/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: fallbackPwd }), }) setFallbackPwd('') setFallbackEnabled(true) window.alert(t('ui.fallbackPasswordSet')) } catch (err) { window.alert(apiMessage(err)) } }, }, t('ui.saveFallbackPassword')), fallbackEnabled && h('button', { type: 'button', className: 'uds-auth-btn uds-auth-btn-danger', style: { width: 'auto', margin: 0 }, onClick: async () => { if (!window.confirm(t('ui.confirmClearFallback'))) return try { await fetchJson('/uds-auth/api/fallback/clear', { method: 'POST' }) setFallbackEnabled(false) } catch (err) { window.alert(apiMessage(err)) } }, }, t('ui.clear')), ), ), canManage && h(UserManagementPanel, null), ) } function AuthBadge(props = {}) { useLocaleTick() const wide = props.wide !== false const rootRef = useRef(null) useLayoutEffect(() => { const host = rootRef.current if (!host) return undefined // Climb to the official footArea that holds footer.action + sidebar.settings. let footArea = null for (let el = host.parentElement; el && el !== document.body; el = el.parentElement) { if (el.childElementCount < 2) continue const mine = [...el.children].some((c) => c.contains(host)) const other = [...el.children].some((c) => !c.contains(host)) if (mine && other) { footArea = el; break } } if (!footArea) return undefined footArea.setAttribute('data-uds-auth-foot', 'row') return () => { footArea.removeAttribute('data-uds-auth-foot') } }, []) const [open, setOpen] = useState(false) const [anchor, setAnchor] = useState(null) const [user, setUser] = useState(null) const [loading, setLoading] = useState(false) const [config, setConfig] = useState({ loginSystemCode: '100000455558', originSystemCode: '' }) const [qrStatus, setQrStatus] = useState('') const [qrImg, setQrImg] = useState('') const [qrExpired, setQrExpired] = useState(false) const [qrFailed, setQrFailed] = useState(false) const [loginMode, setLoginMode] = useState('qr') // Default true (server enables fallback by default). If status fetch fails // while QR is also down, keep the emergency link visible so admins can still sign in. const [fallbackEnabled, setFallbackEnabled] = useState(true) const [fbUser, setFbUser] = useState('administrator') const [fbPass, setFbPass] = useState('') const [fbBusy, setFbBusy] = useState(false) const [fbErr, setFbErr] = useState('') const qrRef = useRef({ key: null, value: null, timer: null, timeout: null, deadline: 0 }) const QR_TIMEOUT_MS = 60 * 1000 useEffect(() => { const perms = user?.permissions || {} const canSettings = user ? !!perms.canAccessSettings : false const canCreateWs = user ? !!perms.canCreateWorkspace : false const prev = document.documentElement.getAttribute('data-uds-logged-in') document.documentElement.setAttribute('data-uds-logged-in', user ? '1' : '0') document.documentElement.setAttribute('data-uds-can-settings', canSettings ? '1' : '0') document.documentElement.setAttribute('data-uds-can-create-ws', canCreateWs ? '1' : '0') // Drop stale remote.mux identity after logout so workspace names disappear. if (prev === '1' && !user) { clearSessionIfAnonymous(window.__udsAuthSessions) try { softReconnectAuth() } catch { /* ignore */ } } return () => { // Keep locked while remounting; do not leave attrs missing (CSS/click lock needs "0"). document.documentElement.setAttribute('data-uds-logged-in', '0') document.documentElement.setAttribute('data-uds-can-settings', '0') document.documentElement.setAttribute('data-uds-can-create-ws', '0') } }, [user]) /* uds-auth: logout-reconnect-on-null */ const stopQr = useCallback(() => { if (qrRef.current.timer) { clearInterval(qrRef.current.timer); qrRef.current.timer = null } if (qrRef.current.timeout) { clearTimeout(qrRef.current.timeout); qrRef.current.timeout = null } qrRef.current.key = null qrRef.current.value = null qrRef.current.deadline = 0 }, []) const markQrExpired = useCallback(() => { stopQr() setQrFailed(false) setQrExpired(true) setQrStatus(t('ui.qrExpired')) }, [stopQr]) const markQrFailed = useCallback((message) => { stopQr() setQrExpired(false) setQrFailed(true) setQrImg('') setQrStatus(message || t('ui.qrGenerateFailed')) }, [stopQr]) const refreshUser = useCallback(async () => { try { const me = await fetchJson('/uds-auth/api/me') if (!me?.authenticated || !me?.data) { setUser(null) window.dispatchEvent(new Event('uds-auth-changed')) return false } const d = me.data setUser({ empNo: d.empNo || d.userId, userName: d.displayName || d.username || d.userName || d.empNo, department: d.department || '', email: d.email || '', phone: d.phone || '', role: d.role || 'user', permissions: d.permissions || {}, }) window.dispatchEvent(new Event('uds-auth-changed')) return true } catch { setUser(null) window.dispatchEvent(new Event('uds-auth-changed')) return false } finally { document.documentElement.setAttribute('data-uds-auth-ready', '1') setLoading(false) } }, []) const startQr = useCallback(async () => { stopQr() setQrExpired(false) setQrFailed(false) setQrStatus(t('ui.qrGenerating')) setQrImg('') // Me check in background — never block QR paint on /api/me. void refreshUser().then((loggedIn) => { if (!loggedIn) return stopQr() setQrStatus('') setQrImg('') setOpen(false) }).catch(() => {}) try { const started = await fetchJson('/uds-auth/qr-start') // Already signed in while QR was starting — drop the code. if (document.documentElement.getAttribute('data-uds-logged-in') === '1') { stopQr() setQrStatus('') setQrImg('') return } const { qrCodeStr, qrCodeKey, qrCodeValue, loginSystemCode, originSystemCode } = started qrRef.current.key = qrCodeKey qrRef.current.value = qrCodeValue qrRef.current.deadline = Date.now() + QR_TIMEOUT_MS setQrImg('/uds-auth/qr?data=' + encodeURIComponent(qrCodeStr)) setQrStatus(t('ui.qrScanPrompt')) qrRef.current.timeout = setTimeout(() => { if (!qrRef.current.key) return markQrExpired() }, QR_TIMEOUT_MS) qrRef.current.timer = setInterval(async () => { if (!qrRef.current.key) return if (qrRef.current.deadline && Date.now() >= qrRef.current.deadline) { markQrExpired() return } try { const verify = await fetchJson( '/uds-auth/verify-code?qrCodeKey=' + encodeURIComponent(qrRef.current.key) + '&qrCodeValue=' + encodeURIComponent(qrRef.current.value) + '&loginClientIp=' + encodeURIComponent('127.0.0.1') + '&loginSystemCode=' + encodeURIComponent(loginSystemCode || config.loginSystemCode) + '&originSystemCode=' + encodeURIComponent(originSystemCode || config.originSystemCode || ''), ) const result = await fetchJson('/uds-auth/qr-proxy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qrCodeKey: qrRef.current.key, qrCodeValue: qrRef.current.value, loginClientIp: '127.0.0.1', originSystemCode: originSystemCode || config.originSystemCode || '', loginSystemCode: loginSystemCode || config.loginSystemCode, verifyCode: verify.verifyCode, }), }) const codeCode = result.code?.code || '' const boCode = result.bo?.code || '' if (codeCode === '0000' && boCode === '0000') { stopQr() setQrExpired(false) setQrFailed(false) const other = result.other || {} const empNo = other.account || other.empNo || '' const token = other.token || other.authValue || '' if (empNo && token) { setCookie('PORTALSSOUser', empNo, 7) setCookie('PORTALSSOCookie', token, 7) try { const info = await fetchJson( '/uds-auth/user-info?empNo=' + encodeURIComponent(empNo) + '&token=' + encodeURIComponent(token), ) const ic = info?.code?.code ?? info?.code const list = Array.isArray(info?.bo) ? info.bo : Array.isArray(info?.bo?.rows) ? info.bo.rows : Array.isArray(info?.bo?.list) ? info.bo.list : [] if ((ic !== '0000' && ic !== 0 && ic !== '0') || !list.length) { setQrStatus(t('ui.userInfoFailed')) console.warn('[uds-auth] user-info after QR failed', info) return } } catch (err) { setQrStatus(t('ui.userInfoFailed') + ': ' + (err.message || err)) return } setQrStatus(t('ui.loginSuccess')) await refreshUser() setOpen(false) reconnectAfterLogin() } else { setQrStatus(t('ui.missingToken')) } return } if (codeCode === '0000' && boCode === '4002') { setQrStatus(t('ui.waitingScan')) return } if (codeCode === '0000' && boCode === '1002') { markQrExpired() return } stopQr() setQrExpired(false) setQrFailed(false) setQrStatus(result.bo?.msg || boCode || t('ui.loginFailed')) } catch { setQrStatus(t('ui.networkError')) } }, 2000) } catch (err) { markQrFailed(err.message || t('ui.qrGenerateFailed')) } }, [config.loginSystemCode, config.originSystemCode, markQrExpired, markQrFailed, refreshUser, stopQr]) const submitFallback = useCallback(async () => { setFbBusy(true) setFbErr('') try { await fetchJson('/uds-auth/api/fallback/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: fbUser.trim(), password: fbPass }), }) setFbPass('') await refreshUser() setOpen(false) reconnectAfterLogin() } catch (err) { setFbErr(apiMessage(err) || t('ui.loginFailed')) } finally { setFbBusy(false) } }, [fbUser, fbPass, refreshUser]) useEffect(() => { // Restore session after login reload / refresh — cookie alone does not set the badge. // (Previously me ran only inside startQr, so a click on「未登录」was required.) void refreshUser() fetchJson('/uds-auth/config.get') .then((data) => { if (data?.value) setConfig((prev) => ({ ...prev, ...data.value })) }) .catch(() => {}) fetchJson('/uds-auth/api/fallback/status') .then((st) => setFallbackEnabled(!!st.enabled)) .catch(() => { /* keep default true — do not hide emergency login */ }) return () => { stopQr() } }, [refreshUser, stopQr]) useEffect(() => { if (!open || user) return undefined // Refresh status each time the panel opens (mount may have raced/failed). let cancelled = false fetchJson('/uds-auth/api/fallback/status') .then((st) => { if (!cancelled) setFallbackEnabled(!!st.enabled) }) .catch(() => {}) return () => { cancelled = true } }, [open, user]) useEffect(() => { if (open && !user && loginMode === 'qr') startQr() if (!open || loginMode !== 'qr') stopQr() }, [open, user, loginMode, startQr, stopQr]) useLayoutEffect(() => { if (!open) { setAnchor(null); return undefined } const place = () => { const rect = rootRef.current?.getBoundingClientRect() if (!rect) return const width = Math.min(300, window.innerWidth - 24) // Keep panel aligned to the badge (footer), not mid-sidebar. const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)) const gap = 8 const bottom = Math.max(8, window.innerHeight - rect.top + gap) setAnchor({ left, bottom, width }) } place() window.addEventListener('resize', place) window.addEventListener('scroll', place, true) return () => { window.removeEventListener('resize', place) window.removeEventListener('scroll', place, true) } }, [open]) useOutsideClose(rootRef, open, setOpen) const displayName = user ? (user.userName || user.name || (t('ui.userPrefix') + user.empNo)) : t('ui.notLoggedIn') const initials = String(displayName).slice(0, 2).toUpperCase() const label = roleLabel(user?.role) const perms = user?.permissions || {} const qrOverlay = qrExpired || qrFailed const qrOverlayTitle = qrExpired ? t('ui.qrExpired') : (qrStatus || t('ui.qrGenerateFailed')) const qrOverlayHint = t('ui.qrExpiredHint') return h('div', { ref: rootRef, className: 'uds-auth-host' + (wide ? '' : ' is-rail'), 'data-uds-auth-host': 'sidebar-footer', }, open && anchor && h('section', { className: 'uds-auth-panel', style: { left: anchor.left, bottom: anchor.bottom, top: 'auto', width: anchor.width, }, 'aria-label': 'UAC', }, !user ? (loginMode === 'qr' ? h(React.Fragment, null, h('div', { className: 'uds-auth-info' }, h('div', { className: 'uds-auth-info-name' }, qrOverlay ? qrOverlayTitle : t('ui.notLoggedIn')), h('div', { className: 'uds-auth-info-detail' }, qrOverlay ? qrOverlayHint : t('ui.pleaseScan')), ), h('div', { className: 'uds-auth-qr' }, (qrImg || qrOverlay) && h('div', { className: 'uds-auth-qr-frame' + (qrOverlay ? ' is-expired' : ''), }, qrImg ? h('img', { src: qrImg, alt: 'QR' }) : h('div', { style: { width: '100%', height: '100%', background: 'var(--dsw-alias-bg-module-platform, #f4f5f7)', }, }), qrOverlay && h('div', { className: 'uds-auth-qr-overlay' }, h('div', { className: 'uds-auth-qr-overlay-title' }, qrOverlayTitle), h('div', { className: 'uds-auth-qr-overlay-hint' }, qrOverlayHint), h('button', { type: 'button', className: 'uds-auth-qr-refresh', 'aria-label': t('ui.refreshQr'), title: t('ui.refreshQr'), onClick: startQr, }, h('svg', { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', }, h('path', { d: 'M21 12a9 9 0 1 1-2.6-6.2' }), h('polyline', { points: '21 3 21 9 15 9' }), ), ), ), ), !qrOverlay && h('div', { className: 'uds-auth-qr-status' }, qrStatus || t('ui.loading')), ), // Always offer emergency login when QR failed/expired; otherwise only if enabled. (fallbackEnabled || qrFailed || qrExpired) && h('button', { type: 'button', className: 'uds-auth-btn-link', onClick: () => { stopQr(); setLoginMode('fallback'); setFbErr('') }, }, t('ui.fallbackLink')), ) : h(React.Fragment, null, h('div', { className: 'uds-auth-info' }, h('div', { className: 'uds-auth-info-name' }, t('ui.fallbackLogin')), h('div', { className: 'uds-auth-info-detail' }, t('ui.fallbackDetail')), ), h('div', { className: 'uds-auth-fallback' }, h('label', { htmlFor: 'uds-fb-user' }, t('ui.username')), h('input', { id: 'uds-fb-user', value: fbUser, onChange: (e) => setFbUser(e.target.value), autoComplete: 'username', }), h('label', { htmlFor: 'uds-fb-pass' }, t('ui.password')), h('input', { id: 'uds-fb-pass', type: 'password', value: fbPass, onChange: (e) => setFbPass(e.target.value), autoComplete: 'current-password', onKeyDown: (e) => { if (e.key === 'Enter') submitFallback() }, }), fbErr && h('div', { className: 'uds-auth-settings-msg err' }, fbErr), h('button', { type: 'button', className: 'uds-auth-btn uds-auth-btn-primary', style: { width: '100%', margin: '12px 0 0' }, disabled: fbBusy, onClick: submitFallback, }, fbBusy ? t('ui.loggingIn') : t('ui.login')), ), h('button', { type: 'button', className: 'uds-auth-btn-link', onClick: () => setLoginMode('qr'), }, t('ui.backToQr')), )) : h(React.Fragment, null, h('div', { className: 'uds-auth-info' }, h('div', { className: 'uds-auth-info-name' }, displayName), h('div', { className: 'uds-auth-info-detail' }, h('span', { className: 'uds-auth-info-detail-label' }, t('ui.empNo')), user.empNo || '-'), h('div', { className: 'uds-auth-info-detail' }, h('span', { className: 'uds-auth-info-detail-label' }, t('ui.role')), label || user.role || '-'), user.department && h('div', { className: 'uds-auth-info-detail' }, h('span', { className: 'uds-auth-info-detail-label' }, t('ui.department')), user.department), ), h('button', { type: 'button', className: 'uds-auth-btn uds-auth-btn-danger', onClick: async () => { try { await fetchJson('/uds-auth/api/logout', { method: 'POST' }) } catch { /* ignore */ } clearAuthCookies() setUser(null) setOpen(false) document.documentElement.setAttribute('data-uds-logged-in', '0') document.documentElement.setAttribute('data-uds-can-settings', '0') document.documentElement.setAttribute('data-uds-can-create-ws', '0') clearSessionIfAnonymous(window.__udsAuthSessions) window.dispatchEvent(new Event('uds-auth-changed')) try { softReconnectAuth() } catch { /* ignore */ } }, }, t('ui.logout')), ), ), h('button', { type: 'button', className: 'uds-auth-badge' + (user ? '' : ' uds-auth-badge-unauth'), 'aria-expanded': open, 'aria-label': displayName || 'UAC', title: displayName || 'UAC', onClick: () => setOpen((v) => !v), }, h('span', { className: 'uds-auth-badge-label' }, displayName), ), ) } function apply(ctx) { localeHost = ctx translate = makeTranslator(ctx) // --- Critical path: login badge must paint first --- // Sync ACL + CSS before any React mount so 「未登录」is styled and locked immediately. { const root = document.documentElement if (!root.getAttribute('data-uds-logged-in')) root.setAttribute('data-uds-logged-in', '0') if (!root.getAttribute('data-uds-auth-ready')) root.setAttribute('data-uds-auth-ready', '0') if (!root.getAttribute('data-uds-can-create-ws')) root.setAttribute('data-uds-can-create-ws', '0') if (!root.getAttribute('data-uds-can-settings')) root.setAttribute('data-uds-can-settings', '0') } if (!document.getElementById('uds-auth-client-css')) { const tag = document.createElement('style') tag.id = 'uds-auth-client-css' tag.setAttribute('data-plugin', name) tag.textContent = CSS document.head.appendChild(tag) } // Register login entry before heavy gates / settings section. ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'uds-auth-login', order: 100, label: 'UAC', }, AuthBadge)) ctx.effect(() => { if (!ctx.locale || typeof ctx.locale.register !== 'function') return undefined try { return ctx.locale.register(LOCALE_NS, { zh: UDS_I18N_MESSAGES.zh, en: UDS_I18N_MESSAGES.en, }) } catch { try { const offZh = ctx.locale.register(LOCALE_NS, 'zh', UDS_I18N_MESSAGES.zh) const offEn = ctx.locale.register(LOCALE_NS, 'en', UDS_I18N_MESSAGES.en) return () => { offZh && offZh(); offEn && offEn() } } catch { return undefined } } }, 'uds-auth: locale') try { ctx.inject(['connection'], (cctx) => { window.__udsAuthReconnect = () => { try { cctx.connection.reconnect() } catch { window.location.reload() } } }) } catch { /* connection may be unavailable */ } // Settings gear: CSS hides it when data-uds-can-settings=0 (no slot override — // registering sidebar.settings early used to crash / delay the whole plugin). // Settings page section — after login badge; not on the critical path for first paint. ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'uds-auth', order: 22, label: () => t('ui.settingsTitle'), locale: LOCALE_NS, icon: 'users', }, AuthSettingsSection)) // Defense in depth: never show Host session rows while UDS cookies are absent. // Covers ALS misses on /api and broadcast api-session/added leaks. // Use ctx.get (optional) — sandboxed clients cannot ctx.inject undeclared services. ctx.effect(() => { let onAuthChanged = null let installedRpc = false let installedSessions = false const install = () => { // Block anonymous calls to dsh-ops-cron (定时任务) HTTP API. if (!window.__udsAuthCronFetchGate) { window.__udsAuthCronFetchGate = true const origFetch = window.fetch.bind(window) window.fetch = async function udsAuthFetch(input, init) { const url = typeof input === 'string' ? input : (input && input.url) || '' if (!isLoggedInInUi() && String(url).includes('/dsh-ops-cron') && !String(url).includes('/dsh-ops-cron/health')) { return new Response(JSON.stringify({ ok: false, error: 'login_required', message: t('ui.cronLoginRequired'), }), { status: 401, headers: { 'content-type': 'application/json' } }) } return origFetch(input, init) } } const connection = ctx.get('connection') const rpc = connection && connection.rpc if (!installedRpc && rpc && typeof rpc.call === 'function' && !rpc.__udsAuthListGate) { installedRpc = true rpc.__udsAuthListGate = true const origCall = rpc.call.bind(rpc) rpc.call = async function udsAuthRpcCall(channel, endpoint, payload, signal) { const result = await origCall(channel, endpoint, payload, signal) if (!isLoggedInInUi()) { if (channel === '/api') { if (endpoint === 'session/list') return { ok: true, value: { items: [] } } if (endpoint === 'session/search') return { ok: true, value: { items: [], hasMore: false } } } } if ( channel === '/api' && ( endpoint === 'directoryPicker/pick' || endpoint === 'directoryPicker/list' || endpoint === 'directoryPicker/createDirectory' ) && document.documentElement.getAttribute('data-uds-can-create-ws') !== '1' ) { return { ok: false, error: { code: 'gateway/forbidden', message: getEmpNo() ? t('ui.workspaceCreateForbidden') : t('ui.workspaceLoginRequired'), }, } } return result } } const sessions = ctx.get('sessions') if (!installedSessions && sessions && !sessions.__udsAuthListGate) { installedSessions = true sessions.__udsAuthListGate = true const origAdded = sessions.handleSessionAdded && sessions.handleSessionAdded.bind(sessions) if (origAdded) { sessions.handleSessionAdded = (summary) => { if (!isLoggedInInUi()) return return origAdded(summary) } } const origActivity = sessions.handleSessionActivity && sessions.handleSessionActivity.bind(sessions) if (origActivity) { sessions.handleSessionActivity = (sessionId, updatedAt) => { if (!isLoggedInInUi()) return return origActivity(sessionId, updatedAt) } } const refresh = () => { if (typeof sessions.refresh === 'function') void sessions.refresh() } refresh() onAuthChanged = refresh window.addEventListener('uds-auth-changed', onAuthChanged) } return installedRpc && installedSessions } install() const timer = installedRpc && installedSessions ? null : setInterval(() => { if (install() && timer) clearInterval(timer) }, 300) return () => { if (timer) clearInterval(timer) if (onAuthChanged) window.removeEventListener('uds-auth-changed', onAuthChanged) } }, 'uds-auth: session-list-gate') ctx.effect(() => { let sessions = null let unsub = null const bind = () => { try { sessions = ctx.get('sessions') || sessions } catch { /* ignore */ } window.__udsAuthSessions = sessions if (unsub) { try { unsub() } catch { /* ignore */ } unsub = null } if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') { unsub = sessions.list.subscribe(() => { clearSessionIfAnonymous(sessions) }) } clearSessionIfAnonymous(sessions) } bind() const onAuth = () => { bind() } window.addEventListener('uds-auth-changed', onAuth) const mo = new MutationObserver(() => { clearSessionIfAnonymous(window.__udsAuthSessions) }) mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-uds-logged-in', 'data-uds-auth-ready'], }) const timer = setInterval(bind, 500) setTimeout(() => clearInterval(timer), 20000) return () => { clearInterval(timer) mo.disconnect() window.removeEventListener('uds-auth-changed', onAuth) if (unsub) { try { unsub() } catch { /* ignore */ } } } }, 'uds-auth: clear-session-when-anonymous') ctx.effect(() => { let disposers = [] const Gate = function UdsAuthDirectoryFlowGate(props) { React.useEffect(() => { if (props && props.open) { try { props.onCancel && props.onCancel() } catch { /* ignore */ } } }, [props && props.open]) return null } const installGate = () => { if (disposers.length) return for (const slotName of [ 'sidebar.workspaces.directoryFlow', 'conversation.hero.workspace.directoryFlow', ]) { try { disposers.push(ctx.slots.register({ name: slotName, id: 'uds-auth-dir-gate', order: 9999, }, Gate)) } catch { /* slot may be undeclared briefly */ } } } const clearGate = () => { for (const d of disposers) { try { d() } catch { /* ignore */ } } disposers = [] } // Deny folder pick by default — no race with native directory picker. const sync = () => { // Attrs come from AuthBadge /api/me (mount + login) — do not poll me here. const canCreate = document.documentElement.getAttribute('data-uds-can-create-ws') === '1' if (canCreate) clearGate() else installGate() } const injectOffs = [ 'sidebar.workspaces.directoryFlow', 'conversation.hero.workspace.directoryFlow', ].map((slotName) => { try { return ctx.slots.inject(slotName, () => { sync() }) } catch { return null } }) sync() const onAuth = () => { sync() } window.addEventListener('uds-auth-changed', onAuth) const mo = new MutationObserver(sync) mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-uds-can-create-ws', 'data-uds-logged-in'], }) return () => { window.removeEventListener('uds-auth-changed', onAuth) mo.disconnect() clearGate() for (const off of injectOffs) { try { if (typeof off === 'function') off() } catch { /* ignore */ } } } }, 'uds-auth: directory-flow-gate') ctx.effect(() => { // Open/choose workspace is super_admin-only (canCreateWorkspace). // Everyone else uses the auto-provisioned per-user workspace and must not open the picker. const CHOOSER = hostAriaSel('chooseWorkspace') // Inert composer: onClick lives on the card (cardWorkspaceTrigger), not the labeled node. const TRIGGER_CARD = '[class*="cardWorkspaceTrigger"]' const SURFACE = CHOOSER + ', ' + TRIGGER_CARD const canOpenWorkspace = () => document.documentElement.getAttribute('data-uds-can-create-ws') === '1' const isChooser = (node) => { if (!node || !node.closest) return null return node.closest(SURFACE) } const unlockEl = (el) => { if (el.dataset.udsWsLocked !== '1') return el.style.display = '' el.style.pointerEvents = '' el.style.opacity = '' el.style.cursor = '' if (el.tagName === 'BUTTON' || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { el.removeAttribute('disabled') } el.removeAttribute('aria-disabled') el.removeAttribute('tabindex') delete el.dataset.udsWsLocked } const lockEl = (el, hide) => { el.dataset.udsWsLocked = '1' if (hide) el.style.display = 'none' el.style.pointerEvents = 'none' el.style.opacity = hide ? '' : '0.45' el.style.cursor = 'not-allowed' el.setAttribute('aria-disabled', 'true') el.setAttribute('tabindex', '-1') if (el.tagName === 'BUTTON' || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { try { el.setAttribute('disabled', 'true') } catch { /* ignore */ } } } const freezeChoosers = () => { const loggedIn = document.documentElement.getAttribute('data-uds-logged-in') === '1' const canCreate = canOpenWorkspace() // Clear stale locks left on the composer card after it leaves trigger mode. document.querySelectorAll('[data-uds-ws-locked="1"]').forEach((el) => { const stillChooser = el.matches && (el.matches(CHOOSER) || el.matches(TRIGGER_CARD)) if (canCreate || !stillChooser || (loggedIn && el.matches(TRIGGER_CARD))) unlockEl(el) }) if (canCreate) { document.querySelectorAll(SURFACE).forEach(unlockEl) return } // Non-creators: hide labeled chooser chips only. document.querySelectorAll(CHOOSER).forEach((el) => lockEl(el, true)) // Lock inert trigger card only while anonymous; logged-in users // auto-bind a personal workspace and must not keep pointer-events:none. if (!loggedIn) { document.querySelectorAll(TRIGGER_CARD).forEach((el) => lockEl(el, false)) } } const block = (event) => { if (canOpenWorkspace()) return const hit = isChooser(event.target) if (!hit) return const loggedIn = document.documentElement.getAttribute('data-uds-logged-in') === '1' // Logged-in non-creators: still block labeled "选择工作区" / trigger clicks // that open the picker; do not leave the card permanently disabled. if (loggedIn && hit.matches && hit.matches(TRIGGER_CARD) && !hit.matches(CHOOSER)) { event.preventDefault() event.stopPropagation() if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation() return } event.preventDefault() event.stopPropagation() if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation() } document.addEventListener('click', block, true) document.addEventListener('pointerdown', block, true) document.addEventListener('mousedown', block, true) document.addEventListener('keydown', (event) => { if (canOpenWorkspace()) return if (event.key !== 'Enter' && event.key !== ' ' && event.key !== 'Spacebar') return if (!isChooser(event.target)) return event.preventDefault() event.stopPropagation() }, true) let workspaceDispose = null const LockedWorkspace = function UdsAuthLockedWorkspace(props) { React.useEffect(() => { if (props && props.open) { try { props.onClose && props.onClose() } catch { /* ignore */ } } }, [props && props.open]) return null } const installWorkspaceLock = () => { if (workspaceDispose) return try { workspaceDispose = ctx.slots.register({ name: 'conversation.hero.workspace', id: 'uds-auth-workspace-lock', order: 9999, }, LockedWorkspace) } catch { /* slot may be undeclared briefly */ } } const clearWorkspaceLock = () => { if (workspaceDispose) { try { workspaceDispose() } catch { /* ignore */ } workspaceDispose = null } } // Deny open-workspace by default until AuthBadge /api/me proves canCreateWorkspace. freezeChoosers() const syncWorkspaceSlot = () => { // Use AuthBadge DOM attrs only — no default /api/me. const canCreate = document.documentElement.getAttribute('data-uds-can-create-ws') === '1' if (canCreate) clearWorkspaceLock() else installWorkspaceLock() freezeChoosers() } let workspaceInjectOff = null try { workspaceInjectOff = ctx.slots.inject('conversation.hero.workspace', () => { syncWorkspaceSlot() }) } catch { /* ignore */ } syncWorkspaceSlot() const onAuth = () => { syncWorkspaceSlot() } window.addEventListener('uds-auth-changed', onAuth) const mo = new MutationObserver(() => { freezeChoosers(); syncWorkspaceSlot() }) mo.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-uds-can-create-ws', 'data-uds-logged-in', 'aria-label', 'class'], }) return () => { mo.disconnect() document.removeEventListener('click', block, true) document.removeEventListener('pointerdown', block, true) document.removeEventListener('mousedown', block, true) window.removeEventListener('uds-auth-changed', onAuth) clearWorkspaceLock() try { if (typeof workspaceInjectOff === 'function') workspaceInjectOff() } catch { /* ignore */ } } }, 'uds-auth: workspace-click-lock') ctx.effect(() => { let busy = false let tries = 0 const bindPersonalWorkspace = async () => { if (busy) return if (document.documentElement.getAttribute('data-uds-logged-in') !== '1') return if (document.documentElement.getAttribute('data-uds-auth-ready') !== '1') return // Super/fallback keep full workspace browser — do not force personal workspace. if (document.documentElement.getAttribute('data-uds-can-create-ws') === '1') return if (document.documentElement.getAttribute('data-uds-can-create-ws') !== '0') return let sessions = null try { sessions = ctx.get('sessions') } catch { sessions = null } if (!sessions || typeof sessions.create !== 'function') return try { const snap = sessions.list && sessions.list.getSnapshot && sessions.list.getSnapshot() if (snap && snap.current != null) return } catch { /* ignore */ } busy = true try { const me = await fetchJson('/uds-auth/api/me') const wsId = me && me.data && me.data.workspaceId if (!me?.authenticated || !wsId) return const sessionId = await sessions.create({ workspaceId: wsId }) if (sessionId && typeof sessions.open === 'function') sessions.open(sessionId) } catch (err) { console.warn('[uds-auth] auto-bind personal workspace failed:', err && err.message ? err.message : err) } finally { busy = false } } const tick = () => { if (tries++ > 40) return void bindPersonalWorkspace() } tick() window.addEventListener('uds-auth-changed', tick) const timer = setInterval(tick, 1500) setTimeout(() => clearInterval(timer), 60000) return () => { clearInterval(timer) window.removeEventListener('uds-auth-changed', tick) } }, 'uds-auth: auto-bind-personal-workspace') ctx.effect(() => { const sync = () => { const root = document.documentElement if (root.getAttribute('data-uds-auth-ready') !== '1') return if (root.getAttribute('data-uds-logged-in') !== '1') { try { window.sessionStorage.removeItem('uds-auth-flat-reloaded') } catch { /* ignore */ } try { window.sessionStorage.removeItem('uds-auth-ws-reloaded') } catch { /* ignore */ } return } // Super/fallback: restore workspace partitions (undo forced flat). if (root.getAttribute('data-uds-can-create-ws') === '1') { const restored = ensureWorkspaceGroupedSidebar() if (restored && !window.sessionStorage.getItem('uds-auth-ws-reloaded')) { try { window.sessionStorage.setItem('uds-auth-ws-reloaded', '1') window.location.reload() } catch { /* ignore */ } } try { window.sessionStorage.removeItem('uds-auth-flat-reloaded') } catch { /* ignore */ } return } if (root.getAttribute('data-uds-can-create-ws') !== '0') return const switched = ensureFlatSessionSidebar() expandHiddenWorkspaceGroups() if (switched && !window.sessionStorage.getItem('uds-auth-flat-reloaded')) { try { window.sessionStorage.setItem('uds-auth-flat-reloaded', '1') window.location.reload() } catch { /* ignore */ } } } sync() const onAuth = () => { sync() } window.addEventListener('uds-auth-changed', onAuth) const moAttrs = new MutationObserver(() => { sync() }) moAttrs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-uds-logged-in', 'data-uds-can-create-ws', 'data-uds-auth-ready'], }) const mo = new MutationObserver(() => { expandHiddenWorkspaceGroups() }) mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-expanded', 'class'] }) const timer = setInterval(expandHiddenWorkspaceGroups, 2000) setTimeout(() => clearInterval(timer), 30000) return () => { clearInterval(timer) mo.disconnect() moAttrs.disconnect() window.removeEventListener('uds-auth-changed', onAuth) } }, 'uds-auth: session-only-sidebar') } exports.name = name exports.inject = inject exports.apply = apply return module.exports }, })