@yunsoft/yuncms-api 0.1.10 → 0.1.12
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/package.json +2 -2
- package/src/error-response.js +2 -0
- package/src/mcp/config.js +154 -0
- package/src/mcp/settings-store.js +78 -0
- package/src/mcp.js +49 -4
- package/src/routes/auth.js +88 -6
- package/src/routes/studio-settings.js +4 -0
- package/src/server.js +4 -2
- package/studio-dist/assets/index-DgZpLhcm.js +11 -0
- package/studio-dist/assets/{index-CeImMX2E.css → index-sB3p3QRk.css} +1 -1
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-CNSmrNm2.js +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yunsoft/yuncms-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Express API runtime and bundled Studio server for YunCMS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@modelcontextprotocol/node": "2.0.0",
|
|
36
36
|
"@modelcontextprotocol/server": "2.0.0",
|
|
37
37
|
"@node-saml/node-saml": "5.1.0",
|
|
38
|
-
"@yunsoft/yuncms-core": "0.1.
|
|
38
|
+
"@yunsoft/yuncms-core": "0.1.12",
|
|
39
39
|
"express": "5.2.1",
|
|
40
40
|
"ldapts": "9.0.0",
|
|
41
41
|
"openid-client": "6.8.7",
|
package/src/error-response.js
CHANGED
|
@@ -5,6 +5,7 @@ const STATUS_BY_CODE = new Map([
|
|
|
5
5
|
['UNAUTHORIZED', 401],
|
|
6
6
|
['FORBIDDEN', 403],
|
|
7
7
|
['FORBIDDEN_FIELD', 403],
|
|
8
|
+
['EMAIL_NOT_VERIFIED', 403],
|
|
8
9
|
['SELF_ADMIN_MUTATION_FORBIDDEN', 403],
|
|
9
10
|
['PROTECTED_ROLE', 403],
|
|
10
11
|
['SYSTEM_SCHEMA_READ_ONLY', 403],
|
|
@@ -49,6 +50,7 @@ const STATUS_BY_CODE = new Map([
|
|
|
49
50
|
['INVALID_MAIL_MESSAGE', 400],
|
|
50
51
|
['INVALID_AI_REQUEST', 400],
|
|
51
52
|
['INVALID_AI_CONFIG', 400],
|
|
53
|
+
['INVALID_MCP_CONFIG', 400],
|
|
52
54
|
['AI_TOOL_ARGUMENTS_INVALID', 400],
|
|
53
55
|
['STORAGE_NOT_FOUND', 400],
|
|
54
56
|
['STORAGE_INVENTORY_UNSUPPORTED', 400],
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export const DEFAULT_MCP_SETTINGS = Object.freeze({
|
|
2
|
+
enabled: false,
|
|
3
|
+
writesEnabled: false,
|
|
4
|
+
requireAuthentication: true,
|
|
5
|
+
allowedOrigins: Object.freeze([]),
|
|
6
|
+
allowedHosts: Object.freeze([]),
|
|
7
|
+
maxItems: 100,
|
|
8
|
+
maxResultBytes: 1_000_000,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const PATCH_KEYS = new Set([
|
|
12
|
+
'enabled',
|
|
13
|
+
'writes_enabled',
|
|
14
|
+
'require_authentication',
|
|
15
|
+
'allowed_origins',
|
|
16
|
+
'allowed_hosts',
|
|
17
|
+
'max_items',
|
|
18
|
+
'max_result_bytes',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function configError(message) {
|
|
22
|
+
const error = new Error(message);
|
|
23
|
+
error.code = 'INVALID_MCP_CONFIG';
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeBoolean(value, name) {
|
|
28
|
+
if (typeof value !== 'boolean') throw configError(`${name} must be a boolean`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeInteger(value, name, { min, max }) {
|
|
33
|
+
const parsed = Number(value);
|
|
34
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
35
|
+
throw configError(`${name} must be an integer between ${min} and ${max}`);
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function listValues(value, name) {
|
|
41
|
+
let source = value;
|
|
42
|
+
if (typeof source === 'string') {
|
|
43
|
+
const trimmed = source.trim();
|
|
44
|
+
if (trimmed.startsWith('[')) {
|
|
45
|
+
try {
|
|
46
|
+
source = JSON.parse(trimmed);
|
|
47
|
+
} catch {
|
|
48
|
+
throw configError(`${name} must be a list`);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
source = source.split(/[\n,]/);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(source)) throw configError(`${name} must be a list`);
|
|
55
|
+
const entries = source.map((entry) => {
|
|
56
|
+
if (typeof entry !== 'string') throw configError(`${name} entries must be strings`);
|
|
57
|
+
return entry.trim();
|
|
58
|
+
}).filter(Boolean);
|
|
59
|
+
if (entries.length > 50) throw configError(`${name} cannot contain more than 50 entries`);
|
|
60
|
+
return entries;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function normalizeMcpOrigins(value) {
|
|
64
|
+
return [...new Set(listValues(value, 'allowed_origins').map((entry) => {
|
|
65
|
+
let url;
|
|
66
|
+
try {
|
|
67
|
+
url = new URL(entry);
|
|
68
|
+
} catch {
|
|
69
|
+
throw configError(`allowed_origins contains an invalid URL: ${entry}`);
|
|
70
|
+
}
|
|
71
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
72
|
+
throw configError(`allowed_origins must use http or https: ${entry}`);
|
|
73
|
+
}
|
|
74
|
+
if (url.username || url.password) {
|
|
75
|
+
throw configError(`allowed_origins cannot include credentials: ${entry}`);
|
|
76
|
+
}
|
|
77
|
+
return url.origin.toLowerCase();
|
|
78
|
+
}))];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function normalizeMcpHosts(value) {
|
|
82
|
+
return [...new Set(listValues(value, 'allowed_hosts').map((entry) => {
|
|
83
|
+
if (/[:][/][/]|[\\/?#@\s\0]/.test(entry)) {
|
|
84
|
+
throw configError(`allowed_hosts contains an invalid host: ${entry}`);
|
|
85
|
+
}
|
|
86
|
+
let url;
|
|
87
|
+
try {
|
|
88
|
+
url = new URL(`http://${entry}`);
|
|
89
|
+
} catch {
|
|
90
|
+
throw configError(`allowed_hosts contains an invalid host: ${entry}`);
|
|
91
|
+
}
|
|
92
|
+
if (url.username || url.password || url.pathname !== '/' || url.search || url.hash || !url.host) {
|
|
93
|
+
throw configError(`allowed_hosts contains an invalid host: ${entry}`);
|
|
94
|
+
}
|
|
95
|
+
return url.host.toLowerCase();
|
|
96
|
+
}))];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function normalizeMcpSettingsPatch(patch = {}) {
|
|
100
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
101
|
+
throw configError('MCP settings patch must be an object');
|
|
102
|
+
}
|
|
103
|
+
const keys = Object.keys(patch);
|
|
104
|
+
if (keys.length === 0 || keys.some((key) => !PATCH_KEYS.has(key))) {
|
|
105
|
+
throw configError('MCP settings patch contains unsupported properties');
|
|
106
|
+
}
|
|
107
|
+
const normalized = {};
|
|
108
|
+
if (Object.hasOwn(patch, 'enabled')) normalized.enabled = normalizeBoolean(patch.enabled, 'enabled');
|
|
109
|
+
if (Object.hasOwn(patch, 'writes_enabled')) normalized.writesEnabled = normalizeBoolean(patch.writes_enabled, 'writes_enabled');
|
|
110
|
+
if (Object.hasOwn(patch, 'require_authentication')) normalized.requireAuthentication = normalizeBoolean(patch.require_authentication, 'require_authentication');
|
|
111
|
+
if (Object.hasOwn(patch, 'allowed_origins')) normalized.allowedOrigins = normalizeMcpOrigins(patch.allowed_origins);
|
|
112
|
+
if (Object.hasOwn(patch, 'allowed_hosts')) normalized.allowedHosts = normalizeMcpHosts(patch.allowed_hosts);
|
|
113
|
+
if (Object.hasOwn(patch, 'max_items')) normalized.maxItems = normalizeInteger(patch.max_items, 'max_items', { min: 1, max: 500 });
|
|
114
|
+
if (Object.hasOwn(patch, 'max_result_bytes')) normalized.maxResultBytes = normalizeInteger(patch.max_result_bytes, 'max_result_bytes', { min: 10_000, max: 10_000_000 });
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rowList(value, name, normalize) {
|
|
119
|
+
try {
|
|
120
|
+
return normalize(value);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error?.code === 'INVALID_MCP_CONFIG') throw error;
|
|
123
|
+
throw configError(`${name} contains invalid saved data`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function mcpRuntimeSettingsFromRow(row) {
|
|
128
|
+
if (!row) throw configError('MCP settings are missing; run YunCMS bootstrap');
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
enabled: Boolean(row.enabled),
|
|
131
|
+
writesEnabled: Boolean(row.writes_enabled),
|
|
132
|
+
requireAuthentication: Boolean(row.require_authentication),
|
|
133
|
+
allowedOrigins: Object.freeze(rowList(row.allowed_origins, 'allowed_origins', normalizeMcpOrigins)),
|
|
134
|
+
allowedHosts: Object.freeze(rowList(row.allowed_hosts, 'allowed_hosts', normalizeMcpHosts)),
|
|
135
|
+
maxItems: normalizeInteger(row.max_items, 'max_items', { min: 1, max: 500 }),
|
|
136
|
+
maxResultBytes: normalizeInteger(row.max_result_bytes, 'max_result_bytes', { min: 10_000, max: 10_000_000 }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function mcpAdminSettingsFromRow(row) {
|
|
141
|
+
const runtime = mcpRuntimeSettingsFromRow(row);
|
|
142
|
+
return {
|
|
143
|
+
enabled: runtime.enabled,
|
|
144
|
+
writes_enabled: runtime.writesEnabled,
|
|
145
|
+
require_authentication: runtime.requireAuthentication,
|
|
146
|
+
allowed_origins: [...runtime.allowedOrigins],
|
|
147
|
+
allowed_hosts: [...runtime.allowedHosts],
|
|
148
|
+
max_items: runtime.maxItems,
|
|
149
|
+
max_result_bytes: runtime.maxResultBytes,
|
|
150
|
+
updated_at: row.updated_at ?? null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { configError };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mcpAdminSettingsFromRow,
|
|
3
|
+
mcpRuntimeSettingsFromRow,
|
|
4
|
+
normalizeMcpSettingsPatch,
|
|
5
|
+
} from './config.js';
|
|
6
|
+
|
|
7
|
+
function settingsError(code, message) {
|
|
8
|
+
const error = new Error(message);
|
|
9
|
+
error.code = code;
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function columnAssignments(normalized) {
|
|
14
|
+
const assignments = [];
|
|
15
|
+
const params = [];
|
|
16
|
+
const add = (column, value) => {
|
|
17
|
+
assignments.push(`${column} = ?`);
|
|
18
|
+
params.push(value);
|
|
19
|
+
};
|
|
20
|
+
if (Object.hasOwn(normalized, 'enabled')) add('enabled', normalized.enabled ? 1 : 0);
|
|
21
|
+
if (Object.hasOwn(normalized, 'writesEnabled')) add('writes_enabled', normalized.writesEnabled ? 1 : 0);
|
|
22
|
+
if (Object.hasOwn(normalized, 'requireAuthentication')) add('require_authentication', normalized.requireAuthentication ? 1 : 0);
|
|
23
|
+
if (Object.hasOwn(normalized, 'allowedOrigins')) add('allowed_origins', JSON.stringify(normalized.allowedOrigins));
|
|
24
|
+
if (Object.hasOwn(normalized, 'allowedHosts')) add('allowed_hosts', JSON.stringify(normalized.allowedHosts));
|
|
25
|
+
if (Object.hasOwn(normalized, 'maxItems')) add('max_items', normalized.maxItems);
|
|
26
|
+
if (Object.hasOwn(normalized, 'maxResultBytes')) add('max_result_bytes', normalized.maxResultBytes);
|
|
27
|
+
return { assignments, params };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class McpSettingsStore {
|
|
31
|
+
constructor({ database } = {}) {
|
|
32
|
+
if (!database) throw new Error('MCP settings store requires database');
|
|
33
|
+
this.database = database;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async #row() {
|
|
37
|
+
const [rows] = await this.database.query(
|
|
38
|
+
`SELECT id, enabled, writes_enabled, require_authentication, allowed_origins,
|
|
39
|
+
allowed_hosts, max_items, max_result_bytes, updated_at
|
|
40
|
+
FROM yuncms_mcp_settings
|
|
41
|
+
WHERE id = 1
|
|
42
|
+
LIMIT 1`,
|
|
43
|
+
);
|
|
44
|
+
const row = rows[0];
|
|
45
|
+
if (!row) throw settingsError('DATABASE_MIGRATION_REQUIRED', 'MCP settings are missing; run YunCMS bootstrap');
|
|
46
|
+
return row;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async readAdmin() {
|
|
50
|
+
return mcpAdminSettingsFromRow(await this.#row());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async readRuntime() {
|
|
54
|
+
return mcpRuntimeSettingsFromRow(await this.#row());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async update(patch = {}) {
|
|
58
|
+
const normalized = normalizeMcpSettingsPatch(patch);
|
|
59
|
+
const current = await this.#row();
|
|
60
|
+
const { assignments, params } = columnAssignments(normalized);
|
|
61
|
+
if (assignments.length === 0) throw settingsError('INVALID_MCP_CONFIG', 'MCP settings patch has no effective changes');
|
|
62
|
+
|
|
63
|
+
const effectiveEnabled = Object.hasOwn(normalized, 'enabled') ? normalized.enabled : Boolean(current.enabled);
|
|
64
|
+
const effectiveAllowedHosts = Object.hasOwn(normalized, 'allowedHosts')
|
|
65
|
+
? normalized.allowedHosts
|
|
66
|
+
: mcpRuntimeSettingsFromRow(current).allowedHosts;
|
|
67
|
+
if (effectiveEnabled && effectiveAllowedHosts.length === 0) {
|
|
68
|
+
throw settingsError('INVALID_MCP_CONFIG', 'Add at least one allowed host before enabling MCP');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
params.push(1);
|
|
72
|
+
await this.database.query(
|
|
73
|
+
`UPDATE yuncms_mcp_settings SET ${assignments.join(', ')} WHERE id = ?`,
|
|
74
|
+
params,
|
|
75
|
+
);
|
|
76
|
+
return this.readAdmin();
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/mcp.js
CHANGED
|
@@ -327,10 +327,55 @@ export function createMcpAccessGuard(mcpConfig = {}) {
|
|
|
327
327
|
};
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
export function
|
|
331
|
-
if (!
|
|
330
|
+
export function requireMcpAdministrator(req) {
|
|
331
|
+
if (req?.authMethod === 'public' || !req?.accountability?.user) {
|
|
332
|
+
const error = new Error('MCP settings require an authenticated YunCMS account');
|
|
333
|
+
error.code = 'UNAUTHORIZED';
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
if (req.accountability?.admin === true || req.accountability?.system === true) return;
|
|
337
|
+
const error = new Error('MCP settings require administrator access');
|
|
338
|
+
error.code = 'FORBIDDEN';
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function createMcpRouter({ settingsStore, logger = console } = {}) {
|
|
343
|
+
if (!settingsStore) throw new Error('MCP settings store is required');
|
|
332
344
|
const router = express.Router();
|
|
333
|
-
|
|
345
|
+
|
|
346
|
+
router.get('/settings', async (req, res, next) => {
|
|
347
|
+
try {
|
|
348
|
+
requireMcpAdministrator(req);
|
|
349
|
+
return res.json({ data: await settingsStore.readAdmin(), request_id: req.id });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return next(error);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
router.patch('/settings', async (req, res, next) => {
|
|
356
|
+
try {
|
|
357
|
+
requireMcpAdministrator(req);
|
|
358
|
+
return res.json({ data: await settingsStore.update(req.body ?? {}), request_id: req.id });
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return next(error);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
router.use(async (req, res, next) => {
|
|
365
|
+
try {
|
|
366
|
+
const mcpConfig = await settingsStore.readRuntime();
|
|
367
|
+
if (!mcpConfig.enabled) {
|
|
368
|
+
return res.status(404).json({
|
|
369
|
+
errors: [{ code: 'MCP_DISABLED', message: 'MCP is disabled', request_id: req.id ?? null }],
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
req.mcpConfig = mcpConfig;
|
|
373
|
+
return createMcpAccessGuard(mcpConfig)(req, res, next);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return next(error);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
334
379
|
router.all('/', (req, res, next) => {
|
|
335
380
|
if (req.method === 'POST') return next();
|
|
336
381
|
res.set('allow', 'POST');
|
|
@@ -344,7 +389,7 @@ export function createMcpRouter({ config, logger = console } = {}) {
|
|
|
344
389
|
code: error?.code ?? null,
|
|
345
390
|
});
|
|
346
391
|
const handler = createMcpHandler(
|
|
347
|
-
() => createRequestMcpServer(req,
|
|
392
|
+
() => createRequestMcpServer(req, req.mcpConfig),
|
|
348
393
|
{ legacy: 'stateless', onerror: reportError },
|
|
349
394
|
);
|
|
350
395
|
const nodeHandler = toNodeHandler(handler, { onerror: reportError });
|
package/src/routes/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSystemAccountability } from '@yunsoft/yuncms-core';
|
|
1
2
|
import express from 'express';
|
|
2
3
|
|
|
3
4
|
import { createFixedWindowRateLimit } from '../rate-limit.js';
|
|
@@ -7,10 +8,20 @@ function service(req, name) {
|
|
|
7
8
|
const Service = req.context.services[name];
|
|
8
9
|
return new Service(serviceOptionsFromRequest(req));
|
|
9
10
|
}
|
|
11
|
+
function systemService(req, name) {
|
|
12
|
+
const Service = req.context.services[name];
|
|
13
|
+
return new Service({
|
|
14
|
+
...serviceOptionsFromRequest(req),
|
|
15
|
+
accountability: createSystemAccountability(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
10
18
|
function authService(req) { return service(req, 'AuthService'); }
|
|
11
19
|
function authTokensService(req) { return service(req, 'AuthTokensService'); }
|
|
20
|
+
function systemAuthTokensService(req) { return systemService(req, 'AuthTokensService'); }
|
|
12
21
|
function apiTokensService(req) { return service(req, 'ApiTokensService'); }
|
|
13
22
|
function usersService(req) { return service(req, 'UsersService'); }
|
|
23
|
+
function systemUsersService(req) { return systemService(req, 'UsersService'); }
|
|
24
|
+
function studioSettingsService(req) { return service(req, 'StudioSettingsService'); }
|
|
14
25
|
function externalAuthService(req, registry) {
|
|
15
26
|
const Service = req.context.services.ExternalAuthService;
|
|
16
27
|
return new Service({
|
|
@@ -45,6 +56,14 @@ function actionUrl(config, action, token) {
|
|
|
45
56
|
return `${config.auth.publicUrl}/?auth_action=${encodeURIComponent(action)}&token=${encodeURIComponent(token)}`;
|
|
46
57
|
}
|
|
47
58
|
|
|
59
|
+
function verificationMessage(config, token) {
|
|
60
|
+
const url = actionUrl(config, 'verify', token);
|
|
61
|
+
return {
|
|
62
|
+
subject: 'Verify your YunCMS email',
|
|
63
|
+
text: `Verify your YunCMS email address by opening this link:\n${url}\n\nIf you did not request this, you can ignore this message.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
function noStore(req, res, next) {
|
|
49
68
|
res.set('cache-control', 'no-store');
|
|
50
69
|
res.set('pragma', 'no-cache');
|
|
@@ -97,6 +116,43 @@ export function createAuthRouter({
|
|
|
97
116
|
res.json({ data: externalAuthRegistry?.publicProviders?.() ?? [] });
|
|
98
117
|
});
|
|
99
118
|
|
|
119
|
+
router.post('/register', actionLimit, async (req, res) => {
|
|
120
|
+
const settings = await studioSettingsService(req).readPublic();
|
|
121
|
+
const verificationRequired = settings.public_registration_require_email_verification === true;
|
|
122
|
+
const transport = verificationRequired ? requireMailer(mailer) : null;
|
|
123
|
+
const data = await usersService(req).registerPublic(req.body ?? {});
|
|
124
|
+
|
|
125
|
+
if (verificationRequired) {
|
|
126
|
+
try {
|
|
127
|
+
const result = await systemAuthTokensService(req).createEmailVerification(data.id);
|
|
128
|
+
const message = verificationMessage(config, result.token);
|
|
129
|
+
await transport.send({ to: data.email, ...message }, {
|
|
130
|
+
accountability: req.accountability,
|
|
131
|
+
requestId: req.id,
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
try {
|
|
135
|
+
await systemUsersService(req).deleteOne(data.id);
|
|
136
|
+
} catch (cleanupError) {
|
|
137
|
+
logger.error?.('YunCMS public registration cleanup failed', {
|
|
138
|
+
requestId: req.id,
|
|
139
|
+
user: data.id,
|
|
140
|
+
code: cleanupError?.code,
|
|
141
|
+
message: cleanupError?.message,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
res.status(201).json({
|
|
149
|
+
data: {
|
|
150
|
+
...data,
|
|
151
|
+
email_verification_required: verificationRequired,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
100
156
|
router.post('/login', loginLimit, async (req, res) => {
|
|
101
157
|
const result = await authService(req).login({ email: req.body?.email, password: req.body?.password, ip: req.ip ?? null, userAgent: req.get('user-agent') ?? null });
|
|
102
158
|
res.json({ data: result });
|
|
@@ -210,12 +266,35 @@ export function createAuthRouter({
|
|
|
210
266
|
res.status(204).end();
|
|
211
267
|
});
|
|
212
268
|
router.post('/email-verification/request', actionLimit, async (req, res) => {
|
|
213
|
-
const transport = requireMailer(mailer);
|
|
214
269
|
if (!req.accountability?.user) {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
270
|
+
const settings = await studioSettingsService(req).readPublic();
|
|
271
|
+
if (settings.public_registration_require_email_verification !== true) {
|
|
272
|
+
res.status(202).json({ data: { accepted: true } });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const transport = requireMailer(mailer);
|
|
277
|
+
const result = await systemAuthTokensService(req).requestEmailVerification(req.body?.email);
|
|
278
|
+
if (result) {
|
|
279
|
+
try {
|
|
280
|
+
const message = verificationMessage(config, result.token);
|
|
281
|
+
await transport.send({ to: result.user.email, ...message }, {
|
|
282
|
+
accountability: req.accountability,
|
|
283
|
+
requestId: req.id,
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger.error?.('YunCMS public email verification mail delivery failed', {
|
|
287
|
+
requestId: req.id,
|
|
288
|
+
code: error?.code,
|
|
289
|
+
message: error?.message,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
res.status(202).json({ data: { accepted: true } });
|
|
294
|
+
return;
|
|
218
295
|
}
|
|
296
|
+
|
|
297
|
+
const transport = requireMailer(mailer);
|
|
219
298
|
const userId = req.body?.user ?? req.accountability.user;
|
|
220
299
|
const user = await usersService(req).readOne(userId);
|
|
221
300
|
if (!user) {
|
|
@@ -224,8 +303,11 @@ export function createAuthRouter({
|
|
|
224
303
|
throw error;
|
|
225
304
|
}
|
|
226
305
|
const result = await authTokensService(req).createEmailVerification(userId);
|
|
227
|
-
const
|
|
228
|
-
await transport.send({ to: user.email,
|
|
306
|
+
const message = verificationMessage(config, result.token);
|
|
307
|
+
await transport.send({ to: user.email, ...message }, {
|
|
308
|
+
accountability: req.accountability,
|
|
309
|
+
requestId: req.id,
|
|
310
|
+
});
|
|
229
311
|
res.status(202).json({ data: { accepted: true } });
|
|
230
312
|
});
|
|
231
313
|
router.post('/email-verification/confirm', actionLimit, async (req, res) => {
|
|
@@ -26,6 +26,10 @@ export function createStudioSettingsRouter() {
|
|
|
26
26
|
sendImageAsset(res, await settingsService(req).readFaviconContent());
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
router.get('/admin', async (req, res) => {
|
|
30
|
+
res.json({ data: await settingsService(req).readOne() });
|
|
31
|
+
});
|
|
32
|
+
|
|
29
33
|
router.get('/', async (req, res) => {
|
|
30
34
|
res.json({ data: await settingsService(req).readPublic() });
|
|
31
35
|
});
|
package/src/server.js
CHANGED
|
@@ -28,6 +28,7 @@ import { loadExternalAuthConfig } from './external-auth/config.js';
|
|
|
28
28
|
import { ExternalAuthProviderRegistry } from './external-auth/providers.js';
|
|
29
29
|
import { loadExtensionRuntime } from './extensions/runtime.js';
|
|
30
30
|
import { createMcpRouter } from './mcp.js';
|
|
31
|
+
import { McpSettingsStore } from './mcp/settings-store.js';
|
|
31
32
|
import { createAiRouter } from './routes/ai.js';
|
|
32
33
|
|
|
33
34
|
loadEnvFileIfPresent();
|
|
@@ -134,7 +135,8 @@ async function start() {
|
|
|
134
135
|
});
|
|
135
136
|
const aiAssistant = new AiAssistantService({ settingsStore: aiSettingsStore, logger });
|
|
136
137
|
const aiRouter = createAiRouter({ assistant: aiAssistant, settingsStore: aiSettingsStore });
|
|
137
|
-
const
|
|
138
|
+
const mcpSettingsStore = new McpSettingsStore({ database: pool });
|
|
139
|
+
const mcpRouter = createMcpRouter({ settingsStore: mcpSettingsStore, logger });
|
|
138
140
|
mailer?.setEmitter(emitter);
|
|
139
141
|
registerInternalAudit({ emitter, services });
|
|
140
142
|
|
|
@@ -219,4 +221,4 @@ start().catch(async (error) => {
|
|
|
219
221
|
await redisClient?.close().catch(() => {});
|
|
220
222
|
await closeDatabasePool(pool).catch(() => {});
|
|
221
223
|
process.exit(1);
|
|
222
|
-
});
|
|
224
|
+
});
|