@kne/fastify-app-manager 0.1.7 → 0.1.8
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 +32 -2
- package/index.js +7 -0
- package/libs/controllers/app.js +26 -0
- package/libs/services/app.js +45 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,6 +193,7 @@ await fastify.listen({ port: 3000 });
|
|
|
193
193
|
| `dbQueryMaxRows` | number | 否 | `500` | 只读 query SQL 最大返回行数 |
|
|
194
194
|
| `pm2Defaults` | object | 否 | 见下表 | 全局 PM2 默认;可被应用 `pm2Config` 覆盖 |
|
|
195
195
|
| `createAuthenticate` | function | 否 | 见说明 | 返回 `onRequest` 钩子数组;默认尝试 admin |
|
|
196
|
+
| `createUserAuthenticate` | function | 否 | 见说明 | 应用中心等普通用户接口的 `onRequest` 钩子数组;默认尝试 `fastify.account.authenticate.user` |
|
|
196
197
|
| `healthCheckPath` | string | 否 | `'/'` | 部署后就绪探测路径 |
|
|
197
198
|
| `healthCheckTimeoutMs` | number | 否 | `30000` | 探测总超时(毫秒) |
|
|
198
199
|
| `healthCheckIntervalMs` | number | 否 | `1000` | 探测轮询间隔(毫秒) |
|
|
@@ -240,11 +241,13 @@ await fastify.listen({ port: 3000 });
|
|
|
240
241
|
| domain | string | 否 | - | 绑定 Host(唯一);冲突返回 409 |
|
|
241
242
|
| icon | string | 否 | - | 图标 |
|
|
242
243
|
| description | string | 否 | - | 描述 |
|
|
244
|
+
| category | object / string | 否 | - | 应用分类(应用中心分组用),写入 `options.category`;推荐传 fastify-group 分组对象,对象只保留 `{ code, name }`,字符串原样保存 |
|
|
245
|
+
| isPublic | boolean | 否 | `true` | 是否在公开应用中心展示,写入 `options.isPublic`;未设置视为公开 |
|
|
243
246
|
| env | object | 否 | `{}` | 应用自有环境变量 |
|
|
244
247
|
| pm2Config | object | 否 | `{}` | PM2 覆盖项 |
|
|
245
248
|
| options | object | 否 | `{}` | 扩展字段;可含 `secretEnvKeys: string[]` 显式密钥键名 |
|
|
246
249
|
|
|
247
|
-
返回脱敏后的应用对象(含 `port`、`pathUrl`、`passthroughEnvKeys`、`secretEnvKeys`、`status: 'idle'` 等)。
|
|
250
|
+
返回脱敏后的应用对象(含 `port`、`pathUrl`、`category`、`passthroughEnvKeys`、`secretEnvKeys`、`status: 'idle'` 等)。
|
|
248
251
|
|
|
249
252
|
##### POST `{prefix}/app/save`
|
|
250
253
|
|
|
@@ -254,6 +257,8 @@ await fastify.listen({ port: 3000 });
|
|
|
254
257
|
|--------|------|------|------|
|
|
255
258
|
| name | string | 是 | 应用 slug |
|
|
256
259
|
| label / domain / icon / description / env / pm2Config / options | - | 否 | 传入的字段才更新;`env` 按 patch 语义合并 |
|
|
260
|
+
| category | object / string / null | 否 | 合并写入 `options.category`(对象只保留 `{ code, name }`),不影响 `options` 其它键;传空值清除分类 |
|
|
261
|
+
| isPublic | boolean | 否 | 合并写入 `options.isPublic`,不影响 `options` 其它键 |
|
|
257
262
|
|
|
258
263
|
##### POST `{prefix}/app/save-env`
|
|
259
264
|
|
|
@@ -397,6 +402,31 @@ await fastify.listen({ port: 3000 });
|
|
|
397
402
|
|
|
398
403
|
返回 `{ pageData, totalCount }`,元素均为脱敏后的应用对象。
|
|
399
404
|
|
|
405
|
+
##### GET `{prefix}/app/center/list`
|
|
406
|
+
|
|
407
|
+
应用中心列表,使用 `createUserAuthenticate` 鉴权(普通登录用户可访问)。仅返回 `status: 'running'` 的应用,且只含公开字段(不含 env、端口、路径等)。
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"pageData": [
|
|
412
|
+
{
|
|
413
|
+
"name": "demo",
|
|
414
|
+
"label": "Demo",
|
|
415
|
+
"icon": "file-id",
|
|
416
|
+
"description": "d",
|
|
417
|
+
"category": { "code": "tools", "name": "工具" },
|
|
418
|
+
"isPublic": true,
|
|
419
|
+
"pathUrl": "/app/demo/"
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
"totalCount": 1
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
##### GET `{prefix}/app/center/public-list`
|
|
427
|
+
|
|
428
|
+
公开应用中心列表,**无需登录**。返回结构同 `app/center/list`,仅包含运行中且 `isPublic` 不为 `false` 的应用。
|
|
429
|
+
|
|
400
430
|
##### GET `{prefix}/app/detail`
|
|
401
431
|
|
|
402
432
|
| 参数名 | 类型 | 必填 | 说明 |
|
|
@@ -527,7 +557,7 @@ data: {"appName":"demo","stream":"out","content":"...","line":1}
|
|
|
527
557
|
| description | string / null | 描述 |
|
|
528
558
|
| env | object | 自有环境变量 |
|
|
529
559
|
| pm2Config | object | PM2 覆盖 |
|
|
530
|
-
| options | object | 扩展字段;`secretEnvKeys`
|
|
560
|
+
| options | object | 扩展字段;`secretEnvKeys` 为显式密钥键名列表,`category` 为应用分类,`isPublic` 为是否公开(未设置视为公开) |
|
|
531
561
|
| port | number | 分配端口 |
|
|
532
562
|
| status | string | `idle` / `deploying` / `running` / `stopped` / `error` |
|
|
533
563
|
| currentVersionId | string / null | 当前部署版本 id |
|
package/index.js
CHANGED
|
@@ -54,6 +54,13 @@ module.exports = fp(
|
|
|
54
54
|
return [fastify.account.authenticate.admin];
|
|
55
55
|
}
|
|
56
56
|
return [];
|
|
57
|
+
},
|
|
58
|
+
/** 普通登录用户可访问的接口(应用中心列表) */
|
|
59
|
+
createUserAuthenticate: () => {
|
|
60
|
+
if (fastify.account?.authenticate?.user) {
|
|
61
|
+
return [fastify.account.authenticate.user];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
57
64
|
}
|
|
58
65
|
},
|
|
59
66
|
options
|
package/libs/controllers/app.js
CHANGED
|
@@ -4,6 +4,7 @@ const { createSseReply } = require('../utils/logStream');
|
|
|
4
4
|
module.exports = fp(async (fastify, options) => {
|
|
5
5
|
const { services } = fastify[options.name];
|
|
6
6
|
const auth = () => options.createAuthenticate();
|
|
7
|
+
const userAuth = () => options.createUserAuthenticate();
|
|
7
8
|
|
|
8
9
|
fastify.post(
|
|
9
10
|
`${options.prefix}/app/create`,
|
|
@@ -20,6 +21,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
20
21
|
domain: { type: 'string' },
|
|
21
22
|
icon: { type: 'string' },
|
|
22
23
|
description: { type: 'string' },
|
|
24
|
+
category: { type: ['object', 'string', 'null'] },
|
|
25
|
+
isPublic: { type: 'boolean' },
|
|
23
26
|
env: { type: 'object' },
|
|
24
27
|
pm2Config: { type: 'object' },
|
|
25
28
|
options: { type: 'object' }
|
|
@@ -45,6 +48,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
45
48
|
domain: { type: 'string' },
|
|
46
49
|
icon: { type: 'string' },
|
|
47
50
|
description: { type: 'string' },
|
|
51
|
+
category: { type: ['object', 'string', 'null'] },
|
|
52
|
+
isPublic: { type: 'boolean' },
|
|
48
53
|
env: { type: 'object' },
|
|
49
54
|
pm2Config: { type: 'object' },
|
|
50
55
|
options: { type: 'object' }
|
|
@@ -55,6 +60,27 @@ module.exports = fp(async (fastify, options) => {
|
|
|
55
60
|
async request => services.app.save(request.body)
|
|
56
61
|
);
|
|
57
62
|
|
|
63
|
+
fastify.get(
|
|
64
|
+
`${options.prefix}/app/center/list`,
|
|
65
|
+
{
|
|
66
|
+
onRequest: userAuth(),
|
|
67
|
+
schema: {
|
|
68
|
+
summary: '应用中心:运行中应用的公开信息(普通登录用户可访问)'
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async () => services.app.centerList()
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
fastify.get(
|
|
75
|
+
`${options.prefix}/app/center/public-list`,
|
|
76
|
+
{
|
|
77
|
+
schema: {
|
|
78
|
+
summary: '应用中心:运行中且公开的应用(无需登录)'
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async () => services.app.centerList({ publicOnly: true })
|
|
82
|
+
);
|
|
83
|
+
|
|
58
84
|
fastify.post(
|
|
59
85
|
`${options.prefix}/app/save-env`,
|
|
60
86
|
{
|
package/libs/services/app.js
CHANGED
|
@@ -31,6 +31,18 @@ module.exports = fp(async (fastify, options) => {
|
|
|
31
31
|
const mountPrefix = name => `${options.pathPrefix.replace(/\/$/, '')}/${name}`;
|
|
32
32
|
|
|
33
33
|
const resolveSecretEnvKeys = appLike => normalizeSecretKeys(appLike?.options?.secretEnvKeys);
|
|
34
|
+
// 未设置视为公开(与远程组件默认公开一致)
|
|
35
|
+
const isAppPublic = appLike => appLike?.options?.isPublic !== false;
|
|
36
|
+
// 分组选择器的值可能是整条分组记录(含 id/children/options),只保留 code/name
|
|
37
|
+
const normalizeCategory = category => {
|
|
38
|
+
if (!category) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (typeof category !== 'object') {
|
|
42
|
+
return String(category);
|
|
43
|
+
}
|
|
44
|
+
return category.code ? { code: category.code, name: category.name || category.code } : null;
|
|
45
|
+
};
|
|
34
46
|
|
|
35
47
|
const toPublicApp = app => {
|
|
36
48
|
if (!app) {
|
|
@@ -43,6 +55,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
43
55
|
json.passthroughEnvKeys = options.passthroughEnvKeys || [];
|
|
44
56
|
json.secretEnvKeys = collectSecretKeys(json.env || {}, envOpts);
|
|
45
57
|
json.pathUrl = `${mountPrefix(json.name)}/`;
|
|
58
|
+
json.category = json.options?.category || null;
|
|
59
|
+
json.isPublic = isAppPublic(json);
|
|
46
60
|
return json;
|
|
47
61
|
};
|
|
48
62
|
|
|
@@ -55,7 +69,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
55
69
|
};
|
|
56
70
|
|
|
57
71
|
const create = async data => {
|
|
58
|
-
const { name, label, domain, icon, description, env, pm2Config, options: appOptions } = data;
|
|
72
|
+
const { name, label, domain, icon, description, category, isPublic, env, pm2Config, options: appOptions } = data;
|
|
59
73
|
if (!name || !NAME_RE.test(name)) {
|
|
60
74
|
httpError(400, 'invalid name: use lowercase slug [a-z0-9-]');
|
|
61
75
|
}
|
|
@@ -89,6 +103,12 @@ module.exports = fp(async (fastify, options) => {
|
|
|
89
103
|
if (normalizedOptions.secretEnvKeys !== undefined) {
|
|
90
104
|
normalizedOptions.secretEnvKeys = normalizeSecretKeys(normalizedOptions.secretEnvKeys);
|
|
91
105
|
}
|
|
106
|
+
if (category !== undefined) {
|
|
107
|
+
normalizedOptions.category = normalizeCategory(category);
|
|
108
|
+
}
|
|
109
|
+
if (isPublic !== undefined) {
|
|
110
|
+
normalizedOptions.isPublic = !!isPublic;
|
|
111
|
+
}
|
|
92
112
|
|
|
93
113
|
const app = await models.app.create({
|
|
94
114
|
name,
|
|
@@ -108,7 +128,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
108
128
|
return toPublicApp(app);
|
|
109
129
|
};
|
|
110
130
|
|
|
111
|
-
const save = async ({ name, ...data }) => {
|
|
131
|
+
const save = async ({ name, category, isPublic, ...data }) => {
|
|
112
132
|
const app = await getByName(name);
|
|
113
133
|
const omit = ['name', 'port', 'rootPath', 'pm2Name', 'status', 'currentVersionId'];
|
|
114
134
|
const patch = {};
|
|
@@ -133,6 +153,12 @@ module.exports = fp(async (fastify, options) => {
|
|
|
133
153
|
secretEnvKeys: normalizeSecretKeys(patch.options.secretEnvKeys)
|
|
134
154
|
});
|
|
135
155
|
}
|
|
156
|
+
if (category !== undefined) {
|
|
157
|
+
patch.options = Object.assign({}, patch.options || app.options || {}, { category: normalizeCategory(category) });
|
|
158
|
+
}
|
|
159
|
+
if (isPublic !== undefined) {
|
|
160
|
+
patch.options = Object.assign({}, patch.options || app.options || {}, { isPublic: !!isPublic });
|
|
161
|
+
}
|
|
136
162
|
await app.update(patch);
|
|
137
163
|
return toPublicApp(app);
|
|
138
164
|
};
|
|
@@ -172,6 +198,22 @@ module.exports = fp(async (fastify, options) => {
|
|
|
172
198
|
};
|
|
173
199
|
};
|
|
174
200
|
|
|
201
|
+
const centerList = async ({ publicOnly = false } = {}) => {
|
|
202
|
+
const rows = await models.app.findAll({ where: { status: 'running' }, order: [['createdAt', 'DESC']] });
|
|
203
|
+
const pageData = rows
|
|
204
|
+
.filter(app => !publicOnly || isAppPublic(app))
|
|
205
|
+
.map(app => ({
|
|
206
|
+
name: app.name,
|
|
207
|
+
label: app.label,
|
|
208
|
+
icon: app.icon,
|
|
209
|
+
description: app.description,
|
|
210
|
+
category: app.options?.category || null,
|
|
211
|
+
isPublic: isAppPublic(app),
|
|
212
|
+
pathUrl: `${mountPrefix(app.name)}/`
|
|
213
|
+
}));
|
|
214
|
+
return { pageData, totalCount: pageData.length };
|
|
215
|
+
};
|
|
216
|
+
|
|
175
217
|
const detail = async ({ name }) => {
|
|
176
218
|
const app = await getByName(name);
|
|
177
219
|
return toPublicApp(app);
|
|
@@ -653,6 +695,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
653
695
|
save,
|
|
654
696
|
saveEnv,
|
|
655
697
|
list,
|
|
698
|
+
centerList,
|
|
656
699
|
detail,
|
|
657
700
|
uploadVersion,
|
|
658
701
|
listVersions,
|