@jeffusion/bungee-core 5.7.0 → 5.8.0
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.
- package/dist/main.js +178 -65
- package/dist/plugins/local-accounts/manifest.json +44 -0
- package/dist/plugins/local-accounts/ui/Login.svelte +34 -23
- package/dist/plugins/local-accounts/ui/RecoveryHelp.svelte +18 -0
- package/dist/plugins/local-accounts/ui/login-errors.test.ts +22 -0
- package/dist/plugins/local-accounts/ui/login-errors.ts +20 -0
- package/package.json +2 -2
|
@@ -86,6 +86,28 @@
|
|
|
86
86
|
"login.password": "密码",
|
|
87
87
|
"login.busy": "正在建立登录会话…",
|
|
88
88
|
"login.submit": "登录",
|
|
89
|
+
"login.technicalDetails": "技术详情",
|
|
90
|
+
"login.correctAddress": "使用正确地址:{origin}",
|
|
91
|
+
"login.errors.invalid_credentials": "账号或密码不正确。请重新输入。",
|
|
92
|
+
"login.errors.invalid_input": "账号格式不正确。账号名只允许字母、数字及 _ . @ -,长度为 1–64。",
|
|
93
|
+
"login.errors.forbidden": "当前登录请求无法完成。请刷新后重试。",
|
|
94
|
+
"login.errors.unauthorized": "登录会话未建立。请重新登录。",
|
|
95
|
+
"login.errors.invalid_origin": "当前访问地址与服务配置不匹配。请使用配置的公开地址登录,或请部署管理员调整公开地址。",
|
|
96
|
+
"login.errors.invalid_csrf": "当前会话或访问地址校验失败。请从配置的公开地址重新登录。",
|
|
97
|
+
"login.errors.login_limited": "登录尝试过多。请等待 15 分钟后重试。",
|
|
98
|
+
"login.errors.session_limit": "登录会话已达到上限。请退出不再使用的会话后重试。",
|
|
99
|
+
"login.errors.management_provider_unavailable": "账号服务暂时不可用。请检查服务状态,恢复后重试。",
|
|
100
|
+
"login.errors.provider_unavailable": "账号服务暂时不可用。请检查服务状态,恢复后重试。",
|
|
101
|
+
"login.errors.control_recovering": "账号服务正在恢复。请稍候重试。",
|
|
102
|
+
"login.errors.session_unavailable": "登录尚未完成。请检查连接后重试,或刷新管理访问状态。",
|
|
103
|
+
"login.errors.unknown": "登录未完成。请检查连接后重试。",
|
|
104
|
+
"recovery.summary": "忘记账号或密码?",
|
|
105
|
+
"recovery.intro": "请在部署服务器上恢复,网页不能直接重置管理员。",
|
|
106
|
+
"recovery.step1": "确认目标实例的配置数据库路径,停止该实例的全部进程并备份数据。",
|
|
107
|
+
"recovery.step2": "在原部署目录、使用原环境变量执行下面的命令,将数据库路径替换为实际绝对路径。",
|
|
108
|
+
"recovery.step3": "按提示输入新的账号和密码,然后重新启动实例。旧会话会失效,路由、服务及 API Key 保留。",
|
|
109
|
+
"recovery.docker": "Docker 部署:停止原容器后,使用同一镜像、原数据卷及环境变量启动临时交互容器,执行同一命令;完成后删除临时容器并启动原容器。请勿另建空数据卷。",
|
|
110
|
+
"recovery.note": "密码交互输入且不显示。若提示恢复失败,请确认实例已完全停止、路径及环境正确;旧版本存在多个管理员时需由部署人员处理。",
|
|
89
111
|
"settings.accountTitle": "管理员账号",
|
|
90
112
|
"settings.accountTag": "账号",
|
|
91
113
|
"settings.passwordTag": "密码",
|
|
@@ -109,6 +131,28 @@
|
|
|
109
131
|
"login.password": "Password",
|
|
110
132
|
"login.busy": "Establishing a sign-in session…",
|
|
111
133
|
"login.submit": "Sign in",
|
|
134
|
+
"login.technicalDetails": "Technical details",
|
|
135
|
+
"login.correctAddress": "Use the correct address: {origin}",
|
|
136
|
+
"login.errors.invalid_credentials": "Incorrect username or password. Enter your credentials again.",
|
|
137
|
+
"login.errors.invalid_input": "Invalid username. Use 1–64 characters containing letters, numbers, and _ . @ - only.",
|
|
138
|
+
"login.errors.forbidden": "This sign-in request could not be completed. Refresh and try again.",
|
|
139
|
+
"login.errors.unauthorized": "A sign-in session was not established. Sign in again.",
|
|
140
|
+
"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.",
|
|
141
|
+
"login.errors.invalid_csrf": "Session or address verification failed. Sign in again at the configured public address.",
|
|
142
|
+
"login.errors.login_limited": "Too many sign-in attempts. Wait 15 minutes before trying again.",
|
|
143
|
+
"login.errors.session_limit": "The sign-in session limit has been reached. Sign out of unused sessions and try again.",
|
|
144
|
+
"login.errors.management_provider_unavailable": "The account service is temporarily unavailable. Check its status and try again after recovery.",
|
|
145
|
+
"login.errors.provider_unavailable": "The account service is temporarily unavailable. Check its status and try again after recovery.",
|
|
146
|
+
"login.errors.control_recovering": "The account service is recovering. Try again shortly.",
|
|
147
|
+
"login.errors.session_unavailable": "Sign-in has not completed. Check your connection and retry, or refresh management access status.",
|
|
148
|
+
"login.errors.unknown": "Sign-in did not complete. Check your connection and try again.",
|
|
149
|
+
"recovery.summary": "Forgot your username or password?",
|
|
150
|
+
"recovery.intro": "Recover access on the deployment server. The web page cannot reset the administrator directly.",
|
|
151
|
+
"recovery.step1": "Confirm the target instance’s configuration database path, stop all its processes, and back up the data.",
|
|
152
|
+
"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.",
|
|
153
|
+
"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.",
|
|
154
|
+
"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.",
|
|
155
|
+
"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.",
|
|
112
156
|
"settings.accountTitle": "Administrator account",
|
|
113
157
|
"settings.accountTag": "Account",
|
|
114
158
|
"settings.passwordTag": "Password",
|
|
@@ -1,36 +1,47 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
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
15
|
async function login(event: SubmitEvent) {
|
|
16
|
-
event.preventDefault();
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
if (busy) return;
|
|
18
|
+
const activeContext = context;
|
|
17
19
|
busy = true; error = ''; detail = '';
|
|
18
20
|
try {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
}
|
|
26
33
|
}
|
|
27
34
|
</script>
|
|
35
|
+
|
|
28
36
|
<form onsubmit={login} aria-label={t('login.title')} aria-busy={busy} class="space-y-4">
|
|
29
|
-
<h2 class="font-mono text-sm text-zinc-100">{t('login.title')}</h2>
|
|
30
37
|
<p class="text-sm text-zinc-400">{t('login.description')}</p>
|
|
31
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>
|
|
32
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>
|
|
33
|
-
{#if error}
|
|
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}
|
|
34
45
|
<RecoveryHelp />
|
|
35
46
|
<button class="nx-btn-primary w-full justify-center" disabled={busy}>{busy ? t('login.busy') : t('login.submit')}</button>
|
|
36
47
|
</form>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { isLoading } from 'svelte-i18n';
|
|
3
|
+
import { _ } from '@bungee/plugin-sdk';
|
|
4
|
+
const t = (key: string) => $isLoading ? '' : $_(`plugins.local-accounts.recovery.${key}`);
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<details class="text-sm text-zinc-400 space-y-2">
|
|
8
|
+
<summary class="cursor-pointer text-nexus-300">{t('summary')}</summary>
|
|
9
|
+
<p>{t('intro')}</p>
|
|
10
|
+
<ol class="list-decimal pl-5 space-y-2">
|
|
11
|
+
<li>{t('step1')}</li>
|
|
12
|
+
<li>{t('step2')}</li>
|
|
13
|
+
<li>{t('step3')}</li>
|
|
14
|
+
</ol>
|
|
15
|
+
<code class="block border border-carbon-600 p-3 font-mono text-xs whitespace-pre-wrap break-all">bun packages/core/dist/main.js --recover-admin /path/to/data/bungee.db</code>
|
|
16
|
+
<p>{t('docker')}</p>
|
|
17
|
+
<p class="text-xs">{t('note')}</p>
|
|
18
|
+
</details>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { loginFailure, requireLoginSuccess } from './login-errors';
|
|
3
|
+
import manifest from '../manifest.json';
|
|
4
|
+
|
|
5
|
+
test('only explicit provider success can proceed to context completion', () => {
|
|
6
|
+
expect(() => requireLoginSuccess({ success: true })).not.toThrow();
|
|
7
|
+
for (const result of [null, undefined, true, {}, { success: 'true' }, { success: false }]) {
|
|
8
|
+
expect(() => requireLoginSuccess(result)).toThrow('login_failed');
|
|
9
|
+
}
|
|
10
|
+
expect(() => requireLoginSuccess({ success: false, error: 'invalid_credentials' })).toThrow('invalid_credentials');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('credential errors are plugin-owned and translated in both locales', () => {
|
|
14
|
+
for (const code of ['invalid_credentials', 'invalid_input', 'forbidden', 'unauthorized', 'invalid_origin', 'invalid_csrf', 'login_limited', 'session_limit', 'management_provider_unavailable', 'provider_unavailable', 'control_recovering']) {
|
|
15
|
+
const failure = loginFailure(new Error(code));
|
|
16
|
+
expect(failure).toEqual({ key: `login.errors.${code}`, detail: code });
|
|
17
|
+
for (const messages of Object.values(manifest.translations)) expect(messages[failure.key as keyof typeof messages]).toBeTruthy();
|
|
18
|
+
}
|
|
19
|
+
expect(loginFailure(new Error('unrecognized')).key).toBe('login.errors.unknown');
|
|
20
|
+
const incomplete = new Error('management_login_initialization_failed'); incomplete.name = 'ManagementAuthenticationError';
|
|
21
|
+
expect(loginFailure(incomplete).key).toBe('login.errors.session_unavailable');
|
|
22
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const loginCodes = new Set([
|
|
2
|
+
'invalid_credentials', 'invalid_input', 'forbidden', 'unauthorized', 'invalid_origin',
|
|
3
|
+
'invalid_csrf', 'login_limited', 'session_limit', 'management_provider_unavailable',
|
|
4
|
+
'provider_unavailable', 'control_recovering',
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
/** Only the local account provider interprets its credential result. */
|
|
8
|
+
export function requireLoginSuccess(result: unknown): void {
|
|
9
|
+
if (typeof result === 'object' && result !== null && 'success' in result && result.success === true) return;
|
|
10
|
+
const code = typeof result === 'object' && result !== null && 'error' in result && typeof result.error === 'string'
|
|
11
|
+
? result.error : 'login_failed';
|
|
12
|
+
throw new Error(code);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loginFailure(error: unknown): { key: string; detail: string } {
|
|
16
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
17
|
+
const code = error instanceof Error && error.name === 'ManagementAuthenticationError'
|
|
18
|
+
? 'session_unavailable' : loginCodes.has(detail) ? detail : 'unknown';
|
|
19
|
+
return { key: `login.errors.${code}`, detail };
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffusion/bungee-core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.0",
|
|
4
4
|
"description": "Bungee reverse proxy core engine",
|
|
5
5
|
"main": "./dist/main.js",
|
|
6
6
|
"types": "./dist/main.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test": "bun test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@jeffusion/bungee-types": "^5.
|
|
15
|
+
"@jeffusion/bungee-types": "^5.8.0",
|
|
16
16
|
"@types/lodash-es": "^4.17.12",
|
|
17
17
|
"acorn": "^8.15.0",
|
|
18
18
|
"canonicalize": "3.0.0",
|