@kne/fastify-live-components-site 0.1.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/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # fastify-live-components-site
2
+
3
+ ### 描述
4
+
5
+ Fastify 插件:为 LiveComponentEditor 提供远程站点文件树与短链访问管理
6
+
7
+ ### 安装
8
+
9
+ ```shell
10
+ npm i --save @kne/fastify-live-components-site
11
+ ```
12
+
13
+ ### 概述
14
+
15
+ # Live Components Site
16
+
17
+ 为 `LiveComponentEditor` 提供远程站点文件树 API,并通过 shorten 短码免登录访问;站点链接由 admin 管理。
18
+
19
+ ## 能力
20
+
21
+ - 管理端:创建/修改/开启关闭/删除站点,签发含 shorten 的访问 host
22
+ - 公开端:`{prefix}/{shorten}/*` 文件树读写,路径 shorten 校验,无需登录
23
+ - `GET {prefix}/content/{contentShorten}`:直出后端已保存 content(`text/plain`);短码由 `content-share` 签发,URL **不含**站点 shorten,可设有效期并可管理删除
24
+
25
+
26
+ ### 示例
27
+
28
+ ### API
29
+
30
+ # API
31
+
32
+ ## 配置项
33
+
34
+ | 属性名 | 说明 | 类型 | 默认值 |
35
+ |-----|----|----|-----|
36
+ | name | 命名空间 | string | `liveComponentsSite` |
37
+ | prefix | 路由前缀 | string | `/api/v1/live-components-site` |
38
+ | dbTableNamePrefix | 表前缀 | string | `t_live_components_site_` |
39
+ | siteTargetType | shorten target.type(站点) | string | `live-components-site` |
40
+ | contentTargetType | shorten target.type(内容分享) | string | `live-components-site-content` |
41
+ | getSiteHost | 由 shorten 组装站点 host | `(shorten) => string` | `` `${prefix}/${shorten}` `` |
42
+ | getContentHost | 内容短链基址(不含站点 shorten) | `() => string` | `` `${prefix}` `` → 访问路径 `` `${prefix}/content/{code}` `` |
43
+ | getShortenServices | 注入 shorten 服务 | `() => services` | `() => fastify.shorten.services` |
44
+ | getAuthenticate | 按功能返回鉴权中间件 | `(type) => middleware[]` | 见下表 |
45
+
46
+ ### getAuthenticate(type)
47
+
48
+ | type | 默认 |
49
+ |------|------|
50
+ | `entry` | 校验 path `:shorten`,写入 `request.authenticatePayload` |
51
+ | `site` / `site:create` / `site:manage` | `[authenticate.user, authenticate.admin]` |
52
+
53
+ ## 管理端(需登录 admin)
54
+
55
+ | 方法 | 路径 | 说明 |
56
+ |------|------|------|
57
+ | GET | `{prefix}/site/list` | 列表,`filter.status` / `filter.keyword`,`perPage` / `currentPage` |
58
+ | GET | `{prefix}/site/detail` | 详情,`?id=` |
59
+ | POST | `{prefix}/site/create` | 创建并签发 shorten,body: `{ name, defaultPermission?, status?, options? }` |
60
+ | POST | `{prefix}/site/save` | 修改,body: `{ id, name?, status?, defaultPermission?, options? }`;`status`: `open` / `closed` |
61
+ | POST | `{prefix}/site/remove` | 删除,body: `{ id }` |
62
+
63
+ 创建返回含 `shorten`、`host`(可直接作为前端 `sites[].host`)。
64
+
65
+ 站点字段:`status` `open` 开启 / `closed` 关闭;关闭后公开 API 返回 403,host/shorten 不变。站点与 shorten 均不过期。
66
+
67
+ ## 公开端(path 含 shorten,无需登录)
68
+
69
+ `host` 形态:`{origin}{prefix}/{shorten}`
70
+
71
+ **CORS**:下列公开接口与内容访问接口均允许任意 Origin(`Access-Control-Allow-Origin: *`),并支持 `OPTIONS` 预检。站点管理接口(`{prefix}/site/*`)不启用该跨域策略。
72
+
73
+ | 方法 | 路径 | 说明 |
74
+ |------|------|------|
75
+ | GET | `{host}/getFolderTree` | 文件树 |
76
+ | GET | `{host}/get?id=` | 读文件(JSON:`{ id, name, permission, content }`) |
77
+ | GET | `{prefix}/content/{contentShorten}` | 直出已保存 content(`text/plain`,无 `{code,data}`;不带站点 shorten) |
78
+ | POST | `{host}/content-share/create` | `{ id, expiresIn? }`,`expiresIn` 为秒,空/省略为永久;返回 `{ shorten, url, expires }`,`url` 为 `{prefix}/content/{shorten}` |
79
+ | GET | `{host}/content-share/list?id=` | 列出该文件未过期的内容短链 |
80
+ | POST | `{host}/content-share/remove` | `{ shorten }` |
81
+ | POST | `{host}/createFolder` | `{ parentId, name }` |
82
+ | POST | `{host}/create` | `{ parentId, name, content? }` |
83
+ | POST | `{host}/save` | `{ id, content }` |
84
+ | POST | `{host}/rename` | `{ id, name }` |
85
+ | POST | `{host}/remove` | `{ ids: string[] }` |
86
+
87
+ 树节点:`{ id, name, type: 'file'|'directory', permission: 'r'|'rw', children? }`。
88
+
89
+ 错误:无效/过期 shorten → 401;站点关闭 → 403。
90
+
91
+ ## Shorten target
92
+
93
+ ```json
94
+ { "type": "live-components-site", "siteId": "<id>" }
95
+ ```
package/index.js ADDED
@@ -0,0 +1,83 @@
1
+ const fp = require('fastify-plugin');
2
+ const path = require('node:path');
3
+
4
+ const SITE_TARGET_TYPE = 'live-components-site';
5
+ const CONTENT_TARGET_TYPE = 'live-components-site-content';
6
+
7
+ module.exports = fp(
8
+ async (fastify, options) => {
9
+ options = Object.assign(
10
+ {},
11
+ {
12
+ dbTableNamePrefix: 't_live_components_site_',
13
+ name: 'liveComponentsSite',
14
+ prefix: '/api/v1/live-components-site',
15
+ siteTargetType: SITE_TARGET_TYPE,
16
+ contentTargetType: CONTENT_TARGET_TYPE,
17
+ getSiteHost: null,
18
+ // 内容短链基址(不含站点 shorten),默认 `${prefix}` → `${prefix}/content/{code}`
19
+ getContentHost: null,
20
+ getShortenServices: () => {
21
+ if (!fastify.shorten) {
22
+ throw new Error('fastify-shorten plugin must be registered before @kne/fastify-live-components-site, or set options.getShortenServices');
23
+ }
24
+ return fastify.shorten.services;
25
+ },
26
+ getAuthenticate: type => {
27
+ if (type === 'entry') {
28
+ return [
29
+ async request => {
30
+ await fastify[options.name].services.site.authenticateEntry(request);
31
+ }
32
+ ];
33
+ }
34
+ if (!fastify.account) {
35
+ return [
36
+ () => {
37
+ throw new Error('fastify-account plugin must be registered before @kne/fastify-live-components-site, or set options.getAuthenticate');
38
+ }
39
+ ];
40
+ }
41
+ const { authenticate } = fastify.account;
42
+ switch (type) {
43
+ case 'site':
44
+ case 'site:create':
45
+ case 'site:manage':
46
+ default:
47
+ return [authenticate.user, authenticate.admin];
48
+ }
49
+ }
50
+ },
51
+ options
52
+ );
53
+
54
+ if (typeof options.getSiteHost !== 'function') {
55
+ options.getSiteHost = shorten => `${options.prefix}/${shorten}`;
56
+ }
57
+ if (typeof options.getContentHost !== 'function') {
58
+ options.getContentHost = () => options.prefix;
59
+ }
60
+
61
+ fastify.register(require('@kne/fastify-namespace'), {
62
+ options,
63
+ name: options.name,
64
+ modules: [
65
+ [
66
+ 'models',
67
+ await fastify.sequelize.addModels(path.resolve(__dirname, './libs/models'), {
68
+ prefix: options.dbTableNamePrefix
69
+ })
70
+ ],
71
+ ['services', path.resolve(__dirname, './libs/services')],
72
+ ['controllers', path.resolve(__dirname, './libs/controllers')]
73
+ ]
74
+ });
75
+ },
76
+ {
77
+ name: 'fastify-live-components-site',
78
+ dependencies: ['fastify-sequelize']
79
+ }
80
+ );
81
+
82
+ module.exports.SITE_TARGET_TYPE = SITE_TARGET_TYPE;
83
+ module.exports.CONTENT_TARGET_TYPE = CONTENT_TARGET_TYPE;
@@ -0,0 +1,321 @@
1
+ const fp = require('fastify-plugin');
2
+ const { allowCors, handleOptions, normalizeHooks } = require('../utils/cors');
3
+
4
+ module.exports = fp(async (fastify, options) => {
5
+ const { services } = fastify[options.name];
6
+ const entryAuth = options.getAuthenticate('entry');
7
+ const withEntryAuth = [allowCors, ...normalizeHooks(entryAuth)];
8
+
9
+ // 预检:内容访问 + 站点公开入口(不含 /site 管理)
10
+ fastify.options(
11
+ `${options.prefix}/content/:contentShorten`,
12
+ {
13
+ schema: { hide: true }
14
+ },
15
+ handleOptions
16
+ );
17
+ fastify.options(
18
+ `${options.prefix}/:shorten/*`,
19
+ {
20
+ schema: { hide: true }
21
+ },
22
+ handleOptions
23
+ );
24
+
25
+ // 公开内容短链:不带站点 shorten,须注册在 /:shorten/* 之前
26
+ fastify.get(
27
+ `${options.prefix}/content/:contentShorten`,
28
+ {
29
+ onRequest: allowCors,
30
+ schema: {
31
+ summary: '通过内容短码直出已保存 content(text/plain,无 code/data;不带站点 shorten)',
32
+ params: {
33
+ type: 'object',
34
+ required: ['contentShorten'],
35
+ properties: {
36
+ contentShorten: { type: 'string' }
37
+ }
38
+ }
39
+ }
40
+ },
41
+ async (request, reply) => {
42
+ const content = await services.contentShare.resolveByCode({
43
+ contentShorten: request.params.contentShorten
44
+ });
45
+ reply.type('text/plain; charset=utf-8');
46
+ return content;
47
+ }
48
+ );
49
+
50
+ fastify.get(
51
+ `${options.prefix}/:shorten/getFolderTree`,
52
+ {
53
+ onRequest: withEntryAuth,
54
+ schema: {
55
+ summary: '获取站点文件树',
56
+ params: {
57
+ type: 'object',
58
+ required: ['shorten'],
59
+ properties: {
60
+ shorten: { type: 'string' }
61
+ }
62
+ }
63
+ }
64
+ },
65
+ async request => {
66
+ return services.node.getFolderTree(request.authenticatePayload);
67
+ }
68
+ );
69
+
70
+ fastify.get(
71
+ `${options.prefix}/:shorten/get`,
72
+ {
73
+ onRequest: withEntryAuth,
74
+ schema: {
75
+ summary: '读取文件内容',
76
+ params: {
77
+ type: 'object',
78
+ required: ['shorten'],
79
+ properties: {
80
+ shorten: { type: 'string' }
81
+ }
82
+ },
83
+ query: {
84
+ type: 'object',
85
+ required: ['id'],
86
+ properties: {
87
+ id: { type: 'string' }
88
+ }
89
+ }
90
+ }
91
+ },
92
+ async request => {
93
+ return services.node.get(request.authenticatePayload, request.query);
94
+ }
95
+ );
96
+
97
+ fastify.post(
98
+ `${options.prefix}/:shorten/content-share/create`,
99
+ {
100
+ onRequest: withEntryAuth,
101
+ schema: {
102
+ summary: '创建文件内容分享短链(可设有效期)',
103
+ params: {
104
+ type: 'object',
105
+ required: ['shorten'],
106
+ properties: {
107
+ shorten: { type: 'string' }
108
+ }
109
+ },
110
+ body: {
111
+ type: 'object',
112
+ required: ['id'],
113
+ properties: {
114
+ id: { type: 'string' },
115
+ expiresIn: { type: ['number', 'null'] }
116
+ }
117
+ }
118
+ }
119
+ },
120
+ async request => {
121
+ return services.contentShare.create(request.authenticatePayload, request.body);
122
+ }
123
+ );
124
+
125
+ fastify.get(
126
+ `${options.prefix}/:shorten/content-share/list`,
127
+ {
128
+ onRequest: withEntryAuth,
129
+ schema: {
130
+ summary: '列出文件已有内容分享短链',
131
+ params: {
132
+ type: 'object',
133
+ required: ['shorten'],
134
+ properties: {
135
+ shorten: { type: 'string' }
136
+ }
137
+ },
138
+ query: {
139
+ type: 'object',
140
+ required: ['id'],
141
+ properties: {
142
+ id: { type: 'string' }
143
+ }
144
+ }
145
+ }
146
+ },
147
+ async request => {
148
+ return services.contentShare.list(request.authenticatePayload, request.query);
149
+ }
150
+ );
151
+
152
+ fastify.post(
153
+ `${options.prefix}/:shorten/content-share/remove`,
154
+ {
155
+ onRequest: withEntryAuth,
156
+ schema: {
157
+ summary: '删除文件内容分享短链',
158
+ params: {
159
+ type: 'object',
160
+ required: ['shorten'],
161
+ properties: {
162
+ shorten: { type: 'string' }
163
+ }
164
+ },
165
+ body: {
166
+ type: 'object',
167
+ required: ['shorten'],
168
+ properties: {
169
+ shorten: { type: 'string' }
170
+ }
171
+ }
172
+ }
173
+ },
174
+ async request => {
175
+ return services.contentShare.remove(request.authenticatePayload, request.body);
176
+ }
177
+ );
178
+
179
+ fastify.post(
180
+ `${options.prefix}/:shorten/createFolder`,
181
+ {
182
+ onRequest: withEntryAuth,
183
+ schema: {
184
+ summary: '创建文件夹',
185
+ params: {
186
+ type: 'object',
187
+ required: ['shorten'],
188
+ properties: {
189
+ shorten: { type: 'string' }
190
+ }
191
+ },
192
+ body: {
193
+ type: 'object',
194
+ required: ['name'],
195
+ properties: {
196
+ parentId: { type: ['string', 'null'] },
197
+ name: { type: 'string' }
198
+ }
199
+ }
200
+ }
201
+ },
202
+ async request => {
203
+ return services.node.createFolder(request.authenticatePayload, request.body);
204
+ }
205
+ );
206
+
207
+ fastify.post(
208
+ `${options.prefix}/:shorten/create`,
209
+ {
210
+ onRequest: withEntryAuth,
211
+ schema: {
212
+ summary: '创建文件',
213
+ params: {
214
+ type: 'object',
215
+ required: ['shorten'],
216
+ properties: {
217
+ shorten: { type: 'string' }
218
+ }
219
+ },
220
+ body: {
221
+ type: 'object',
222
+ required: ['name'],
223
+ properties: {
224
+ parentId: { type: ['string', 'null'] },
225
+ name: { type: 'string' },
226
+ content: { type: 'string' }
227
+ }
228
+ }
229
+ }
230
+ },
231
+ async request => {
232
+ return services.node.create(request.authenticatePayload, request.body);
233
+ }
234
+ );
235
+
236
+ fastify.post(
237
+ `${options.prefix}/:shorten/save`,
238
+ {
239
+ onRequest: withEntryAuth,
240
+ schema: {
241
+ summary: '保存文件内容',
242
+ params: {
243
+ type: 'object',
244
+ required: ['shorten'],
245
+ properties: {
246
+ shorten: { type: 'string' }
247
+ }
248
+ },
249
+ body: {
250
+ type: 'object',
251
+ required: ['id'],
252
+ properties: {
253
+ id: { type: 'string' },
254
+ content: { type: 'string' }
255
+ }
256
+ }
257
+ }
258
+ },
259
+ async request => {
260
+ return services.node.save(request.authenticatePayload, request.body);
261
+ }
262
+ );
263
+
264
+ fastify.post(
265
+ `${options.prefix}/:shorten/rename`,
266
+ {
267
+ onRequest: withEntryAuth,
268
+ schema: {
269
+ summary: '重命名节点',
270
+ params: {
271
+ type: 'object',
272
+ required: ['shorten'],
273
+ properties: {
274
+ shorten: { type: 'string' }
275
+ }
276
+ },
277
+ body: {
278
+ type: 'object',
279
+ required: ['id', 'name'],
280
+ properties: {
281
+ id: { type: 'string' },
282
+ name: { type: 'string' }
283
+ }
284
+ }
285
+ }
286
+ },
287
+ async request => {
288
+ return services.node.rename(request.authenticatePayload, request.body);
289
+ }
290
+ );
291
+
292
+ fastify.post(
293
+ `${options.prefix}/:shorten/remove`,
294
+ {
295
+ onRequest: withEntryAuth,
296
+ schema: {
297
+ summary: '删除节点',
298
+ params: {
299
+ type: 'object',
300
+ required: ['shorten'],
301
+ properties: {
302
+ shorten: { type: 'string' }
303
+ }
304
+ },
305
+ body: {
306
+ type: 'object',
307
+ required: ['ids'],
308
+ properties: {
309
+ ids: {
310
+ type: 'array',
311
+ items: { type: 'string' }
312
+ }
313
+ }
314
+ }
315
+ }
316
+ },
317
+ async request => {
318
+ return services.node.remove(request.authenticatePayload, request.body);
319
+ }
320
+ );
321
+ });
@@ -0,0 +1,115 @@
1
+ const fp = require('fastify-plugin');
2
+
3
+ module.exports = fp(async (fastify, options) => {
4
+ const { services } = fastify[options.name];
5
+
6
+ fastify.get(
7
+ `${options.prefix}/site/list`,
8
+ {
9
+ onRequest: options.getAuthenticate('site'),
10
+ schema: {
11
+ summary: '站点列表',
12
+ query: {
13
+ type: 'object',
14
+ properties: {
15
+ filter: { type: 'object', default: {} },
16
+ perPage: { type: 'number', default: 20 },
17
+ currentPage: { type: 'number', default: 1 }
18
+ }
19
+ }
20
+ }
21
+ },
22
+ async request => {
23
+ return services.site.list(request.userInfo || {}, request.query);
24
+ }
25
+ );
26
+
27
+ fastify.get(
28
+ `${options.prefix}/site/detail`,
29
+ {
30
+ onRequest: options.getAuthenticate('site'),
31
+ schema: {
32
+ summary: '站点详情',
33
+ query: {
34
+ type: 'object',
35
+ required: ['id'],
36
+ properties: {
37
+ id: { type: 'string' }
38
+ }
39
+ }
40
+ }
41
+ },
42
+ async request => {
43
+ return services.site.detail(request.userInfo || {}, request.query);
44
+ }
45
+ );
46
+
47
+ fastify.post(
48
+ `${options.prefix}/site/create`,
49
+ {
50
+ onRequest: options.getAuthenticate('site:create'),
51
+ schema: {
52
+ summary: '创建站点并签发 shorten 链接',
53
+ body: {
54
+ type: 'object',
55
+ required: ['name'],
56
+ properties: {
57
+ name: { type: 'string' },
58
+ defaultPermission: { type: 'string', enum: ['r', 'rw'] },
59
+ status: { type: 'string', enum: ['open', 'closed'] },
60
+ options: { type: 'object' }
61
+ }
62
+ }
63
+ }
64
+ },
65
+ async request => {
66
+ return services.site.create(request.userInfo || {}, request.body);
67
+ }
68
+ );
69
+
70
+ fastify.post(
71
+ `${options.prefix}/site/save`,
72
+ {
73
+ onRequest: options.getAuthenticate('site:manage'),
74
+ schema: {
75
+ summary: '修改站点(含开启/关闭)',
76
+ body: {
77
+ type: 'object',
78
+ required: ['id'],
79
+ properties: {
80
+ id: { type: 'string' },
81
+ name: { type: 'string' },
82
+ status: { type: 'string', enum: ['open', 'closed'] },
83
+ defaultPermission: { type: 'string', enum: ['r', 'rw'] },
84
+ options: { type: 'object' }
85
+ }
86
+ }
87
+ }
88
+ },
89
+ async request => {
90
+ await services.site.save(request.userInfo || {}, request.body);
91
+ return {};
92
+ }
93
+ );
94
+
95
+ fastify.post(
96
+ `${options.prefix}/site/remove`,
97
+ {
98
+ onRequest: options.getAuthenticate('site:manage'),
99
+ schema: {
100
+ summary: '删除站点',
101
+ body: {
102
+ type: 'object',
103
+ required: ['id'],
104
+ properties: {
105
+ id: { type: 'string' }
106
+ }
107
+ }
108
+ }
109
+ },
110
+ async request => {
111
+ await services.site.remove(request.userInfo || {}, request.body);
112
+ return {};
113
+ }
114
+ );
115
+ });
@@ -0,0 +1,39 @@
1
+ module.exports = ({ DataTypes, definePrimaryType }) => {
2
+ return {
3
+ model: {
4
+ siteId: Object.assign(definePrimaryType('siteId'), {
5
+ allowNull: false,
6
+ comment: '所属站点'
7
+ }),
8
+ nodeId: Object.assign(definePrimaryType('nodeId'), {
9
+ allowNull: false,
10
+ comment: '文件节点'
11
+ }),
12
+ shorten: {
13
+ type: DataTypes.STRING,
14
+ allowNull: false,
15
+ comment: '内容访问短码'
16
+ },
17
+ expires: {
18
+ type: DataTypes.DATE,
19
+ allowNull: true,
20
+ comment: '过期时间,空表示永久'
21
+ }
22
+ },
23
+ associate: ({ contentShare, site, node }) => {
24
+ contentShare.belongsTo(site, { foreignKey: 'siteId' });
25
+ contentShare.belongsTo(node, { foreignKey: 'nodeId' });
26
+ },
27
+ options: {
28
+ comment: 'Live 组件文件内容分享短链',
29
+ indexes: [
30
+ { fields: ['site_id', 'node_id'] },
31
+ {
32
+ fields: ['shorten'],
33
+ unique: true,
34
+ where: { deleted_at: null }
35
+ }
36
+ ]
37
+ }
38
+ };
39
+ };
@@ -0,0 +1,48 @@
1
+ module.exports = ({ DataTypes, definePrimaryType }) => {
2
+ return {
3
+ model: {
4
+ parentId: Object.assign(definePrimaryType('parentId'), {
5
+ allowNull: true,
6
+ comment: '父节点 id,根节点为空'
7
+ }),
8
+ name: {
9
+ type: DataTypes.STRING,
10
+ allowNull: false,
11
+ comment: '节点名称'
12
+ },
13
+ type: {
14
+ type: DataTypes.STRING,
15
+ allowNull: false,
16
+ comment: '节点类型: file | directory'
17
+ },
18
+ permission: {
19
+ type: DataTypes.STRING,
20
+ allowNull: false,
21
+ defaultValue: 'rw',
22
+ comment: '权限: r | rw'
23
+ },
24
+ content: {
25
+ type: DataTypes.TEXT,
26
+ comment: '文件内容,目录为空'
27
+ }
28
+ },
29
+ associate: ({ node, site }) => {
30
+ node.belongsTo(site, {
31
+ foreignKey: 'siteId',
32
+ comment: '所属站点'
33
+ });
34
+ },
35
+ options: {
36
+ comment: 'Live 组件站点文件树节点',
37
+ indexes: [
38
+ { fields: ['site_id'] },
39
+ { fields: ['parent_id'] },
40
+ {
41
+ fields: ['site_id', 'parent_id', 'name'],
42
+ unique: true,
43
+ where: { deleted_at: null }
44
+ }
45
+ ]
46
+ }
47
+ };
48
+ };