apifm-admin-mcp 26.5.3 → 26.5.4
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/README.md +3 -3
- package/package.json +1 -1
- package/src/browser-auth.js +442 -211
- package/src/index.js +4 -4
- package/src/self-check.js +22 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ It is designed for Kiro, Cursor, Claude Code, Codex, Windsurf, Trae, Qoder, and
|
|
|
8
8
|
|
|
9
9
|
Secrets must not be pasted into model chat.
|
|
10
10
|
|
|
11
|
-
This MCP exposes `apifm_admin_start_auth`, which returns a one-time localhost URL. The user enters Basic Authentication credentials, an existing `X-Token`, username/password login details, email/password login details, or
|
|
11
|
+
This MCP exposes `apifm_admin_start_auth`, which returns a one-time localhost URL. The user enters Basic Authentication credentials, an existing `X-Token`, username/password login details, email/password login details, email signup details, or mobile signup details in that local browser page. Those secrets stay in the local MCP process memory and are never part of MCP tool arguments.
|
|
12
12
|
|
|
13
13
|
If an API call is attempted before authorization, `apifm_admin_call` and `apifm_admin_find_and_call` return the authorization URL directly in both visible text and structured content as `authUrl`. The agent should show that URL immediately instead of asking the user to call another tool.
|
|
14
14
|
|
|
@@ -53,7 +53,7 @@ If installed globally:
|
|
|
53
53
|
|
|
54
54
|
## Tools
|
|
55
55
|
|
|
56
|
-
- `apifm_admin_start_auth`: Starts a local secure authorization page
|
|
56
|
+
- `apifm_admin_start_auth`: Starts a local secure authorization page. The page separates "Log in" from "Register", supports username login, email login, Basic Auth, X-Token, email registration, and mobile registration. Basic Auth fields are merchant number and merchant key. The page supports Simplified Chinese, Traditional Chinese, and English.
|
|
57
57
|
- `apifm_admin_accounts`: Lists local account aliases without revealing secrets.
|
|
58
58
|
- `apifm_admin_switch_account`: Switches the active account alias.
|
|
59
59
|
- `apifm_admin_remove_account`: Clears an account alias and its in-memory secrets.
|
|
@@ -68,7 +68,7 @@ For real backend data, agents should call `apifm_admin_find_and_call` or `apifm_
|
|
|
68
68
|
|
|
69
69
|
1. User: "Log in to my admin account and read the user list."
|
|
70
70
|
2. Agent calls `apifm_admin_find_and_call` with `query: "user list"` and a non-sensitive payload such as `{ "page": 1, "pageSize": 20 }`.
|
|
71
|
-
3. If not authorized yet, the tool returns `authUrl` immediately. The user opens that URL and chooses
|
|
71
|
+
3. If not authorized yet, the tool returns `authUrl` immediately. The user opens that URL and chooses a login method or a registration method.
|
|
72
72
|
4. User completes authorization in the local browser page.
|
|
73
73
|
5. Agent retries the same API call and answers from the returned `apiResult`, not from method documentation.
|
|
74
74
|
|
package/package.json
CHANGED
package/src/browser-auth.js
CHANGED
|
@@ -6,156 +6,238 @@ import { getSdk, resetSdkConfig } from './sdk.js'
|
|
|
6
6
|
|
|
7
7
|
const pendingSessions = new Map()
|
|
8
8
|
|
|
9
|
+
const AUTH_TYPES = new Set([
|
|
10
|
+
'all',
|
|
11
|
+
'basic',
|
|
12
|
+
'x-token',
|
|
13
|
+
'username-password',
|
|
14
|
+
'email-password',
|
|
15
|
+
'register-email',
|
|
16
|
+
'register-mobile'
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
const OPTIONAL_FIELDS = new Set([
|
|
20
|
+
'alias',
|
|
21
|
+
'pdomain',
|
|
22
|
+
'imgcode',
|
|
23
|
+
'k',
|
|
24
|
+
'registerName',
|
|
25
|
+
'registerType',
|
|
26
|
+
'referrer',
|
|
27
|
+
'agentKey',
|
|
28
|
+
'mailCode',
|
|
29
|
+
'smsCode',
|
|
30
|
+
'smsImgCode',
|
|
31
|
+
'smsK'
|
|
32
|
+
])
|
|
33
|
+
|
|
9
34
|
const I18N = {
|
|
10
35
|
'zh-CN': {
|
|
11
36
|
brand: 'APIFM Admin MCP',
|
|
12
|
-
title: '
|
|
37
|
+
title: '安全连接 APIFM 后台',
|
|
13
38
|
lead: '在本机页面完成授权,密钥只保存在当前 MCP 进程内存,不会进入聊天上下文。',
|
|
14
39
|
trustLocal: '本地 127.0.0.1 页面提交',
|
|
15
40
|
trustMemory: '凭证仅在内存中保存',
|
|
16
41
|
trustSwitch: '支持多个后台账号切换',
|
|
17
42
|
footer: '授权完成后回到 Agent,重新执行刚才的后台操作。',
|
|
18
|
-
formTitle: '
|
|
43
|
+
formTitle: '授权方式',
|
|
19
44
|
expires: '页面过期时间',
|
|
20
45
|
alias: '账号别名',
|
|
21
46
|
aliasPh: 'default-admin',
|
|
47
|
+
tabLogin: '登录现有账号',
|
|
48
|
+
tabRegister: '注册新账号',
|
|
22
49
|
methodUsername: '用户名登录',
|
|
23
50
|
methodUsernameSub: '用户名 + 密码',
|
|
24
51
|
methodEmail: '邮箱登录',
|
|
25
52
|
methodEmailSub: '邮箱 + 密码',
|
|
26
53
|
methodBasic: 'Basic Auth',
|
|
27
|
-
methodBasicSub: '
|
|
54
|
+
methodBasicSub: '商户号 + 商户秘钥',
|
|
28
55
|
methodToken: 'X-Token',
|
|
29
56
|
methodTokenSub: '直接使用令牌',
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
methodRegisterEmail: '邮箱注册',
|
|
58
|
+
methodRegisterEmailSub: '邮箱验证码',
|
|
59
|
+
methodRegisterMobile: '手机号注册',
|
|
60
|
+
methodRegisterMobileSub: '短信验证码',
|
|
32
61
|
usernameHint: '使用后台用户名和密码登录,MCP 会通过 SDK 换取 X-Token。',
|
|
33
62
|
emailHint: '使用后台邮箱和密码登录,MCP 会通过 SDK 换取 X-Token。',
|
|
34
63
|
basicHint: '填写 Basic Authentication 信息,将作为 Authorization 请求头调用后台接口。',
|
|
35
64
|
tokenHint: '直接填写管理员登录后的 X-Token,用于后续所有后台接口调用。',
|
|
36
|
-
|
|
65
|
+
registerEmailHint: '使用邮箱注册开通新后台账号,提交成功后 MCP 会尝试自动登录。',
|
|
66
|
+
registerMobileHint: '使用手机号注册开通新后台账号,提交成功后 MCP 会尝试自动登录。',
|
|
37
67
|
username: '用户名',
|
|
38
68
|
usernamePh: '请输入后台用户名',
|
|
39
69
|
password: '密码',
|
|
40
70
|
passwordPh: '请输入登录密码',
|
|
41
71
|
pdomain: '专属域名,可选',
|
|
42
72
|
pdomainPh: '例如 yourdomain',
|
|
73
|
+
imgcode: '图形验证码,可选',
|
|
74
|
+
imgcodePh: '请输入图形验证码',
|
|
75
|
+
k: '验证码随机数,可选',
|
|
76
|
+
kPh: '请输入验证码随机数',
|
|
43
77
|
email: '邮箱地址',
|
|
44
78
|
emailPh: 'name@example.com',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
mobile: '手机号码',
|
|
80
|
+
mobilePh: '请输入手机号码',
|
|
81
|
+
merchantNo: '商户号',
|
|
82
|
+
merchantNoPh: '请输入商户号',
|
|
83
|
+
merchantKey: '商户秘钥',
|
|
84
|
+
merchantKeyPh: '请输入商户秘钥',
|
|
49
85
|
xToken: 'X-Token',
|
|
50
86
|
xTokenPh: '粘贴管理员 X-Token',
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
registerPassword: '登录密码',
|
|
88
|
+
registerPasswordPh: '设置登录密码',
|
|
53
89
|
name: '姓名或昵称',
|
|
54
90
|
namePh: '可选',
|
|
55
91
|
mailCode: '邮箱验证码',
|
|
56
|
-
mailCodePh: '
|
|
92
|
+
mailCodePh: '请输入邮箱验证码',
|
|
93
|
+
smsCode: '短信验证码',
|
|
94
|
+
smsCodePh: '请输入短信验证码',
|
|
95
|
+
type: '注册类型,可选',
|
|
96
|
+
typePh: '不填默认为 apifm',
|
|
97
|
+
referrer: '推荐人用户ID,可选',
|
|
98
|
+
referrerPh: '请输入推荐人用户ID',
|
|
99
|
+
agentKey: 'Agent Key,可选',
|
|
100
|
+
agentKeyPh: '请输入 agentKey',
|
|
57
101
|
submit: '完成授权',
|
|
58
102
|
security: '请不要把密码、Token 或 Basic Authentication 内容粘贴到聊天窗口。',
|
|
59
103
|
closeNote: '提交成功后可以关闭本页面。'
|
|
60
104
|
},
|
|
61
105
|
'zh-TW': {
|
|
62
106
|
brand: 'APIFM Admin MCP',
|
|
63
|
-
title: '
|
|
107
|
+
title: '安全連接 APIFM 後台',
|
|
64
108
|
lead: '在本機頁面完成授權,密鑰只保存在目前 MCP 程序記憶體,不會進入聊天上下文。',
|
|
65
109
|
trustLocal: '本地 127.0.0.1 頁面提交',
|
|
66
110
|
trustMemory: '憑證僅在記憶體中保存',
|
|
67
111
|
trustSwitch: '支援多個後台帳號切換',
|
|
68
112
|
footer: '授權完成後回到 Agent,重新執行剛才的後台操作。',
|
|
69
|
-
formTitle: '
|
|
113
|
+
formTitle: '授權方式',
|
|
70
114
|
expires: '頁面過期時間',
|
|
71
115
|
alias: '帳號別名',
|
|
72
116
|
aliasPh: 'default-admin',
|
|
117
|
+
tabLogin: '登入現有帳號',
|
|
118
|
+
tabRegister: '註冊新帳號',
|
|
73
119
|
methodUsername: '使用者名稱登入',
|
|
74
120
|
methodUsernameSub: '使用者名稱 + 密碼',
|
|
75
121
|
methodEmail: '郵箱登入',
|
|
76
122
|
methodEmailSub: '郵箱 + 密碼',
|
|
77
123
|
methodBasic: 'Basic Auth',
|
|
78
|
-
methodBasicSub: '
|
|
124
|
+
methodBasicSub: '商戶號 + 商戶秘鑰',
|
|
79
125
|
methodToken: 'X-Token',
|
|
80
126
|
methodTokenSub: '直接使用令牌',
|
|
81
|
-
|
|
82
|
-
|
|
127
|
+
methodRegisterEmail: '郵箱註冊',
|
|
128
|
+
methodRegisterEmailSub: '郵箱驗證碼',
|
|
129
|
+
methodRegisterMobile: '手機號註冊',
|
|
130
|
+
methodRegisterMobileSub: '簡訊驗證碼',
|
|
83
131
|
usernameHint: '使用後台使用者名稱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
|
|
84
132
|
emailHint: '使用後台郵箱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
|
|
85
133
|
basicHint: '填寫 Basic Authentication 資訊,將作為 Authorization 請求頭呼叫後台介面。',
|
|
86
134
|
tokenHint: '直接填寫管理員登入後的 X-Token,用於後續所有後台介面呼叫。',
|
|
87
|
-
|
|
135
|
+
registerEmailHint: '使用郵箱註冊開通新後台帳號,提交成功後 MCP 會嘗試自動登入。',
|
|
136
|
+
registerMobileHint: '使用手機號註冊開通新後台帳號,提交成功後 MCP 會嘗試自動登入。',
|
|
88
137
|
username: '使用者名稱',
|
|
89
138
|
usernamePh: '請輸入後台使用者名稱',
|
|
90
139
|
password: '密碼',
|
|
91
140
|
passwordPh: '請輸入登入密碼',
|
|
92
141
|
pdomain: '專屬網域,可選',
|
|
93
142
|
pdomainPh: '例如 yourdomain',
|
|
143
|
+
imgcode: '圖形驗證碼,可選',
|
|
144
|
+
imgcodePh: '請輸入圖形驗證碼',
|
|
145
|
+
k: '驗證碼隨機數,可選',
|
|
146
|
+
kPh: '請輸入驗證碼隨機數',
|
|
94
147
|
email: '郵箱地址',
|
|
95
148
|
emailPh: 'name@example.com',
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
149
|
+
mobile: '手機號碼',
|
|
150
|
+
mobilePh: '請輸入手機號碼',
|
|
151
|
+
merchantNo: '商戶號',
|
|
152
|
+
merchantNoPh: '請輸入商戶號',
|
|
153
|
+
merchantKey: '商戶秘鑰',
|
|
154
|
+
merchantKeyPh: '請輸入商戶秘鑰',
|
|
100
155
|
xToken: 'X-Token',
|
|
101
156
|
xTokenPh: '貼上管理員 X-Token',
|
|
102
|
-
|
|
103
|
-
|
|
157
|
+
registerPassword: '登入密碼',
|
|
158
|
+
registerPasswordPh: '設定登入密碼',
|
|
104
159
|
name: '姓名或暱稱',
|
|
105
160
|
namePh: '可選',
|
|
106
161
|
mailCode: '郵箱驗證碼',
|
|
107
|
-
mailCodePh: '
|
|
162
|
+
mailCodePh: '請輸入郵箱驗證碼',
|
|
163
|
+
smsCode: '簡訊驗證碼',
|
|
164
|
+
smsCodePh: '請輸入簡訊驗證碼',
|
|
165
|
+
type: '註冊類型,可選',
|
|
166
|
+
typePh: '不填預設為 apifm',
|
|
167
|
+
referrer: '推薦人使用者ID,可選',
|
|
168
|
+
referrerPh: '請輸入推薦人使用者ID',
|
|
169
|
+
agentKey: 'Agent Key,可選',
|
|
170
|
+
agentKeyPh: '請輸入 agentKey',
|
|
108
171
|
submit: '完成授權',
|
|
109
172
|
security: '請不要把密碼、Token 或 Basic Authentication 內容貼到聊天視窗。',
|
|
110
173
|
closeNote: '提交成功後可以關閉本頁面。'
|
|
111
174
|
},
|
|
112
175
|
en: {
|
|
113
176
|
brand: 'APIFM Admin MCP',
|
|
114
|
-
title: 'Connect
|
|
177
|
+
title: 'Connect APIFM admin safely',
|
|
115
178
|
lead: 'Authorize on this local page. Secrets stay in this MCP process memory and never enter the chat transcript.',
|
|
116
179
|
trustLocal: 'Submitted to local 127.0.0.1 only',
|
|
117
180
|
trustMemory: 'Credentials are memory-only',
|
|
118
181
|
trustSwitch: 'Multiple admin accounts supported',
|
|
119
182
|
footer: 'After authorization, return to the agent and run the backend action again.',
|
|
120
|
-
formTitle: '
|
|
183
|
+
formTitle: 'Authorization method',
|
|
121
184
|
expires: 'Page expires at',
|
|
122
185
|
alias: 'Account alias',
|
|
123
186
|
aliasPh: 'default-admin',
|
|
187
|
+
tabLogin: 'Log in',
|
|
188
|
+
tabRegister: 'Register',
|
|
124
189
|
methodUsername: 'Username login',
|
|
125
190
|
methodUsernameSub: 'Username + password',
|
|
126
191
|
methodEmail: 'Email login',
|
|
127
192
|
methodEmailSub: 'Email + password',
|
|
128
193
|
methodBasic: 'Basic Auth',
|
|
129
|
-
methodBasicSub: '
|
|
194
|
+
methodBasicSub: 'Merchant no. + key',
|
|
130
195
|
methodToken: 'X-Token',
|
|
131
196
|
methodTokenSub: 'Use an existing token',
|
|
132
|
-
|
|
133
|
-
|
|
197
|
+
methodRegisterEmail: 'Email signup',
|
|
198
|
+
methodRegisterEmailSub: 'Email code',
|
|
199
|
+
methodRegisterMobile: 'Mobile signup',
|
|
200
|
+
methodRegisterMobileSub: 'SMS code',
|
|
134
201
|
usernameHint: 'Log in with an admin username and password. The MCP will exchange them for an X-Token through the SDK.',
|
|
135
202
|
emailHint: 'Log in with an admin email and password. The MCP will exchange them for an X-Token through the SDK.',
|
|
136
203
|
basicHint: 'Enter Basic Authentication credentials. They will be sent as the Authorization header for API calls.',
|
|
137
204
|
tokenHint: 'Enter an existing admin X-Token for later backend API calls.',
|
|
138
|
-
|
|
205
|
+
registerEmailHint: 'Create a new admin account by email. After signup, the MCP will try to log in automatically.',
|
|
206
|
+
registerMobileHint: 'Create a new admin account by mobile. After signup, the MCP will try to log in automatically.',
|
|
139
207
|
username: 'Username',
|
|
140
208
|
usernamePh: 'Enter admin username',
|
|
141
209
|
password: 'Password',
|
|
142
210
|
passwordPh: 'Enter login password',
|
|
143
211
|
pdomain: 'Private domain, optional',
|
|
144
212
|
pdomainPh: 'Example: yourdomain',
|
|
213
|
+
imgcode: 'Image code, optional',
|
|
214
|
+
imgcodePh: 'Enter image code',
|
|
215
|
+
k: 'Captcha nonce, optional',
|
|
216
|
+
kPh: 'Enter captcha nonce',
|
|
145
217
|
email: 'Email address',
|
|
146
218
|
emailPh: 'name@example.com',
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
219
|
+
mobile: 'Mobile number',
|
|
220
|
+
mobilePh: 'Enter mobile number',
|
|
221
|
+
merchantNo: 'Merchant number',
|
|
222
|
+
merchantNoPh: 'Enter merchant number',
|
|
223
|
+
merchantKey: 'Merchant key',
|
|
224
|
+
merchantKeyPh: 'Enter merchant key',
|
|
151
225
|
xToken: 'X-Token',
|
|
152
226
|
xTokenPh: 'Paste admin X-Token',
|
|
153
|
-
|
|
154
|
-
|
|
227
|
+
registerPassword: 'Login password',
|
|
228
|
+
registerPasswordPh: 'Set login password',
|
|
155
229
|
name: 'Name or nickname',
|
|
156
230
|
namePh: 'Optional',
|
|
157
231
|
mailCode: 'Email verification code',
|
|
158
|
-
mailCodePh: 'Enter
|
|
232
|
+
mailCodePh: 'Enter email code',
|
|
233
|
+
smsCode: 'SMS verification code',
|
|
234
|
+
smsCodePh: 'Enter SMS code',
|
|
235
|
+
type: 'Signup type, optional',
|
|
236
|
+
typePh: 'Defaults to apifm',
|
|
237
|
+
referrer: 'Referrer user ID, optional',
|
|
238
|
+
referrerPh: 'Enter referrer user ID',
|
|
239
|
+
agentKey: 'Agent Key, optional',
|
|
240
|
+
agentKeyPh: 'Enter agentKey',
|
|
159
241
|
submit: 'Authorize account',
|
|
160
242
|
security: 'Do not paste passwords, tokens, or Basic Authentication values into the chat window.',
|
|
161
243
|
closeNote: 'You can close this page after authorization succeeds.'
|
|
@@ -224,13 +306,14 @@ export async function createAuthSession({ authType = 'all', alias = '', domains
|
|
|
224
306
|
|
|
225
307
|
async function finishAuth({ authType, alias, domains, fields }) {
|
|
226
308
|
const selectedAuthType = normalizeAuthType(authType)
|
|
309
|
+
|
|
227
310
|
if (selectedAuthType === 'basic') {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
311
|
+
const merchantNo = required(fields.basicUsername, 'merchant number')
|
|
312
|
+
const merchantKey = required(fields.basicPassword, 'merchant key')
|
|
230
313
|
return upsertAccount({
|
|
231
314
|
alias: fields.alias || alias,
|
|
232
315
|
authType: 'basic',
|
|
233
|
-
basicAuth: `Basic ${Buffer.from(`${
|
|
316
|
+
basicAuth: `Basic ${Buffer.from(`${merchantNo}:${merchantKey}`).toString('base64')}`,
|
|
234
317
|
domains
|
|
235
318
|
})
|
|
236
319
|
}
|
|
@@ -244,36 +327,8 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
244
327
|
})
|
|
245
328
|
}
|
|
246
329
|
|
|
247
|
-
if (selectedAuthType === 'username-password' || selectedAuthType === 'email-password'
|
|
248
|
-
const
|
|
249
|
-
const loginPayload =
|
|
250
|
-
loginMode === 'email'
|
|
251
|
-
? {
|
|
252
|
-
email: required(fields.loginEmail, 'email'),
|
|
253
|
-
pwd: required(fields.loginPassword, 'password'),
|
|
254
|
-
rememberMe: true
|
|
255
|
-
}
|
|
256
|
-
: {
|
|
257
|
-
userName: required(fields.loginUsername, 'username'),
|
|
258
|
-
pwd: required(fields.loginPassword, 'password'),
|
|
259
|
-
rememberMe: true,
|
|
260
|
-
pdomain: fields.pdomain || undefined
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const sdk = getSdk()
|
|
264
|
-
resetSdkConfig()
|
|
265
|
-
if (domains && Object.keys(domains).length) {
|
|
266
|
-
sdk.setDomains(domains)
|
|
267
|
-
}
|
|
268
|
-
const methodName = loginMode === 'email' ? 'loginAdminEmail' : 'loginAdminUserName'
|
|
269
|
-
if (typeof sdk[methodName] !== 'function') {
|
|
270
|
-
throw new Error(`apifm-admin does not expose ${methodName}`)
|
|
271
|
-
}
|
|
272
|
-
const result = await sdk[methodName](loginPayload)
|
|
273
|
-
const token = extractToken(result)
|
|
274
|
-
if (!token) {
|
|
275
|
-
throw new Error('Login succeeded but no X-Token was found in the SDK response.')
|
|
276
|
-
}
|
|
330
|
+
if (selectedAuthType === 'username-password' || selectedAuthType === 'email-password') {
|
|
331
|
+
const token = await loginAndExtractToken({ authType: selectedAuthType, domains, fields })
|
|
277
332
|
return upsertAccount({
|
|
278
333
|
alias: fields.alias || alias,
|
|
279
334
|
authType: selectedAuthType,
|
|
@@ -283,18 +338,41 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
283
338
|
}
|
|
284
339
|
|
|
285
340
|
if (selectedAuthType === 'register-email') {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
341
|
+
await callSdkMethod({
|
|
342
|
+
methodName: 'registerAdminSaveEmail',
|
|
343
|
+
domains,
|
|
344
|
+
payload: {
|
|
345
|
+
email: required(fields.registerEmail, 'email'),
|
|
346
|
+
pwd: required(fields.registerPassword, 'password'),
|
|
347
|
+
name: fields.registerName || undefined,
|
|
348
|
+
mailCode: fields.mailCode || undefined,
|
|
349
|
+
referrer: fields.referrer || undefined
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
const token = await loginAndExtractToken({ authType: 'email-password', domains, fields })
|
|
353
|
+
return upsertAccount({
|
|
354
|
+
alias: fields.alias || alias,
|
|
355
|
+
authType: selectedAuthType,
|
|
356
|
+
token,
|
|
357
|
+
domains
|
|
296
358
|
})
|
|
297
|
-
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (selectedAuthType === 'register-mobile') {
|
|
362
|
+
await callSdkMethod({
|
|
363
|
+
methodName: 'registerAdminSave',
|
|
364
|
+
domains,
|
|
365
|
+
payload: {
|
|
366
|
+
type: fields.registerType || undefined,
|
|
367
|
+
mobile: required(fields.registerMobile, 'mobile'),
|
|
368
|
+
pwd: required(fields.registerPassword, 'password'),
|
|
369
|
+
name: fields.registerName || undefined,
|
|
370
|
+
smsCode: fields.smsCode || undefined,
|
|
371
|
+
referrer: fields.referrer || undefined,
|
|
372
|
+
agentKey: fields.agentKey || undefined
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
const token = await loginAndExtractToken({ authType: 'mobile-password', domains, fields })
|
|
298
376
|
return upsertAccount({
|
|
299
377
|
alias: fields.alias || alias,
|
|
300
378
|
authType: selectedAuthType,
|
|
@@ -306,28 +384,141 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
306
384
|
throw new Error(`Unsupported auth type: ${authType}`)
|
|
307
385
|
}
|
|
308
386
|
|
|
387
|
+
async function loginAndExtractToken({ authType, domains, fields }) {
|
|
388
|
+
const loginConfig = {
|
|
389
|
+
'username-password': {
|
|
390
|
+
methodName: 'loginAdminUserName',
|
|
391
|
+
payload: {
|
|
392
|
+
userName: required(fields.loginUsername, 'username'),
|
|
393
|
+
pwd: required(fields.loginPassword, 'password'),
|
|
394
|
+
rememberMe: true,
|
|
395
|
+
pdomain: fields.pdomain || undefined,
|
|
396
|
+
imgcode: fields.imgcode || undefined,
|
|
397
|
+
k: fields.k || undefined
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
'email-password': {
|
|
401
|
+
methodName: 'loginAdminEmail',
|
|
402
|
+
payload: {
|
|
403
|
+
email: required(fields.loginEmail || fields.registerEmail, 'email'),
|
|
404
|
+
pwd: required(fields.loginPassword || fields.registerPassword, 'password'),
|
|
405
|
+
rememberMe: true,
|
|
406
|
+
imgcode: fields.imgcode || undefined,
|
|
407
|
+
k: fields.k || undefined
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
'mobile-password': {
|
|
411
|
+
methodName: 'loginAdminMobile',
|
|
412
|
+
payload: {
|
|
413
|
+
mobile: required(fields.registerMobile, 'mobile'),
|
|
414
|
+
pwd: required(fields.registerPassword, 'password'),
|
|
415
|
+
rememberMe: true,
|
|
416
|
+
imgcode: fields.smsImgCode || fields.imgcode || undefined,
|
|
417
|
+
k: fields.smsK || fields.k || undefined
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}[authType]
|
|
421
|
+
|
|
422
|
+
const result = await callSdkMethod({
|
|
423
|
+
methodName: loginConfig.methodName,
|
|
424
|
+
domains,
|
|
425
|
+
payload: loginConfig.payload
|
|
426
|
+
})
|
|
427
|
+
const token = extractToken(result)
|
|
428
|
+
if (!token) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`${loginConfig.methodName} did not return an X-Token. Response shape was: ${JSON.stringify(maskResponseForError(result)).slice(0, 600)}`
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
return token
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function callSdkMethod({ methodName, domains, payload }) {
|
|
437
|
+
const sdk = getSdk()
|
|
438
|
+
resetSdkConfig()
|
|
439
|
+
if (domains && Object.keys(domains).length) {
|
|
440
|
+
sdk.setDomains(domains)
|
|
441
|
+
}
|
|
442
|
+
if (typeof sdk[methodName] !== 'function') {
|
|
443
|
+
throw new Error(`apifm-admin does not expose ${methodName}`)
|
|
444
|
+
}
|
|
445
|
+
return sdk[methodName](compactObject(payload))
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function compactObject(input) {
|
|
449
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== ''))
|
|
450
|
+
}
|
|
451
|
+
|
|
309
452
|
function normalizeAuthType(authType) {
|
|
310
453
|
if (authType === 'password') return 'username-password'
|
|
311
|
-
if (
|
|
312
|
-
['all', 'basic', 'x-token', 'username-password', 'email-password', 'register-email'].includes(authType)
|
|
313
|
-
) {
|
|
314
|
-
return authType
|
|
315
|
-
}
|
|
454
|
+
if (AUTH_TYPES.has(authType)) return authType
|
|
316
455
|
return 'all'
|
|
317
456
|
}
|
|
318
457
|
|
|
319
458
|
function extractToken(response) {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
459
|
+
const directPaths = [
|
|
460
|
+
['data', 'token'],
|
|
461
|
+
['data', 'xToken'],
|
|
462
|
+
['data', 'xtoken'],
|
|
463
|
+
['data', 'x_token'],
|
|
464
|
+
['data', 'x-token'],
|
|
465
|
+
['data', 'X-Token'],
|
|
466
|
+
['data', 'loginToken'],
|
|
467
|
+
['data', 'adminToken'],
|
|
468
|
+
['token'],
|
|
469
|
+
['xToken'],
|
|
470
|
+
['xtoken'],
|
|
471
|
+
['x_token'],
|
|
472
|
+
['x-token'],
|
|
473
|
+
['X-Token']
|
|
329
474
|
]
|
|
330
|
-
|
|
475
|
+
|
|
476
|
+
for (const path of directPaths) {
|
|
477
|
+
const value = getPath(response, path)
|
|
478
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const found = findTokenDeep(response)
|
|
482
|
+
return found || ''
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function getPath(value, path) {
|
|
486
|
+
return path.reduce((current, key) => (current && typeof current === 'object' ? current[key] : undefined), value)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function findTokenDeep(value, depth = 0) {
|
|
490
|
+
if (!value || typeof value !== 'object' || depth > 5) return ''
|
|
491
|
+
if (Array.isArray(value)) {
|
|
492
|
+
for (const item of value) {
|
|
493
|
+
const found = findTokenDeep(item, depth + 1)
|
|
494
|
+
if (found) return found
|
|
495
|
+
}
|
|
496
|
+
return ''
|
|
497
|
+
}
|
|
498
|
+
for (const [key, child] of Object.entries(value)) {
|
|
499
|
+
const compactKey = key.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
500
|
+
if (
|
|
501
|
+
typeof child === 'string' &&
|
|
502
|
+
child.trim() &&
|
|
503
|
+
['token', 'xtoken', 'xToken', 'xstoken', 'admintoken', 'logintoken'].map((item) => item.toLowerCase()).includes(compactKey)
|
|
504
|
+
) {
|
|
505
|
+
return child.trim()
|
|
506
|
+
}
|
|
507
|
+
const found = findTokenDeep(child, depth + 1)
|
|
508
|
+
if (found) return found
|
|
509
|
+
}
|
|
510
|
+
return ''
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function maskResponseForError(value) {
|
|
514
|
+
if (!value || typeof value !== 'object') return value
|
|
515
|
+
if (Array.isArray(value)) return value.map(maskResponseForError)
|
|
516
|
+
return Object.fromEntries(
|
|
517
|
+
Object.entries(value).map(([key, child]) => [
|
|
518
|
+
key,
|
|
519
|
+
/token|password|pwd|secret|key/i.test(key) ? '[REDACTED]' : maskResponseForError(child)
|
|
520
|
+
])
|
|
521
|
+
)
|
|
331
522
|
}
|
|
332
523
|
|
|
333
524
|
function required(value, label) {
|
|
@@ -373,6 +564,7 @@ function escapeHtml(value) {
|
|
|
373
564
|
|
|
374
565
|
function renderForm({ authType, alias, expiresAt }) {
|
|
375
566
|
const initialType = authType === 'all' ? 'username-password' : authType
|
|
567
|
+
const initialGroup = initialType.startsWith('register-') ? 'register' : 'login'
|
|
376
568
|
return `<!doctype html>
|
|
377
569
|
<html lang="zh-CN">
|
|
378
570
|
<head>
|
|
@@ -392,8 +584,6 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
392
584
|
--accent-ink: oklch(1 0 0);
|
|
393
585
|
--accent-soft: oklch(0.94 0.035 255);
|
|
394
586
|
--success: oklch(0.58 0.15 155);
|
|
395
|
-
--danger: oklch(0.55 0.18 25);
|
|
396
|
-
--radius: 12px;
|
|
397
587
|
}
|
|
398
588
|
* { box-sizing: border-box; }
|
|
399
589
|
body {
|
|
@@ -406,7 +596,7 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
406
596
|
color: var(--ink);
|
|
407
597
|
}
|
|
408
598
|
.shell {
|
|
409
|
-
width: min(
|
|
599
|
+
width: min(1120px, calc(100vw - 32px));
|
|
410
600
|
min-height: 100vh;
|
|
411
601
|
margin: 0 auto;
|
|
412
602
|
display: grid;
|
|
@@ -416,7 +606,7 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
416
606
|
.panel {
|
|
417
607
|
width: 100%;
|
|
418
608
|
display: grid;
|
|
419
|
-
grid-template-columns: minmax(280px, 0.
|
|
609
|
+
grid-template-columns: minmax(280px, 0.78fr) minmax(340px, 1.22fr);
|
|
420
610
|
background: var(--surface);
|
|
421
611
|
border: 1px solid var(--line);
|
|
422
612
|
border-radius: 16px;
|
|
@@ -449,25 +639,35 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
449
639
|
.trust li { display: flex; gap: 10px; align-items: flex-start; color: oklch(0.91 0.015 250); }
|
|
450
640
|
.dot { width: 8px; height: 8px; margin-top: 7px; border-radius: 999px; background: oklch(0.78 0.16 155); flex: 0 0 auto; }
|
|
451
641
|
.content { padding: 30px; }
|
|
452
|
-
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom:
|
|
642
|
+
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
|
|
453
643
|
.meta { color: var(--muted); font-size: 0.92rem; }
|
|
454
|
-
.lang {
|
|
455
|
-
|
|
644
|
+
.lang, .tabs {
|
|
645
|
+
display: inline-grid;
|
|
646
|
+
grid-auto-flow: column;
|
|
647
|
+
gap: 4px;
|
|
648
|
+
padding: 4px;
|
|
649
|
+
background: var(--surface-2);
|
|
650
|
+
border: 1px solid var(--line);
|
|
651
|
+
border-radius: 999px;
|
|
652
|
+
}
|
|
653
|
+
.lang button, .tabs button, .method button {
|
|
456
654
|
appearance: none;
|
|
457
655
|
border: 0;
|
|
458
656
|
font: inherit;
|
|
459
657
|
cursor: pointer;
|
|
460
658
|
}
|
|
461
|
-
.lang button { padding:
|
|
462
|
-
.lang button[aria-pressed="true"] { background: var(--ink); color: white; }
|
|
659
|
+
.lang button, .tabs button { padding: 7px 12px; border-radius: 999px; color: var(--muted); background: transparent; }
|
|
660
|
+
.lang button[aria-pressed="true"], .tabs button[aria-pressed="true"] { background: var(--ink); color: white; }
|
|
661
|
+
.tabs { margin-bottom: 14px; }
|
|
463
662
|
.method {
|
|
464
663
|
display: grid;
|
|
465
|
-
grid-template-columns: repeat(
|
|
664
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
466
665
|
gap: 8px;
|
|
467
666
|
margin-bottom: 22px;
|
|
468
667
|
}
|
|
668
|
+
.method[data-group="register"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
469
669
|
.method button {
|
|
470
|
-
min-height:
|
|
670
|
+
min-height: 62px;
|
|
471
671
|
padding: 10px;
|
|
472
672
|
border: 1px solid var(--line);
|
|
473
673
|
border-radius: 10px;
|
|
@@ -519,11 +719,7 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
519
719
|
transition: transform 160ms ease, filter 160ms ease;
|
|
520
720
|
}
|
|
521
721
|
.submit:hover { filter: brightness(1.03); transform: translateY(-1px); }
|
|
522
|
-
.note {
|
|
523
|
-
margin: 12px 0 0;
|
|
524
|
-
color: var(--muted);
|
|
525
|
-
font-size: 0.9rem;
|
|
526
|
-
}
|
|
722
|
+
.note { margin: 12px 0 0; color: var(--muted); font-size: 0.9rem; }
|
|
527
723
|
.security {
|
|
528
724
|
margin-top: 20px;
|
|
529
725
|
padding: 12px 14px;
|
|
@@ -533,17 +729,17 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
533
729
|
font-size: 0.9rem;
|
|
534
730
|
}
|
|
535
731
|
[hidden] { display: none !important; }
|
|
536
|
-
@media (max-width:
|
|
732
|
+
@media (max-width: 900px) {
|
|
537
733
|
.panel { grid-template-columns: 1fr; }
|
|
538
734
|
.aside { padding: 26px; }
|
|
539
735
|
.method { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
540
736
|
.grid { grid-template-columns: 1fr; }
|
|
541
737
|
}
|
|
542
738
|
@media (max-width: 520px) {
|
|
543
|
-
.shell { width: min(100vw - 20px,
|
|
739
|
+
.shell { width: min(100vw - 20px, 1120px); padding: 10px 0; }
|
|
544
740
|
.content { padding: 20px; }
|
|
545
741
|
.topbar { align-items: flex-start; flex-direction: column; }
|
|
546
|
-
.method { grid-template-columns: 1fr; }
|
|
742
|
+
.method, .method[data-group="register"] { grid-template-columns: 1fr; }
|
|
547
743
|
h1 { font-size: 1.55rem; }
|
|
548
744
|
}
|
|
549
745
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -556,22 +752,22 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
556
752
|
<section class="panel" aria-labelledby="title">
|
|
557
753
|
<aside class="aside">
|
|
558
754
|
<div>
|
|
559
|
-
<div class="brand"><span class="mark">A</span><span data-i18n="brand"
|
|
560
|
-
<h1 id="title" data-i18n="title"
|
|
561
|
-
<p class="lead" data-i18n="lead"
|
|
755
|
+
<div class="brand"><span class="mark">A</span><span data-i18n="brand"></span></div>
|
|
756
|
+
<h1 id="title" data-i18n="title"></h1>
|
|
757
|
+
<p class="lead" data-i18n="lead"></p>
|
|
562
758
|
<ul class="trust">
|
|
563
|
-
<li><span class="dot"></span><span data-i18n="trustLocal"
|
|
564
|
-
<li><span class="dot"></span><span data-i18n="trustMemory"
|
|
565
|
-
<li><span class="dot"></span><span data-i18n="trustSwitch"
|
|
759
|
+
<li><span class="dot"></span><span data-i18n="trustLocal"></span></li>
|
|
760
|
+
<li><span class="dot"></span><span data-i18n="trustMemory"></span></li>
|
|
761
|
+
<li><span class="dot"></span><span data-i18n="trustSwitch"></span></li>
|
|
566
762
|
</ul>
|
|
567
763
|
</div>
|
|
568
|
-
<p class="lead" data-i18n="footer"
|
|
764
|
+
<p class="lead" data-i18n="footer"></p>
|
|
569
765
|
</aside>
|
|
570
766
|
<div class="content">
|
|
571
767
|
<div class="topbar">
|
|
572
768
|
<div>
|
|
573
|
-
<strong data-i18n="formTitle"
|
|
574
|
-
<div class="meta"><span data-i18n="expires"
|
|
769
|
+
<strong data-i18n="formTitle"></strong>
|
|
770
|
+
<div class="meta"><span data-i18n="expires"></span>: ${escapeHtml(new Date(expiresAt).toLocaleString())}</div>
|
|
575
771
|
</div>
|
|
576
772
|
<div class="lang" aria-label="Language">
|
|
577
773
|
<button type="button" data-lang="zh-CN" aria-pressed="true">简</button>
|
|
@@ -580,112 +776,57 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
580
776
|
</div>
|
|
581
777
|
</div>
|
|
582
778
|
|
|
583
|
-
<div class="
|
|
779
|
+
<div class="tabs" role="tablist" aria-label="Account mode">
|
|
780
|
+
<button type="button" data-group-tab="login" aria-pressed="${initialGroup === 'login'}" data-i18n="tabLogin"></button>
|
|
781
|
+
<button type="button" data-group-tab="register" aria-pressed="${initialGroup === 'register'}" data-i18n="tabRegister"></button>
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<div class="method" data-group="login">
|
|
584
785
|
${renderMethodButton('username-password', initialType)}
|
|
585
786
|
${renderMethodButton('email-password', initialType)}
|
|
586
787
|
${renderMethodButton('basic', initialType)}
|
|
587
788
|
${renderMethodButton('x-token', initialType)}
|
|
789
|
+
</div>
|
|
790
|
+
<div class="method" data-group="register" hidden>
|
|
588
791
|
${renderMethodButton('register-email', initialType)}
|
|
792
|
+
${renderMethodButton('register-mobile', initialType)}
|
|
589
793
|
</div>
|
|
590
794
|
|
|
591
795
|
<form method="post" autocomplete="off" id="authForm">
|
|
592
796
|
<input type="hidden" id="authType" name="authType" value="${escapeHtml(initialType)}">
|
|
593
797
|
<div class="field">
|
|
594
|
-
<label for="alias" data-i18n="alias"
|
|
595
|
-
<input id="alias" name="alias" value="${escapeHtml(alias)}"
|
|
798
|
+
<label for="alias" data-i18n="alias"></label>
|
|
799
|
+
<input id="alias" name="alias" value="${escapeHtml(alias)}" data-i18n-placeholder="aliasPh">
|
|
596
800
|
</div>
|
|
597
801
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
<div class="field">
|
|
602
|
-
<label for="loginUsername" data-i18n="username">用户名</label>
|
|
603
|
-
<input id="loginUsername" name="loginUsername" data-i18n-placeholder="usernamePh">
|
|
604
|
-
</div>
|
|
605
|
-
<div class="field">
|
|
606
|
-
<label for="loginPasswordUser" data-i18n="password">密码</label>
|
|
607
|
-
<input id="loginPasswordUser" name="loginPassword" type="password" data-i18n-placeholder="passwordPh">
|
|
608
|
-
</div>
|
|
609
|
-
</div>
|
|
610
|
-
<div class="field" style="margin-top:14px">
|
|
611
|
-
<label for="pdomain" data-i18n="pdomain">专属域名,可选</label>
|
|
612
|
-
<input id="pdomain" name="pdomain" data-i18n-placeholder="pdomainPh">
|
|
613
|
-
</div>
|
|
614
|
-
</section>
|
|
615
|
-
|
|
616
|
-
<section class="section" data-section="email-password">
|
|
617
|
-
<p class="section-head" data-i18n="emailHint">使用后台邮箱和密码登录,MCP 会通过 SDK 换取 X-Token。</p>
|
|
618
|
-
<div class="grid">
|
|
619
|
-
<div class="field">
|
|
620
|
-
<label for="loginEmail" data-i18n="email">邮箱地址</label>
|
|
621
|
-
<input id="loginEmail" name="loginEmail" type="email" data-i18n-placeholder="emailPh">
|
|
622
|
-
</div>
|
|
623
|
-
<div class="field">
|
|
624
|
-
<label for="loginPasswordEmail" data-i18n="password">密码</label>
|
|
625
|
-
<input id="loginPasswordEmail" name="loginPassword" type="password" data-i18n-placeholder="passwordPh">
|
|
626
|
-
</div>
|
|
627
|
-
</div>
|
|
628
|
-
</section>
|
|
629
|
-
|
|
630
|
-
<section class="section" data-section="basic">
|
|
631
|
-
<p class="section-head" data-i18n="basicHint">填写 Basic Authentication 信息,将作为 Authorization 请求头调用后台接口。</p>
|
|
632
|
-
<div class="grid">
|
|
633
|
-
<div class="field">
|
|
634
|
-
<label for="basicUsername" data-i18n="basicUser">Basic 用户名</label>
|
|
635
|
-
<input id="basicUsername" name="basicUsername" data-i18n-placeholder="basicUserPh">
|
|
636
|
-
</div>
|
|
637
|
-
<div class="field">
|
|
638
|
-
<label for="basicPassword" data-i18n="basicPass">Basic 密码</label>
|
|
639
|
-
<input id="basicPassword" name="basicPassword" type="password" data-i18n-placeholder="basicPassPh">
|
|
640
|
-
</div>
|
|
641
|
-
</div>
|
|
642
|
-
</section>
|
|
643
|
-
|
|
644
|
-
<section class="section" data-section="x-token">
|
|
645
|
-
<p class="section-head" data-i18n="tokenHint">直接填写管理员登录后的 X-Token,用于后续所有后台接口调用。</p>
|
|
646
|
-
<div class="field">
|
|
647
|
-
<label for="xToken" data-i18n="xToken">X-Token</label>
|
|
648
|
-
<input id="xToken" name="xToken" type="password" data-i18n-placeholder="xTokenPh">
|
|
649
|
-
</div>
|
|
650
|
-
</section>
|
|
651
|
-
|
|
652
|
-
<section class="section" data-section="register-email">
|
|
653
|
-
<p class="section-head" data-i18n="registerHint">使用邮箱注册开通新后台账号。需要验证码时,请先通过 SDK 获取邮箱验证码。</p>
|
|
654
|
-
<div class="grid">
|
|
655
|
-
<div class="field">
|
|
656
|
-
<label for="registerEmail" data-i18n="email">邮箱地址</label>
|
|
657
|
-
<input id="registerEmail" name="registerEmail" type="email" data-i18n-placeholder="emailPh">
|
|
658
|
-
</div>
|
|
659
|
-
<div class="field">
|
|
660
|
-
<label for="registerPassword" data-i18n="newPassword">登录密码</label>
|
|
661
|
-
<input id="registerPassword" name="registerPassword" type="password" data-i18n-placeholder="newPasswordPh">
|
|
662
|
-
</div>
|
|
663
|
-
<div class="field">
|
|
664
|
-
<label for="registerName" data-i18n="name">姓名或昵称</label>
|
|
665
|
-
<input id="registerName" name="registerName" data-i18n-placeholder="namePh">
|
|
666
|
-
</div>
|
|
667
|
-
<div class="field">
|
|
668
|
-
<label for="mailCode" data-i18n="mailCode">邮箱验证码</label>
|
|
669
|
-
<input id="mailCode" name="mailCode" data-i18n-placeholder="mailCodePh">
|
|
670
|
-
</div>
|
|
671
|
-
</div>
|
|
672
|
-
</section>
|
|
673
|
-
|
|
674
|
-
<button class="submit" type="submit" data-i18n="submit">完成授权</button>
|
|
802
|
+
${renderSections()}
|
|
803
|
+
|
|
804
|
+
<button class="submit" type="submit" data-i18n="submit"></button>
|
|
675
805
|
</form>
|
|
676
|
-
<div class="security" data-i18n="security"
|
|
677
|
-
<p class="note" data-i18n="closeNote"
|
|
806
|
+
<div class="security" data-i18n="security"></div>
|
|
807
|
+
<p class="note" data-i18n="closeNote"></p>
|
|
678
808
|
</div>
|
|
679
809
|
</section>
|
|
680
810
|
</main>
|
|
681
811
|
<script>
|
|
682
812
|
const messages = ${JSON.stringify(I18N)}
|
|
683
813
|
const initialType = ${JSON.stringify(initialType)}
|
|
814
|
+
const methodGroups = {
|
|
815
|
+
'username-password': 'login',
|
|
816
|
+
'email-password': 'login',
|
|
817
|
+
basic: 'login',
|
|
818
|
+
'x-token': 'login',
|
|
819
|
+
'register-email': 'register',
|
|
820
|
+
'register-mobile': 'register'
|
|
821
|
+
}
|
|
822
|
+
const optionalFields = new Set(${JSON.stringify([...OPTIONAL_FIELDS])})
|
|
684
823
|
let activeLang = localStorage.getItem('apifm-auth-lang') || 'zh-CN'
|
|
685
824
|
|
|
686
825
|
const authTypeInput = document.querySelector('#authType')
|
|
687
826
|
const sections = [...document.querySelectorAll('[data-section]')]
|
|
688
827
|
const methodButtons = [...document.querySelectorAll('[data-method]')]
|
|
828
|
+
const groupTabs = [...document.querySelectorAll('[data-group-tab]')]
|
|
829
|
+
const methodGroupsEl = [...document.querySelectorAll('.method[data-group]')]
|
|
689
830
|
const langButtons = [...document.querySelectorAll('[data-lang]')]
|
|
690
831
|
|
|
691
832
|
function t(key) {
|
|
@@ -707,14 +848,32 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
707
848
|
})
|
|
708
849
|
}
|
|
709
850
|
|
|
851
|
+
function setGroup(group) {
|
|
852
|
+
groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
|
|
853
|
+
methodGroupsEl.forEach((node) => {
|
|
854
|
+
node.hidden = node.dataset.group !== group
|
|
855
|
+
})
|
|
856
|
+
const currentMethod = authTypeInput.value
|
|
857
|
+
if (methodGroups[currentMethod] !== group) {
|
|
858
|
+
const firstMethod = methodButtons.find((button) => methodGroups[button.dataset.method] === group)?.dataset.method
|
|
859
|
+
setMethod(firstMethod)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
710
863
|
function setMethod(method) {
|
|
864
|
+
if (!method) return
|
|
711
865
|
authTypeInput.value = method
|
|
866
|
+
const group = methodGroups[method]
|
|
867
|
+
groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
|
|
868
|
+
methodGroupsEl.forEach((node) => {
|
|
869
|
+
node.hidden = node.dataset.group !== group
|
|
870
|
+
})
|
|
712
871
|
sections.forEach((section) => {
|
|
713
872
|
const active = section.dataset.section === method
|
|
714
873
|
section.classList.toggle('active', active)
|
|
715
874
|
section.querySelectorAll('input').forEach((input) => {
|
|
716
875
|
input.disabled = !active
|
|
717
|
-
input.required = active &&
|
|
876
|
+
input.required = active && !optionalFields.has(input.name)
|
|
718
877
|
})
|
|
719
878
|
})
|
|
720
879
|
methodButtons.forEach((button) => {
|
|
@@ -723,6 +882,7 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
723
882
|
}
|
|
724
883
|
|
|
725
884
|
methodButtons.forEach((button) => button.addEventListener('click', () => setMethod(button.dataset.method)))
|
|
885
|
+
groupTabs.forEach((button) => button.addEventListener('click', () => setGroup(button.dataset.groupTab)))
|
|
726
886
|
langButtons.forEach((button) => button.addEventListener('click', () => setLang(button.dataset.lang)))
|
|
727
887
|
setLang(activeLang)
|
|
728
888
|
setMethod(initialType)
|
|
@@ -731,13 +891,84 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
731
891
|
</html>`
|
|
732
892
|
}
|
|
733
893
|
|
|
894
|
+
function renderSections() {
|
|
895
|
+
return `
|
|
896
|
+
<section class="section" data-section="username-password">
|
|
897
|
+
<p class="section-head" data-i18n="usernameHint"></p>
|
|
898
|
+
<div class="grid">
|
|
899
|
+
${field('loginUsername', 'loginUsername', 'username', 'usernamePh')}
|
|
900
|
+
${field('loginPasswordUser', 'loginPassword', 'password', 'passwordPh', 'password')}
|
|
901
|
+
${field('pdomain', 'pdomain', 'pdomain', 'pdomainPh')}
|
|
902
|
+
${field('imgcode', 'imgcode', 'imgcode', 'imgcodePh')}
|
|
903
|
+
${field('k', 'k', 'k', 'kPh')}
|
|
904
|
+
</div>
|
|
905
|
+
</section>
|
|
906
|
+
|
|
907
|
+
<section class="section" data-section="email-password">
|
|
908
|
+
<p class="section-head" data-i18n="emailHint"></p>
|
|
909
|
+
<div class="grid">
|
|
910
|
+
${field('loginEmail', 'loginEmail', 'email', 'emailPh', 'email')}
|
|
911
|
+
${field('loginPasswordEmail', 'loginPassword', 'password', 'passwordPh', 'password')}
|
|
912
|
+
${field('imgcodeEmail', 'imgcode', 'imgcode', 'imgcodePh')}
|
|
913
|
+
${field('kEmail', 'k', 'k', 'kPh')}
|
|
914
|
+
</div>
|
|
915
|
+
</section>
|
|
916
|
+
|
|
917
|
+
<section class="section" data-section="basic">
|
|
918
|
+
<p class="section-head" data-i18n="basicHint"></p>
|
|
919
|
+
<div class="grid">
|
|
920
|
+
${field('basicUsername', 'basicUsername', 'merchantNo', 'merchantNoPh')}
|
|
921
|
+
${field('basicPassword', 'basicPassword', 'merchantKey', 'merchantKeyPh', 'password')}
|
|
922
|
+
</div>
|
|
923
|
+
</section>
|
|
924
|
+
|
|
925
|
+
<section class="section" data-section="x-token">
|
|
926
|
+
<p class="section-head" data-i18n="tokenHint"></p>
|
|
927
|
+
${field('xToken', 'xToken', 'xToken', 'xTokenPh', 'password')}
|
|
928
|
+
</section>
|
|
929
|
+
|
|
930
|
+
<section class="section" data-section="register-email">
|
|
931
|
+
<p class="section-head" data-i18n="registerEmailHint"></p>
|
|
932
|
+
<div class="grid">
|
|
933
|
+
${field('registerEmail', 'registerEmail', 'email', 'emailPh', 'email')}
|
|
934
|
+
${field('registerPasswordEmail', 'registerPassword', 'registerPassword', 'registerPasswordPh', 'password')}
|
|
935
|
+
${field('registerNameEmail', 'registerName', 'name', 'namePh')}
|
|
936
|
+
${field('mailCode', 'mailCode', 'mailCode', 'mailCodePh')}
|
|
937
|
+
${field('referrerEmail', 'referrer', 'referrer', 'referrerPh')}
|
|
938
|
+
</div>
|
|
939
|
+
</section>
|
|
940
|
+
|
|
941
|
+
<section class="section" data-section="register-mobile">
|
|
942
|
+
<p class="section-head" data-i18n="registerMobileHint"></p>
|
|
943
|
+
<div class="grid">
|
|
944
|
+
${field('registerMobile', 'registerMobile', 'mobile', 'mobilePh', 'tel')}
|
|
945
|
+
${field('registerPasswordMobile', 'registerPassword', 'registerPassword', 'registerPasswordPh', 'password')}
|
|
946
|
+
${field('registerNameMobile', 'registerName', 'name', 'namePh')}
|
|
947
|
+
${field('smsCode', 'smsCode', 'smsCode', 'smsCodePh')}
|
|
948
|
+
${field('registerType', 'registerType', 'type', 'typePh')}
|
|
949
|
+
${field('referrerMobile', 'referrer', 'referrer', 'referrerPh')}
|
|
950
|
+
${field('agentKey', 'agentKey', 'agentKey', 'agentKeyPh')}
|
|
951
|
+
${field('smsImgCode', 'smsImgCode', 'imgcode', 'imgcodePh')}
|
|
952
|
+
${field('smsK', 'smsK', 'k', 'kPh')}
|
|
953
|
+
</div>
|
|
954
|
+
</section>`
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function field(id, name, labelKey, placeholderKey, type = 'text') {
|
|
958
|
+
return `<div class="field">
|
|
959
|
+
<label for="${id}" data-i18n="${labelKey}"></label>
|
|
960
|
+
<input id="${id}" name="${name}" type="${type}" data-i18n-placeholder="${placeholderKey}">
|
|
961
|
+
</div>`
|
|
962
|
+
}
|
|
963
|
+
|
|
734
964
|
function renderMethodButton(method, activeType) {
|
|
735
965
|
const keys = {
|
|
736
966
|
'username-password': ['methodUsername', 'methodUsernameSub'],
|
|
737
967
|
'email-password': ['methodEmail', 'methodEmailSub'],
|
|
738
968
|
basic: ['methodBasic', 'methodBasicSub'],
|
|
739
969
|
'x-token': ['methodToken', 'methodTokenSub'],
|
|
740
|
-
'register-email': ['
|
|
970
|
+
'register-email': ['methodRegisterEmail', 'methodRegisterEmailSub'],
|
|
971
|
+
'register-mobile': ['methodRegisterMobile', 'methodRegisterMobileSub']
|
|
741
972
|
}[method]
|
|
742
973
|
return `<button type="button" role="tab" data-method="${method}" aria-pressed="${method === activeType}">
|
|
743
974
|
<strong data-i18n="${keys[0]}"></strong>
|
|
@@ -750,5 +981,5 @@ function renderSuccess(account) {
|
|
|
750
981
|
}
|
|
751
982
|
|
|
752
983
|
function renderError(error) {
|
|
753
|
-
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px"><
|
|
984
|
+
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px;background:#f7f8fb;color:#202124"><main style="max-width:620px;margin:10vh auto;background:white;border:1px solid #dde2ea;border-radius:14px;padding:32px"><h1>授权失败</h1><p>${escapeHtml(error.message)}</p><p>请返回上一页检查填写内容后重新提交。</p></main></body></html>`
|
|
754
985
|
}
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
summarizeMetadata
|
|
22
22
|
} from './sdk.js'
|
|
23
23
|
|
|
24
|
-
const VERSION = '26.5.
|
|
24
|
+
const VERSION = '26.5.4'
|
|
25
25
|
|
|
26
26
|
const server = new McpServer(
|
|
27
27
|
{
|
|
@@ -44,13 +44,13 @@ server.registerTool(
|
|
|
44
44
|
{
|
|
45
45
|
title: 'Start secure APIFM admin authorization',
|
|
46
46
|
description:
|
|
47
|
-
'Creates a localhost browser page for
|
|
47
|
+
'Creates a localhost browser page for login, Basic Auth, X-Token, email registration, or mobile registration. Secrets are stored only in this MCP process memory and must not be pasted into chat.',
|
|
48
48
|
inputSchema: z.object({
|
|
49
49
|
authType: z
|
|
50
|
-
.enum(['all', 'basic', 'x-token', 'password', 'username-password', 'email-password', 'register-email'])
|
|
50
|
+
.enum(['all', 'basic', 'x-token', 'password', 'username-password', 'email-password', 'register-email', 'register-mobile'])
|
|
51
51
|
.optional()
|
|
52
52
|
.describe(
|
|
53
|
-
'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token, username-password/email-password = SDK login, register-email = create admin account
|
|
53
|
+
'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token, username-password/email-password = SDK login, register-email/register-mobile = create admin account.'
|
|
54
54
|
),
|
|
55
55
|
alias: z.string().optional().describe('Friendly local account alias, for example prod or test-shop.'),
|
|
56
56
|
domains: z
|
package/src/self-check.js
CHANGED
|
@@ -41,12 +41,33 @@ try {
|
|
|
41
41
|
throw new Error('start_auth should return a local authorization URL.')
|
|
42
42
|
}
|
|
43
43
|
const authPage = await fetch(authData.url).then((response) => response.text())
|
|
44
|
-
for (const expected of [
|
|
44
|
+
for (const expected of [
|
|
45
|
+
'data-lang="zh-CN"',
|
|
46
|
+
'data-lang="zh-TW"',
|
|
47
|
+
'data-lang="en"',
|
|
48
|
+
'data-group-tab="login"',
|
|
49
|
+
'data-group-tab="register"',
|
|
50
|
+
'data-method="basic"',
|
|
51
|
+
'data-method="x-token"',
|
|
52
|
+
'data-method="register-mobile"',
|
|
53
|
+
'merchantNo',
|
|
54
|
+
'merchantKey'
|
|
55
|
+
]) {
|
|
45
56
|
if (!authPage.includes(expected)) {
|
|
46
57
|
throw new Error(`Authorization page is missing ${expected}.`)
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
|
|
61
|
+
const mobileAuth = await client.callTool({
|
|
62
|
+
name: 'apifm_admin_start_auth',
|
|
63
|
+
arguments: { authType: 'register-mobile' }
|
|
64
|
+
})
|
|
65
|
+
const mobileAuthData = JSON.parse(mobileAuth.content[0].text)
|
|
66
|
+
const mobileAuthPage = await fetch(mobileAuthData.url).then((response) => response.text())
|
|
67
|
+
if (!mobileAuthPage.includes('value="register-mobile"')) {
|
|
68
|
+
throw new Error('register-mobile should be accepted as an initial auth type.')
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
const search = await client.callTool({
|
|
51
72
|
name: 'apifm_admin_search_methods',
|
|
52
73
|
arguments: { query: 'user list', limit: 5 }
|