@nocobase/plugin-public-forms 2.2.0-beta.13 → 2.2.0-beta.14

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.
@@ -8,12 +8,13 @@
8
8
  */
9
9
 
10
10
  module.exports = {
11
- "@nocobase/client": "2.2.0-beta.13",
11
+ "@nocobase/client": "2.2.0-beta.14",
12
12
  "react-i18next": "11.18.6",
13
- "@nocobase/flow-engine": "2.2.0-beta.13",
14
- "@nocobase/client-v2": "2.2.0-beta.13",
15
- "@nocobase/plugin-ui-schema-storage": "2.2.0-beta.13",
16
- "@nocobase/server": "2.2.0-beta.13",
13
+ "@nocobase/flow-engine": "2.2.0-beta.14",
14
+ "@nocobase/client-v2": "2.2.0-beta.14",
15
+ "@nocobase/plugin-ui-schema-storage": "2.2.0-beta.14",
16
+ "@nocobase/server": "2.2.0-beta.14",
17
+ "@nocobase/utils": "2.2.0-beta.14",
17
18
  "react": "18.2.0",
18
19
  "@ant-design/icons": "5.6.1",
19
20
  "antd": "5.24.2",
@@ -26,6 +27,6 @@ module.exports = {
26
27
  "@formily/core": "2.3.7",
27
28
  "@formily/shared": "2.3.7",
28
29
  "@formily/reactive": "2.3.7",
29
- "@nocobase/database": "2.2.0-beta.13",
30
+ "@nocobase/database": "2.2.0-beta.14",
30
31
  "dayjs": "1.11.13"
31
32
  };
@@ -7,8 +7,27 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Plugin } from '@nocobase/server';
10
+ type PublicFormTokenPayload = {
11
+ collectionName: string;
12
+ formKey: string;
13
+ targetCollections: string[];
14
+ exp?: number;
15
+ };
16
+ type PublicFormCookiePayload = PublicFormTokenPayload & {
17
+ v: 1;
18
+ appName?: string;
19
+ files: Record<string, Array<string | number>>;
20
+ };
21
+ type FileAccessAuthorizeParams = {
22
+ appName: string;
23
+ dataSourceKey: string;
24
+ collectionName: string;
25
+ id: string;
26
+ preview: boolean;
27
+ };
10
28
  export declare class PluginPublicFormsServer extends Plugin {
11
29
  protected associationFieldTypes: string[];
30
+ protected fileAccessAuthorizerRegistered: boolean;
12
31
  parseCollectionData(dataSourceKey: any, formCollection: any, appends: any): Promise<any[]>;
13
32
  getFlowModelTree(uid: string, options?: {
14
33
  includeAsyncNode?: boolean;
@@ -45,10 +64,17 @@ export declare class PluginPublicFormsServer extends Plugin {
45
64
  title: any;
46
65
  passwordRequired?: undefined;
47
66
  }>;
67
+ getPublicFormCookieName(formKey: string): string;
68
+ decodePublicFormCookie(value?: string | null): Promise<PublicFormCookiePayload | null>;
69
+ readPublicFormCookie(ctx: any, formKey: string): Promise<PublicFormCookiePayload>;
70
+ setPublicFormCookie(ctx: any, payload: PublicFormCookiePayload, maxAge?: number): void;
48
71
  getPublicFormsMeta: (ctx: any, next: any) => Promise<void>;
49
72
  getPublicFormFlowModel: (ctx: any, next: any) => Promise<void>;
50
73
  parseToken: (ctx: any, next: any) => Promise<any>;
51
74
  parseACL: (ctx: any, next: any) => Promise<any>;
75
+ appendUploadedFileToPublicFormCookie: (ctx: any, next: any) => Promise<void>;
76
+ authorizePublicFormFileAccess: (ctx: any, params: FileAccessAuthorizeParams) => Promise<boolean>;
77
+ registerFileAccessAuthorizer(): void;
52
78
  load(): Promise<void>;
53
79
  install(): Promise<void>;
54
80
  afterEnable(): Promise<void>;
@@ -7,9 +7,11 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ var __create = Object.create;
10
11
  var __defProp = Object.defineProperty;
11
12
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
13
  var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
13
15
  var __hasOwnProp = Object.prototype.hasOwnProperty;
14
16
  var __export = (target, all) => {
15
17
  for (var name in all)
@@ -23,6 +25,14 @@ var __copyProps = (to, from, except, desc) => {
23
25
  }
24
26
  return to;
25
27
  };
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
29
+ // If the importer is in node compatibility mode or this is not an ESM
30
+ // file that has been converted to a CommonJS file using a Babel-
31
+ // compatible transform (i.e. "__esModule" has not been set), then set
32
+ // "default" to the CommonJS "module.exports" for node compatibility.
33
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
34
+ mod
35
+ ));
26
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
37
  var plugin_exports = {};
28
38
  __export(plugin_exports, {
@@ -31,11 +41,66 @@ __export(plugin_exports, {
31
41
  });
32
42
  module.exports = __toCommonJS(plugin_exports);
33
43
  var import_server = require("@nocobase/server");
44
+ var import_utils = require("@nocobase/utils");
45
+ var import_crypto = __toESM(require("crypto"));
34
46
  var import_hook = require("./hook");
35
47
  class PasswordError extends Error {
36
48
  }
49
+ const PUBLIC_FORM_COOKIE_PREFIX = "nb_pf_";
50
+ const PUBLIC_FORM_TOKEN_EXPIRES_IN = "1h";
51
+ const PUBLIC_FORM_COOKIE_MAX_AGE = 60 * 60 * 1e3;
52
+ function parseCookieHeader(header) {
53
+ const cookies = [];
54
+ if (!header) {
55
+ return cookies;
56
+ }
57
+ for (const item of header.split(";")) {
58
+ const index = item.indexOf("=");
59
+ if (index < 0) {
60
+ continue;
61
+ }
62
+ const name = item.slice(0, index).trim();
63
+ const value = item.slice(index + 1).trim();
64
+ if (!name) {
65
+ continue;
66
+ }
67
+ try {
68
+ cookies.push([name, decodeURIComponent(value)]);
69
+ } catch {
70
+ cookies.push([name, value]);
71
+ }
72
+ }
73
+ return cookies;
74
+ }
75
+ function normalizeFiles(value) {
76
+ const files = {};
77
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
78
+ return files;
79
+ }
80
+ for (const [key, ids] of Object.entries(value)) {
81
+ if (typeof key !== "string" || !Array.isArray(ids)) {
82
+ continue;
83
+ }
84
+ files[key] = ids.filter((id) => typeof id === "string" || typeof id === "number");
85
+ }
86
+ return files;
87
+ }
88
+ function getResponseRecordId(body) {
89
+ const responseBody = body && typeof body === "object" && "data" in body ? body.data : body;
90
+ if (!responseBody || typeof responseBody !== "object") {
91
+ return null;
92
+ }
93
+ if ("get" in responseBody && typeof responseBody.get === "function") {
94
+ return responseBody.get("id");
95
+ }
96
+ if ("id" in responseBody) {
97
+ return responseBody.id;
98
+ }
99
+ return null;
100
+ }
37
101
  class PluginPublicFormsServer extends import_server.Plugin {
38
102
  associationFieldTypes = ["hasOne", "hasMany", "belongsTo", "belongsToMany", "belongsToArray"];
103
+ fileAccessAuthorizerRegistered = false;
39
104
  async parseCollectionData(dataSourceKey, formCollection, appends) {
40
105
  const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey);
41
106
  const serializeCollection = (collection2) => ({
@@ -223,33 +288,92 @@ class PluginPublicFormsServer extends import_server.Plugin {
223
288
  ])
224
289
  ];
225
290
  const collections = await this.parseCollectionData(dataSourceKey, collectionName, appends);
291
+ const tokenPayload = {
292
+ collectionName,
293
+ formKey: filterByTk,
294
+ targetCollections: appends
295
+ };
226
296
  return {
227
297
  dataSource: {
228
298
  key: dataSourceKey,
229
299
  displayName: dataSourceKey,
230
300
  collections
231
301
  },
232
- token: this.app.authManager.jwt.sign(
233
- {
234
- collectionName,
235
- formKey: filterByTk,
236
- targetCollections: appends
237
- },
238
- {
239
- expiresIn: "1h"
240
- }
241
- ),
302
+ token: this.app.authManager.jwt.sign(tokenPayload, {
303
+ expiresIn: PUBLIC_FORM_TOKEN_EXPIRES_IN
304
+ }),
242
305
  schema,
243
306
  flowModel,
244
307
  title
245
308
  };
246
309
  }
310
+ getPublicFormCookieName(formKey) {
311
+ const appName = this.app.name || "main";
312
+ const hash = import_crypto.default.createHash("sha256").update(`${appName}:${formKey}`).digest("hex").slice(0, 32);
313
+ return `${PUBLIC_FORM_COOKIE_PREFIX}${hash}`;
314
+ }
315
+ async decodePublicFormCookie(value) {
316
+ if (!value) {
317
+ return null;
318
+ }
319
+ const decoded = await this.app.authManager.jwt.decode(value);
320
+ if ((decoded == null ? void 0 : decoded.v) !== 1 || typeof decoded.formKey !== "string" || typeof decoded.collectionName !== "string" || !Array.isArray(decoded.targetCollections)) {
321
+ return null;
322
+ }
323
+ return {
324
+ v: 1,
325
+ appName: typeof decoded.appName === "string" ? decoded.appName : void 0,
326
+ formKey: decoded.formKey,
327
+ collectionName: decoded.collectionName,
328
+ targetCollections: decoded.targetCollections.filter((name) => typeof name === "string"),
329
+ files: normalizeFiles(decoded.files),
330
+ exp: typeof decoded.exp === "number" ? decoded.exp : void 0
331
+ };
332
+ }
333
+ async readPublicFormCookie(ctx, formKey) {
334
+ var _a;
335
+ try {
336
+ return await this.decodePublicFormCookie((_a = ctx.cookies) == null ? void 0 : _a.get(this.getPublicFormCookieName(formKey)));
337
+ } catch {
338
+ return null;
339
+ }
340
+ }
341
+ setPublicFormCookie(ctx, payload, maxAge = PUBLIC_FORM_COOKIE_MAX_AGE) {
342
+ var _a;
343
+ (_a = ctx.cookies) == null ? void 0 : _a.set(
344
+ this.getPublicFormCookieName(payload.formKey),
345
+ this.app.authManager.jwt.sign(
346
+ {
347
+ v: 1,
348
+ appName: this.app.name || "main",
349
+ formKey: payload.formKey,
350
+ collectionName: payload.collectionName,
351
+ targetCollections: payload.targetCollections,
352
+ files: normalizeFiles(payload.files)
353
+ },
354
+ { expiresIn: Math.max(1, Math.floor(maxAge / 1e3)) }
355
+ ),
356
+ (0, import_utils.getAuthCookieOptions)(ctx, true, maxAge)
357
+ );
358
+ }
247
359
  // TODO
248
360
  getPublicFormsMeta = async (ctx, next) => {
361
+ var _a;
249
362
  const token = ctx.get("X-Form-Token");
250
363
  const { filterByTk, password } = ctx.action.params;
251
364
  try {
365
+ const existingCookie = password === void 0 ? await this.readPublicFormCookie(ctx, filterByTk) : null;
252
366
  ctx.body = await this.getMetaByTk(filterByTk, { password, token });
367
+ if (((_a = ctx.body) == null ? void 0 : _a.token) && (password !== void 0 || !existingCookie)) {
368
+ const tokenPayload = await this.app.authManager.jwt.decode(ctx.body.token);
369
+ this.setPublicFormCookie(ctx, {
370
+ v: 1,
371
+ formKey: tokenPayload.formKey,
372
+ collectionName: tokenPayload.collectionName,
373
+ targetCollections: tokenPayload.targetCollections,
374
+ files: (existingCookie == null ? void 0 : existingCookie.formKey) === tokenPayload.formKey ? existingCookie.files : {}
375
+ });
376
+ }
253
377
  } catch (error) {
254
378
  if (error instanceof PasswordError) {
255
379
  ctx.throw(401, error.message);
@@ -285,7 +409,8 @@ class PluginPublicFormsServer extends import_server.Plugin {
285
409
  ctx.PublicForm = {
286
410
  collectionName: tokenData.collectionName,
287
411
  formKey: tokenData.formKey,
288
- targetCollections: tokenData.targetCollections
412
+ targetCollections: tokenData.targetCollections,
413
+ exp: tokenData.exp
289
414
  };
290
415
  const publicForms = this.db.getRepository("publicForms");
291
416
  const instance = await publicForms.findOne({
@@ -320,13 +445,100 @@ class PluginPublicFormsServer extends import_server.Plugin {
320
445
  ctx.permission = {
321
446
  skip: true
322
447
  };
323
- } else if (["list", "get"].includes(actionName) && ctx.PublicForm["targetCollections"].includes(resourceName) || (collection == null ? void 0 : collection.options.template) === "file" && actionName === "create" || resourceName === "storages" && ["getBasicInfo", "createPresignedUrl", "check"].includes(actionName) || resourceName === "vditor" && ["check"].includes(actionName) || resourceName === "map-configuration" && actionName === "get") {
448
+ } else if (["list", "get"].includes(actionName) && ctx.PublicForm["targetCollections"].includes(resourceName) || (collection == null ? void 0 : collection.options.template) === "file" && ["create", "upload"].includes(actionName) || resourceName === "storages" && ["getBasicInfo", "createPresignedUrl", "check"].includes(actionName) || resourceName === "vditor" && ["check"].includes(actionName) || resourceName === "map-configuration" && actionName === "get") {
324
449
  ctx.permission = {
325
450
  skip: true
326
451
  };
327
452
  }
328
453
  await next();
329
454
  };
455
+ appendUploadedFileToPublicFormCookie = async (ctx, next) => {
456
+ var _a, _b, _c, _d, _e, _f, _g, _h;
457
+ await next();
458
+ if (!ctx.PublicForm || !["create", "upload"].includes((_a = ctx.action) == null ? void 0 : _a.actionName)) {
459
+ return;
460
+ }
461
+ const collection = ((_c = (_b = ctx.dataSource) == null ? void 0 : _b.collectionManager) == null ? void 0 : _c.getCollection(ctx.action.resourceName)) || ((_e = (_d = ctx.db) == null ? void 0 : _d.getCollection) == null ? void 0 : _e.call(_d, ctx.action.resourceName));
462
+ if (((_f = collection == null ? void 0 : collection.options) == null ? void 0 : _f.template) !== "file") {
463
+ return;
464
+ }
465
+ const id = getResponseRecordId(ctx.body);
466
+ if (id === null || id === void 0 || id === "") {
467
+ return;
468
+ }
469
+ const formKey = ctx.PublicForm.formKey;
470
+ const existingCookie = await this.readPublicFormCookie(ctx, formKey);
471
+ const files = normalizeFiles(existingCookie == null ? void 0 : existingCookie.files);
472
+ const dataSourceKey = ((_g = ctx.dataSource) == null ? void 0 : _g.name) || ((_h = ctx.action.params) == null ? void 0 : _h.dataSourceKey) || "main";
473
+ const fileKey = `${dataSourceKey}/${collection.name}`;
474
+ const ids = files[fileKey] || [];
475
+ if (!ids.some((existingId) => String(existingId) === String(id))) {
476
+ ids.push(id);
477
+ }
478
+ files[fileKey] = ids;
479
+ const expiresAt = (existingCookie == null ? void 0 : existingCookie.exp) || ctx.PublicForm.exp;
480
+ const maxAge = expiresAt ? expiresAt * 1e3 - Date.now() : PUBLIC_FORM_COOKIE_MAX_AGE;
481
+ if (maxAge <= 0) {
482
+ return;
483
+ }
484
+ this.setPublicFormCookie(
485
+ ctx,
486
+ {
487
+ v: 1,
488
+ formKey,
489
+ collectionName: ctx.PublicForm.collectionName,
490
+ targetCollections: ctx.PublicForm.targetCollections || [],
491
+ files
492
+ },
493
+ maxAge
494
+ );
495
+ };
496
+ authorizePublicFormFileAccess = async (ctx, params) => {
497
+ var _a, _b;
498
+ const cookies = parseCookieHeader((_a = ctx.get) == null ? void 0 : _a.call(ctx, "cookie"));
499
+ const fileKey = `${params.dataSourceKey}/${params.collectionName}`;
500
+ for (const [name, value] of cookies) {
501
+ if (!name.startsWith(PUBLIC_FORM_COOKIE_PREFIX)) {
502
+ continue;
503
+ }
504
+ try {
505
+ const payload = await this.decodePublicFormCookie(value);
506
+ if (!payload) {
507
+ continue;
508
+ }
509
+ if (payload.appName && payload.appName !== params.appName) {
510
+ continue;
511
+ }
512
+ if ((_b = payload.files[fileKey]) == null ? void 0 : _b.some((id) => String(id) === String(params.id))) {
513
+ const instance = await this.db.getRepository("publicForms").findOne({
514
+ filter: {
515
+ key: payload.formKey
516
+ }
517
+ });
518
+ if (instance == null ? void 0 : instance.get("enabled")) {
519
+ return true;
520
+ }
521
+ }
522
+ } catch {
523
+ continue;
524
+ }
525
+ }
526
+ return false;
527
+ };
528
+ registerFileAccessAuthorizer() {
529
+ if (this.fileAccessAuthorizerRegistered) {
530
+ return;
531
+ }
532
+ const fileManager = this.app.pm.get("file-manager") || this.app.pm.get("@nocobase/plugin-file-manager");
533
+ if (typeof (fileManager == null ? void 0 : fileManager.registerFileAccessAuthorizer) !== "function") {
534
+ return;
535
+ }
536
+ fileManager.registerFileAccessAuthorizer({
537
+ name: "public-forms",
538
+ authorize: this.authorizePublicFormFileAccess
539
+ });
540
+ this.fileAccessAuthorizerRegistered = true;
541
+ }
330
542
  async load() {
331
543
  this.app.acl.registerSnippet({
332
544
  name: `pm.${this.name}`,
@@ -337,10 +549,17 @@ class PluginPublicFormsServer extends import_server.Plugin {
337
549
  "publicForms:getMeta": this.getPublicFormsMeta,
338
550
  "publicForms:getFlowModel": this.getPublicFormFlowModel
339
551
  });
552
+ this.registerFileAccessAuthorizer();
553
+ this.app.on("afterLoad", () => {
554
+ this.registerFileAccessAuthorizer();
555
+ });
340
556
  this.app.dataSourceManager.afterAddDataSource((dataSource) => {
341
557
  dataSource.resourceManager.use(this.parseToken, {
342
558
  before: "auth"
343
559
  });
560
+ dataSource.resourceManager.use(this.appendUploadedFileToPublicFormCookie, {
561
+ after: "acl"
562
+ });
344
563
  dataSource.acl.use(this.parseACL, {
345
564
  before: "core"
346
565
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/plugin-public-forms",
3
- "version": "2.2.0-beta.13",
3
+ "version": "2.2.0-beta.14",
4
4
  "main": "dist/server/index.js",
5
5
  "displayName": "Public forms",
6
6
  "displayName.ru-RU": "Публичные формы",
@@ -20,10 +20,11 @@
20
20
  "@nocobase/plugin-flow-engine": "2.x",
21
21
  "@nocobase/plugin-ui-schema-storage": "2.x",
22
22
  "@nocobase/server": "2.x",
23
- "@nocobase/test": "2.x"
23
+ "@nocobase/test": "2.x",
24
+ "@nocobase/utils": "2.x"
24
25
  },
25
26
  "devDependencies": {
26
27
  "react-device-detect": "2.2.3"
27
28
  },
28
- "gitHead": "8fff12b34f41f4336075cdafcc700ec694f9b78a"
29
+ "gitHead": "1ef021f3ce0a84469e40f181a19551a50b6f850c"
29
30
  }