@crowi/api 2.0.0-alpha.1 → 2.0.0-alpha.2
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/dist/hono/handlers/activation.d.ts +3 -3
- package/dist/hono/handlers/admin/users.d.ts +118 -0
- package/dist/hono/handlers/admin/users.js +28 -0
- package/dist/hono/handlers/admin/users.js.map +1 -1
- package/dist/hono/handlers/app.d.ts +1 -0
- package/dist/hono/handlers/app.js +11 -0
- package/dist/hono/handlers/app.js.map +1 -1
- package/dist/hono/handlers/attachment-stream.js +23 -0
- package/dist/hono/handlers/attachment-stream.js.map +1 -1
- package/dist/hono/handlers/draft.js +10 -0
- package/dist/hono/handlers/draft.js.map +1 -1
- package/dist/hono/handlers/emailChange.d.ts +4 -4
- package/dist/hono/handlers/inviteAccept.d.ts +6 -6
- package/dist/hono/handlers/page.d.ts +251 -0
- package/dist/hono/handlers/page.js +123 -6
- package/dist/hono/handlers/page.js.map +1 -1
- package/dist/hono/handlers/passwordReset.d.ts +5 -5
- package/dist/hono/handlers/tokenAuth.d.ts +7 -7
- package/dist/mcp/result.d.ts +42 -16
- package/dist/mcp/result.js +56 -10
- package/dist/mcp/result.js.map +1 -1
- package/dist/mcp/tools/page.js +21 -1
- package/dist/mcp/tools/page.js.map +1 -1
- package/dist/mcp/tools/search.d.ts +12 -0
- package/dist/mcp/tools/search.js +21 -5
- package/dist/mcp/tools/search.js.map +1 -1
- package/dist/migration/helpers.d.ts +13 -0
- package/dist/migration/helpers.js +29 -0
- package/dist/migration/helpers.js.map +1 -0
- package/dist/migration/migrations/files-url-to-attachments.d.ts +35 -0
- package/dist/migration/migrations/files-url-to-attachments.js +291 -0
- package/dist/migration/migrations/files-url-to-attachments.js.map +1 -0
- package/dist/migration/migrations/index.js +4 -0
- package/dist/migration/migrations/index.js.map +1 -1
- package/dist/migration/migrations/published-current-revision.d.ts +47 -0
- package/dist/migration/migrations/published-current-revision.js +90 -0
- package/dist/migration/migrations/published-current-revision.js.map +1 -0
- package/dist/migration/migrations/wikilink-format.d.ts +0 -11
- package/dist/migration/migrations/wikilink-format.js +5 -156
- package/dist/migration/migrations/wikilink-format.js.map +1 -1
- package/dist/migration/migrations/wikilink-html-recover.d.ts +116 -0
- package/dist/migration/migrations/wikilink-html-recover.js +314 -0
- package/dist/migration/migrations/wikilink-html-recover.js.map +1 -0
- package/dist/models/page.d.ts +3 -0
- package/dist/models/page.js +31 -0
- package/dist/models/page.js.map +1 -1
- package/dist/models/user.d.ts +1 -0
- package/dist/models/user.js +40 -21
- package/dist/models/user.js.map +1 -1
- package/dist/renderer/core/headings.d.ts +12 -1
- package/dist/renderer/core/headings.js +48 -8
- package/dist/renderer/core/headings.js.map +1 -1
- package/dist/renderer/pipeline.d.ts +6 -0
- package/dist/renderer/pipeline.js.map +1 -1
- package/dist/util/page-response.js +19 -2
- package/dist/util/page-response.js.map +1 -1
- package/package.json +12 -6
- package/views/mail/layout.mjml +7 -5
- package/dist/common/functions/path2name.d.ts +0 -1
- package/dist/common/functions/path2name.js +0 -22
- package/dist/common/functions/path2name.js.map +0 -1
- package/dist/common/functions/renderIcon.d.ts +0 -1
- package/dist/common/functions/renderIcon.js +0 -9
- package/dist/common/functions/renderIcon.js.map +0 -1
- package/dist/controllers/admin.d.ts +0 -3
- package/dist/controllers/admin.js +0 -474
- package/dist/controllers/admin.js.map +0 -1
- package/dist/controllers/attachment.d.ts +0 -4
- package/dist/controllers/attachment.js +0 -200
- package/dist/controllers/attachment.js.map +0 -1
- package/dist/controllers/backlink.d.ts +0 -3
- package/dist/controllers/backlink.js +0 -42
- package/dist/controllers/backlink.js.map +0 -1
- package/dist/controllers/bookmark.d.ts +0 -3
- package/dist/controllers/bookmark.js +0 -100
- package/dist/controllers/bookmark.js.map +0 -1
- package/dist/controllers/comment.d.ts +0 -3
- package/dist/controllers/comment.js +0 -111
- package/dist/controllers/comment.js.map +0 -1
- package/dist/controllers/index.d.ts +0 -25
- package/dist/controllers/index.js +0 -44
- package/dist/controllers/index.js.map +0 -1
- package/dist/controllers/installer.d.ts +0 -3
- package/dist/controllers/installer.js +0 -48
- package/dist/controllers/installer.js.map +0 -1
- package/dist/controllers/login.d.ts +0 -4
- package/dist/controllers/login.js +0 -438
- package/dist/controllers/login.js.map +0 -1
- package/dist/controllers/logout.d.ts +0 -5
- package/dist/controllers/logout.js +0 -11
- package/dist/controllers/logout.js.map +0 -1
- package/dist/controllers/me.d.ts +0 -4
- package/dist/controllers/me.js +0 -369
- package/dist/controllers/me.js.map +0 -1
- package/dist/controllers/notification.d.ts +0 -3
- package/dist/controllers/notification.js +0 -88
- package/dist/controllers/notification.js.map +0 -1
- package/dist/controllers/page.d.ts +0 -3
- package/dist/controllers/page.js +0 -881
- package/dist/controllers/page.js.map +0 -1
- package/dist/controllers/revision.d.ts +0 -3
- package/dist/controllers/revision.js +0 -91
- package/dist/controllers/revision.js.map +0 -1
- package/dist/controllers/search.d.ts +0 -3
- package/dist/controllers/search.js +0 -93
- package/dist/controllers/search.js.map +0 -1
- package/dist/controllers/share.d.ts +0 -3
- package/dist/controllers/share.js +0 -207
- package/dist/controllers/share.js.map +0 -1
- package/dist/controllers/shareAccess.d.ts +0 -3
- package/dist/controllers/shareAccess.js +0 -28
- package/dist/controllers/shareAccess.js.map +0 -1
- package/dist/controllers/slack.d.ts +0 -3
- package/dist/controllers/slack.js +0 -87
- package/dist/controllers/slack.js.map +0 -1
- package/dist/controllers/tokenAuth.d.ts +0 -10
- package/dist/controllers/tokenAuth.js +0 -292
- package/dist/controllers/tokenAuth.js.map +0 -1
- package/dist/controllers/user.d.ts +0 -3
- package/dist/controllers/user.js +0 -67
- package/dist/controllers/user.js.map +0 -1
- package/dist/controllers/version.d.ts +0 -4
- package/dist/controllers/version.js +0 -19
- package/dist/controllers/version.js.map +0 -1
- package/dist/crowi/express-init.d.ts +0 -4
- package/dist/crowi/express-init.js +0 -101
- package/dist/crowi/express-init.js.map +0 -1
- package/dist/form/admin/app.d.ts +0 -2
- package/dist/form/admin/app.js +0 -9
- package/dist/form/admin/app.js.map +0 -1
- package/dist/form/admin/auth.d.ts +0 -2
- package/dist/form/admin/auth.js +0 -9
- package/dist/form/admin/auth.js.map +0 -1
- package/dist/form/admin/aws.d.ts +0 -2
- package/dist/form/admin/aws.js +0 -13
- package/dist/form/admin/aws.js.map +0 -1
- package/dist/form/admin/github.d.ts +0 -2
- package/dist/form/admin/github.js +0 -15
- package/dist/form/admin/github.js.map +0 -1
- package/dist/form/admin/google.d.ts +0 -2
- package/dist/form/admin/google.js +0 -13
- package/dist/form/admin/google.js.map +0 -1
- package/dist/form/admin/mail.d.ts +0 -2
- package/dist/form/admin/mail.js +0 -13
- package/dist/form/admin/mail.js.map +0 -1
- package/dist/form/admin/sec.d.ts +0 -2
- package/dist/form/admin/sec.js +0 -10
- package/dist/form/admin/sec.js.map +0 -1
- package/dist/form/admin/slackSetting.d.ts +0 -2
- package/dist/form/admin/slackSetting.js +0 -13
- package/dist/form/admin/slackSetting.js.map +0 -1
- package/dist/form/admin/userEdit.d.ts +0 -2
- package/dist/form/admin/userEdit.js +0 -9
- package/dist/form/admin/userEdit.js.map +0 -1
- package/dist/form/admin/userInvite.d.ts +0 -2
- package/dist/form/admin/userInvite.js +0 -9
- package/dist/form/admin/userInvite.js.map +0 -1
- package/dist/form/comment.d.ts +0 -2
- package/dist/form/comment.js +0 -9
- package/dist/form/comment.js.map +0 -1
- package/dist/form/index.d.ts +0 -25
- package/dist/form/index.js +0 -48
- package/dist/form/index.js.map +0 -1
- package/dist/form/invited.d.ts +0 -2
- package/dist/form/invited.js +0 -13
- package/dist/form/invited.js.map +0 -1
- package/dist/form/login.d.ts +0 -2
- package/dist/form/login.js +0 -11
- package/dist/form/login.js.map +0 -1
- package/dist/form/me/apiToken.d.ts +0 -2
- package/dist/form/me/apiToken.js +0 -9
- package/dist/form/me/apiToken.js.map +0 -1
- package/dist/form/me/password.d.ts +0 -2
- package/dist/form/me/password.js +0 -11
- package/dist/form/me/password.js.map +0 -1
- package/dist/form/me/user.d.ts +0 -2
- package/dist/form/me/user.js +0 -9
- package/dist/form/me/user.js.map +0 -1
- package/dist/form/register.d.ts +0 -2
- package/dist/form/register.js +0 -13
- package/dist/form/register.js.map +0 -1
- package/dist/form/revision.d.ts +0 -2
- package/dist/form/revision.js +0 -13
- package/dist/form/revision.js.map +0 -1
- package/dist/hono/handlers/admin/share.d.ts +0 -106
- package/dist/hono/handlers/admin/share.js +0 -55
- package/dist/hono/handlers/admin/share.js.map +0 -1
- package/dist/middlewares/accessTokenParser.d.ts +0 -4
- package/dist/middlewares/accessTokenParser.js +0 -29
- package/dist/middlewares/accessTokenParser.js.map +0 -1
- package/dist/middlewares/adminRequired.d.ts +0 -10
- package/dist/middlewares/adminRequired.js +0 -35
- package/dist/middlewares/adminRequired.js.map +0 -1
- package/dist/middlewares/applicationInstalled.d.ts +0 -3
- package/dist/middlewares/applicationInstalled.js +0 -20
- package/dist/middlewares/applicationInstalled.js.map +0 -1
- package/dist/middlewares/applicationNotInstalled.d.ts +0 -3
- package/dist/middlewares/applicationNotInstalled.js +0 -13
- package/dist/middlewares/applicationNotInstalled.js.map +0 -1
- package/dist/middlewares/basicAuth.d.ts +0 -4
- package/dist/middlewares/basicAuth.js +0 -23
- package/dist/middlewares/basicAuth.js.map +0 -1
- package/dist/middlewares/csrfVerify.d.ts +0 -4
- package/dist/middlewares/csrfVerify.js +0 -24
- package/dist/middlewares/csrfVerify.js.map +0 -1
- package/dist/middlewares/encodeSpace.d.ts +0 -3
- package/dist/middlewares/encodeSpace.js +0 -14
- package/dist/middlewares/encodeSpace.js.map +0 -1
- package/dist/middlewares/fileAccessRightOrLoginRequired.d.ts +0 -4
- package/dist/middlewares/fileAccessRightOrLoginRequired.js +0 -29
- package/dist/middlewares/fileAccessRightOrLoginRequired.js.map +0 -1
- package/dist/middlewares/index.d.ts +0 -16
- package/dist/middlewares/index.js +0 -30
- package/dist/middlewares/index.js.map +0 -1
- package/dist/middlewares/jwtAdminRequired.d.ts +0 -8
- package/dist/middlewares/jwtAdminRequired.js +0 -35
- package/dist/middlewares/jwtAdminRequired.js.map +0 -1
- package/dist/middlewares/jwtAuth.d.ts +0 -4
- package/dist/middlewares/jwtAuth.js +0 -104
- package/dist/middlewares/jwtAuth.js.map +0 -1
- package/dist/middlewares/loginChecker.d.ts +0 -4
- package/dist/middlewares/loginChecker.js +0 -32
- package/dist/middlewares/loginChecker.js.map +0 -1
- package/dist/middlewares/loginRequired.d.ts +0 -4
- package/dist/middlewares/loginRequired.js +0 -88
- package/dist/middlewares/loginRequired.js.map +0 -1
- package/dist/routes/admin.d.ts +0 -4
- package/dist/routes/admin.js +0 -17
- package/dist/routes/admin.js.map +0 -1
- package/dist/routes/api/admin.d.ts +0 -4
- package/dist/routes/api/admin.js +0 -37
- package/dist/routes/api/admin.js.map +0 -1
- package/dist/routes/api/attachment.d.ts +0 -4
- package/dist/routes/api/attachment.js +0 -19
- package/dist/routes/api/attachment.js.map +0 -1
- package/dist/routes/api/bookmark.d.ts +0 -4
- package/dist/routes/api/bookmark.js +0 -15
- package/dist/routes/api/bookmark.js.map +0 -1
- package/dist/routes/api/comment.d.ts +0 -4
- package/dist/routes/api/comment.js +0 -14
- package/dist/routes/api/comment.js.map +0 -1
- package/dist/routes/api/index.d.ts +0 -4
- package/dist/routes/api/index.js +0 -36
- package/dist/routes/api/index.js.map +0 -1
- package/dist/routes/api/like.d.ts +0 -4
- package/dist/routes/api/like.js +0 -13
- package/dist/routes/api/like.js.map +0 -1
- package/dist/routes/api/notification.d.ts +0 -4
- package/dist/routes/api/notification.js +0 -15
- package/dist/routes/api/notification.js.map +0 -1
- package/dist/routes/api/page.d.ts +0 -4
- package/dist/routes/api/page.js +0 -24
- package/dist/routes/api/page.js.map +0 -1
- package/dist/routes/api/revision.d.ts +0 -4
- package/dist/routes/api/revision.js +0 -14
- package/dist/routes/api/revision.js.map +0 -1
- package/dist/routes/api/share.d.ts +0 -4
- package/dist/routes/api/share.js +0 -16
- package/dist/routes/api/share.js.map +0 -1
- package/dist/routes/api/version.d.ts +0 -4
- package/dist/routes/api/version.js +0 -10
- package/dist/routes/api/version.js.map +0 -1
- package/dist/routes/index.d.ts +0 -4
- package/dist/routes/index.js +0 -71
- package/dist/routes/index.js.map +0 -1
- package/dist/routes/login.d.ts +0 -4
- package/dist/routes/login.js +0 -18
- package/dist/routes/login.js.map +0 -1
- package/dist/routes/me.d.ts +0 -4
- package/dist/routes/me.js +0 -24
- package/dist/routes/me.js.map +0 -1
- package/dist/routes/ts-rest/admin/app.d.ts +0 -4
- package/dist/routes/ts-rest/admin/app.js +0 -67
- package/dist/routes/ts-rest/admin/app.js.map +0 -1
- package/dist/routes/ts-rest/admin/auth.d.ts +0 -4
- package/dist/routes/ts-rest/admin/auth.js +0 -95
- package/dist/routes/ts-rest/admin/auth.js.map +0 -1
- package/dist/routes/ts-rest/admin/index.d.ts +0 -10
- package/dist/routes/ts-rest/admin/index.js +0 -35
- package/dist/routes/ts-rest/admin/index.js.map +0 -1
- package/dist/routes/ts-rest/admin/mail.d.ts +0 -4
- package/dist/routes/ts-rest/admin/mail.js +0 -156
- package/dist/routes/ts-rest/admin/mail.js.map +0 -1
- package/dist/routes/ts-rest/admin/plugins.d.ts +0 -4
- package/dist/routes/ts-rest/admin/plugins.js +0 -317
- package/dist/routes/ts-rest/admin/plugins.js.map +0 -1
- package/dist/routes/ts-rest/admin/search.d.ts +0 -4
- package/dist/routes/ts-rest/admin/search.js +0 -67
- package/dist/routes/ts-rest/admin/search.js.map +0 -1
- package/dist/routes/ts-rest/admin/security.d.ts +0 -4
- package/dist/routes/ts-rest/admin/security.js +0 -114
- package/dist/routes/ts-rest/admin/security.js.map +0 -1
- package/dist/routes/ts-rest/admin/share.d.ts +0 -4
- package/dist/routes/ts-rest/admin/share.js +0 -69
- package/dist/routes/ts-rest/admin/share.js.map +0 -1
- package/dist/routes/ts-rest/admin/storage.d.ts +0 -4
- package/dist/routes/ts-rest/admin/storage.js +0 -59
- package/dist/routes/ts-rest/admin/storage.js.map +0 -1
- package/dist/routes/ts-rest/admin/users.d.ts +0 -4
- package/dist/routes/ts-rest/admin/users.js +0 -215
- package/dist/routes/ts-rest/admin/users.js.map +0 -1
- package/dist/routes/ts-rest/adminCrypto.d.ts +0 -4
- package/dist/routes/ts-rest/adminCrypto.js +0 -111
- package/dist/routes/ts-rest/adminCrypto.js.map +0 -1
- package/dist/routes/ts-rest/app.d.ts +0 -4
- package/dist/routes/ts-rest/app.js +0 -23
- package/dist/routes/ts-rest/app.js.map +0 -1
- package/dist/routes/ts-rest/attachment.d.ts +0 -4
- package/dist/routes/ts-rest/attachment.js +0 -830
- package/dist/routes/ts-rest/attachment.js.map +0 -1
- package/dist/routes/ts-rest/auth.d.ts +0 -4
- package/dist/routes/ts-rest/auth.js +0 -70
- package/dist/routes/ts-rest/auth.js.map +0 -1
- package/dist/routes/ts-rest/autocomplete.d.ts +0 -30
- package/dist/routes/ts-rest/autocomplete.js +0 -189
- package/dist/routes/ts-rest/autocomplete.js.map +0 -1
- package/dist/routes/ts-rest/backlink.d.ts +0 -4
- package/dist/routes/ts-rest/backlink.js +0 -106
- package/dist/routes/ts-rest/backlink.js.map +0 -1
- package/dist/routes/ts-rest/bookmark.d.ts +0 -4
- package/dist/routes/ts-rest/bookmark.js +0 -189
- package/dist/routes/ts-rest/bookmark.js.map +0 -1
- package/dist/routes/ts-rest/comment.d.ts +0 -4
- package/dist/routes/ts-rest/comment.js +0 -217
- package/dist/routes/ts-rest/comment.js.map +0 -1
- package/dist/routes/ts-rest/draft.d.ts +0 -22
- package/dist/routes/ts-rest/draft.js +0 -200
- package/dist/routes/ts-rest/draft.js.map +0 -1
- package/dist/routes/ts-rest/index.d.ts +0 -4
- package/dist/routes/ts-rest/index.js +0 -103
- package/dist/routes/ts-rest/index.js.map +0 -1
- package/dist/routes/ts-rest/installer.d.ts +0 -4
- package/dist/routes/ts-rest/installer.js +0 -77
- package/dist/routes/ts-rest/installer.js.map +0 -1
- package/dist/routes/ts-rest/me.d.ts +0 -4
- package/dist/routes/ts-rest/me.js +0 -410
- package/dist/routes/ts-rest/me.js.map +0 -1
- package/dist/routes/ts-rest/notification.d.ts +0 -4
- package/dist/routes/ts-rest/notification.js +0 -241
- package/dist/routes/ts-rest/notification.js.map +0 -1
- package/dist/routes/ts-rest/page-collab.d.ts +0 -29
- package/dist/routes/ts-rest/page-collab.js +0 -90
- package/dist/routes/ts-rest/page-collab.js.map +0 -1
- package/dist/routes/ts-rest/page-preview.d.ts +0 -26
- package/dist/routes/ts-rest/page-preview.js +0 -80
- package/dist/routes/ts-rest/page-preview.js.map +0 -1
- package/dist/routes/ts-rest/page.d.ts +0 -4
- package/dist/routes/ts-rest/page.js +0 -676
- package/dist/routes/ts-rest/page.js.map +0 -1
- package/dist/routes/ts-rest/presence.d.ts +0 -30
- package/dist/routes/ts-rest/presence.js +0 -155
- package/dist/routes/ts-rest/presence.js.map +0 -1
- package/dist/routes/ts-rest/revision.d.ts +0 -4
- package/dist/routes/ts-rest/revision.js +0 -240
- package/dist/routes/ts-rest/revision.js.map +0 -1
- package/dist/routes/ts-rest/search.d.ts +0 -4
- package/dist/routes/ts-rest/search.js +0 -121
- package/dist/routes/ts-rest/search.js.map +0 -1
- package/dist/routes/ts-rest/tokenAuth.d.ts +0 -4
- package/dist/routes/ts-rest/tokenAuth.js +0 -94
- package/dist/routes/ts-rest/tokenAuth.js.map +0 -1
- package/dist/routes/ts-rest/user.d.ts +0 -4
- package/dist/routes/ts-rest/user.js +0 -307
- package/dist/routes/ts-rest/user.js.map +0 -1
- package/dist/types/express.d.ts +0 -34
- package/dist/types/express.js +0 -50
- package/dist/types/express.js.map +0 -1
- package/dist/util/accessTokenParser.d.ts +0 -1
- package/dist/util/accessTokenParser.js +0 -34
- package/dist/util/accessTokenParser.js.map +0 -1
- package/dist/util/apiPaginate.d.ts +0 -11
- package/dist/util/apiPaginate.js +0 -33
- package/dist/util/apiPaginate.js.map +0 -1
- package/dist/util/apiResponse.d.ts +0 -9
- package/dist/util/apiResponse.js +0 -23
- package/dist/util/apiResponse.js.map +0 -1
- package/dist/util/auth.d.ts +0 -11
- package/dist/util/auth.js +0 -48
- package/dist/util/auth.js.map +0 -1
- package/dist/util/aws-config-migration.d.ts +0 -11
- package/dist/util/aws-config-migration.js +0 -68
- package/dist/util/aws-config-migration.js.map +0 -1
- package/dist/util/formUtil.d.ts +0 -2
- package/dist/util/formUtil.js +0 -15
- package/dist/util/formUtil.js.map +0 -1
- package/dist/util/githubAuth.d.ts +0 -2
- package/dist/util/githubAuth.js +0 -82
- package/dist/util/githubAuth.js.map +0 -1
- package/dist/util/googleAuth.d.ts +0 -2
- package/dist/util/googleAuth.js +0 -85
- package/dist/util/googleAuth.js.map +0 -1
- package/dist/util/mailer.d.ts +0 -7
- package/dist/util/mailer.js +0 -98
- package/dist/util/mailer.js.map +0 -1
- package/dist/util/page-status-migration.d.ts +0 -23
- package/dist/util/page-status-migration.js +0 -48
- package/dist/util/page-status-migration.js.map +0 -1
- package/dist/util/ssr.d.ts +0 -3
- package/dist/util/ssr.js +0 -9
- package/dist/util/ssr.js.map +0 -1
- package/dist/util/view.d.ts +0 -10
- package/dist/util/view.js +0 -99
- package/dist/util/view.js.map +0 -1
|
@@ -1,830 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const express_1 = require("@ts-rest/express");
|
|
7
|
-
const api_contract_1 = require("@crowi/api-contract");
|
|
8
|
-
const express_2 = require("express");
|
|
9
|
-
const multer_1 = __importDefault(require("multer"));
|
|
10
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
-
const mongoose_1 = require("mongoose");
|
|
13
|
-
const debug_1 = __importDefault(require("debug"));
|
|
14
|
-
const fileUploader_1 = __importDefault(require("../../util/fileUploader"));
|
|
15
|
-
const rate_limit_1 = require("../../util/rate-limit");
|
|
16
|
-
const ts_rest_helpers_1 = require("../../util/ts-rest-helpers");
|
|
17
|
-
const debug = (0, debug_1.default)('crowi:routes:ts-rest:attachment');
|
|
18
|
-
/**
|
|
19
|
-
* RFC-0004 Phase 6/7 — intent-aware limits for `POST /api/v2/attachments/upload`.
|
|
20
|
-
*
|
|
21
|
-
* The endpoint serves two editor intents with different ceilings:
|
|
22
|
-
* - `paste` (RFC §"Image paste limits"): 10 MB, images only — a
|
|
23
|
-
* clipboard image blob is always an image.
|
|
24
|
-
* - `dnd` (RFC §"D&D limits"): 50 MB, images + documents (`.pdf`,
|
|
25
|
-
* `.txt`, `.md`, `.csv`) + archives (`.zip`).
|
|
26
|
-
*
|
|
27
|
-
* multer is configured with the larger (50 MB) cap so the multipart
|
|
28
|
-
* parse never aborts a legitimate D&D upload; the per-intent size cap
|
|
29
|
-
* is then enforced in-handler once `intent` has been parsed. The
|
|
30
|
-
* per-intent MIME allow-list is likewise applied after the parse.
|
|
31
|
-
*/
|
|
32
|
-
const PASTE_MAX_BYTES = 10 * 1024 * 1024;
|
|
33
|
-
const DND_MAX_BYTES = 50 * 1024 * 1024;
|
|
34
|
-
/** multer-level hard cap — the larger of the two intents (D&D). */
|
|
35
|
-
const UPLOAD_MULTER_MAX_BYTES = DND_MAX_BYTES;
|
|
36
|
-
// MIME allow-lists shared with the web editor via `@crowi/api-contract`
|
|
37
|
-
// so client-side rejection and this authoritative check cannot drift.
|
|
38
|
-
const PASTE_ALLOWED_MIME = new Set(api_contract_1.IMAGE_UPLOAD_MIME);
|
|
39
|
-
const DND_ALLOWED_MIME = new Set([...api_contract_1.IMAGE_UPLOAD_MIME, ...api_contract_1.DND_EXTRA_UPLOAD_MIME]);
|
|
40
|
-
/** Resolve the size cap + MIME allow-list for one upload intent. */
|
|
41
|
-
const limitsForIntent = (intent) => intent === 'dnd' ? { maxBytes: DND_MAX_BYTES, allowedMime: DND_ALLOWED_MIME } : { maxBytes: PASTE_MAX_BYTES, allowedMime: PASTE_ALLOWED_MIME };
|
|
42
|
-
/** Per-user budget for the editor upload endpoint — RFC §"Attachment upload endpoint". */
|
|
43
|
-
const UPLOAD_RATE_LIMIT = 20;
|
|
44
|
-
const UPLOAD_RATE_WINDOW_MS = 60_000;
|
|
45
|
-
/**
|
|
46
|
-
* Mime types we allow over the public `by-key` route. The route is intended
|
|
47
|
-
* for profile pictures only (key prefix `user/`); image/* covers every
|
|
48
|
-
* format `User.createUserPictureFilePath` can produce.
|
|
49
|
-
*/
|
|
50
|
-
const BY_KEY_ALLOWED_PREFIX = 'user/';
|
|
51
|
-
const KEY_EXT_TO_MIME = {
|
|
52
|
-
png: 'image/png',
|
|
53
|
-
jpg: 'image/jpeg',
|
|
54
|
-
jpeg: 'image/jpeg',
|
|
55
|
-
gif: 'image/gif',
|
|
56
|
-
webp: 'image/webp',
|
|
57
|
-
svg: 'image/svg+xml',
|
|
58
|
-
bmp: 'image/bmp',
|
|
59
|
-
};
|
|
60
|
-
const guessMimeFromKey = (key) => {
|
|
61
|
-
const m = key.match(/\.([^.]+)$/);
|
|
62
|
-
if (!m)
|
|
63
|
-
return 'application/octet-stream';
|
|
64
|
-
return KEY_EXT_TO_MIME[m[1].toLowerCase()] || 'application/octet-stream';
|
|
65
|
-
};
|
|
66
|
-
const errorBody = (code, message) => ({ error: { code, message } });
|
|
67
|
-
/**
|
|
68
|
-
* Lowercase RFC-0004 error envelope for `POST /attachments/upload`. Kept
|
|
69
|
-
* separate from `errorBody` because the upload endpoint's error codes
|
|
70
|
-
* are lowercase + RFC-pinned (the editor maps each to a specific toast),
|
|
71
|
-
* distinct from the uppercase codes of the list / add / delete routes.
|
|
72
|
-
*/
|
|
73
|
-
const uploadErrorBody = (error, message, details) => ({
|
|
74
|
-
error,
|
|
75
|
-
message,
|
|
76
|
-
...(details ? { details } : {}),
|
|
77
|
-
});
|
|
78
|
-
const invalidPageIdResponse = {
|
|
79
|
-
status: 400,
|
|
80
|
-
body: errorBody('INVALID_PAGE_ID', 'Invalid pageId'),
|
|
81
|
-
};
|
|
82
|
-
const pageNotFoundResponse = {
|
|
83
|
-
status: 404,
|
|
84
|
-
body: errorBody('PAGE_NOT_FOUND', 'Page not found'),
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* Phase 7 — extract the set of attachment ObjectId hex strings referenced by
|
|
88
|
-
* a revision body. We scan the raw Markdown source (not the rendered AST)
|
|
89
|
-
* because embed URLs appear verbatim in the source. Two URI forms are
|
|
90
|
-
* matched: the current `/api/v2/attachments/<id>` (the `fileUrl` virtual /
|
|
91
|
-
* stream route) and the legacy `/files/<id>` form still present in bodies
|
|
92
|
-
* saved before the migration. Ids are lower-cased for a defensive,
|
|
93
|
-
* case-insensitive `Set` lookup against `attachment._id.toString()`.
|
|
94
|
-
*/
|
|
95
|
-
const ATTACHMENT_URI_RE = /(?:\/api\/v2\/attachments\/|\/files\/)([0-9a-f]{24})/gi;
|
|
96
|
-
const collectReferencedAttachmentIds = (body) => {
|
|
97
|
-
const ids = new Set();
|
|
98
|
-
for (const match of body.matchAll(ATTACHMENT_URI_RE)) {
|
|
99
|
-
ids.add(match[1].toLowerCase());
|
|
100
|
-
}
|
|
101
|
-
return ids;
|
|
102
|
-
};
|
|
103
|
-
/**
|
|
104
|
-
* Convert an AttachmentDocument (with optional populated `creator`) into the
|
|
105
|
-
* wire response. The model's `fileUrl` virtual returns
|
|
106
|
-
* `/api/v2/attachments/:id` after this migration, so we surface that as
|
|
107
|
-
* `url`.
|
|
108
|
-
*
|
|
109
|
-
* `inUse` (Phase 7) is supplied by the caller: `listAttachments` derives it
|
|
110
|
-
* from the latest revision body scan, while `addAttachment` passes `false`
|
|
111
|
-
* because a just-uploaded file is not yet referenced in the body.
|
|
112
|
-
*/
|
|
113
|
-
const attachmentToResponse = (attachment, inUse) => {
|
|
114
|
-
// Re-read off a JSON-serialized clone so populated subdocs (creator) come
|
|
115
|
-
// through plainly. attachmentSchema has `toJSON: { virtuals: true }` so the
|
|
116
|
-
// `fileUrl` virtual is included automatically.
|
|
117
|
-
const obj = attachment.toJSON();
|
|
118
|
-
const creator = obj.creator;
|
|
119
|
-
const creatorPublic = (0, ts_rest_helpers_1.isPopulatedUser)(creator)
|
|
120
|
-
? (0, ts_rest_helpers_1.toUserPublic)(creator)
|
|
121
|
-
: // Fallback when creator is unpopulated (shouldn't happen on our paths
|
|
122
|
-
// because list / add both populate, but the schema requires the public
|
|
123
|
-
// shape so we synthesize the minimum surface).
|
|
124
|
-
(0, ts_rest_helpers_1.toUserPublic)({ _id: creator ? (0, ts_rest_helpers_1.toStringId)(creator) : '' });
|
|
125
|
-
return {
|
|
126
|
-
_id: (0, ts_rest_helpers_1.toStringId)(obj._id),
|
|
127
|
-
page: (0, ts_rest_helpers_1.toStringId)(obj.page),
|
|
128
|
-
creator: creatorPublic,
|
|
129
|
-
filePath: obj.filePath,
|
|
130
|
-
fileName: obj.fileName,
|
|
131
|
-
originalName: obj.originalName ?? '',
|
|
132
|
-
fileFormat: obj.fileFormat,
|
|
133
|
-
fileSize: obj.fileSize,
|
|
134
|
-
createdAt: (0, ts_rest_helpers_1.toISOStringOrNull)(obj.createdAt) ?? new Date(0).toISOString(),
|
|
135
|
-
url: obj.fileUrl,
|
|
136
|
-
inUse,
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* The `AttachmentMeta` projection (`AttachmentSchema` minus `inUse`) used by
|
|
141
|
-
* `GET /attachments/:id/meta`. `inUse` is a page-scoped derivation; the meta
|
|
142
|
-
* endpoint resolves a bare id and has no page context, so the field is
|
|
143
|
-
* dropped rather than faked. Reuses `attachmentToResponse` and strips the
|
|
144
|
-
* flag so the two stay in sync.
|
|
145
|
-
*/
|
|
146
|
-
const attachmentToMetaResponse = (attachment) => {
|
|
147
|
-
const { inUse: _inUse, ...meta } = attachmentToResponse(attachment, false);
|
|
148
|
-
return meta;
|
|
149
|
-
};
|
|
150
|
-
exports.default = (crowi, _app) => {
|
|
151
|
-
const s = (0, express_1.initServer)();
|
|
152
|
-
const router = (0, express_2.Router)();
|
|
153
|
-
const Attachment = crowi.model('Attachment');
|
|
154
|
-
const Page = crowi.model('Page');
|
|
155
|
-
const fileUploader = (0, fileUploader_1.default)(crowi);
|
|
156
|
-
const upload = (0, multer_1.default)({ dest: crowi.tmpDir });
|
|
157
|
-
/** Absolute path to the placeholder image served when an attachment is gone. */
|
|
158
|
-
const FILE_NOT_FOUND_IMAGE = node_path_1.default.join(crowi.publicDir, 'images', 'file-not-found.png');
|
|
159
|
-
/**
|
|
160
|
-
* Stream the `file-not-found.png` placeholder as a `200 image/png` response.
|
|
161
|
-
*
|
|
162
|
-
* Phase 3 — used by `GET /api/v2/attachments/:id` when the attachment record
|
|
163
|
-
* is missing OR its backing object is gone from storage. We deliberately
|
|
164
|
-
* return `200` (not `404`) so an embedded `<img>` in a wiki page renders the
|
|
165
|
-
* placeholder inline instead of a broken-image glyph. No `Content-Disposition`
|
|
166
|
-
* is set so the image displays inline.
|
|
167
|
-
*/
|
|
168
|
-
const servePlaceholder = (res) => {
|
|
169
|
-
const stream = node_fs_1.default.createReadStream(FILE_NOT_FOUND_IMAGE);
|
|
170
|
-
stream.on('error', (err) => {
|
|
171
|
-
debug('placeholder stream error', err);
|
|
172
|
-
if (!res.headersSent) {
|
|
173
|
-
res.status(500).end();
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
res.end();
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
res.status(200).setHeader('Content-Type', 'image/png');
|
|
180
|
-
stream.pipe(res);
|
|
181
|
-
};
|
|
182
|
-
/**
|
|
183
|
-
* Whether a storage-driver `get()` rejection means the object is simply
|
|
184
|
-
* missing (as opposed to a real failure). The local driver throws
|
|
185
|
-
* `code: 'ENOENT'`; the S3 driver surfaces a missing object as the AWS SDK
|
|
186
|
-
* v3 `NoSuchKey` error (`$metadata.httpStatusCode === 404`, no `code`).
|
|
187
|
-
*/
|
|
188
|
-
const isMissingFileError = (err) => {
|
|
189
|
-
const e = err;
|
|
190
|
-
return e.code === 'ENOENT' || e.name === 'NoSuchKey' || e.$metadata?.httpStatusCode === 404;
|
|
191
|
-
};
|
|
192
|
-
// RFC-0004 Phase 6/7 — the editor-upload endpoint caps multer at the
|
|
193
|
-
// larger D&D ceiling (50 MB) so a legitimate drag-and-drop upload is
|
|
194
|
-
// never aborted mid-parse. The per-intent cap (paste 10 MB / dnd
|
|
195
|
-
// 50 MB) is then enforced in-handler once `intent` has been parsed.
|
|
196
|
-
const editorUpload = (0, multer_1.default)({ dest: crowi.tmpDir, limits: { fileSize: UPLOAD_MULTER_MAX_BYTES } });
|
|
197
|
-
// One shared upload limiter per process. `crowi.redis` is `null` in
|
|
198
|
-
// single-instance dev, which selects the in-memory fallback.
|
|
199
|
-
const uploadLimiter = (0, rate_limit_1.createRateLimiter)({
|
|
200
|
-
name: 'attachment-upload',
|
|
201
|
-
limit: UPLOAD_RATE_LIMIT,
|
|
202
|
-
windowMs: UPLOAD_RATE_WINDOW_MS,
|
|
203
|
-
redisClient: crowi.redis ?? null,
|
|
204
|
-
});
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
// Raw Express endpoints (registered BEFORE createExpressEndpoints so they
|
|
207
|
-
// are matched before ts-rest's path matcher attempts to dispatch).
|
|
208
|
-
//
|
|
209
|
-
// These deliver bytes via Readable.pipe(), which ts-rest's "return body"
|
|
210
|
-
// handler model cannot express without buffering the entire file. Authn
|
|
211
|
-
// is already provided by the parent authenticatedRouter (jwtAuth).
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
/**
|
|
214
|
-
* GET /api/v2/attachments/by-key/:key(*)
|
|
215
|
-
*
|
|
216
|
-
* Streams a stored object identified by storage key. To prevent the route
|
|
217
|
-
* from acting as an arbitrary read primitive, we whitelist the `user/`
|
|
218
|
-
* prefix only — these are profile pictures whose URL is computed by
|
|
219
|
-
* `fileUploader.generateUrl` and stored in `user.image`. Attachment-row
|
|
220
|
-
* backed files are served by `/api/v2/attachments/:id` via grant checks.
|
|
221
|
-
*/
|
|
222
|
-
router.get('/attachments/by-key/*', async (req, res) => {
|
|
223
|
-
const rawKey = req.params[0];
|
|
224
|
-
if (typeof rawKey !== 'string' || rawKey.length === 0) {
|
|
225
|
-
res.status(400).json(errorBody('FILE_MISSING', 'Missing storage key'));
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
let key;
|
|
229
|
-
try {
|
|
230
|
-
key = decodeURIComponent(rawKey);
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
res.status(400).json(errorBody('FILE_MISSING', 'Invalid storage key'));
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
if (!key.startsWith(BY_KEY_ALLOWED_PREFIX)) {
|
|
237
|
-
res.status(403).json(errorBody('FORBIDDEN_FOR_DELETE', 'Storage key not permitted by this endpoint'));
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
let stream;
|
|
241
|
-
try {
|
|
242
|
-
stream = await fileUploader.findDeliveryFile(null, key);
|
|
243
|
-
}
|
|
244
|
-
catch (err) {
|
|
245
|
-
const e = err;
|
|
246
|
-
if (e.code === 'ENOENT') {
|
|
247
|
-
res.status(404).json(errorBody('ATTACHMENT_NOT_FOUND', 'File not found'));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
debug('by-key delivery error', err);
|
|
251
|
-
res.status(500).json(errorBody('UPLOAD_FAILED', 'Failed to deliver file'));
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
res.setHeader('Content-Type', guessMimeFromKey(key));
|
|
255
|
-
stream.on('error', (err) => {
|
|
256
|
-
debug('by-key stream error', err);
|
|
257
|
-
if (!res.headersSent) {
|
|
258
|
-
res.status(500).end();
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
res.end();
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
stream.pipe(res);
|
|
265
|
-
});
|
|
266
|
-
/**
|
|
267
|
-
* GET /api/v2/attachments/:id
|
|
268
|
-
*
|
|
269
|
-
* Streams an attachment by Mongo ObjectId. Authorization: the caller must
|
|
270
|
-
* be able to view the page that owns the attachment (loadGrantedPage).
|
|
271
|
-
* 404 (not 403) when grant fails to avoid leaking the existence of a
|
|
272
|
-
* page the caller cannot view.
|
|
273
|
-
*/
|
|
274
|
-
router.get('/attachments/:id([0-9a-f]{24})', async (req, res) => {
|
|
275
|
-
const user = req.user;
|
|
276
|
-
if (!user) {
|
|
277
|
-
res.status(401).json({ error: { code: 'AUTHENTICATION_REQUIRED', message: 'Authentication is required' } });
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const id = req.params.id;
|
|
281
|
-
if (!(0, ts_rest_helpers_1.isValidObjectId)(id)) {
|
|
282
|
-
res.status(400).json(errorBody('INVALID_ATTACHMENT_ID', 'Invalid attachment id'));
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
let attachment;
|
|
286
|
-
try {
|
|
287
|
-
attachment = (await Attachment.findById(id));
|
|
288
|
-
}
|
|
289
|
-
catch (err) {
|
|
290
|
-
debug('attachment lookup error', err);
|
|
291
|
-
res.status(500).json(errorBody('UPLOAD_FAILED', 'Failed to load attachment'));
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (!attachment) {
|
|
295
|
-
// Phase 3 — a missing attachment record means the file was deleted or
|
|
296
|
-
// never existed; serve the placeholder image instead of a 404 so
|
|
297
|
-
// embedded references render gracefully.
|
|
298
|
-
servePlaceholder(res);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
const grant = await (0, ts_rest_helpers_1.loadGrantedPage)(Page, attachment.page.toString(), user);
|
|
302
|
-
if ('error' in grant) {
|
|
303
|
-
// Collapse INVALID_PAGE_ID + PAGE_NOT_FOUND alike to 404 here — the
|
|
304
|
-
// page id comes from the persisted attachment, so an INVALID_PAGE_ID
|
|
305
|
-
// would only mean the document is corrupt.
|
|
306
|
-
res.status(404).json(errorBody('ATTACHMENT_NOT_FOUND', 'Attachment not found'));
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
let stream;
|
|
310
|
-
try {
|
|
311
|
-
stream = await Attachment.findDeliveryFile(attachment);
|
|
312
|
-
}
|
|
313
|
-
catch (err) {
|
|
314
|
-
// Phase 3 — the record exists but the backing object is gone from
|
|
315
|
-
// storage (local `ENOENT` / S3 `NoSuchKey`). Serve the placeholder
|
|
316
|
-
// image so embedded references render gracefully. Any other driver
|
|
317
|
-
// error is a genuine failure → 500.
|
|
318
|
-
if (isMissingFileError(err)) {
|
|
319
|
-
servePlaceholder(res);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
debug('attachment delivery error', err);
|
|
323
|
-
res.status(500).json(errorBody('UPLOAD_FAILED', 'Failed to deliver file'));
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
res.setHeader('Content-Type', attachment.fileFormat);
|
|
327
|
-
res.setHeader('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(attachment.originalName || attachment.fileName)}`);
|
|
328
|
-
stream.on('error', (err) => {
|
|
329
|
-
debug('attachment stream error', err);
|
|
330
|
-
if (!res.headersSent) {
|
|
331
|
-
res.status(500).end();
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
res.end();
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
stream.pipe(res);
|
|
338
|
-
});
|
|
339
|
-
// ---------------------------------------------------------------------------
|
|
340
|
-
// ts-rest contract handlers
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
|
-
const attachmentRouter = s.router(api_contract_1.apiContract.attachment, {
|
|
343
|
-
/**
|
|
344
|
-
* GET /api/v2/pages/:pageId/attachments
|
|
345
|
-
*/
|
|
346
|
-
listAttachments: async ({ params, req }) => {
|
|
347
|
-
const user = req.user;
|
|
348
|
-
const { pageId } = params;
|
|
349
|
-
const grant = await (0, ts_rest_helpers_1.loadGrantedPage)(Page, pageId, user);
|
|
350
|
-
if ('error' in grant) {
|
|
351
|
-
// Collapse 400 / 404 into the contract-typed shape.
|
|
352
|
-
if (grant.error.status === 400) {
|
|
353
|
-
return invalidPageIdResponse;
|
|
354
|
-
}
|
|
355
|
-
return pageNotFoundResponse;
|
|
356
|
-
}
|
|
357
|
-
try {
|
|
358
|
-
const attachments = (await Attachment.getListByPageId(new mongoose_1.Types.ObjectId(pageId)));
|
|
359
|
-
// Phase 7 — derive `inUse` from the page's latest revision body. The
|
|
360
|
-
// page is already loaded via `loadGrantedPage` (with no revisionId,
|
|
361
|
-
// so `grant.page.revision` is the latest revision). We read just the
|
|
362
|
-
// body and scan it once for attachment URIs. If the revision is
|
|
363
|
-
// missing or its body is empty we cannot determine references, so we
|
|
364
|
-
// fall back to `inUse: true` for every attachment rather than hide
|
|
365
|
-
// files while the reference state is undetermined.
|
|
366
|
-
const revisionId = grant.page.revision;
|
|
367
|
-
let referencedIds = null;
|
|
368
|
-
if (revisionId) {
|
|
369
|
-
const Revision = crowi.model('Revision');
|
|
370
|
-
const revision = (await Revision.findById(revisionId).select('body'));
|
|
371
|
-
if (revision?.body) {
|
|
372
|
-
referencedIds = collectReferencedAttachmentIds(revision.body);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return {
|
|
376
|
-
status: 200,
|
|
377
|
-
body: {
|
|
378
|
-
attachments: attachments.map((a) => attachmentToResponse(a, referencedIds === null ? true : referencedIds.has(a._id.toString().toLowerCase()))),
|
|
379
|
-
},
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
catch (err) {
|
|
383
|
-
debug('listAttachments error', err);
|
|
384
|
-
return ts_rest_helpers_1.internalServerErrorResponse;
|
|
385
|
-
}
|
|
386
|
-
},
|
|
387
|
-
/**
|
|
388
|
-
* GET /api/v2/pages/:pageId/attachments/usage
|
|
389
|
-
*
|
|
390
|
-
* Phase 8 — full attachment usage breakdown for a page. Scans every
|
|
391
|
-
* revision body of the page (via the `path` key) for attachment embed
|
|
392
|
-
* URIs and splits the page's attachments into:
|
|
393
|
-
* - `latest`: referenced by the page's current revision body.
|
|
394
|
-
* - `past`: referenced only by older revisions (plus orphans referenced
|
|
395
|
-
* by none), each carrying the revisions that used it.
|
|
396
|
-
*
|
|
397
|
-
* On-demand (no caching) — `/_attachments` is a deliberate navigation,
|
|
398
|
-
* not a hot path. The revision query deliberately omits `renderedAst`
|
|
399
|
-
* (multi-MB per page); only `body` is needed for the scan.
|
|
400
|
-
*/
|
|
401
|
-
getAttachmentUsage: async ({ params, req }) => {
|
|
402
|
-
const user = req.user;
|
|
403
|
-
const { pageId } = params;
|
|
404
|
-
const grant = await (0, ts_rest_helpers_1.loadGrantedPage)(Page, pageId, user);
|
|
405
|
-
if ('error' in grant) {
|
|
406
|
-
if (grant.error.status === 400) {
|
|
407
|
-
return invalidPageIdResponse;
|
|
408
|
-
}
|
|
409
|
-
return pageNotFoundResponse;
|
|
410
|
-
}
|
|
411
|
-
const page = grant.page;
|
|
412
|
-
try {
|
|
413
|
-
const Revision = crowi.model('Revision');
|
|
414
|
-
// All revisions of the page, newest-first. `author` is populated for
|
|
415
|
-
// the past-revision link rendering; `renderedAst` is intentionally
|
|
416
|
-
// excluded — it is heavy and the scan only needs the raw body.
|
|
417
|
-
const revisions = (await Revision.find({ path: page.path }).select('_id body createdAt author').sort({ createdAt: -1 }).populate('author'));
|
|
418
|
-
// `page.revision` may be a bare ObjectId or a populated Revision
|
|
419
|
-
// document (findPageById populates it). Normalise to the hex id.
|
|
420
|
-
const rawRevision = page.revision;
|
|
421
|
-
const latestRevisionId = rawRevision == null
|
|
422
|
-
? null
|
|
423
|
-
: typeof rawRevision === 'object' && rawRevision !== null && '_id' in rawRevision
|
|
424
|
-
? (0, ts_rest_helpers_1.toStringId)(rawRevision._id)
|
|
425
|
-
: (0, ts_rest_helpers_1.toStringId)(rawRevision);
|
|
426
|
-
// Per-revision referenced-id sets, plus the aggregate of which past
|
|
427
|
-
// (non-latest) revisions reference each attachment id.
|
|
428
|
-
let latestIds = new Set();
|
|
429
|
-
const referencedByPast = new Map();
|
|
430
|
-
for (const revision of revisions) {
|
|
431
|
-
const ids = revision.body ? collectReferencedAttachmentIds(revision.body) : new Set();
|
|
432
|
-
const isLatest = latestRevisionId !== null && revision._id.toString() === latestRevisionId;
|
|
433
|
-
if (isLatest) {
|
|
434
|
-
latestIds = ids;
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
for (const id of ids) {
|
|
438
|
-
const list = referencedByPast.get(id) ?? [];
|
|
439
|
-
list.push({
|
|
440
|
-
revisionId: revision._id.toString(),
|
|
441
|
-
createdAt: (0, ts_rest_helpers_1.toISOStringOrNull)(revision.createdAt) ?? new Date(0).toISOString(),
|
|
442
|
-
author: revision.author ?? null,
|
|
443
|
-
});
|
|
444
|
-
referencedByPast.set(id, list);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
const attachments = (await Attachment.getListByPageId(new mongoose_1.Types.ObjectId(pageId)));
|
|
448
|
-
const latest = [];
|
|
449
|
-
const past = [];
|
|
450
|
-
for (const att of attachments) {
|
|
451
|
-
const attId = att._id.toString().toLowerCase();
|
|
452
|
-
if (latestIds.has(attId)) {
|
|
453
|
-
latest.push(attachmentToResponse(att, true));
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
// Past-only or orphan (orphan → empty referencingRevisions).
|
|
457
|
-
const refs = referencedByPast.get(attId) ?? [];
|
|
458
|
-
past.push({
|
|
459
|
-
attachment: attachmentToResponse(att, false),
|
|
460
|
-
referencingRevisions: refs.map((r) => ({
|
|
461
|
-
revisionId: r.revisionId,
|
|
462
|
-
createdAt: r.createdAt,
|
|
463
|
-
author: (0, ts_rest_helpers_1.isPopulatedUser)(r.author)
|
|
464
|
-
? (0, ts_rest_helpers_1.toUserPublic)(r.author)
|
|
465
|
-
: (0, ts_rest_helpers_1.toUserPublic)({ _id: r.author ? (0, ts_rest_helpers_1.toStringId)(r.author) : '' }),
|
|
466
|
-
})),
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
return {
|
|
470
|
-
status: 200,
|
|
471
|
-
body: { pagePath: page.path, latest, past },
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
catch (err) {
|
|
475
|
-
debug('getAttachmentUsage error', err);
|
|
476
|
-
return ts_rest_helpers_1.internalServerErrorResponse;
|
|
477
|
-
}
|
|
478
|
-
},
|
|
479
|
-
/**
|
|
480
|
-
* GET /api/v2/attachments/:id/meta
|
|
481
|
-
*
|
|
482
|
-
* Metadata for a single attachment by id — backs the in-body attachment
|
|
483
|
-
* modal. Authorization mirrors the streaming route
|
|
484
|
-
* `GET /api/v2/attachments/:id`: the caller must be able to view the
|
|
485
|
-
* owning page (`loadGrantedPage`). 404 (not 403) on any failure so the
|
|
486
|
-
* existence of a hidden page / attachment is not leaked. Unlike the
|
|
487
|
-
* streaming route there is no placeholder fallback — a missing record is
|
|
488
|
-
* a plain 404 because the JSON consumer (the modal) cannot render an
|
|
489
|
-
* image placeholder.
|
|
490
|
-
*/
|
|
491
|
-
getAttachmentMeta: async ({ params, req }) => {
|
|
492
|
-
const user = req.user;
|
|
493
|
-
const { id } = params;
|
|
494
|
-
if (!(0, ts_rest_helpers_1.isValidObjectId)(id)) {
|
|
495
|
-
return {
|
|
496
|
-
status: 400,
|
|
497
|
-
body: errorBody('INVALID_ATTACHMENT_ID', 'Invalid attachment id'),
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
let attachment;
|
|
501
|
-
try {
|
|
502
|
-
attachment = (await Attachment.findById(id).populate('creator'));
|
|
503
|
-
}
|
|
504
|
-
catch (err) {
|
|
505
|
-
debug('getAttachmentMeta lookup error', err);
|
|
506
|
-
return ts_rest_helpers_1.internalServerErrorResponse;
|
|
507
|
-
}
|
|
508
|
-
if (!attachment) {
|
|
509
|
-
return {
|
|
510
|
-
status: 404,
|
|
511
|
-
body: errorBody('ATTACHMENT_NOT_FOUND', 'Attachment not found'),
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
const grant = await (0, ts_rest_helpers_1.loadGrantedPage)(Page, attachment.page.toString(), user);
|
|
515
|
-
if ('error' in grant) {
|
|
516
|
-
// Collapse INVALID_PAGE_ID + PAGE_NOT_FOUND alike to 404 — the page
|
|
517
|
-
// id comes from the persisted attachment, and a hidden page must not
|
|
518
|
-
// be distinguishable from a missing one.
|
|
519
|
-
return {
|
|
520
|
-
status: 404,
|
|
521
|
-
body: errorBody('ATTACHMENT_NOT_FOUND', 'Attachment not found'),
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
return {
|
|
525
|
-
status: 200,
|
|
526
|
-
body: attachmentToMetaResponse(attachment),
|
|
527
|
-
};
|
|
528
|
-
},
|
|
529
|
-
/**
|
|
530
|
-
* POST /api/v2/pages/:pageId/attachments (multipart/form-data)
|
|
531
|
-
*
|
|
532
|
-
* The legacy controller supported `page_id=0` + path to implicitly create
|
|
533
|
-
* the page. We dropped that — the client must call `createPage` first.
|
|
534
|
-
* This is documented as a semantic change in the task plan.
|
|
535
|
-
*/
|
|
536
|
-
addAttachment: async ({ params, req, res }) => {
|
|
537
|
-
const user = req.user;
|
|
538
|
-
const { pageId } = params;
|
|
539
|
-
const grant = await (0, ts_rest_helpers_1.loadGrantedPage)(Page, pageId, user);
|
|
540
|
-
if ('error' in grant) {
|
|
541
|
-
if (grant.error.status === 400) {
|
|
542
|
-
return invalidPageIdResponse;
|
|
543
|
-
}
|
|
544
|
-
return pageNotFoundResponse;
|
|
545
|
-
}
|
|
546
|
-
const pageData = grant.page;
|
|
547
|
-
// Multer is run inside the handler — same pattern as `me.uploadPicture`.
|
|
548
|
-
// We resolve the ts-rest response from a Promise so the handler can
|
|
549
|
-
// wait for multer's async parse to complete.
|
|
550
|
-
return new Promise((resolve) => {
|
|
551
|
-
upload.single('file')(req, res, async (multerErr) => {
|
|
552
|
-
if (multerErr) {
|
|
553
|
-
debug('multer error', multerErr);
|
|
554
|
-
return resolve({
|
|
555
|
-
status: 400,
|
|
556
|
-
body: errorBody('FILE_MISSING', 'File upload error'),
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
const tmpFile = req.file || null;
|
|
560
|
-
if (!tmpFile) {
|
|
561
|
-
return resolve({
|
|
562
|
-
status: 400,
|
|
563
|
-
body: errorBody('FILE_MISSING', 'No file provided'),
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
const tmpPath = tmpFile.path;
|
|
567
|
-
const cleanupTmp = () => {
|
|
568
|
-
node_fs_1.default.unlink(tmpPath, (unlinkErr) => {
|
|
569
|
-
if (unlinkErr)
|
|
570
|
-
debug('failed to unlink tmp file', unlinkErr);
|
|
571
|
-
});
|
|
572
|
-
};
|
|
573
|
-
const originalName = tmpFile.originalname;
|
|
574
|
-
const fileName = tmpFile.filename + tmpFile.originalname;
|
|
575
|
-
const fileType = tmpFile.mimetype;
|
|
576
|
-
const fileSize = tmpFile.size;
|
|
577
|
-
const creator = user._id;
|
|
578
|
-
try {
|
|
579
|
-
const filePath = Attachment.createAttachmentFilePath(pageData._id, fileName, fileType);
|
|
580
|
-
const tmpFileStream = node_fs_1.default.createReadStream(tmpPath, { flags: 'r', autoClose: true });
|
|
581
|
-
await fileUploader.uploadFile(filePath, fileType, tmpFileStream, {});
|
|
582
|
-
const created = (await Attachment.create({
|
|
583
|
-
page: pageData._id,
|
|
584
|
-
creator,
|
|
585
|
-
filePath,
|
|
586
|
-
originalName,
|
|
587
|
-
fileName,
|
|
588
|
-
fileFormat: fileType,
|
|
589
|
-
fileSize,
|
|
590
|
-
}));
|
|
591
|
-
// Populate `creator` so the response shape matches list output.
|
|
592
|
-
await created.populate('creator');
|
|
593
|
-
cleanupTmp();
|
|
594
|
-
// Phase 7 — a freshly uploaded file is not yet referenced in the
|
|
595
|
-
// page body, so it starts `inUse: false`. The next
|
|
596
|
-
// `listAttachments` recomputes this from the latest revision.
|
|
597
|
-
const body = attachmentToResponse(created, false);
|
|
598
|
-
return resolve({
|
|
599
|
-
status: 200,
|
|
600
|
-
body: { attachment: body, url: body.url },
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
catch (err) {
|
|
604
|
-
debug('attachment upload error', err);
|
|
605
|
-
cleanupTmp();
|
|
606
|
-
return resolve({
|
|
607
|
-
status: 500,
|
|
608
|
-
body: errorBody('UPLOAD_FAILED', 'Failed to save attachment'),
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
});
|
|
613
|
-
},
|
|
614
|
-
/**
|
|
615
|
-
* POST /api/v2/attachments/upload (multipart/form-data)
|
|
616
|
-
*
|
|
617
|
-
* RFC-0004 Phase 6 — direct upload for the editor's paste / D&D
|
|
618
|
-
* handlers. Differs from `addAttachment` in three ways:
|
|
619
|
-
* 1. Rate-limited to 20 uploads/min/user (429 + `Retry-After`).
|
|
620
|
-
* 2. Enforces the editor size (10 MB) + MIME allow-list, returning
|
|
621
|
-
* the RFC's lowercase `{ error, message, details? }` envelope.
|
|
622
|
-
* 3. Returns the lean `{ url, filename, mimeType, sizeBytes }`
|
|
623
|
-
* shape the editor splices straight into the Markdown source.
|
|
624
|
-
*
|
|
625
|
-
* `pageId` / `intent` are multipart text fields parsed by multer
|
|
626
|
-
* (not validated by ts-rest — see the contract comment), so they
|
|
627
|
-
* are validated in-handler after the parse completes. Upload
|
|
628
|
-
* progress is observed entirely client-side via
|
|
629
|
-
* `XMLHttpRequest.upload.onprogress`; the server receives the
|
|
630
|
-
* multipart body with no special streaming protocol.
|
|
631
|
-
*/
|
|
632
|
-
uploadAttachment: async ({ req, res }) => {
|
|
633
|
-
const user = req.user;
|
|
634
|
-
// 1. Rate limit before touching the (potentially large) body.
|
|
635
|
-
const rate = await uploadLimiter.hit(user._id.toString());
|
|
636
|
-
if (!rate.allowed) {
|
|
637
|
-
res.setHeader('Retry-After', String(rate.retryAfterSeconds));
|
|
638
|
-
return {
|
|
639
|
-
status: 429,
|
|
640
|
-
body: uploadErrorBody('rate_limited', `Upload limit reached. Try again in ${rate.retryAfterSeconds} seconds.`, {
|
|
641
|
-
retryAfterSeconds: rate.retryAfterSeconds,
|
|
642
|
-
}),
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
// Multer runs inside the handler (same pattern as `addAttachment`)
|
|
646
|
-
// so the ts-rest response can await the async multipart parse.
|
|
647
|
-
return new Promise((resolve) => {
|
|
648
|
-
editorUpload.single('file')(req, res, async (multerErr) => {
|
|
649
|
-
const tmpFile = req.file || null;
|
|
650
|
-
const cleanupTmp = () => {
|
|
651
|
-
if (!tmpFile)
|
|
652
|
-
return;
|
|
653
|
-
node_fs_1.default.unlink(tmpFile.path, (unlinkErr) => {
|
|
654
|
-
if (unlinkErr)
|
|
655
|
-
debug('failed to unlink tmp file', unlinkErr);
|
|
656
|
-
});
|
|
657
|
-
};
|
|
658
|
-
if (multerErr) {
|
|
659
|
-
cleanupTmp();
|
|
660
|
-
// multer raises `LIMIT_FILE_SIZE` when the body exceeds the
|
|
661
|
-
// configured (50 MB / D&D) cap — surface it as the RFC's
|
|
662
|
-
// `too_large` (413). The per-intent paste cap (10 MB) is a
|
|
663
|
-
// smaller in-handler check below.
|
|
664
|
-
const code = multerErr.code;
|
|
665
|
-
if (code === 'LIMIT_FILE_SIZE') {
|
|
666
|
-
return resolve({
|
|
667
|
-
status: 413,
|
|
668
|
-
body: uploadErrorBody('too_large', 'The file is too large to upload.', { maxBytes: UPLOAD_MULTER_MAX_BYTES }),
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
debug('editor upload multer error', multerErr);
|
|
672
|
-
return resolve({
|
|
673
|
-
status: 400,
|
|
674
|
-
body: uploadErrorBody('disallowed_type', 'File upload error.'),
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
// --- Validate the multipart text fields (multer has parsed them) ---
|
|
678
|
-
const body = req.body;
|
|
679
|
-
const pageId = typeof body.pageId === 'string' ? body.pageId : '';
|
|
680
|
-
const intent = body.intent === 'paste' || body.intent === 'dnd' ? body.intent : null;
|
|
681
|
-
if (!tmpFile) {
|
|
682
|
-
cleanupTmp();
|
|
683
|
-
return resolve({
|
|
684
|
-
status: 400,
|
|
685
|
-
body: uploadErrorBody('disallowed_type', 'No file provided.'),
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
if (!(0, ts_rest_helpers_1.isValidObjectId)(pageId)) {
|
|
689
|
-
cleanupTmp();
|
|
690
|
-
return resolve({
|
|
691
|
-
status: 400,
|
|
692
|
-
body: uploadErrorBody('no_permission', 'A valid pageId is required.'),
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
if (!intent) {
|
|
696
|
-
cleanupTmp();
|
|
697
|
-
return resolve({
|
|
698
|
-
status: 400,
|
|
699
|
-
body: uploadErrorBody('disallowed_type', "The intent field must be 'paste' or 'dnd'."),
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
// --- Intent-aware size + MIME enforcement ---
|
|
703
|
-
// multer's 50 MB cap is the D&D ceiling; a `paste` upload
|
|
704
|
-
// (clipboard image) is held to the smaller 10 MB cap here.
|
|
705
|
-
const { maxBytes, allowedMime } = limitsForIntent(intent);
|
|
706
|
-
if (tmpFile.size > maxBytes) {
|
|
707
|
-
cleanupTmp();
|
|
708
|
-
return resolve({
|
|
709
|
-
status: 413,
|
|
710
|
-
body: uploadErrorBody('too_large', 'The file is too large to upload.', { maxBytes }),
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
const fileType = tmpFile.mimetype;
|
|
714
|
-
if (!allowedMime.has(fileType)) {
|
|
715
|
-
cleanupTmp();
|
|
716
|
-
return resolve({
|
|
717
|
-
status: 415,
|
|
718
|
-
body: uploadErrorBody('disallowed_type', `Files of type ${fileType} cannot be uploaded.`, { mimeType: fileType }),
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
// --- Permission: a caller who can view the page can attach to
|
|
722
|
-
// it (same posture as `addAttachment`). Grant failure → 403. ---
|
|
723
|
-
const grant = await (0, ts_rest_helpers_1.loadGrantedPage)(Page, pageId, user);
|
|
724
|
-
if ('error' in grant) {
|
|
725
|
-
cleanupTmp();
|
|
726
|
-
return resolve({
|
|
727
|
-
status: 403,
|
|
728
|
-
body: uploadErrorBody('no_permission', 'You do not have permission to attach files to this page.'),
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
const pageData = grant.page;
|
|
732
|
-
const originalName = tmpFile.originalname;
|
|
733
|
-
const fileName = tmpFile.filename + tmpFile.originalname;
|
|
734
|
-
const fileSize = tmpFile.size;
|
|
735
|
-
try {
|
|
736
|
-
const filePath = Attachment.createAttachmentFilePath(pageData._id, fileName, fileType);
|
|
737
|
-
const tmpFileStream = node_fs_1.default.createReadStream(tmpFile.path, { flags: 'r', autoClose: true });
|
|
738
|
-
await fileUploader.uploadFile(filePath, fileType, tmpFileStream, {});
|
|
739
|
-
const created = (await Attachment.create({
|
|
740
|
-
page: pageData._id,
|
|
741
|
-
creator: user._id,
|
|
742
|
-
filePath,
|
|
743
|
-
originalName,
|
|
744
|
-
fileName,
|
|
745
|
-
fileFormat: fileType,
|
|
746
|
-
fileSize,
|
|
747
|
-
}));
|
|
748
|
-
cleanupTmp();
|
|
749
|
-
debug('editor upload ok', { intent, pageId, attachmentId: created._id.toString() });
|
|
750
|
-
return resolve({
|
|
751
|
-
status: 200,
|
|
752
|
-
body: {
|
|
753
|
-
url: created.fileUrl,
|
|
754
|
-
filename: originalName,
|
|
755
|
-
mimeType: fileType,
|
|
756
|
-
sizeBytes: fileSize,
|
|
757
|
-
},
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
catch (err) {
|
|
761
|
-
debug('editor upload error', err);
|
|
762
|
-
cleanupTmp();
|
|
763
|
-
return resolve(ts_rest_helpers_1.internalServerErrorResponse);
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
});
|
|
767
|
-
},
|
|
768
|
-
/**
|
|
769
|
-
* DELETE /api/v2/attachments/:id
|
|
770
|
-
*
|
|
771
|
-
* Authorization (wiki policy): any authenticated user who can view the
|
|
772
|
-
* owning page may delete an attachment — the same open-collaboration
|
|
773
|
-
* posture as page editing. The caller still has to pass the page grant
|
|
774
|
-
* check (so an attachment on a page they cannot see stays a 404), but
|
|
775
|
-
* we no longer restrict deletion to the creator / admin / grantedUsers.
|
|
776
|
-
* The legacy `/_api/attachments.remove` allowed even anonymous
|
|
777
|
-
* deletion; we keep it authenticated-only (`authenticatedRouter`).
|
|
778
|
-
*/
|
|
779
|
-
removeAttachment: async ({ params, req }) => {
|
|
780
|
-
const user = req.user;
|
|
781
|
-
const { id } = params;
|
|
782
|
-
if (!(0, ts_rest_helpers_1.isValidObjectId)(id)) {
|
|
783
|
-
return {
|
|
784
|
-
status: 400,
|
|
785
|
-
body: errorBody('INVALID_ATTACHMENT_ID', 'Invalid attachment id'),
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
let attachment;
|
|
789
|
-
try {
|
|
790
|
-
attachment = (await Attachment.findById(id));
|
|
791
|
-
}
|
|
792
|
-
catch (err) {
|
|
793
|
-
debug('attachment lookup error', err);
|
|
794
|
-
return {
|
|
795
|
-
status: 500,
|
|
796
|
-
body: errorBody('REMOVE_FAILED', 'Failed to load attachment'),
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
if (!attachment) {
|
|
800
|
-
return {
|
|
801
|
-
status: 404,
|
|
802
|
-
body: errorBody('ATTACHMENT_NOT_FOUND', 'Attachment not found'),
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
// Resolve the page for the view-grant check. Any authenticated user
|
|
806
|
-
// who can view the page may delete its attachments (wiki policy).
|
|
807
|
-
const grant = await (0, ts_rest_helpers_1.loadGrantedPage)(Page, attachment.page.toString(), user);
|
|
808
|
-
if ('error' in grant) {
|
|
809
|
-
return {
|
|
810
|
-
status: 404,
|
|
811
|
-
body: errorBody('ATTACHMENT_NOT_FOUND', 'Attachment not found'),
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
try {
|
|
815
|
-
await Attachment.removeAttachment(attachment);
|
|
816
|
-
return { status: 200, body: { success: true } };
|
|
817
|
-
}
|
|
818
|
-
catch (err) {
|
|
819
|
-
debug('removeAttachment error', err);
|
|
820
|
-
return {
|
|
821
|
-
status: 500,
|
|
822
|
-
body: errorBody('REMOVE_FAILED', 'Failed to delete attachment'),
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
},
|
|
826
|
-
});
|
|
827
|
-
(0, express_1.createExpressEndpoints)(api_contract_1.apiContract.attachment, attachmentRouter, router);
|
|
828
|
-
return router;
|
|
829
|
-
};
|
|
830
|
-
//# sourceMappingURL=attachment.js.map
|