@jeffusion/bungee-core 5.26.0 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/config-storage-worker.js +455 -223
  2. package/dist/main.js +803 -437
  3. package/dist/observability-worker.js +0 -10
  4. package/dist/plugins/anthropic-request-sanitizer/index.js.map +2 -2
  5. package/dist/plugins/anthropic-request-sanitizer/manifest.json +1 -1
  6. package/dist/plugins/anthropic-tool-name-transformer/index.js.map +2 -2
  7. package/dist/plugins/anthropic-tool-name-transformer/manifest.json +1 -1
  8. package/dist/plugins/chatgpt-oauth/control.js.map +1 -1
  9. package/dist/plugins/chatgpt-oauth/index.js.map +2 -2
  10. package/dist/plugins/chatgpt-oauth/manifest.json +1 -1
  11. package/dist/plugins/codex-router/control.js.map +1 -1
  12. package/dist/plugins/codex-router/index.js +2 -2
  13. package/dist/plugins/codex-router/index.js.map +3 -3
  14. package/dist/plugins/codex-router/manifest.json +1 -1
  15. package/dist/plugins/deepseek-reasoning-fix/index.js.map +2 -2
  16. package/dist/plugins/deepseek-reasoning-fix/manifest.json +1 -1
  17. package/dist/plugins/key-access/control.js.map +1 -1
  18. package/dist/plugins/key-access/index.js.map +2 -2
  19. package/dist/plugins/key-access/ingress.js.map +1 -1
  20. package/dist/plugins/key-access/manifest.json +1 -1
  21. package/dist/plugins/key-rate-limit/index.js.map +2 -2
  22. package/dist/plugins/key-rate-limit/manifest.json +1 -1
  23. package/dist/plugins/llm-protocol-adapter/control.js +2 -2
  24. package/dist/plugins/llm-protocol-adapter/control.js.map +4 -4
  25. package/dist/plugins/llm-protocol-adapter/index.js +2 -2
  26. package/dist/plugins/llm-protocol-adapter/index.js.map +3 -3
  27. package/dist/plugins/llm-protocol-adapter/manifest.json +1 -1
  28. package/dist/plugins/model-mapping/index.js.map +2 -2
  29. package/dist/plugins/model-mapping/manifest.json +1 -1
  30. package/dist/plugins/models-dev/index.js.map +2 -2
  31. package/dist/plugins/models-dev/manifest.json +1 -1
  32. package/dist/plugins/signature-repair/index.js.map +2 -2
  33. package/dist/plugins/signature-repair/manifest.json +1 -1
  34. package/dist/plugins/token-budget/control.js.map +1 -1
  35. package/dist/plugins/token-budget/index.js.map +2 -2
  36. package/dist/plugins/token-budget/manifest.json +1 -1
  37. package/dist/plugins/token-metering/index.js.map +2 -2
  38. package/dist/plugins/token-metering/manifest.json +1 -1
  39. package/dist/plugins/token-stats/control.js.map +1 -1
  40. package/dist/plugins/token-stats/index.js.map +2 -2
  41. package/dist/plugins/token-stats/manifest.json +1 -1
  42. package/dist/plugins/user-auth/control.js +3911 -0
  43. package/dist/plugins/{local-accounts → user-auth}/control.js.map +8 -5
  44. package/dist/plugins/{local-accounts → user-auth}/index.js +6 -6
  45. package/dist/plugins/{local-accounts → user-auth}/index.js.map +6 -6
  46. package/dist/plugins/user-auth/manifest.json +735 -0
  47. package/dist/plugins/user-auth/ui/ConnectionEditor.svelte +137 -0
  48. package/dist/plugins/user-auth/ui/Login.svelte +87 -0
  49. package/dist/plugins/user-auth/ui/LoginProviderIcon.svelte +19 -0
  50. package/dist/plugins/user-auth/ui/LoginSettings.svelte +141 -0
  51. package/dist/plugins/user-auth/ui/Profile.svelte +185 -0
  52. package/dist/plugins/{local-accounts → user-auth}/ui/RecoveryHelp.svelte +3 -3
  53. package/dist/plugins/user-auth/ui/SessionPolicy.svelte +104 -0
  54. package/dist/plugins/user-auth/ui/Settings.svelte +14 -0
  55. package/dist/plugins/user-auth/ui/SettingsSection.svelte +32 -0
  56. package/dist/plugins/user-auth/ui/Setup.svelte +38 -0
  57. package/dist/plugins/user-auth/ui/TurnstileChallenge.svelte +26 -0
  58. package/dist/plugins/user-auth/ui/TurnstileSettings.svelte +81 -0
  59. package/dist/plugins/user-auth/ui/Users.svelte +95 -0
  60. package/dist/plugins/user-auth/ui/assets/LICENSE.txt +30 -0
  61. package/dist/plugins/user-auth/ui/assets/github.svg +1 -0
  62. package/dist/plugins/user-auth/ui/assets/google.svg +1 -0
  63. package/dist/plugins/user-auth/ui/assets/microsoft.svg +1 -0
  64. package/dist/plugins/user-auth/ui/contracts.ts +16 -0
  65. package/dist/plugins/{local-accounts → user-auth}/ui/login-errors.ts +4 -1
  66. package/dist/plugins/user-auth/ui/turnstile.ts +21 -0
  67. package/package.json +2 -2
  68. package/dist/plugins/local-accounts/control.js +0 -715
  69. package/dist/plugins/local-accounts/manifest.json +0 -223
  70. package/dist/plugins/local-accounts/ui/Login.svelte +0 -47
  71. package/dist/plugins/local-accounts/ui/Settings.svelte +0 -117
@@ -1,223 +0,0 @@
1
- {
2
- "name": "local-accounts",
3
- "version": "1.0.0",
4
- "schemaVersion": 3,
5
- "artifactKind": "runtime-plugin",
6
- "main": "index.js",
7
- "capabilities": [
8
- "hooks",
9
- "api",
10
- "dynamicRuntimeLoad",
11
- "controlPlane",
12
- "nativeWidgetsStatic"
13
- ],
14
- "uiExtensionMode": "native-static",
15
- "engines": {
16
- "bungee": "^5.0.0"
17
- },
18
- "runtimeScope": "global",
19
- "control": {
20
- "entry": "control.js",
21
- "rpc": []
22
- },
23
- "management": {
24
- "loginComponent": "LocalAccountsLogin"
25
- },
26
- "builtin": true,
27
- "ui": {
28
- "components": [
29
- {
30
- "name": "LocalAccountsSettings",
31
- "entry": "ui/Settings.svelte"
32
- },
33
- {
34
- "name": "LocalAccountsLogin",
35
- "entry": "ui/Login.svelte"
36
- }
37
- ]
38
- },
39
- "contributes": {
40
- "api": [
41
- {
42
- "path": "/password",
43
- "methods": [
44
- "POST"
45
- ],
46
- "handler": "changePassword",
47
- "execution": "control",
48
- "capability": "self.password"
49
- },
50
- {
51
- "path": "/session-policy",
52
- "methods": [
53
- "GET",
54
- "PUT"
55
- ],
56
- "handler": "sessionPolicy",
57
- "execution": "control",
58
- "capability": "config.write"
59
- },
60
- {
61
- "path": "/logout",
62
- "methods": [
63
- "POST"
64
- ],
65
- "handler": "logout",
66
- "execution": "control",
67
- "capability": "self.logout"
68
- },
69
- {
70
- "path": "/self",
71
- "methods": [
72
- "GET"
73
- ],
74
- "handler": "self",
75
- "execution": "control",
76
- "capability": "self.password"
77
- }
78
- ],
79
- "navigation": [
80
- {
81
- "label": "metadata.name",
82
- "path": "/settings",
83
- "component": "LocalAccountsSettings",
84
- "icon": "manage_accounts",
85
- "target": "header"
86
- }
87
- ]
88
- },
89
- "metadata": {
90
- "name": "metadata.name",
91
- "description": "metadata.description",
92
- "icon": "manage_accounts"
93
- },
94
- "configSchema": [],
95
- "translations": {
96
- "zh-CN": {
97
- "metadata.name": "认证管理",
98
- "metadata.description": "可选的单管理员账号密码认证",
99
- "login.title": "账号登录",
100
- "login.description": "使用认证管理插件中设置的唯一管理员账号。",
101
- "login.username": "账号",
102
- "login.password": "密码",
103
- "login.busy": "正在建立登录会话…",
104
- "login.submit": "登录",
105
- "login.technicalDetails": "技术详情",
106
- "login.correctAddress": "使用正确地址:{origin}",
107
- "login.errors.invalid_credentials": "账号或密码不正确。请重新输入。",
108
- "login.errors.invalid_input": "账号格式不正确。账号名只允许字母、数字及 _ . @ -,长度为 1–64。",
109
- "login.errors.forbidden": "当前登录请求无法完成。请刷新后重试。",
110
- "login.errors.unauthorized": "登录会话未建立。请重新登录。",
111
- "login.errors.invalid_origin": "当前访问地址与服务配置不匹配。请使用配置的公开地址登录,或请部署管理员调整公开地址。",
112
- "login.errors.invalid_csrf": "当前会话或访问地址校验失败。请从配置的公开地址重新登录。",
113
- "login.errors.login_limited": "登录尝试过多。请等待 15 分钟后重试。",
114
- "login.errors.session_limit": "登录会话已达到上限。请退出不再使用的会话后重试。",
115
- "login.errors.management_provider_unavailable": "账号服务暂时不可用。请检查服务状态,恢复后重试。",
116
- "login.errors.provider_unavailable": "账号服务暂时不可用。请检查服务状态,恢复后重试。",
117
- "login.errors.control_recovering": "账号服务正在恢复。请稍候重试。",
118
- "login.errors.session_unavailable": "登录尚未完成。请检查连接后重试,或刷新管理访问状态。",
119
- "login.errors.unknown": "登录未完成。请检查连接后重试。",
120
- "recovery.summary": "忘记账号或密码?",
121
- "recovery.intro": "请在部署服务器上恢复,网页不能直接重置管理员。",
122
- "recovery.step1": "确认目标实例的配置数据库路径,停止该实例的全部进程并备份数据。",
123
- "recovery.step2": "在原部署目录、使用原环境变量执行下面的命令,将数据库路径替换为实际绝对路径。",
124
- "recovery.step3": "按提示输入新的账号和密码,然后重新启动实例。旧会话会失效,路由、服务及 API Key 保留。",
125
- "recovery.docker": "Docker 部署:停止原容器后,使用同一镜像、原数据卷及环境变量启动临时交互容器,执行同一命令;完成后删除临时容器并启动原容器。请勿另建空数据卷。",
126
- "recovery.note": "密码交互输入且不显示。若提示恢复失败,请确认实例已完全停止、路径及环境正确;旧版本存在多个管理员时需由部署人员处理。",
127
- "settings.accountTitle": "管理员账号",
128
- "settings.accountTag": "账号",
129
- "settings.passwordTag": "密码",
130
- "settings.loading": "正在读取管理员账号",
131
- "settings.accountDescription": "认证管理使用此唯一管理员账号。停用插件后恢复匿名管理访问。",
132
- "settings.passwordTitle": "修改登录密码",
133
- "settings.passwordDescription": "修改后当前登录会话失效,需要重新登录。",
134
- "settings.currentPassword": "当前密码",
135
- "settings.newPassword": "新密码(6–64 个字符)",
136
- "settings.confirmation": "确认新密码",
137
- "settings.updating": "正在更新…",
138
- "settings.submit": "修改并重新登录",
139
- "settings.logout": "退出当前会话",
140
- "session.title": "登录会话",
141
- "session.tag": "有效期",
142
- "session.description": "保存后仅影响新登录的会话,已有会话保留登录时的设置。时长设为 0 可关闭对应超时。",
143
- "session.idle": "空闲超时",
144
- "session.absolute": "登录最长有效期",
145
- "session.minutes": "分钟",
146
- "session.hours": "小时",
147
- "session.days": "天",
148
- "session.unit": "时长单位",
149
- "session.browserNote": "关闭超时不影响退出登录、改密和身份恢复时撤销会话。浏览器仍可能清理 Cookie;正常核验会话时会续期 Cookie,但不会延长登录最长有效期。",
150
- "session.loading": "正在读取会话设置",
151
- "session.save": "保存会话设置",
152
- "session.saving": "正在保存…",
153
- "session.saved": "已保存,下次登录生效。",
154
- "session.invalid": "请输入非负整数时长;0 表示关闭超时。",
155
- "session.conflict": "设置已被其他页面修改。请重新读取后再保存。",
156
- "session.failed": "会话设置读取或保存失败。",
157
- "session.reload": "重新读取"
158
- },
159
- "en": {
160
- "metadata.name": "Authentication Management",
161
- "metadata.description": "Optional username and password authentication for a single administrator",
162
- "login.title": "Account sign-in",
163
- "login.description": "Use the sole administrator account configured in the management authentication plugin.",
164
- "login.username": "Username",
165
- "login.password": "Password",
166
- "login.busy": "Establishing a sign-in session…",
167
- "login.submit": "Sign in",
168
- "login.technicalDetails": "Technical details",
169
- "login.correctAddress": "Use the correct address: {origin}",
170
- "login.errors.invalid_credentials": "Incorrect username or password. Enter your credentials again.",
171
- "login.errors.invalid_input": "Invalid username. Use 1–64 characters containing letters, numbers, and _ . @ - only.",
172
- "login.errors.forbidden": "This sign-in request could not be completed. Refresh and try again.",
173
- "login.errors.unauthorized": "A sign-in session was not established. Sign in again.",
174
- "login.errors.invalid_origin": "This address does not match the service configuration. Sign in at the configured public address, or ask the deployment administrator to update it.",
175
- "login.errors.invalid_csrf": "Session or address verification failed. Sign in again at the configured public address.",
176
- "login.errors.login_limited": "Too many sign-in attempts. Wait 15 minutes before trying again.",
177
- "login.errors.session_limit": "The sign-in session limit has been reached. Sign out of unused sessions and try again.",
178
- "login.errors.management_provider_unavailable": "The account service is temporarily unavailable. Check its status and try again after recovery.",
179
- "login.errors.provider_unavailable": "The account service is temporarily unavailable. Check its status and try again after recovery.",
180
- "login.errors.control_recovering": "The account service is recovering. Try again shortly.",
181
- "login.errors.session_unavailable": "Sign-in has not completed. Check your connection and retry, or refresh management access status.",
182
- "login.errors.unknown": "Sign-in did not complete. Check your connection and try again.",
183
- "recovery.summary": "Forgot your username or password?",
184
- "recovery.intro": "Recover access on the deployment server. The web page cannot reset the administrator directly.",
185
- "recovery.step1": "Confirm the target instance’s configuration database path, stop all its processes, and back up the data.",
186
- "recovery.step2": "Run the following command from the original deployment directory with the original environment variables. Replace the database path with its actual absolute path.",
187
- "recovery.step3": "Enter a new username and password when prompted, then restart the instance. Old sessions will expire; routes, services, and API Keys will be retained.",
188
- "recovery.docker": "For Docker deployments, stop the original container, start a temporary interactive container with the same image, original data volume, and environment variables, and run the same command. Afterwards, remove the temporary container and restart the original one. Do not create an empty data volume.",
189
- "recovery.note": "Passwords are entered interactively and remain hidden. If recovery fails, confirm the instance is fully stopped and the path and environment are correct. Deployment staff must handle older versions with multiple administrators.",
190
- "settings.accountTitle": "Administrator account",
191
- "settings.accountTag": "Account",
192
- "settings.passwordTag": "Password",
193
- "settings.loading": "Loading administrator account",
194
- "settings.accountDescription": "Management authentication uses this sole administrator account. Disabling the plugin restores anonymous management access.",
195
- "settings.passwordTitle": "Change sign-in password",
196
- "settings.passwordDescription": "Changing the password invalidates the current session. Sign in again afterwards.",
197
- "settings.currentPassword": "Current password",
198
- "settings.newPassword": "New password (6–64 characters)",
199
- "settings.confirmation": "Confirm new password",
200
- "settings.updating": "Updating…",
201
- "settings.submit": "Change and sign in again",
202
- "settings.logout": "Sign out of current session",
203
- "session.title": "Sign-in sessions",
204
- "session.tag": "LIFETIME",
205
- "session.description": "Saved settings apply only to new sign-ins. Existing sessions retain their original settings. Set a duration to 0 to disable that timeout.",
206
- "session.idle": "Idle timeout",
207
- "session.absolute": "Maximum sign-in lifetime",
208
- "session.minutes": "Minutes",
209
- "session.hours": "Hours",
210
- "session.days": "Days",
211
- "session.unit": "Duration unit",
212
- "session.browserNote": "Disabling timeouts does not prevent sign-out, password changes or identity recovery from revoking sessions. Browsers may still clear cookies. Session verification renews the cookie without extending the maximum sign-in lifetime.",
213
- "session.loading": "Loading session settings",
214
- "session.save": "Save session settings",
215
- "session.saving": "Saving…",
216
- "session.saved": "Saved. Applies to your next sign-in.",
217
- "session.invalid": "Enter a non-negative whole-number duration. 0 disables the timeout.",
218
- "session.conflict": "Another page changed these settings. Reload before saving.",
219
- "session.failed": "Could not read or save session settings.",
220
- "session.reload": "Reload settings"
221
- }
222
- }
223
- }
@@ -1,47 +0,0 @@
1
- <script lang="ts">
2
- import { onDestroy } from 'svelte';
3
- import { isLoading } from 'svelte-i18n';
4
- import { _, isManagementLoginStaleError, type ManagementLoginContext } from '@bungee/plugin-sdk';
5
- import RecoveryHelp from './RecoveryHelp.svelte';
6
- import { loginFailure, requireLoginSuccess } from './login-errors';
7
-
8
- let { context }: { context: ManagementLoginContext } = $props();
9
- const t = (key: string, values: Record<string, string> = {}) => $isLoading ? '' : $_(`plugins.local-accounts.${key}`, { values });
10
- let username = $state(''), password = $state(''), busy = $state(false);
11
- let error = $state(''), detail = $state('');
12
- let destroyed = false;
13
- onDestroy(() => { destroyed = true; });
14
-
15
- async function login(event: SubmitEvent) {
16
- event.preventDefault();
17
- if (busy) return;
18
- const activeContext = context;
19
- busy = true; error = ''; detail = '';
20
- try {
21
- const result = await activeContext.login({ username, password, transport: 'cookie' });
22
- if (destroyed || context !== activeContext || result === undefined) return;
23
- requireLoginSuccess(result);
24
- await activeContext.complete();
25
- } catch (cause) {
26
- if (destroyed || context !== activeContext || isManagementLoginStaleError(cause)) return;
27
- const failure = loginFailure(cause);
28
- error = failure.key; detail = failure.detail;
29
- } finally {
30
- password = '';
31
- if (!destroyed && context === activeContext) busy = false;
32
- }
33
- }
34
- </script>
35
-
36
- <form onsubmit={login} aria-label={t('login.title')} aria-busy={busy} class="space-y-4">
37
- <p class="text-sm text-zinc-400">{t('login.description')}</p>
38
- <label class="grid gap-1"><span class="nx-field-label">{t('login.username')}</span><input class="nx-input" bind:value={username} autocomplete="username" required maxlength="64" disabled={busy} /></label>
39
- <label class="grid gap-1"><span class="nx-field-label">{t('login.password')}</span><input class="nx-input" bind:value={password} type="password" autocomplete="current-password" required disabled={busy} /></label>
40
- {#if error}
41
- <p role="alert" class="text-sm text-red-400">{t(error)}</p>
42
- {#if detail === 'invalid_origin' && context.provider.publicOrigin}<a class="text-sm text-nexus-300 underline break-all" href={`${context.provider.publicOrigin}/#/login`}>{t('login.correctAddress', { origin: context.provider.publicOrigin })}</a>{/if}
43
- <details class="text-sm text-zinc-400"><summary>{t('login.technicalDetails')}</summary><pre class="whitespace-pre-wrap break-all">{detail}</pre></details>
44
- {/if}
45
- <RecoveryHelp />
46
- <button class="nx-btn-primary w-full justify-center" disabled={busy}>{busy ? t('login.busy') : t('login.submit')}</button>
47
- </form>
@@ -1,117 +0,0 @@
1
- <script lang="ts">
2
- import { _, isLoading } from 'svelte-i18n';
3
- import { getPluginText } from '$utils/plugin-i18n';
4
- import { onMount } from 'svelte';
5
- import { requestPluginControl } from '$api/client';
6
- import { logout } from '$stores/auth';
7
- import { PanelCard, LoadingIndicator, BSelect } from '$components/industrial';
8
- import { Input } from '$components/ui/input';
9
- import { Button } from '$components/ui/button';
10
- import { accountError } from '$components/domain/plugin/activation-state';
11
- const t = (key: string) => $isLoading ? '' : getPluginText(key, 'local-accounts', (id, options) => $_(id, options));
12
- const core = (key: string, values: Record<string, string> = {}) => $isLoading ? '' : $_(key, { values });
13
- let { pluginName = 'local-accounts' }: { pluginName?: string; apiBase?: string } = $props();
14
- let administrator = $state<{id:string;username:string;requiresPasswordChange?:boolean} | null>(null);
15
- let currentPassword = $state(''), password = $state(''), confirmation = $state(''), error = $state(''), detail = $state(''), busy = $state(false), loading = $state(true);
16
- type SessionPolicy = {idleTimeoutMinutes: number; absoluteTimeoutMinutes: number};
17
- type PolicyResult = {version: number; policy: SessionPolicy};
18
- const factors: Record<string, number> = {minutes: 1, hours: 60, days: 1440};
19
- const maxMinutes = Math.floor(Number.MAX_SAFE_INTEGER / 60_000);
20
- let durations = $state<{key: keyof SessionPolicy; label: string; value: number | undefined; unit: string}[]>([
21
- {key: 'idleTimeoutMinutes', label: 'session.idle', value: 30, unit: 'minutes'},
22
- {key: 'absoluteTimeoutMinutes', label: 'session.absolute', value: 8, unit: 'hours'},
23
- ]);
24
- let policyVersion = $state<number | null>(null), savedPolicy = $state<SessionPolicy | null>(null);
25
- let policyLoading = $state(true), policyBusy = $state(false), policyError = $state(''), policyDetail = $state(''), policySaved = $state(false);
26
- let units = $derived(['minutes', 'hours', 'days'].map(unit => ({value: unit, label: t('session.' + unit)})));
27
- let draftPolicy = $derived(Object.fromEntries(durations.map(field => [field.key, field.value === undefined ? NaN : field.value * factors[field.unit]!])) as SessionPolicy);
28
- let validPolicy = $derived(durations.every(field => Number.isSafeInteger(field.value) && field.value! >= 0)
29
- && Object.values(draftPolicy).every(value => Number.isSafeInteger(value) && value >= 0 && value <= maxMinutes));
30
- let policyDirty = $derived(savedPolicy !== null && (draftPolicy.idleTimeoutMinutes !== savedPolicy.idleTimeoutMinutes || draftPolicy.absoluteTimeoutMinutes !== savedPolicy.absoluteTimeoutMinutes));
31
- function applyPolicy(result: PolicyResult) {
32
- policyVersion = result.version; savedPolicy = result.policy;
33
- for (const field of durations) {
34
- const minutes = result.policy[field.key];
35
- field.unit = minutes > 0 && minutes % 1440 === 0 ? 'days' : minutes > 0 && minutes % 60 === 0 ? 'hours' : 'minutes';
36
- field.value = minutes / factors[field.unit]!;
37
- }
38
- }
39
- function policyFailure(e: unknown) {
40
- const result = accountError(e); policyDetail = result.detail;
41
- policyError = result.detail === 'version_conflict' ? 'session.conflict' : result.detail === 'invalid_session_policy' ? 'session.invalid' : 'session.failed';
42
- }
43
- async function loadPolicy() {
44
- if (policyBusy) return;
45
- policyLoading = true; policyError = ''; policyDetail = ''; policySaved = false;
46
- try { applyPolicy(await requestPluginControl<PolicyResult>(pluginName, '/session-policy', 'GET')); }
47
- catch (e) { policyFailure(e); }
48
- finally { policyLoading = false; }
49
- }
50
- async function savePolicy(event: SubmitEvent) {
51
- event.preventDefault(); if (policyBusy || policyLoading || policyVersion === null) return;
52
- if (!validPolicy) { policyError = 'session.invalid'; return; }
53
- policyBusy = true; policyError = ''; policyDetail = ''; policySaved = false;
54
- try {
55
- applyPolicy(await requestPluginControl<PolicyResult>(pluginName, '/session-policy', 'PUT', {version: policyVersion, policy: draftPolicy}));
56
- policySaved = true;
57
- } catch (e) { policyFailure(e); }
58
- finally { policyBusy = false; }
59
- }
60
- function failure(e: unknown) { const result = accountError(e); error = result.key; detail = result.detail; }
61
- onMount(() => { void requestPluginControl<{administrator: typeof administrator}>(pluginName, '/self', 'GET').then(result => { administrator = result.administrator; }).catch(failure).finally(() => { loading = false; }); });
62
- onMount(() => { void loadPolicy(); });
63
- async function change(event: SubmitEvent) {
64
- event.preventDefault(); if (busy) return;
65
- if (password !== confirmation) { failure(new Error('password_confirmation')); return; }
66
- if ([...password].length < 6 || [...password].length > 64) { failure(new Error('invalid_password')); return; }
67
- busy = true; error = ''; detail = '';
68
- try { await requestPluginControl(pluginName, '/password', 'POST', {currentPassword,password,passwordConfirmation:confirmation}, undefined, {preserveSessionOnUnauthorized:true}); logout(); window.location.hash = '#/login'; }
69
- catch(e) { failure(e); }
70
- finally { currentPassword = ''; password = ''; confirmation = ''; busy = false; }
71
- }
72
- async function leave() {
73
- if (busy) return; busy = true;
74
- try { await requestPluginControl(pluginName, '/logout', 'POST'); logout(); window.location.hash = '#/login'; }
75
- catch(e) { failure(e); } finally { busy = false; }
76
- }
77
- </script>
78
- <div class="space-y-4" data-testid="management-auth-settings">
79
- <PanelCard title={t('settings.accountTitle')} tag={t('settings.accountTag')}>
80
- {#if loading}<LoadingIndicator label={t('settings.loading')} height="sm" />{:else if administrator}<p class="text-sm text-zinc-200">{administrator.username}</p><p class="text-sm text-zinc-400 mt-2">{t('settings.accountDescription')}</p>{/if}
81
- </PanelCard>
82
- <PanelCard title={t('session.title')} tag={t('session.tag')}>
83
- <p class="text-sm text-zinc-400 mb-4">{t('session.description')}</p>
84
- {#if policyLoading}<LoadingIndicator label={t('session.loading')} height="sm" />{:else}
85
- <form class="space-y-3" onsubmit={savePolicy} aria-busy={policyBusy} data-testid="session-policy-form">
86
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
87
- {#each durations as field (field.key)}
88
- <div class="grid gap-1 min-w-0">
89
- <label class="nx-field-label" for={'session-' + field.key}>{t(field.label)}</label>
90
- <div class="flex items-center gap-2">
91
- <Input id={'session-' + field.key} type="number" min={0} max={Math.floor(maxMinutes / factors[field.unit]!)} step={1} required bind:value={field.value} disabled={policyBusy || policyVersion === null} class="min-w-0 flex-1" />
92
- <BSelect bind:value={field.unit} options={units} ariaLabel={t(field.label) + ' · ' + t('session.unit')} width="6rem" disabled={policyBusy || policyVersion === null} />
93
- </div>
94
- </div>
95
- {/each}
96
- </div>
97
- <p class="text-sm text-zinc-400">{t('session.browserNote')}</p>
98
- <div class="flex flex-wrap gap-2">
99
- <Button type="submit" disabled={policyBusy || policyVersion === null || !policyDirty || !validPolicy}>{policyBusy ? t('session.saving') : t('session.save')}</Button>
100
- {#if policyError}<Button type="button" variant="ghost" onclick={loadPolicy} disabled={policyBusy}>{t('session.reload')}</Button>{/if}
101
- </div>
102
- {#if policySaved && !policyDirty}<p role="status" class="text-sm text-emerald-400">{t('session.saved')}</p>{/if}
103
- </form>
104
- {/if}
105
- {#if policyError}<p role="alert" class="text-sm text-red-400 mt-3">{t(policyError)}</p><details class="text-sm text-zinc-400"><summary>{core('pluginActivation.technicalDetails')}</summary><pre class="whitespace-pre-wrap break-all">{policyDetail}</pre></details>{/if}
106
- </PanelCard>
107
- <PanelCard title={t('settings.passwordTitle')} tag={t('settings.passwordTag')}>
108
- <p class="text-sm text-zinc-400 mb-4">{t('settings.passwordDescription')}</p>
109
- <form class="space-y-3" onsubmit={change} aria-busy={busy}>
110
- <label class="grid gap-1"><span class="nx-field-label">{t('settings.currentPassword')}</span><input class="nx-input" type="password" autocomplete="current-password" bind:value={currentPassword} required disabled={busy || !administrator} /></label>
111
- <label class="grid gap-1"><span class="nx-field-label">{t('settings.newPassword')}</span><input class="nx-input" type="password" autocomplete="new-password" bind:value={password} required disabled={busy || !administrator} /></label>
112
- <label class="grid gap-1"><span class="nx-field-label">{t('settings.confirmation')}</span><input class="nx-input" type="password" autocomplete="new-password" bind:value={confirmation} required disabled={busy || !administrator} /></label>
113
- <div class="flex gap-2"><button class="nx-btn-primary" disabled={busy || !administrator}>{busy ? t('settings.updating') : t('settings.submit')}</button><button type="button" class="nx-btn-ghost" disabled={busy || !administrator} onclick={leave}>{t('settings.logout')}</button></div>
114
- </form>
115
- {#if error}<p role="alert" class="text-sm text-red-400 mt-3">{core(error)}</p><details class="text-sm text-zinc-400"><summary>{core('pluginActivation.technicalDetails')}</summary><pre class="whitespace-pre-wrap break-all">{detail}</pre></details>{/if}
116
- </PanelCard>
117
- </div>