@intlayer/backend 8.12.2 → 8.12.4-canary.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/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/bundle_optimization.json +9954 -6953
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/configuration.json +1 -1
- package/dist/esm/controllers/ai.controller.mjs.map +1 -1
- package/dist/esm/controllers/audit.controller.mjs.map +1 -1
- package/dist/esm/controllers/bitbucket.controller.mjs.map +1 -1
- package/dist/esm/controllers/cliSessionToken.controller.mjs.map +1 -1
- package/dist/esm/controllers/demo.controller.mjs +32 -25
- package/dist/esm/controllers/demo.controller.mjs.map +1 -1
- package/dist/esm/controllers/dictionary.controller.mjs +1 -0
- package/dist/esm/controllers/dictionary.controller.mjs.map +1 -1
- package/dist/esm/controllers/environment.controller.mjs.map +1 -1
- package/dist/esm/controllers/eventListener.controller.mjs.map +1 -1
- package/dist/esm/controllers/github.controller.mjs.map +1 -1
- package/dist/esm/controllers/gitlab.controller.mjs.map +1 -1
- package/dist/esm/controllers/newsletter.controller.mjs.map +1 -1
- package/dist/esm/controllers/oAuth2.controller.mjs.map +1 -1
- package/dist/esm/controllers/organization.controller.mjs.map +1 -1
- package/dist/esm/controllers/project.controller.mjs.map +1 -1
- package/dist/esm/controllers/projectAccessKey.controller.mjs.map +1 -1
- package/dist/esm/controllers/projectMemberAccess.controller.mjs.map +1 -1
- package/dist/esm/controllers/recursiveAudit.controller.mjs.map +1 -1
- package/dist/esm/controllers/reviewer.controller.mjs.map +1 -1
- package/dist/esm/controllers/searchDoc.controller.mjs.map +1 -1
- package/dist/esm/controllers/showcaseProject.controller.mjs.map +1 -1
- package/dist/esm/controllers/stripe.controller.mjs.map +1 -1
- package/dist/esm/controllers/tag.controller.mjs.map +1 -1
- package/dist/esm/controllers/translation.controller.mjs.map +1 -1
- package/dist/esm/controllers/user.controller.mjs.map +1 -1
- package/dist/esm/emails/AffiliateActivatedEmail.mjs.map +1 -1
- package/dist/esm/emails/AffiliateConversionEmail.mjs.map +1 -1
- package/dist/esm/emails/AffiliateInvitationEmail.mjs.map +1 -1
- package/dist/esm/emails/AffiliateWelcomeEmail.mjs.map +1 -1
- package/dist/esm/emails/InviteUserEmail.mjs.map +1 -1
- package/dist/esm/emails/MagicLinkEmail.mjs.map +1 -1
- package/dist/esm/emails/MissionRequestedClientEmail.mjs.map +1 -1
- package/dist/esm/emails/MissionRequestedReviewerEmail.mjs.map +1 -1
- package/dist/esm/emails/OAuthTokenCreatedEmail.mjs.map +1 -1
- package/dist/esm/emails/PasswordChangeConfirmation.mjs.map +1 -1
- package/dist/esm/emails/ResetUserPassword.mjs.map +1 -1
- package/dist/esm/emails/ReviewerApplicationEmail.mjs.map +1 -1
- package/dist/esm/emails/ReviewerApprovedEmail.mjs.map +1 -1
- package/dist/esm/emails/ReviewerContactEmail.mjs.map +1 -1
- package/dist/esm/emails/SubscriptionPaymentCancellation.mjs.map +1 -1
- package/dist/esm/emails/SubscriptionPaymentError.mjs.map +1 -1
- package/dist/esm/emails/SubscriptionPaymentSuccess.mjs.map +1 -1
- package/dist/esm/emails/ValidateUserEmail.mjs.map +1 -1
- package/dist/esm/emails/Welcome.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/logger/index.mjs.map +1 -1
- package/dist/esm/middlewares/oAuth2.middleware.mjs.map +1 -1
- package/dist/esm/middlewares/sessionAuth.middleware.mjs.map +1 -1
- package/dist/esm/routes/ai.routes.mjs.map +1 -1
- package/dist/esm/routes/audit.routes.mjs.map +1 -1
- package/dist/esm/routes/bitbucket.routes.mjs.map +1 -1
- package/dist/esm/routes/demo.routes.mjs.map +1 -1
- package/dist/esm/routes/dictionary.routes.mjs.map +1 -1
- package/dist/esm/routes/environment.routes.mjs.map +1 -1
- package/dist/esm/routes/eventListener.routes.mjs.map +1 -1
- package/dist/esm/routes/github.routes.mjs.map +1 -1
- package/dist/esm/routes/gitlab.routes.mjs.map +1 -1
- package/dist/esm/routes/newsletter.routes.mjs.map +1 -1
- package/dist/esm/routes/organization.routes.mjs.map +1 -1
- package/dist/esm/routes/paramsSchemas.mjs.map +1 -1
- package/dist/esm/routes/project.routes.mjs.map +1 -1
- package/dist/esm/routes/reviewer.routes.mjs.map +1 -1
- package/dist/esm/routes/search.routes.mjs.map +1 -1
- package/dist/esm/routes/showcaseProject.routes.mjs.map +1 -1
- package/dist/esm/routes/stripe.routes.mjs.map +1 -1
- package/dist/esm/routes/tags.routes.mjs.map +1 -1
- package/dist/esm/routes/translate.routes.mjs.map +1 -1
- package/dist/esm/routes/user.routes.mjs.map +1 -1
- package/dist/esm/schemas/account.schema.mjs.map +1 -1
- package/dist/esm/schemas/affiliate.schema.mjs.map +1 -1
- package/dist/esm/schemas/affiliateInvitation.schema.mjs.map +1 -1
- package/dist/esm/schemas/audit.schema.mjs.map +1 -1
- package/dist/esm/schemas/auditJob.schema.mjs.map +1 -1
- package/dist/esm/schemas/auditPage.schema.mjs.map +1 -1
- package/dist/esm/schemas/cliSessionToken.schema.mjs.map +1 -1
- package/dist/esm/schemas/dictionary.schema.mjs.map +1 -1
- package/dist/esm/schemas/discussion.schema.mjs.map +1 -1
- package/dist/esm/schemas/oAuth2.schema.mjs.map +1 -1
- package/dist/esm/schemas/organization.schema.mjs.map +1 -1
- package/dist/esm/schemas/plans.schema.mjs.map +1 -1
- package/dist/esm/schemas/project.schema.mjs.map +1 -1
- package/dist/esm/schemas/promoCode.schema.mjs.map +1 -1
- package/dist/esm/schemas/reviewer.schema.mjs.map +1 -1
- package/dist/esm/schemas/session.schema.mjs.map +1 -1
- package/dist/esm/schemas/showcaseProject.schema.mjs.map +1 -1
- package/dist/esm/schemas/tag.schema.mjs.map +1 -1
- package/dist/esm/schemas/user.schema.mjs.map +1 -1
- package/dist/esm/services/affiliate.service.mjs.map +1 -1
- package/dist/esm/services/audit/analysis/analyzeBundleContent.mjs.map +1 -1
- package/dist/esm/services/audit/analysis/analyzeLinguisticStructure.mjs.map +1 -1
- package/dist/esm/services/audit/analysis/analyzeMetadata.mjs.map +1 -1
- package/dist/esm/services/audit/analysis/analyzeRobots.mjs.map +1 -1
- package/dist/esm/services/audit/analysis/analyzeSitemap.mjs.map +1 -1
- package/dist/esm/services/audit/analysis/analyzeUrlStructure.mjs.map +1 -1
- package/dist/esm/services/audit/analysis/calculateScore.mjs.map +1 -1
- package/dist/esm/services/audit/checkers/bundleChecker.mjs.map +1 -1
- package/dist/esm/services/audit/checkers/linguisticChecker.mjs.map +1 -1
- package/dist/esm/services/audit/checkers/metadataChecker.mjs.map +1 -1
- package/dist/esm/services/audit/checkers/pageChecker.mjs.map +1 -1
- package/dist/esm/services/audit/checkers/robotsChecker.mjs.map +1 -1
- package/dist/esm/services/audit/checkers/sitemapChecker.mjs.map +1 -1
- package/dist/esm/services/audit/checkers/urlChecker.mjs.map +1 -1
- package/dist/esm/services/audit/recursiveAudit.service.mjs.map +1 -1
- package/dist/esm/services/audit/seoAudit.service.mjs.map +1 -1
- package/dist/esm/services/bitbucket.service.mjs.map +1 -1
- package/dist/esm/services/ci.service.mjs.map +1 -1
- package/dist/esm/services/cliSessionToken.service.mjs.map +1 -1
- package/dist/esm/services/dictionary.service.mjs.map +1 -1
- package/dist/esm/services/email.service.mjs.map +1 -1
- package/dist/esm/services/environment.service.mjs.map +1 -1
- package/dist/esm/services/github.service.mjs.map +1 -1
- package/dist/esm/services/gitlab.service.mjs.map +1 -1
- package/dist/esm/services/oAuth2.service.mjs.map +1 -1
- package/dist/esm/services/organization.service.mjs.map +1 -1
- package/dist/esm/services/project/projectScreenshot.service.mjs.map +1 -1
- package/dist/esm/services/project.service.mjs.map +1 -1
- package/dist/esm/services/projectAccessKey.service.mjs.map +1 -1
- package/dist/esm/services/promoCode.service.mjs.map +1 -1
- package/dist/esm/services/reviewer/pictureUpload.service.mjs.map +1 -1
- package/dist/esm/services/reviewer.service.mjs.map +1 -1
- package/dist/esm/services/reviewerMessage.service.mjs.map +1 -1
- package/dist/esm/services/reviewerMission.service.mjs.map +1 -1
- package/dist/esm/services/session.service.mjs.map +1 -1
- package/dist/esm/services/showcase/showcaseProject.service.mjs.map +1 -1
- package/dist/esm/services/showcase/showcaseScan.service.mjs.map +1 -1
- package/dist/esm/services/showcase/showcaseUploadScreenshot.service.mjs.map +1 -1
- package/dist/esm/services/showcase/showcaseVerifyBundle.service.mjs.map +1 -1
- package/dist/esm/services/showcase/showcaseVerifyGithub.service.mjs.map +1 -1
- package/dist/esm/services/subscription.service.mjs.map +1 -1
- package/dist/esm/services/tag.service.mjs.map +1 -1
- package/dist/esm/services/translationQueue.service.mjs.map +1 -1
- package/dist/esm/services/translationWorker.service.mjs.map +1 -1
- package/dist/esm/services/user/avatarUpload.service.mjs.map +1 -1
- package/dist/esm/services/user.service.mjs.map +1 -1
- package/dist/esm/services/webhook.service.mjs.map +1 -1
- package/dist/esm/types/user.types.mjs.map +1 -1
- package/dist/esm/utils/AI/askDocQuestion/askDocQuestion.mjs.map +1 -1
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/bundle_optimization.json +9954 -6953
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/configuration.json +1 -1
- package/dist/esm/utils/AI/askDocQuestion/indexMarkdownFiles.mjs.map +1 -1
- package/dist/esm/utils/AI/auditDictionary/index.mjs.map +1 -1
- package/dist/esm/utils/AI/auditDictionaryField/index.mjs.map +1 -1
- package/dist/esm/utils/AI/auditDictionaryMetadata/index.mjs.map +1 -1
- package/dist/esm/utils/AI/auditTag/index.mjs.map +1 -1
- package/dist/esm/utils/AI/autocomplete/index.mjs.map +1 -1
- package/dist/esm/utils/AI/chat/index.mjs.map +1 -1
- package/dist/esm/utils/AI/chat/mcpInProcessTools.mjs.map +1 -1
- package/dist/esm/utils/AI/chat/sessionTools.mjs.map +1 -1
- package/dist/esm/utils/AI/customQuery/index.mjs.map +1 -1
- package/dist/esm/utils/AI/getProjectAIOptions.mjs.map +1 -1
- package/dist/esm/utils/AI/translateDictionaryDB.mjs.map +1 -1
- package/dist/esm/utils/AI/translateJSON/index.mjs.map +1 -1
- package/dist/esm/utils/accessControl.mjs.map +1 -1
- package/dist/esm/utils/auth/getAuth.mjs.map +1 -1
- package/dist/esm/utils/cors.mjs +2 -13
- package/dist/esm/utils/cors.mjs.map +1 -1
- package/dist/esm/utils/demoDictionaries.mjs.map +1 -1
- package/dist/esm/utils/ensureArrayQueryFilter.mjs.map +1 -1
- package/dist/esm/utils/ensureMongoDocumentToObject.mjs.map +1 -1
- package/dist/esm/utils/errors/ErrorHandler.mjs.map +1 -1
- package/dist/esm/utils/errors/ErrorsClass.mjs.map +1 -1
- package/dist/esm/utils/errors/errorCodes.mjs.map +1 -1
- package/dist/esm/utils/errors/index.mjs +1 -0
- package/dist/esm/utils/filtersAndPagination/getDictionaryFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getDiscussionFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getFiltersAndPaginationFromBody.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getOrganizationFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getProjectFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getTagFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getUserFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/getFaviconUrl.mjs.map +1 -1
- package/dist/esm/utils/github/connectGithub.mjs.map +1 -1
- package/dist/esm/utils/httpStatusCodes.mjs.map +1 -1
- package/dist/esm/utils/image/resizeImage.mjs.map +1 -1
- package/dist/esm/utils/mapper/dictionary.mjs.map +1 -1
- package/dist/esm/utils/mapper/organization.mjs.map +1 -1
- package/dist/esm/utils/mapper/project.mjs.map +1 -1
- package/dist/esm/utils/mapper/session.mjs.map +1 -1
- package/dist/esm/utils/mapper/showcaseProject.mjs.map +1 -1
- package/dist/esm/utils/mapper/tag.mjs.map +1 -1
- package/dist/esm/utils/mapper/user.mjs.map +1 -1
- package/dist/esm/utils/mongoDB/connectDB.mjs.map +1 -1
- package/dist/esm/utils/oAuth2.mjs.map +1 -1
- package/dist/esm/utils/permissions.mjs.map +1 -1
- package/dist/esm/utils/plan.mjs.map +1 -1
- package/dist/esm/utils/puppeteer/launchBrowser.mjs.map +1 -1
- package/dist/esm/utils/rateLimiter.mjs.map +1 -1
- package/dist/esm/utils/redis/connectRedis.mjs.map +1 -1
- package/dist/esm/utils/removeObjectKeys.mjs.map +1 -1
- package/dist/esm/utils/responseData.mjs.map +1 -1
- package/dist/esm/utils/s3/s3Client.mjs.map +1 -1
- package/dist/esm/utils/validation/validateDictionary.mjs.map +1 -1
- package/dist/esm/utils/validation/validateOrganization.mjs.map +1 -1
- package/dist/esm/utils/validation/validateProject.mjs.map +1 -1
- package/dist/esm/utils/validation/validateTag.mjs.map +1 -1
- package/dist/esm/utils/validation/validateUser.mjs.map +1 -1
- package/dist/esm/webhooks/stripe.webhook.mjs.map +1 -1
- package/dist/types/controllers/ai.controller.d.ts.map +1 -1
- package/dist/types/controllers/bitbucket.controller.d.ts.map +1 -1
- package/dist/types/controllers/cliSessionToken.controller.d.ts.map +1 -1
- package/dist/types/controllers/demo.controller.d.ts.map +1 -1
- package/dist/types/controllers/dictionary.controller.d.ts.map +1 -1
- package/dist/types/controllers/environment.controller.d.ts.map +1 -1
- package/dist/types/controllers/eventListener.controller.d.ts.map +1 -1
- package/dist/types/controllers/github.controller.d.ts.map +1 -1
- package/dist/types/controllers/gitlab.controller.d.ts.map +1 -1
- package/dist/types/controllers/newsletter.controller.d.ts.map +1 -1
- package/dist/types/controllers/oAuth2.controller.d.ts.map +1 -1
- package/dist/types/controllers/organization.controller.d.ts.map +1 -1
- package/dist/types/controllers/project.controller.d.ts.map +1 -1
- package/dist/types/controllers/projectAccessKey.controller.d.ts.map +1 -1
- package/dist/types/controllers/projectMemberAccess.controller.d.ts.map +1 -1
- package/dist/types/controllers/recursiveAudit.controller.d.ts.map +1 -1
- package/dist/types/controllers/reviewer.controller.d.ts.map +1 -1
- package/dist/types/controllers/searchDoc.controller.d.ts.map +1 -1
- package/dist/types/controllers/showcaseProject.controller.d.ts.map +1 -1
- package/dist/types/controllers/stripe.controller.d.ts.map +1 -1
- package/dist/types/controllers/tag.controller.d.ts.map +1 -1
- package/dist/types/controllers/translation.controller.d.ts.map +1 -1
- package/dist/types/controllers/user.controller.d.ts.map +1 -1
- package/dist/types/emails/AffiliateActivatedEmail.d.ts +20 -18
- package/dist/types/emails/AffiliateActivatedEmail.d.ts.map +1 -1
- package/dist/types/emails/AffiliateConversionEmail.d.ts +21 -19
- package/dist/types/emails/AffiliateConversionEmail.d.ts.map +1 -1
- package/dist/types/emails/AffiliateInvitationEmail.d.ts +20 -18
- package/dist/types/emails/AffiliateInvitationEmail.d.ts.map +1 -1
- package/dist/types/emails/AffiliateWelcomeEmail.d.ts +20 -18
- package/dist/types/emails/AffiliateWelcomeEmail.d.ts.map +1 -1
- package/dist/types/emails/InviteUserEmail.d.ts +20 -18
- package/dist/types/emails/InviteUserEmail.d.ts.map +1 -1
- package/dist/types/emails/MagicLinkEmail.d.ts +20 -18
- package/dist/types/emails/MagicLinkEmail.d.ts.map +1 -1
- package/dist/types/emails/MissionRequestedClientEmail.d.ts +20 -18
- package/dist/types/emails/MissionRequestedClientEmail.d.ts.map +1 -1
- package/dist/types/emails/MissionRequestedReviewerEmail.d.ts +20 -18
- package/dist/types/emails/MissionRequestedReviewerEmail.d.ts.map +1 -1
- package/dist/types/emails/OAuthTokenCreatedEmail.d.ts +20 -18
- package/dist/types/emails/OAuthTokenCreatedEmail.d.ts.map +1 -1
- package/dist/types/emails/PasswordChangeConfirmation.d.ts +20 -18
- package/dist/types/emails/PasswordChangeConfirmation.d.ts.map +1 -1
- package/dist/types/emails/ResetUserPassword.d.ts +20 -18
- package/dist/types/emails/ResetUserPassword.d.ts.map +1 -1
- package/dist/types/emails/ReviewerApplicationEmail.d.ts +20 -18
- package/dist/types/emails/ReviewerApplicationEmail.d.ts.map +1 -1
- package/dist/types/emails/ReviewerApprovedEmail.d.ts +20 -18
- package/dist/types/emails/ReviewerApprovedEmail.d.ts.map +1 -1
- package/dist/types/emails/ReviewerContactEmail.d.ts +3 -1
- package/dist/types/emails/ReviewerContactEmail.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentCancellation.d.ts +20 -18
- package/dist/types/emails/SubscriptionPaymentCancellation.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentError.d.ts +20 -18
- package/dist/types/emails/SubscriptionPaymentError.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentSuccess.d.ts +20 -18
- package/dist/types/emails/SubscriptionPaymentSuccess.d.ts.map +1 -1
- package/dist/types/emails/ValidateUserEmail.d.ts +20 -18
- package/dist/types/emails/ValidateUserEmail.d.ts.map +1 -1
- package/dist/types/emails/Welcome.d.ts +20 -18
- package/dist/types/emails/Welcome.d.ts.map +1 -1
- package/dist/types/export.d.ts +1 -1
- package/dist/types/logger/index.d.ts +3 -1
- package/dist/types/logger/index.d.ts.map +1 -1
- package/dist/types/middlewares/oAuth2.middleware.d.ts.map +1 -1
- package/dist/types/middlewares/sessionAuth.middleware.d.ts.map +1 -1
- package/dist/types/routes/demo.routes.d.ts.map +1 -1
- package/dist/types/schemas/account.schema.d.ts +35 -34
- package/dist/types/schemas/account.schema.d.ts.map +1 -1
- package/dist/types/schemas/affiliate.schema.d.ts +109 -108
- package/dist/types/schemas/affiliate.schema.d.ts.map +1 -1
- package/dist/types/schemas/affiliateInvitation.schema.d.ts +49 -48
- package/dist/types/schemas/affiliateInvitation.schema.d.ts.map +1 -1
- package/dist/types/schemas/audit.schema.d.ts.map +1 -1
- package/dist/types/schemas/auditJob.schema.d.ts +6 -6
- package/dist/types/schemas/auditPage.schema.d.ts +6 -6
- package/dist/types/schemas/cliSessionToken.schema.d.ts +14 -13
- package/dist/types/schemas/cliSessionToken.schema.d.ts.map +1 -1
- package/dist/types/schemas/dictionary.schema.d.ts +60 -59
- package/dist/types/schemas/dictionary.schema.d.ts.map +1 -1
- package/dist/types/schemas/discussion.schema.d.ts +51 -50
- package/dist/types/schemas/discussion.schema.d.ts.map +1 -1
- package/dist/types/schemas/oAuth2.schema.d.ts +18 -17
- package/dist/types/schemas/oAuth2.schema.d.ts.map +1 -1
- package/dist/types/schemas/organization.schema.d.ts +45 -44
- package/dist/types/schemas/organization.schema.d.ts.map +1 -1
- package/dist/types/schemas/plans.schema.d.ts +45 -44
- package/dist/types/schemas/plans.schema.d.ts.map +1 -1
- package/dist/types/schemas/project.schema.d.ts +73 -72
- package/dist/types/schemas/project.schema.d.ts.map +1 -1
- package/dist/types/schemas/promoCode.schema.d.ts +61 -60
- package/dist/types/schemas/promoCode.schema.d.ts.map +1 -1
- package/dist/types/schemas/reviewer.schema.d.ts +221 -220
- package/dist/types/schemas/reviewer.schema.d.ts.map +1 -1
- package/dist/types/schemas/session.schema.d.ts +54 -52
- package/dist/types/schemas/session.schema.d.ts.map +1 -1
- package/dist/types/schemas/showcaseProject.schema.d.ts +61 -60
- package/dist/types/schemas/showcaseProject.schema.d.ts.map +1 -1
- package/dist/types/schemas/tag.schema.d.ts +45 -44
- package/dist/types/schemas/tag.schema.d.ts.map +1 -1
- package/dist/types/schemas/user.schema.d.ts +71 -70
- package/dist/types/schemas/user.schema.d.ts.map +1 -1
- package/dist/types/services/affiliate.service.d.ts.map +1 -1
- package/dist/types/services/audit/analysis/analyzeBundleContent.d.ts.map +1 -1
- package/dist/types/services/audit/analysis/analyzeLinguisticStructure.d.ts.map +1 -1
- package/dist/types/services/audit/analysis/analyzeRobots.d.ts.map +1 -1
- package/dist/types/services/audit/analysis/analyzeSitemap.d.ts.map +1 -1
- package/dist/types/services/audit/analysis/calculateScore.d.ts.map +1 -1
- package/dist/types/services/audit/checkers/bundleChecker.d.ts.map +1 -1
- package/dist/types/services/audit/recursiveAudit.service.d.ts +5 -4
- package/dist/types/services/audit/recursiveAudit.service.d.ts.map +1 -1
- package/dist/types/services/audit/types.d.ts.map +1 -1
- package/dist/types/services/bitbucket.service.d.ts.map +1 -1
- package/dist/types/services/cliSessionToken.service.d.ts.map +1 -1
- package/dist/types/services/dictionary.service.d.ts.map +1 -1
- package/dist/types/services/email.service.d.ts.map +1 -1
- package/dist/types/services/github.service.d.ts.map +1 -1
- package/dist/types/services/gitlab.service.d.ts.map +1 -1
- package/dist/types/services/oAuth2.service.d.ts.map +1 -1
- package/dist/types/services/organization.service.d.ts.map +1 -1
- package/dist/types/services/project/projectScreenshot.service.d.ts.map +1 -1
- package/dist/types/services/project.service.d.ts.map +1 -1
- package/dist/types/services/promoCode.service.d.ts.map +1 -1
- package/dist/types/services/reviewer/pictureUpload.service.d.ts.map +1 -1
- package/dist/types/services/reviewer.service.d.ts.map +1 -1
- package/dist/types/services/reviewerMessage.service.d.ts.map +1 -1
- package/dist/types/services/reviewerMission.service.d.ts.map +1 -1
- package/dist/types/services/session.service.d.ts.map +1 -1
- package/dist/types/services/showcase/showcaseProject.service.d.ts.map +1 -1
- package/dist/types/services/showcase/showcaseScan.service.d.ts.map +1 -1
- package/dist/types/services/showcase/showcaseUploadScreenshot.service.d.ts.map +1 -1
- package/dist/types/services/showcase/showcaseVerifyBundle.service.d.ts.map +1 -1
- package/dist/types/services/showcase/showcaseVerifyGithub.service.d.ts.map +1 -1
- package/dist/types/services/subscription.service.d.ts.map +1 -1
- package/dist/types/services/tag.service.d.ts.map +1 -1
- package/dist/types/services/translationQueue.service.d.ts +2 -1
- package/dist/types/services/translationQueue.service.d.ts.map +1 -1
- package/dist/types/services/translationWorker.service.d.ts.map +1 -1
- package/dist/types/services/user/avatarUpload.service.d.ts.map +1 -1
- package/dist/types/services/user.service.d.ts.map +1 -1
- package/dist/types/services/webhook.service.d.ts.map +1 -1
- package/dist/types/types/Routes.d.ts.map +1 -1
- package/dist/types/types/account.types.d.ts.map +1 -1
- package/dist/types/types/affiliate.types.d.ts.map +1 -1
- package/dist/types/types/affiliateInvitation.types.d.ts.map +1 -1
- package/dist/types/types/dictionary.types.d.ts.map +1 -1
- package/dist/types/types/discussion.types.d.ts.map +1 -1
- package/dist/types/types/oAuth2.types.d.ts.map +1 -1
- package/dist/types/types/organization.types.d.ts.map +1 -1
- package/dist/types/types/plan.types.d.ts.map +1 -1
- package/dist/types/types/project.types.d.ts.map +1 -1
- package/dist/types/types/promoCode.types.d.ts.map +1 -1
- package/dist/types/types/reviewer.types.d.ts.map +1 -1
- package/dist/types/types/session.types.d.ts.map +1 -1
- package/dist/types/types/showcaseProject.types.d.ts.map +1 -1
- package/dist/types/types/tag.types.d.ts.map +1 -1
- package/dist/types/types/user.types.d.ts.map +1 -1
- package/dist/types/utils/AI/askDocQuestion/askDocQuestion.d.ts.map +1 -1
- package/dist/types/utils/AI/askDocQuestion/indexMarkdownFiles.d.ts.map +1 -1
- package/dist/types/utils/AI/auditDictionary/index.d.ts.map +1 -1
- package/dist/types/utils/AI/auditDictionaryField/index.d.ts.map +1 -1
- package/dist/types/utils/AI/auditDictionaryMetadata/index.d.ts.map +1 -1
- package/dist/types/utils/AI/auditTag/index.d.ts.map +1 -1
- package/dist/types/utils/AI/autocomplete/index.d.ts.map +1 -1
- package/dist/types/utils/AI/chat/index.d.ts.map +1 -1
- package/dist/types/utils/AI/chat/mcpInProcessTools.d.ts.map +1 -1
- package/dist/types/utils/AI/chat/sessionTools.d.ts +25 -24
- package/dist/types/utils/AI/chat/sessionTools.d.ts.map +1 -1
- package/dist/types/utils/AI/customQuery/index.d.ts.map +1 -1
- package/dist/types/utils/AI/translateDictionaryDB.d.ts.map +1 -1
- package/dist/types/utils/AI/translateJSON/index.d.ts.map +1 -1
- package/dist/types/utils/auth/getAuth.d.ts.map +1 -1
- package/dist/types/utils/cors.d.ts +1 -7
- package/dist/types/utils/cors.d.ts.map +1 -1
- package/dist/types/utils/demoDictionaries.d.ts.map +1 -1
- package/dist/types/utils/ensureArrayQueryFilter.d.ts.map +1 -1
- package/dist/types/utils/errors/ErrorHandler.d.ts +8 -6
- package/dist/types/utils/errors/ErrorHandler.d.ts.map +1 -1
- package/dist/types/utils/errors/ErrorsClass.d.ts.map +1 -1
- package/dist/types/utils/errors/errorCodes.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getDictionaryFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getDiscussionFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getFiltersAndPaginationFromBody.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getOrganizationFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getProjectFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +7 -6
- package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getUserFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/getFaviconUrl.d.ts.map +1 -1
- package/dist/types/utils/github/connectGithub.d.ts.map +1 -1
- package/dist/types/utils/httpStatusCodes.d.ts.map +1 -1
- package/dist/types/utils/image/resizeImage.d.ts.map +1 -1
- package/dist/types/utils/mapper/dictionary.d.ts.map +1 -1
- package/dist/types/utils/mapper/project.d.ts.map +1 -1
- package/dist/types/utils/mapper/session.d.ts.map +1 -1
- package/dist/types/utils/mapper/showcaseProject.d.ts.map +1 -1
- package/dist/types/utils/mapper/tag.d.ts.map +1 -1
- package/dist/types/utils/mergeFunctionTypes.d.ts.map +1 -1
- package/dist/types/utils/mongoDB/connectDB.d.ts.map +1 -1
- package/dist/types/utils/mongoDB/types.d.ts.map +1 -1
- package/dist/types/utils/oAuth2.d.ts.map +1 -1
- package/dist/types/utils/permissions.d.ts +2 -1
- package/dist/types/utils/permissions.d.ts.map +1 -1
- package/dist/types/utils/plan.d.ts.map +1 -1
- package/dist/types/utils/rateLimiter.d.ts.map +1 -1
- package/dist/types/utils/redis/connectRedis.d.ts.map +1 -1
- package/dist/types/utils/responseData.d.ts.map +1 -1
- package/dist/types/utils/s3/s3Client.d.ts.map +1 -1
- package/dist/types/utils/validation/validateDictionary.d.ts.map +1 -1
- package/dist/types/utils/validation/validateOrganization.d.ts.map +1 -1
- package/dist/types/utils/validation/validateProject.d.ts.map +1 -1
- package/dist/types/utils/validation/validateTag.d.ts.map +1 -1
- package/dist/types/utils/validation/validateUser.d.ts.map +1 -1
- package/package.json +17 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stripe.controller.mjs","names":["subscriptionService.getPricing","subscriptionService.getCouponId","affiliateService.getAffiliateById","affiliateService.trackReferral","subscriptionService.cancelSubscription","emailService.sendEmail","affiliateService.createAffiliate","affiliateService.findAffiliates","affiliateService.countAffiliates","affiliateService.getAffiliateByUserId","affiliateService.createAccountSession","affiliateService.createOnboardingLink","affiliateService.getAffiliateStats","affiliateService.findAffiliateInvitations","affiliateService.countAffiliateInvitations","affiliateService.createAffiliateInvitation","affiliateService.getAffiliateInvitationByToken","affiliateService.setAffiliateStatus","affiliateService.acceptAffiliateInvitation","promoCodeService.getPromoCodes","promoCodeService.countPromoCodes","promoCodeService.getPromoCodeById","promoCodeService.createPromoCode","promoCodeService.updatePromoCode","promoCodeService.deletePromoCode","affiliateService.getAffiliateByCode"],"sources":["../../../src/controllers/stripe.controller.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport { PromoCodeModel } from '@schemas/promoCode.schema';\nimport * as affiliateService from '@services/affiliate.service';\nimport * as emailService from '@services/email.service';\nimport * as promoCodeService from '@services/promoCode.service';\nimport * as subscriptionService from '@services/subscription.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport {\n type FiltersAndPagination,\n getFiltersAndPaginationFromBody,\n} from '@utils/filtersAndPagination/getFiltersAndPaginationFromBody';\nimport { isLifetimePriceId, retrievePlanInformation } from '@utils/plan';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type PaginatedResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport Stripe from 'stripe';\nimport type { AffiliateAPI, AffiliateStats } from '@/types/affiliate.types';\nimport type { AffiliateInvitationAPI } from '@/types/affiliateInvitation.types';\nimport type { Organization } from '@/types/organization.types';\nimport type { PromoCodeAPI } from '@/types/promoCode.types';\n\nexport type GetPricingBody = {\n priceIds?: string[];\n promoCode?: string;\n};\n\nexport type GetPricingResult = ResponseData<subscriptionService.PricingResult>;\n\n/**\n * Simulate pricing for a given set of prices and a promotion code.\n *\n * @param request - The request object containing the price IDs and promotion code.\n * @param reply - The response object to send the simulated pricing result.\n */\nexport const getPricing = async (\n request: FastifyRequest<{ Body: GetPricingBody }>,\n reply: FastifyReply\n) => {\n const { priceIds, promoCode } = request.body;\n\n const pricingResult = await subscriptionService.getPricing(\n priceIds,\n promoCode\n );\n\n const formattedPricingResult =\n formatResponse<subscriptionService.PricingResult>({\n data: pricingResult,\n });\n\n reply.code(200).send(formattedPricingResult);\n};\n\nexport type GetCheckoutSessionBody = {\n priceId: string;\n promoCode?: string;\n referralCode?: string;\n};\n\nexport type GetCheckoutSessionResult = ResponseData<{\n subscription?: Stripe.Response<Stripe.Subscription>;\n paymentIntent?: Stripe.Response<Stripe.PaymentIntent>;\n clientSecret: string;\n}>;\n\n/**\n * Handles subscription creation or update with Stripe and returns a ClientSecret.\n */\nexport const getSubscription = async (\n request: FastifyRequest<{ Body: GetCheckoutSessionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n // Extract organization and user from request locals (set by authentication middleware)\n const { organization, user } = request.session || {};\n const { priceId, promoCode, referralCode } = request.body;\n\n // Validate that the organization exists\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_FOUND'\n );\n }\n\n // Validate that the user exists\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { period, type } = retrievePlanInformation(priceId);\n\n if (\n organization.plan?.subscriptionId &&\n organization.plan?.type === type &&\n organization.plan?.period === period &&\n organization.plan?.status === 'active'\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ALREADY_SUBSCRIBED',\n {\n organizationId: organization.id,\n }\n );\n }\n\n // Attempt to retrieve the Stripe customer ID from the organization's plan\n let customerId = organization.plan?.customerId;\n\n if (!customerId) {\n // If no customer ID exists, create a new Stripe customer for the organization\n const customer = await stripe.customers.create({\n metadata: {\n organizationId: String(organization.id),\n userId: String(user.id),\n // Include the locale for potential localization\n locale: (request.session as unknown as { locale: Locale }).locale,\n },\n });\n customerId = customer.id;\n }\n\n const { couponId: promoCodeId, affiliateId: promoAffiliateId } = promoCode\n ? await subscriptionService.getCouponId(promoCode)\n : { couponId: null, affiliateId: undefined };\n\n let referralCodeToUse = referralCode;\n if (!referralCodeToUse && promoAffiliateId) {\n const promoAffiliate =\n await affiliateService.getAffiliateById(promoAffiliateId);\n if (promoAffiliate) {\n referralCodeToUse = promoAffiliate.referralCode;\n }\n }\n\n // Lifetime / one-time payment — handled with a PaymentIntent rather than a\n // recurring subscription so the price is charged once.\n if (isLifetimePriceId(priceId)) {\n const price = await stripe.prices.retrieve(priceId);\n\n if (!price.unit_amount) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n { user, organization, priceId }\n );\n }\n\n let amount = price.unit_amount;\n\n if (promoCodeId) {\n const coupon = await stripe.coupons.retrieve(promoCodeId);\n if (coupon.percent_off) {\n amount = Math.round(amount * (1 - coupon.percent_off / 100));\n } else if (coupon.amount_off) {\n amount = Math.max(0, amount - coupon.amount_off);\n }\n }\n\n const paymentIntent = await stripe.paymentIntents.create({\n customer: customerId,\n amount,\n currency: price.currency,\n payment_method_types: ['card'],\n metadata: {\n organizationId: String(organization.id),\n userId: String(user.id),\n priceId,\n purchaseType: 'lifetime',\n },\n });\n\n if (!paymentIntent?.client_secret) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n { user, organization, priceId }\n );\n }\n\n if (referralCodeToUse) {\n await affiliateService.trackReferral(\n referralCodeToUse,\n organization.id,\n paymentIntent.id,\n paymentIntent.amount,\n paymentIntent.currency\n );\n }\n\n return reply.send(\n formatResponse<GetCheckoutSessionResult['data']>({\n data: {\n paymentIntent,\n clientSecret: paymentIntent.client_secret,\n },\n })\n );\n }\n\n const discounts: Stripe.SubscriptionCreateParams.Discount[] = promoCodeId\n ? [{ coupon: promoCodeId }]\n : [];\n\n // If no subscription exists, create a new one\n const subscription = await stripe.subscriptions.create({\n customer: customerId, // Associate the subscription with the customer\n items: [{ price: priceId }], // Set the price ID for the subscription\n expand: ['latest_invoice.confirmation_secret'],\n payment_settings: {\n payment_method_types: ['card'], // Specify payment method types\n },\n payment_behavior: 'default_incomplete', // Create the subscription in an incomplete state until payment is confirmed\n discounts,\n });\n\n // Handle subscription creation failure\n if (!subscription) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n }\n\n const clientSecret = (\n subscription.latest_invoice as Stripe.Invoice & {\n confirmation_secret?: { client_secret: string };\n }\n )?.confirmation_secret?.client_secret;\n\n // Handle subscription creation failure\n if (!clientSecret) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n }\n\n if (referralCodeToUse) {\n await affiliateService.trackReferral(\n referralCodeToUse,\n organization.id,\n subscription.id\n );\n }\n\n // Prepare the response data with subscription details\n const responseData = formatResponse<GetCheckoutSessionResult['data']>({\n data: { subscription, clientSecret },\n });\n\n // Send the response back to the client\n return reply.send(responseData);\n } catch (error) {\n // Handle any errors that occur during the process\n\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\ntype CancelSubscriptionData = Organization['plan'];\nexport type CancelSubscriptionResult = ResponseData<CancelSubscriptionData>;\n\n/**\n * Cancels a subscription for an organization.\n */\nexport const cancelSubscription = async (\n _request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // Extract the organization and user from the request locals\n // These are typically set by authentication middleware earlier in the request pipeline\n const { organization, user } = _request.session || {};\n\n // Validate that the organization exists\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_FOUND'\n );\n }\n\n // Validate that the user exists\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n // Try to get the subscription ID from the organization's plan\n const subscriptionId = organization.plan?.subscriptionId;\n\n // Only attempt to cancel on Stripe if it's a subscription (not LIFETIME or FREE)\n if (subscriptionId && organization.plan?.type !== 'LIFETIME') {\n try {\n // Cancel the subscription on Stripe immediately using the subscription ID\n await stripe.subscriptions.cancel(subscriptionId);\n } catch (error) {\n // If the subscription is already canceled or not found on Stripe, we log it and continue\n // to ensure our local database remains in sync.\n console.error(\n `Stripe subscription cancellation failed for ${subscriptionId}:`,\n error\n );\n }\n }\n\n // Update the organization's plan in the database to reflect the cancellation\n const plan = await subscriptionService.cancelSubscription(\n subscriptionId,\n String(organization.id)\n );\n\n // If the plan could not be updated in the database, handle the error\n if (!plan) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n // Prepare a formatted response with a success message and the updated plan data\n const formattedPlan = formatResponse<CancelSubscriptionData>({\n message: t({\n en: 'Subscription cancelled successfully',\n 'en-GB': 'Subscription cancelled successfully',\n fr: 'Souscription annulée avec succès',\n es: 'Suscripción cancelada con éxito',\n ru: 'Подписка успешно отменена',\n ja: 'サブスクリプションが正常にキャンセルされました',\n ko: '구독이 성공적으로 취소되었습니다',\n zh: '订阅已成功取消',\n de: 'Abonnement erfolgreich gekündigt',\n ar: 'تم إلغاء الاشتراك بنجاح',\n it: 'Abbonamento annullato con successo',\n pt: 'Assinatura cancelada com sucesso',\n hi: 'सदस्यता सफलतापूर्वक रद्द कर दी गई',\n tr: 'Abonelik başarıyla iptal edildi',\n pl: 'Subskrypcja została pomyślnie anulowana',\n id: 'Langganan berhasil dibatalkan',\n vi: 'Đăng ký đã được hủy thành công',\n uk: 'Передплату успішно скасовано',\n }),\n description: t({\n en: 'Your subscription has been cancelled successfully',\n 'en-GB': 'Your subscription has been cancelled successfully',\n fr: 'Votre souscription a été annulée avec succès',\n es: 'Su suscripción ha sido cancelada con éxito',\n ru: 'Ваша подписка была успешно отменена',\n ja: 'サブスクリプションは正常にキャンセルされました',\n ko: '구독이 성공적으로 취소되었습니다',\n zh: '您的订阅已成功取消',\n de: 'Ihr Abonnement wurde erfolgreich gekündigt',\n ar: 'لقد تم إلغاء اشتراكك بنجاح',\n it: 'Il tuo abbonamento è stato annullato con successo',\n pt: 'Sua assinatura foi cancelada com sucesso',\n hi: 'आपकी सदस्यता सफलतापूर्वक रद्द कर दी गई है',\n tr: 'Aboneliğiniz başarıyla iptal edildi',\n pl: 'Twoja subskrypcja została pomyślnie anulowana',\n id: 'Langganan Anda telah berhasil dibatalkan',\n vi: 'Đăng ký của bạn đã được hủy thành công',\n uk: 'Вашу передплату успішно скасовано',\n }),\n data: plan!,\n });\n\n // Send the response back to the client\n reply.send(formattedPlan);\n\n await emailService.sendEmail({\n type: 'subscriptionPaymentCancellation',\n to: user.email,\n email: user.email,\n cancellationDate: new Date().toLocaleDateString(),\n reactivateLink: `${process.env.APP_URL}/pricing`,\n username: user.name,\n organizationName: organization.name,\n planName: plan.type,\n });\n } catch (error) {\n // Handle any errors that occur during the cancellation process\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n/** Pulls the Stripe customer ID from the authenticated organization's plan. */\nconst getCustomerIdFromSession = (\n request: FastifyRequest\n): string | undefined => request.session?.organization?.plan?.customerId;\n\nexport type GetInvoicesResult = ResponseData<Stripe.Invoice[]>;\n\n/**\n * Lists Stripe invoices for the authenticated organization's customer.\n */\nexport const getInvoices = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const invoices = await stripe.invoices.list({ customer: customerId });\n\n return reply.send(\n formatResponse<GetInvoicesResult['data']>({ data: invoices.data })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetPaymentMethodResult = ResponseData<Stripe.PaymentMethod | null>;\n\n/**\n * Returns the first card payment method attached to the authenticated\n * organization's Stripe customer (or null if none).\n */\nexport const getPaymentMethod = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const paymentMethods = await stripe.paymentMethods.list({\n customer: customerId,\n type: 'card',\n });\n\n return reply.send(\n formatResponse<GetPaymentMethodResult['data']>({\n data: paymentMethods.data[0] ?? null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type CreatePortalSessionResult = ResponseData<{ url: string }>;\n\n/**\n * Creates a Stripe Billing Portal session for the authenticated organization\n * so the user can manage their payment method and subscription.\n */\nexport const createPortalSession = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const session = await stripe.billingPortal.sessions.create({\n customer: customerId,\n return_url: `${process.env.APP_URL ?? 'http://localhost:3000'}/dashboard`,\n });\n\n return reply.send(\n formatResponse<CreatePortalSessionResult['data']>({\n data: { url: session.url },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GrantAffiliateAccessBody = {\n userId: string;\n commissionRate?: number;\n commissionType?: 'recurring' | 'one_time';\n country?: string;\n};\n\nexport type GrantAffiliateAccessResult = ResponseData<AffiliateAPI>;\n\n/**\n * Admin-only: creates a Stripe Connect account and affiliate record for a user.\n */\nexport const grantAffiliateAccess = async (\n request: FastifyRequest<{ Body: GrantAffiliateAccessBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { userId, commissionRate, commissionType, country } = request.body;\n\n const affiliate = await affiliateService.createAffiliate(userId as any, {\n commissionRate,\n commissionType,\n country,\n });\n\n return reply.send(\n formatResponse<GrantAffiliateAccessResult['data']>({\n data: affiliate.toJSON() as unknown as AffiliateAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliatesParams = FiltersAndPagination<{ search?: string }>;\nexport type GetAffiliatesResult = PaginatedResponse<AffiliateAPI>;\n\n/**\n * Admin-only: returns a paginated list of all affiliate records.\n */\nexport const getAffiliates = async (\n request: FastifyRequest<{ Querystring: GetAffiliatesParams }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { filters, skip, pageSize, page, getNumberOfPages } =\n getFiltersAndPaginationFromBody<{ search?: string }>(request);\n\n const query: Record<string, unknown> = {};\n if (filters.search) {\n query.referralCode = { $regex: filters.search, $options: 'i' };\n }\n\n const affiliates = await affiliateService.findAffiliates(\n query,\n skip,\n pageSize\n );\n const totalItems = await affiliateService.countAffiliates(query);\n\n return reply.send(\n formatPaginatedResponse<AffiliateAPI>({\n data: affiliates.map(\n (affiliate) => affiliate.toJSON() as unknown as AffiliateAPI\n ),\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateByIdResult = ResponseData<AffiliateAPI | null>;\n\n/**\n * Admin-only: returns a single affiliate by ID.\n */\nexport const getAffiliateById = async (\n request: FastifyRequest<{ Params: { id: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const affiliate = await affiliateService.getAffiliateById(\n request.params.id\n );\n\n return reply.send(\n formatResponse<GetAffiliateByIdResult['data']>({\n data: affiliate\n ? (affiliate.toJSON() as unknown as AffiliateAPI)\n : null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateResult = ResponseData<AffiliateAPI | null>;\n\n/**\n * Returns the affiliate record for the authenticated user, or null if not an affiliate.\n */\nexport const getAffiliate = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const affiliate = await affiliateService.getAffiliateByUserId(user.id);\n\n return reply.send(\n formatResponse<GetAffiliateResult['data']>({\n data: affiliate\n ? (affiliate.toJSON() as unknown as AffiliateAPI)\n : null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateAccountSessionResult = ResponseData<{\n clientSecret: string;\n}>;\n\n/**\n * Creates a Stripe Connect account session for the authenticated affiliate.\n */\nexport const getAffiliateAccountSession = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const affiliate = await affiliateService.getAffiliateByUserId(user.id);\n\n if (!affiliate?.stripeAccountId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'AFFILIATE_NOT_FOUND'\n );\n }\n\n const clientSecret = await affiliateService.createAccountSession(\n affiliate.stripeAccountId\n );\n\n return reply.send(\n formatResponse<GetAffiliateAccountSessionResult['data']>({\n data: { clientSecret },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateOnboardingLinkResult = ResponseData<{ url: string }>;\n\n/**\n * Returns a Stripe-hosted onboarding URL for the authenticated affiliate.\n * Accepts an optional `returnUrl` query param to redirect back after onboarding.\n */\nexport const getAffiliateOnboardingLink = async (\n request: FastifyRequest<{ Querystring: { returnUrl?: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const affiliate = await affiliateService.getAffiliateByUserId(user.id);\n\n if (!affiliate?.stripeAccountId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'AFFILIATE_NOT_FOUND'\n );\n }\n\n const appUrl = process.env.APP_URL;\n const fallback = `${appUrl}/dashboard/affiliate`;\n const returnUrl = request.query.returnUrl ?? fallback;\n const refreshUrl = request.query.returnUrl ?? fallback;\n\n const url = await affiliateService.createOnboardingLink(\n affiliate.stripeAccountId,\n returnUrl,\n refreshUrl\n );\n\n return reply.send(\n formatResponse<GetAffiliateOnboardingLinkResult['data']>({\n data: { url },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateStatsResult = ResponseData<AffiliateStats | null>;\n\n/**\n * Returns referral stats for the authenticated affiliate.\n */\nexport const getAffiliateStats = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const stats = await affiliateService.getAffiliateStats(user.id);\n\n return reply.send(\n formatResponse<GetAffiliateStatsResult['data']>({ data: stats })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateInvitationsResult =\n PaginatedResponse<AffiliateInvitationAPI>;\n\n/**\n * Admin-only: returns a paginated list of all affiliate invitations.\n */\nexport const getAffiliateInvitations = async (\n request: FastifyRequest<{ Querystring: GetAffiliatesParams }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { filters, skip, pageSize, page, getNumberOfPages } =\n getFiltersAndPaginationFromBody<{ search?: string }>(request);\n\n const query: Record<string, unknown> = {};\n if (filters.search) {\n query.email = { $regex: filters.search, $options: 'i' };\n }\n\n const invitations = await affiliateService.findAffiliateInvitations(\n query,\n skip,\n pageSize\n );\n const totalItems = await affiliateService.countAffiliateInvitations(query);\n\n return reply.send(\n formatPaginatedResponse<AffiliateInvitationAPI>({\n data: invitations.map(\n (invitation) =>\n invitation.toJSON() as unknown as AffiliateInvitationAPI\n ),\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type SendAffiliateInvitationBody = {\n email: string;\n commissionRate?: number;\n commissionType?: 'recurring' | 'one_time';\n country?: string;\n};\n\nexport type SendAffiliateInvitationResult = ResponseData<{ sent: boolean }>;\n\n/**\n * Admin-only: creates an affiliate invitation and sends the email.\n */\nexport const sendAffiliateInvitation = async (\n request: FastifyRequest<{ Body: SendAffiliateInvitationBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { email, commissionRate, commissionType, country } = request.body;\n\n const invitation = await affiliateService.createAffiliateInvitation(\n email,\n user.id,\n { commissionRate, commissionType, country }\n );\n\n const inviteLink = `${process.env.APP_URL}/affiliation/${invitation.token}`;\n\n await emailService.sendEmail({\n type: 'affiliateInvitation',\n to: email,\n inviteLink,\n commissionRate: invitation.commissionRate,\n });\n\n return reply.send(\n formatResponse<SendAffiliateInvitationResult['data']>({\n data: { sent: true },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateInvitationResult =\n ResponseData<AffiliateInvitationAPI | null>;\n\n/**\n * Public: returns invitation details by token (for rendering the landing page).\n */\nexport const getAffiliateInvitation = async (\n request: FastifyRequest<{ Params: { token: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { token } = request.params;\n const invitation =\n await affiliateService.getAffiliateInvitationByToken(token);\n\n if (!invitation) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'AFFILIATE_INVITATION_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<GetAffiliateInvitationResult['data']>({\n data: invitation.toJSON() as unknown as AffiliateInvitationAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UpdateAffiliateStatusBody = {\n status?: 'active' | 'suspended';\n};\nexport type UpdateAffiliateStatusResult = ResponseData<AffiliateAPI>;\n\n/**\n * Admin-only: updates an affiliate's status.\n */\nexport const updateAffiliateStatus = async (\n request: FastifyRequest<{\n Params: { id: string };\n Body: UpdateAffiliateStatusBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { id } = request.params;\n const { status } = request.body;\n\n const affiliate = await affiliateService.setAffiliateStatus(id, {\n status,\n });\n\n if (!affiliate) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'AFFILIATE_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<UpdateAffiliateStatusResult['data']>({\n data: affiliate.toJSON() as unknown as AffiliateAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AcceptAffiliateInvitationResult = ResponseData<AffiliateAPI>;\n\n/**\n * Authenticated: accepts an invitation, creates the Stripe Connect account and affiliate record.\n */\nexport type AcceptAffiliateInvitationBody = {\n country?: string;\n stripeAccountType?: 'express' | 'standard';\n};\n\nexport const acceptAffiliateInvitation = async (\n request: FastifyRequest<{\n Params: { token: string };\n Body: AcceptAffiliateInvitationBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { token } = request.params;\n const { country, stripeAccountType } = request.body ?? {};\n\n const affiliate = await affiliateService.acceptAffiliateInvitation(\n token,\n user.id,\n { country, stripeAccountType, email: user.email }\n );\n\n const appUrl = process.env.APP_URL;\n\n await emailService.sendEmail({\n type: 'affiliateWelcome',\n to: user.email,\n dashboardLink: `${appUrl}/affiliation/${token}`,\n commissionRate: affiliate.commissionRate,\n });\n\n return reply.send(\n formatResponse<AcceptAffiliateInvitationResult['data']>({\n data: affiliate.toJSON() as unknown as AffiliateAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetPromoCodesQuerystring = GetAffiliatesParams & {\n affiliateId?: string;\n};\nexport type GetPromoCodesResult = PaginatedResponse<PromoCodeAPI>;\n\nexport const getPromoCodes = async (\n request: FastifyRequest<{ Querystring: GetPromoCodesQuerystring }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { skip, pageSize, page, getNumberOfPages } =\n getFiltersAndPaginationFromBody<{ search?: string }>(request);\n\n const affiliateIdFilter = (request.query as GetPromoCodesQuerystring)\n .affiliateId;\n\n const promoCodes = await promoCodeService.getPromoCodes(\n skip,\n pageSize,\n affiliateIdFilter\n );\n const totalItems =\n await promoCodeService.countPromoCodes(affiliateIdFilter);\n\n return reply.send(\n formatPaginatedResponse<PromoCodeAPI>({\n data: promoCodes.map(\n (promoCode) => promoCode.toJSON() as unknown as PromoCodeAPI\n ),\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetPromoCodeByIdResult = ResponseData<PromoCodeAPI>;\n\nexport const getPromoCodeById = async (\n request: FastifyRequest<{ Params: { id: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { id } = request.params;\n const promoCode = await promoCodeService.getPromoCodeById(id);\n\n if (!promoCode) {\n return reply.status(404).send({ error: 'Promo code not found' });\n }\n\n return reply.send(\n formatResponse<PromoCodeAPI>({\n data: promoCode.toJSON() as unknown as PromoCodeAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type CreatePromoCodeBody = {\n code: string;\n discountType: 'percentage' | 'amount';\n discountValue: number;\n currency?: string;\n affiliateId?: string;\n maxRedemptions?: number;\n expiresAt?: string;\n};\nexport type CreatePromoCodeResult = ResponseData<PromoCodeAPI>;\n\nexport const createPromoCode = async (\n request: FastifyRequest<{ Body: CreatePromoCodeBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const {\n code,\n discountType,\n discountValue,\n currency,\n affiliateId,\n maxRedemptions,\n expiresAt,\n } = request.body;\n\n const newPromo = await promoCodeService.createPromoCode({\n code,\n discountType,\n discountValue,\n currency,\n affiliateId,\n maxRedemptions,\n expiresAt: expiresAt ? new Date(expiresAt) : undefined,\n });\n\n return reply.send(\n formatResponse<PromoCodeAPI>({\n data: newPromo.toJSON() as unknown as PromoCodeAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UpdatePromoCodeBody = {\n affiliateId?: string | null;\n active?: boolean;\n maxRedemptions?: number;\n expiresAt?: string;\n};\nexport type UpdatePromoCodeResult = ResponseData<PromoCodeAPI>;\n\nexport const updatePromoCode = async (\n request: FastifyRequest<{\n Params: { id: string };\n Body: UpdatePromoCodeBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { id } = request.params;\n const { affiliateId, active, maxRedemptions, expiresAt } = request.body;\n\n const updated = await promoCodeService.updatePromoCode(id, {\n affiliateId,\n active,\n maxRedemptions,\n expiresAt: expiresAt ? new Date(expiresAt) : undefined,\n });\n\n if (!updated) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROMO_CODE_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<PromoCodeAPI>({\n data: updated.toJSON() as unknown as PromoCodeAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type DeletePromoCodeResult = ResponseData<{ deleted: boolean }>;\n\nexport const deletePromoCode = async (\n request: FastifyRequest<{ Params: { id: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { id } = request.params;\n const deleted = await promoCodeService.deletePromoCode(id);\n\n if (!deleted) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROMO_CODE_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<DeletePromoCodeResult['data']>({\n data: { deleted: true },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport const getAffiliatePromoCode = async (\n request: FastifyRequest<{ Params: { referralCode: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { referralCode } = request.params;\n const affiliate = await affiliateService.getAffiliateByCode(referralCode);\n\n if (!affiliate) {\n return reply.send(formatResponse<any>({ data: null }));\n }\n\n const promoCode = await PromoCodeModel.findOne({\n affiliateId: affiliate._id,\n active: true,\n });\n\n return reply.send(\n formatResponse<any>({\n data: promoCode ? promoCode.code : null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stripe.controller.mjs","names":["subscriptionService.getPricing","subscriptionService.getCouponId","affiliateService.getAffiliateById","affiliateService.trackReferral","subscriptionService.cancelSubscription","emailService.sendEmail","affiliateService.createAffiliate","affiliateService.findAffiliates","affiliateService.countAffiliates","affiliateService.getAffiliateByUserId","affiliateService.createAccountSession","affiliateService.createOnboardingLink","affiliateService.getAffiliateStats","affiliateService.findAffiliateInvitations","affiliateService.countAffiliateInvitations","affiliateService.createAffiliateInvitation","affiliateService.getAffiliateInvitationByToken","affiliateService.setAffiliateStatus","affiliateService.acceptAffiliateInvitation","promoCodeService.getPromoCodes","promoCodeService.countPromoCodes","promoCodeService.getPromoCodeById","promoCodeService.createPromoCode","promoCodeService.updatePromoCode","promoCodeService.deletePromoCode","affiliateService.getAffiliateByCode"],"sources":["../../../src/controllers/stripe.controller.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport { PromoCodeModel } from '@schemas/promoCode.schema';\nimport * as affiliateService from '@services/affiliate.service';\nimport * as emailService from '@services/email.service';\nimport * as promoCodeService from '@services/promoCode.service';\nimport * as subscriptionService from '@services/subscription.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport {\n type FiltersAndPagination,\n getFiltersAndPaginationFromBody,\n} from '@utils/filtersAndPagination/getFiltersAndPaginationFromBody';\nimport { isLifetimePriceId, retrievePlanInformation } from '@utils/plan';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type PaginatedResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport Stripe from 'stripe';\nimport type { AffiliateAPI, AffiliateStats } from '@/types/affiliate.types';\nimport type { AffiliateInvitationAPI } from '@/types/affiliateInvitation.types';\nimport type { Organization } from '@/types/organization.types';\nimport type { PromoCodeAPI } from '@/types/promoCode.types';\n\nexport type GetPricingBody = {\n priceIds?: string[];\n promoCode?: string;\n};\n\nexport type GetPricingResult = ResponseData<subscriptionService.PricingResult>;\n\n/**\n * Simulate pricing for a given set of prices and a promotion code.\n *\n * @param request - The request object containing the price IDs and promotion code.\n * @param reply - The response object to send the simulated pricing result.\n */\nexport const getPricing = async (\n request: FastifyRequest<{ Body: GetPricingBody }>,\n reply: FastifyReply\n) => {\n const { priceIds, promoCode } = request.body;\n\n const pricingResult = await subscriptionService.getPricing(\n priceIds,\n promoCode\n );\n\n const formattedPricingResult =\n formatResponse<subscriptionService.PricingResult>({\n data: pricingResult,\n });\n\n reply.code(200).send(formattedPricingResult);\n};\n\nexport type GetCheckoutSessionBody = {\n priceId: string;\n promoCode?: string;\n referralCode?: string;\n};\n\nexport type GetCheckoutSessionResult = ResponseData<{\n subscription?: Stripe.Response<Stripe.Subscription>;\n paymentIntent?: Stripe.Response<Stripe.PaymentIntent>;\n clientSecret: string;\n}>;\n\n/**\n * Handles subscription creation or update with Stripe and returns a ClientSecret.\n */\nexport const getSubscription = async (\n request: FastifyRequest<{ Body: GetCheckoutSessionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n // Extract organization and user from request locals (set by authentication middleware)\n const { organization, user } = request.session || {};\n const { priceId, promoCode, referralCode } = request.body;\n\n // Validate that the organization exists\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_FOUND'\n );\n }\n\n // Validate that the user exists\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { period, type } = retrievePlanInformation(priceId);\n\n if (\n organization.plan?.subscriptionId &&\n organization.plan?.type === type &&\n organization.plan?.period === period &&\n organization.plan?.status === 'active'\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ALREADY_SUBSCRIBED',\n {\n organizationId: organization.id,\n }\n );\n }\n\n // Attempt to retrieve the Stripe customer ID from the organization's plan\n let customerId = organization.plan?.customerId;\n\n if (!customerId) {\n // If no customer ID exists, create a new Stripe customer for the organization\n const customer = await stripe.customers.create({\n metadata: {\n organizationId: String(organization.id),\n userId: String(user.id),\n // Include the locale for potential localization\n locale: (request.session as unknown as { locale: Locale }).locale,\n },\n });\n customerId = customer.id;\n }\n\n const { couponId: promoCodeId, affiliateId: promoAffiliateId } = promoCode\n ? await subscriptionService.getCouponId(promoCode)\n : { couponId: null, affiliateId: undefined };\n\n let referralCodeToUse = referralCode;\n if (!referralCodeToUse && promoAffiliateId) {\n const promoAffiliate =\n await affiliateService.getAffiliateById(promoAffiliateId);\n if (promoAffiliate) {\n referralCodeToUse = promoAffiliate.referralCode;\n }\n }\n\n // Lifetime / one-time payment — handled with a PaymentIntent rather than a\n // recurring subscription so the price is charged once.\n if (isLifetimePriceId(priceId)) {\n const price = await stripe.prices.retrieve(priceId);\n\n if (!price.unit_amount) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n { user, organization, priceId }\n );\n }\n\n let amount = price.unit_amount;\n\n if (promoCodeId) {\n const coupon = await stripe.coupons.retrieve(promoCodeId);\n if (coupon.percent_off) {\n amount = Math.round(amount * (1 - coupon.percent_off / 100));\n } else if (coupon.amount_off) {\n amount = Math.max(0, amount - coupon.amount_off);\n }\n }\n\n const paymentIntent = await stripe.paymentIntents.create({\n customer: customerId,\n amount,\n currency: price.currency,\n payment_method_types: ['card'],\n metadata: {\n organizationId: String(organization.id),\n userId: String(user.id),\n priceId,\n purchaseType: 'lifetime',\n },\n });\n\n if (!paymentIntent?.client_secret) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n { user, organization, priceId }\n );\n }\n\n if (referralCodeToUse) {\n await affiliateService.trackReferral(\n referralCodeToUse,\n organization.id,\n paymentIntent.id,\n paymentIntent.amount,\n paymentIntent.currency\n );\n }\n\n return reply.send(\n formatResponse<GetCheckoutSessionResult['data']>({\n data: {\n paymentIntent,\n clientSecret: paymentIntent.client_secret,\n },\n })\n );\n }\n\n const discounts: Stripe.SubscriptionCreateParams.Discount[] = promoCodeId\n ? [{ coupon: promoCodeId }]\n : [];\n\n // If no subscription exists, create a new one\n const subscription = await stripe.subscriptions.create({\n customer: customerId, // Associate the subscription with the customer\n items: [{ price: priceId }], // Set the price ID for the subscription\n expand: ['latest_invoice.confirmation_secret'],\n payment_settings: {\n payment_method_types: ['card'], // Specify payment method types\n },\n payment_behavior: 'default_incomplete', // Create the subscription in an incomplete state until payment is confirmed\n discounts,\n });\n\n // Handle subscription creation failure\n if (!subscription) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n }\n\n const clientSecret = (\n subscription.latest_invoice as Stripe.Invoice & {\n confirmation_secret?: { client_secret: string };\n }\n )?.confirmation_secret?.client_secret;\n\n // Handle subscription creation failure\n if (!clientSecret) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n }\n\n if (referralCodeToUse) {\n await affiliateService.trackReferral(\n referralCodeToUse,\n organization.id,\n subscription.id\n );\n }\n\n // Prepare the response data with subscription details\n const responseData = formatResponse<GetCheckoutSessionResult['data']>({\n data: { subscription, clientSecret },\n });\n\n // Send the response back to the client\n return reply.send(responseData);\n } catch (error) {\n // Handle any errors that occur during the process\n\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\ntype CancelSubscriptionData = Organization['plan'];\nexport type CancelSubscriptionResult = ResponseData<CancelSubscriptionData>;\n\n/**\n * Cancels a subscription for an organization.\n */\nexport const cancelSubscription = async (\n _request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // Extract the organization and user from the request locals\n // These are typically set by authentication middleware earlier in the request pipeline\n const { organization, user } = _request.session || {};\n\n // Validate that the organization exists\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_FOUND'\n );\n }\n\n // Validate that the user exists\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n // Try to get the subscription ID from the organization's plan\n const subscriptionId = organization.plan?.subscriptionId;\n\n // Only attempt to cancel on Stripe if it's a subscription (not LIFETIME or FREE)\n if (subscriptionId && organization.plan?.type !== 'LIFETIME') {\n try {\n // Cancel the subscription on Stripe immediately using the subscription ID\n await stripe.subscriptions.cancel(subscriptionId);\n } catch (error) {\n // If the subscription is already canceled or not found on Stripe, we log it and continue\n // to ensure our local database remains in sync.\n console.error(\n `Stripe subscription cancellation failed for ${subscriptionId}:`,\n error\n );\n }\n }\n\n // Update the organization's plan in the database to reflect the cancellation\n const plan = await subscriptionService.cancelSubscription(\n subscriptionId,\n String(organization.id)\n );\n\n // If the plan could not be updated in the database, handle the error\n if (!plan) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n // Prepare a formatted response with a success message and the updated plan data\n const formattedPlan = formatResponse<CancelSubscriptionData>({\n message: t({\n en: 'Subscription cancelled successfully',\n 'en-GB': 'Subscription cancelled successfully',\n fr: 'Souscription annulée avec succès',\n es: 'Suscripción cancelada con éxito',\n ru: 'Подписка успешно отменена',\n ja: 'サブスクリプションが正常にキャンセルされました',\n ko: '구독이 성공적으로 취소되었습니다',\n zh: '订阅已成功取消',\n de: 'Abonnement erfolgreich gekündigt',\n ar: 'تم إلغاء الاشتراك بنجاح',\n it: 'Abbonamento annullato con successo',\n pt: 'Assinatura cancelada com sucesso',\n hi: 'सदस्यता सफलतापूर्वक रद्द कर दी गई',\n tr: 'Abonelik başarıyla iptal edildi',\n pl: 'Subskrypcja została pomyślnie anulowana',\n id: 'Langganan berhasil dibatalkan',\n vi: 'Đăng ký đã được hủy thành công',\n uk: 'Передплату успішно скасовано',\n }),\n description: t({\n en: 'Your subscription has been cancelled successfully',\n 'en-GB': 'Your subscription has been cancelled successfully',\n fr: 'Votre souscription a été annulée avec succès',\n es: 'Su suscripción ha sido cancelada con éxito',\n ru: 'Ваша подписка была успешно отменена',\n ja: 'サブスクリプションは正常にキャンセルされました',\n ko: '구독이 성공적으로 취소되었습니다',\n zh: '您的订阅已成功取消',\n de: 'Ihr Abonnement wurde erfolgreich gekündigt',\n ar: 'لقد تم إلغاء اشتراكك بنجاح',\n it: 'Il tuo abbonamento è stato annullato con successo',\n pt: 'Sua assinatura foi cancelada com sucesso',\n hi: 'आपकी सदस्यता सफलतापूर्वक रद्द कर दी गई है',\n tr: 'Aboneliğiniz başarıyla iptal edildi',\n pl: 'Twoja subskrypcja została pomyślnie anulowana',\n id: 'Langganan Anda telah berhasil dibatalkan',\n vi: 'Đăng ký của bạn đã được hủy thành công',\n uk: 'Вашу передплату успішно скасовано',\n }),\n data: plan!,\n });\n\n // Send the response back to the client\n reply.send(formattedPlan);\n\n await emailService.sendEmail({\n type: 'subscriptionPaymentCancellation',\n to: user.email,\n email: user.email,\n cancellationDate: new Date().toLocaleDateString(),\n reactivateLink: `${process.env.APP_URL}/pricing`,\n username: user.name,\n organizationName: organization.name,\n planName: plan.type,\n });\n } catch (error) {\n // Handle any errors that occur during the cancellation process\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n/** Pulls the Stripe customer ID from the authenticated organization's plan. */\nconst getCustomerIdFromSession = (\n request: FastifyRequest\n): string | undefined => request.session?.organization?.plan?.customerId;\n\nexport type GetInvoicesResult = ResponseData<Stripe.Invoice[]>;\n\n/**\n * Lists Stripe invoices for the authenticated organization's customer.\n */\nexport const getInvoices = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const invoices = await stripe.invoices.list({ customer: customerId });\n\n return reply.send(\n formatResponse<GetInvoicesResult['data']>({ data: invoices.data })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetPaymentMethodResult = ResponseData<Stripe.PaymentMethod | null>;\n\n/**\n * Returns the first card payment method attached to the authenticated\n * organization's Stripe customer (or null if none).\n */\nexport const getPaymentMethod = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const paymentMethods = await stripe.paymentMethods.list({\n customer: customerId,\n type: 'card',\n });\n\n return reply.send(\n formatResponse<GetPaymentMethodResult['data']>({\n data: paymentMethods.data[0] ?? null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type CreatePortalSessionResult = ResponseData<{ url: string }>;\n\n/**\n * Creates a Stripe Billing Portal session for the authenticated organization\n * so the user can manage their payment method and subscription.\n */\nexport const createPortalSession = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const session = await stripe.billingPortal.sessions.create({\n customer: customerId,\n return_url: `${process.env.APP_URL ?? 'http://localhost:3000'}/dashboard`,\n });\n\n return reply.send(\n formatResponse<CreatePortalSessionResult['data']>({\n data: { url: session.url },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GrantAffiliateAccessBody = {\n userId: string;\n commissionRate?: number;\n commissionType?: 'recurring' | 'one_time';\n country?: string;\n};\n\nexport type GrantAffiliateAccessResult = ResponseData<AffiliateAPI>;\n\n/**\n * Admin-only: creates a Stripe Connect account and affiliate record for a user.\n */\nexport const grantAffiliateAccess = async (\n request: FastifyRequest<{ Body: GrantAffiliateAccessBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { userId, commissionRate, commissionType, country } = request.body;\n\n const affiliate = await affiliateService.createAffiliate(userId as any, {\n commissionRate,\n commissionType,\n country,\n });\n\n return reply.send(\n formatResponse<GrantAffiliateAccessResult['data']>({\n data: affiliate.toJSON() as unknown as AffiliateAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliatesParams = FiltersAndPagination<{ search?: string }>;\nexport type GetAffiliatesResult = PaginatedResponse<AffiliateAPI>;\n\n/**\n * Admin-only: returns a paginated list of all affiliate records.\n */\nexport const getAffiliates = async (\n request: FastifyRequest<{ Querystring: GetAffiliatesParams }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { filters, skip, pageSize, page, getNumberOfPages } =\n getFiltersAndPaginationFromBody<{ search?: string }>(request);\n\n const query: Record<string, unknown> = {};\n if (filters.search) {\n query.referralCode = { $regex: filters.search, $options: 'i' };\n }\n\n const affiliates = await affiliateService.findAffiliates(\n query,\n skip,\n pageSize\n );\n const totalItems = await affiliateService.countAffiliates(query);\n\n return reply.send(\n formatPaginatedResponse<AffiliateAPI>({\n data: affiliates.map(\n (affiliate) => affiliate.toJSON() as unknown as AffiliateAPI\n ),\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateByIdResult = ResponseData<AffiliateAPI | null>;\n\n/**\n * Admin-only: returns a single affiliate by ID.\n */\nexport const getAffiliateById = async (\n request: FastifyRequest<{ Params: { id: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const affiliate = await affiliateService.getAffiliateById(\n request.params.id\n );\n\n return reply.send(\n formatResponse<GetAffiliateByIdResult['data']>({\n data: affiliate\n ? (affiliate.toJSON() as unknown as AffiliateAPI)\n : null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateResult = ResponseData<AffiliateAPI | null>;\n\n/**\n * Returns the affiliate record for the authenticated user, or null if not an affiliate.\n */\nexport const getAffiliate = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const affiliate = await affiliateService.getAffiliateByUserId(user.id);\n\n return reply.send(\n formatResponse<GetAffiliateResult['data']>({\n data: affiliate\n ? (affiliate.toJSON() as unknown as AffiliateAPI)\n : null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateAccountSessionResult = ResponseData<{\n clientSecret: string;\n}>;\n\n/**\n * Creates a Stripe Connect account session for the authenticated affiliate.\n */\nexport const getAffiliateAccountSession = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const affiliate = await affiliateService.getAffiliateByUserId(user.id);\n\n if (!affiliate?.stripeAccountId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'AFFILIATE_NOT_FOUND'\n );\n }\n\n const clientSecret = await affiliateService.createAccountSession(\n affiliate.stripeAccountId\n );\n\n return reply.send(\n formatResponse<GetAffiliateAccountSessionResult['data']>({\n data: { clientSecret },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateOnboardingLinkResult = ResponseData<{ url: string }>;\n\n/**\n * Returns a Stripe-hosted onboarding URL for the authenticated affiliate.\n * Accepts an optional `returnUrl` query param to redirect back after onboarding.\n */\nexport const getAffiliateOnboardingLink = async (\n request: FastifyRequest<{ Querystring: { returnUrl?: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const affiliate = await affiliateService.getAffiliateByUserId(user.id);\n\n if (!affiliate?.stripeAccountId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'AFFILIATE_NOT_FOUND'\n );\n }\n\n const appUrl = process.env.APP_URL;\n const fallback = `${appUrl}/dashboard/affiliate`;\n const returnUrl = request.query.returnUrl ?? fallback;\n const refreshUrl = request.query.returnUrl ?? fallback;\n\n const url = await affiliateService.createOnboardingLink(\n affiliate.stripeAccountId,\n returnUrl,\n refreshUrl\n );\n\n return reply.send(\n formatResponse<GetAffiliateOnboardingLinkResult['data']>({\n data: { url },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateStatsResult = ResponseData<AffiliateStats | null>;\n\n/**\n * Returns referral stats for the authenticated affiliate.\n */\nexport const getAffiliateStats = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const stats = await affiliateService.getAffiliateStats(user.id);\n\n return reply.send(\n formatResponse<GetAffiliateStatsResult['data']>({ data: stats })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateInvitationsResult =\n PaginatedResponse<AffiliateInvitationAPI>;\n\n/**\n * Admin-only: returns a paginated list of all affiliate invitations.\n */\nexport const getAffiliateInvitations = async (\n request: FastifyRequest<{ Querystring: GetAffiliatesParams }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { filters, skip, pageSize, page, getNumberOfPages } =\n getFiltersAndPaginationFromBody<{ search?: string }>(request);\n\n const query: Record<string, unknown> = {};\n if (filters.search) {\n query.email = { $regex: filters.search, $options: 'i' };\n }\n\n const invitations = await affiliateService.findAffiliateInvitations(\n query,\n skip,\n pageSize\n );\n const totalItems = await affiliateService.countAffiliateInvitations(query);\n\n return reply.send(\n formatPaginatedResponse<AffiliateInvitationAPI>({\n data: invitations.map(\n (invitation) =>\n invitation.toJSON() as unknown as AffiliateInvitationAPI\n ),\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type SendAffiliateInvitationBody = {\n email: string;\n commissionRate?: number;\n commissionType?: 'recurring' | 'one_time';\n country?: string;\n};\n\nexport type SendAffiliateInvitationResult = ResponseData<{ sent: boolean }>;\n\n/**\n * Admin-only: creates an affiliate invitation and sends the email.\n */\nexport const sendAffiliateInvitation = async (\n request: FastifyRequest<{ Body: SendAffiliateInvitationBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { email, commissionRate, commissionType, country } = request.body;\n\n const invitation = await affiliateService.createAffiliateInvitation(\n email,\n user.id,\n { commissionRate, commissionType, country }\n );\n\n const inviteLink = `${process.env.APP_URL}/affiliation/${invitation.token}`;\n\n await emailService.sendEmail({\n type: 'affiliateInvitation',\n to: email,\n inviteLink,\n commissionRate: invitation.commissionRate,\n });\n\n return reply.send(\n formatResponse<SendAffiliateInvitationResult['data']>({\n data: { sent: true },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetAffiliateInvitationResult =\n ResponseData<AffiliateInvitationAPI | null>;\n\n/**\n * Public: returns invitation details by token (for rendering the landing page).\n */\nexport const getAffiliateInvitation = async (\n request: FastifyRequest<{ Params: { token: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { token } = request.params;\n const invitation =\n await affiliateService.getAffiliateInvitationByToken(token);\n\n if (!invitation) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'AFFILIATE_INVITATION_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<GetAffiliateInvitationResult['data']>({\n data: invitation.toJSON() as unknown as AffiliateInvitationAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UpdateAffiliateStatusBody = {\n status?: 'active' | 'suspended';\n};\nexport type UpdateAffiliateStatusResult = ResponseData<AffiliateAPI>;\n\n/**\n * Admin-only: updates an affiliate's status.\n */\nexport const updateAffiliateStatus = async (\n request: FastifyRequest<{\n Params: { id: string };\n Body: UpdateAffiliateStatusBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { id } = request.params;\n const { status } = request.body;\n\n const affiliate = await affiliateService.setAffiliateStatus(id, {\n status,\n });\n\n if (!affiliate) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'AFFILIATE_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<UpdateAffiliateStatusResult['data']>({\n data: affiliate.toJSON() as unknown as AffiliateAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AcceptAffiliateInvitationResult = ResponseData<AffiliateAPI>;\n\n/**\n * Authenticated: accepts an invitation, creates the Stripe Connect account and affiliate record.\n */\nexport type AcceptAffiliateInvitationBody = {\n country?: string;\n stripeAccountType?: 'express' | 'standard';\n};\n\nexport const acceptAffiliateInvitation = async (\n request: FastifyRequest<{\n Params: { token: string };\n Body: AcceptAffiliateInvitationBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { token } = request.params;\n const { country, stripeAccountType } = request.body ?? {};\n\n const affiliate = await affiliateService.acceptAffiliateInvitation(\n token,\n user.id,\n { country, stripeAccountType, email: user.email }\n );\n\n const appUrl = process.env.APP_URL;\n\n await emailService.sendEmail({\n type: 'affiliateWelcome',\n to: user.email,\n dashboardLink: `${appUrl}/affiliation/${token}`,\n commissionRate: affiliate.commissionRate,\n });\n\n return reply.send(\n formatResponse<AcceptAffiliateInvitationResult['data']>({\n data: affiliate.toJSON() as unknown as AffiliateAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetPromoCodesQuerystring = GetAffiliatesParams & {\n affiliateId?: string;\n};\nexport type GetPromoCodesResult = PaginatedResponse<PromoCodeAPI>;\n\nexport const getPromoCodes = async (\n request: FastifyRequest<{ Querystring: GetPromoCodesQuerystring }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { skip, pageSize, page, getNumberOfPages } =\n getFiltersAndPaginationFromBody<{ search?: string }>(request);\n\n const affiliateIdFilter = (request.query as GetPromoCodesQuerystring)\n .affiliateId;\n\n const promoCodes = await promoCodeService.getPromoCodes(\n skip,\n pageSize,\n affiliateIdFilter\n );\n const totalItems =\n await promoCodeService.countPromoCodes(affiliateIdFilter);\n\n return reply.send(\n formatPaginatedResponse<PromoCodeAPI>({\n data: promoCodes.map(\n (promoCode) => promoCode.toJSON() as unknown as PromoCodeAPI\n ),\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetPromoCodeByIdResult = ResponseData<PromoCodeAPI>;\n\nexport const getPromoCodeById = async (\n request: FastifyRequest<{ Params: { id: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { id } = request.params;\n const promoCode = await promoCodeService.getPromoCodeById(id);\n\n if (!promoCode) {\n return reply.status(404).send({ error: 'Promo code not found' });\n }\n\n return reply.send(\n formatResponse<PromoCodeAPI>({\n data: promoCode.toJSON() as unknown as PromoCodeAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type CreatePromoCodeBody = {\n code: string;\n discountType: 'percentage' | 'amount';\n discountValue: number;\n currency?: string;\n affiliateId?: string;\n maxRedemptions?: number;\n expiresAt?: string;\n};\nexport type CreatePromoCodeResult = ResponseData<PromoCodeAPI>;\n\nexport const createPromoCode = async (\n request: FastifyRequest<{ Body: CreatePromoCodeBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const {\n code,\n discountType,\n discountValue,\n currency,\n affiliateId,\n maxRedemptions,\n expiresAt,\n } = request.body;\n\n const newPromo = await promoCodeService.createPromoCode({\n code,\n discountType,\n discountValue,\n currency,\n affiliateId,\n maxRedemptions,\n expiresAt: expiresAt ? new Date(expiresAt) : undefined,\n });\n\n return reply.send(\n formatResponse<PromoCodeAPI>({\n data: newPromo.toJSON() as unknown as PromoCodeAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UpdatePromoCodeBody = {\n affiliateId?: string | null;\n active?: boolean;\n maxRedemptions?: number;\n expiresAt?: string;\n};\nexport type UpdatePromoCodeResult = ResponseData<PromoCodeAPI>;\n\nexport const updatePromoCode = async (\n request: FastifyRequest<{\n Params: { id: string };\n Body: UpdatePromoCodeBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { id } = request.params;\n const { affiliateId, active, maxRedemptions, expiresAt } = request.body;\n\n const updated = await promoCodeService.updatePromoCode(id, {\n affiliateId,\n active,\n maxRedemptions,\n expiresAt: expiresAt ? new Date(expiresAt) : undefined,\n });\n\n if (!updated) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROMO_CODE_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<PromoCodeAPI>({\n data: updated.toJSON() as unknown as PromoCodeAPI,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type DeletePromoCodeResult = ResponseData<{ deleted: boolean }>;\n\nexport const deletePromoCode = async (\n request: FastifyRequest<{ Params: { id: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { user } = request.session || {};\n\n if (user?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { id } = request.params;\n const deleted = await promoCodeService.deletePromoCode(id);\n\n if (!deleted) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROMO_CODE_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<DeletePromoCodeResult['data']>({\n data: { deleted: true },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport const getAffiliatePromoCode = async (\n request: FastifyRequest<{ Params: { referralCode: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const { referralCode } = request.params;\n const affiliate = await affiliateService.getAffiliateByCode(referralCode);\n\n if (!affiliate) {\n return reply.send(formatResponse<any>({ data: null }));\n }\n\n const promoCode = await PromoCodeModel.findOne({\n affiliateId: affiliate._id,\n active: true,\n });\n\n return reply.send(\n formatResponse<any>({\n data: promoCode ? promoCode.code : null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.controller.mjs","names":["tagService.findTags","tagService.countTags","tagService.createTag","tagService.getTagById","tagService.updateTagById","tagService.deleteTagById"],"sources":["../../../src/controllers/tag.controller.ts"],"sourcesContent":["import { logger } from '@logger';\nimport * as tagService from '@services/tag.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport type { FiltersAndPagination } from '@utils/filtersAndPagination/getFiltersAndPaginationFromBody';\nimport {\n getTagFiltersAndPagination,\n type TagFiltersParams,\n} from '@utils/filtersAndPagination/getTagFiltersAndPagination';\nimport { mapTagsToAPI, mapTagToAPI } from '@utils/mapper/tag';\nimport { hasPermission } from '@utils/permissions';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type PaginatedResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport type {\n Tag,\n TagAPI,\n TagCreationData,\n TagData,\n TagSchema,\n} from '@/types/tag.types';\n\nexport type GetTagsParams = FiltersAndPagination<TagFiltersParams>;\nexport type GetTagsResult = PaginatedResponse<TagAPI>;\n\n/**\n * Retrieves a list of tags based on filters and pagination.\n */\nexport const getTags = async (\n request: FastifyRequest<{ Querystring: GetTagsParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, organization, roles } = request.session || {};\n const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } =\n getTagFiltersAndPagination(request);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_DEFINED'\n );\n }\n\n try {\n const tags = await tagService.findTags(\n filters,\n skip,\n pageSize,\n sortOptions\n );\n\n if (\n !hasPermission(\n roles || [],\n 'tag:read'\n )({\n ...request.session,\n targetTags: tags,\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n const totalItems = await tagService.countTags(filters);\n\n const formattedTags = mapTagsToAPI(tags);\n\n const responseData = formatPaginatedResponse<TagAPI>({\n data: formattedTags,\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AddTagBody = TagCreationData;\nexport type AddTagResult = ResponseData<TagAPI>;\n\n/**\n * Adds a new tag to the database.\n */\nexport const addTag = async (\n request: FastifyRequest<{ Body: AddTagBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { organization, project, user, roles } = request.session || {};\n const tagData = request.body;\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_DEFINED'\n );\n }\n\n if (!project) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROJECT_NOT_DEFINED'\n );\n }\n\n if (!tagData) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROJECT_DATA_NOT_FOUND'\n );\n }\n\n const tag: TagData = {\n creatorId: user.id,\n organizationId: organization.id,\n projectId: project.id,\n ...tagData,\n };\n\n if (\n !hasPermission(\n roles || [],\n 'tag:admin'\n )({\n ...request.session,\n targetTags: [tag as Tag],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n try {\n const newTag = await tagService.createTag(tag);\n\n const formattedTag = mapTagToAPI(newTag);\n\n const responseData = formatResponse<TagAPI>({\n message: t({\n en: 'Tag created successfully',\n 'en-GB': 'Tag created successfully',\n fr: 'Tag créé avec succès',\n es: 'Tag creado con éxito',\n ru: 'Тег успешно создан',\n ja: 'タグが正常に作成されました',\n ko: '태그가 성공적으로 생성되었습니다',\n zh: '标签已成功创建',\n de: 'Tag erfolgreich erstellt',\n ar: 'تم إنشاء العلامة بنجاح',\n it: 'Tag creato con successo',\n pt: 'Tag criada com sucesso',\n hi: 'टैग सफलतापूर्वक बनाया गया',\n tr: 'Etiket başarıyla oluşturuldu',\n pl: 'Tag został pomyślnie utworzony',\n id: 'Tag berhasil dibuat',\n vi: 'Thẻ đã được tạo thành công',\n uk: 'Тег успішно створено',\n }),\n description: t({\n en: 'Your tag has been created successfully',\n 'en-GB': 'Your tag has been created successfully',\n fr: 'Votre tag a été créé avec succès',\n es: 'Su tag ha sido creado con éxito',\n ru: 'Ваш тег был успешно создан',\n ja: 'タグは正常に作成されました',\n ko: '태그가 성공적으로 생성되었습니다',\n zh: '您的标签已成功创建',\n de: 'Ihr Tag wurde erfolgreich erstellt',\n ar: 'لقد تم إنشاء علامتك بنجاح',\n it: 'Il tuo tag è stato creato con successo',\n pt: 'Sua tag foi criada com sucesso',\n hi: 'आपका टैग सफलतापूर्वक बना लिया गया है',\n tr: 'Etiketiniz başarıyla oluşturuldu',\n pl: 'Twój tag został pomyślnie utworzony',\n id: 'Tag Anda telah berhasil dibuat',\n vi: 'Thẻ của bạn đã được tạo thành công',\n uk: 'Ваш тег успішно створено',\n }),\n data: formattedTag,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UpdateTagParams = { tagId: string | Tag['id'] };\nexport type UpdateTagBody = Partial<TagData>;\nexport type UpdateTagResult = ResponseData<TagAPI>;\n\n/**\n * Updates an existing tag in the database.\n */\nexport const updateTag = async (\n request: FastifyRequest<{ Params: UpdateTagParams; Body: UpdateTagBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { tagId } = request.params;\n const { organization, user, roles } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_DEFINED'\n );\n }\n\n try {\n const tag = {\n _id: tagId,\n name: request.body.name,\n key: request.body.key,\n description: request.body.description,\n instructions: request.body.instructions,\n } as Partial<TagSchema> & { _id: Tag['id'] };\n\n const tagToDelete = await tagService.getTagById(tagId);\n\n if (\n !hasPermission(\n roles || [],\n 'tag:write'\n )({\n ...request.session,\n targetTags: [tagToDelete],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n if (String(tagToDelete.organizationId) !== String(organization.id)) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'TAG_NOT_IN_ORGANIZATION'\n );\n }\n\n const updatedTag = await tagService.updateTagById(tag._id, tag);\n\n const formattedTag = mapTagToAPI(updatedTag);\n\n const responseData = formatResponse<TagAPI>({\n message: t({\n en: 'Tag updated successfully',\n 'en-GB': 'Tag updated successfully',\n fr: 'Tag mis à jour avec succès',\n es: 'Tag actualizado con éxito',\n ru: 'Тег успешно обновлен',\n ja: 'タグが正常に更新されました',\n ko: '태그가 성공적으로 업데이트되었습니다',\n zh: '标签已成功更新',\n de: 'Tag erfolgreich aktualisiert',\n ar: 'تم تحديث العلامة بنجاح',\n it: 'Tag aggiornato con successo',\n pt: 'Tag atualizada com sucesso',\n hi: 'टैग सफलतापूर्वक अपडेट किया गया',\n tr: 'Etiket başarıyla güncellendi',\n pl: 'Tag został pomyślnie zaktualizowany',\n id: 'Tag berhasil diperbarui',\n vi: 'Thẻ đã được cập nhật thành công',\n uk: 'Тег успішно оновлено',\n }),\n description: t({\n en: 'Your tag has been updated successfully',\n 'en-GB': 'Your tag has been updated successfully',\n fr: 'Votre tag a été mis à jour avec succès',\n es: 'Su tag ha sido actualizado con éxito',\n ru: 'Ваш тег был успешно обновлен',\n ja: 'タグは正常に更新されました',\n ko: '태그가 성공적으로 업데이트되었습니다',\n zh: '您的标签已成功更新',\n de: 'Ihr Tag wurde erfolgreich aktualisiert',\n ar: 'لقد تم تحديث علامتك بنجاح',\n it: 'Il tuo tag è stato aggiornato con successo',\n pt: 'Sua tag foi atualizada com sucesso',\n hi: 'आपका टैग सफलतापूर्वक अपडेट कर दिया गया है',\n tr: 'Etiketiniz başarıyla güncellendi',\n pl: 'Twój tag został pomyślnie zaktualizowany',\n id: 'Tag Anda telah berhasil diperbarui',\n vi: 'Thẻ của bạn đã được cập nhật thành công',\n uk: 'Ваш тег успішно оновлено',\n }),\n data: formattedTag,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type DeleteTagParams = { tagId: string | Tag['id'] };\nexport type DeleteTagResult = ResponseData<TagAPI>;\n\n/**\n * Deletes a tag from the database by its ID.\n */\nexport const deleteTag = async (\n request: FastifyRequest<{ Params: DeleteTagParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, organization, roles } = request.session || {};\n const { tagId } = request.params;\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_DEFINED'\n );\n }\n\n if (!tagId) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'TAG_ID_NOT_FOUND');\n }\n\n try {\n const tagToDelete = await tagService.getTagById(tagId);\n\n if (\n !hasPermission(\n roles || [],\n 'tag:admin'\n )({\n ...request.session,\n targetTags: [tagToDelete],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n if (String(tagToDelete.organizationId) !== String(organization.id)) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'TAG_NOT_IN_ORGANIZATION'\n );\n }\n\n const deletedTag = await tagService.deleteTagById(tagId);\n\n if (!deletedTag) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'TAG_NOT_FOUND', {\n tagId,\n });\n }\n\n logger.info(`Tag deleted: ${String(deletedTag.id)}`);\n\n const formattedTag = mapTagToAPI(deletedTag);\n\n const responseData = formatResponse<TagAPI>({\n message: t({\n en: 'Tag deleted successfully',\n 'en-GB': 'Tag deleted successfully',\n fr: 'Tag supprimé avec succès',\n es: 'Tag eliminado con éxito',\n ru: 'Тег успешно удален',\n ja: 'タグが正常に削除されました',\n ko: '태그가 성공적으로 삭제되었습니다',\n zh: '标签已成功删除',\n de: 'Tag erfolgreich gelöscht',\n ar: 'تم حذف العلامة بنجاح',\n it: 'Tag eliminato con successo',\n pt: 'Tag excluída com sucesso',\n hi: 'टैग सफलतापूर्वक हटा दिया गया',\n tr: 'Etiket başarıyla silindi',\n pl: 'Tag został pomyślnie usunięty',\n id: 'Tag berhasil dihapus',\n vi: 'Thẻ đã được xóa thành công',\n uk: 'Тег успішно видалено',\n }),\n description: t({\n en: 'Your tag has been deleted successfully',\n 'en-GB': 'Your tag has been deleted successfully',\n fr: 'Votre tag a été supprimé avec succès',\n es: 'Su tag ha sido eliminado con éxito',\n ru: 'Ваш тег был успешно удален',\n ja: 'タグは正常に削除されました',\n ko: '태그가 성공적으로 삭제되었습니다',\n zh: '您的标签已成功删除',\n de: 'Ihr Tag wurde erfolgreich gelöscht',\n ar: 'لقد تم حذف علامتك بنجاح',\n it: 'Il tuo tag è stato eliminato con successo',\n pt: 'Sua tag foi excluída com sucesso',\n hi: 'आपका टैग सफलतापूर्वक हटा दिया गया है',\n tr: 'Etiketiniz başarıyla silindi',\n pl: 'Twój tag został pomyślnie usunięty',\n id: 'Tag Anda telah berhasil dihapus',\n vi: 'Thẻ của bạn đã được xóa thành công',\n uk: 'Ваш тег успішно видалено',\n }),\n data: formattedTag,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tag.controller.mjs","names":["tagService.findTags","tagService.countTags","tagService.createTag","tagService.getTagById","tagService.updateTagById","tagService.deleteTagById"],"sources":["../../../src/controllers/tag.controller.ts"],"sourcesContent":["import { logger } from '@logger';\nimport * as tagService from '@services/tag.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport type { FiltersAndPagination } from '@utils/filtersAndPagination/getFiltersAndPaginationFromBody';\nimport {\n getTagFiltersAndPagination,\n type TagFiltersParams,\n} from '@utils/filtersAndPagination/getTagFiltersAndPagination';\nimport { mapTagsToAPI, mapTagToAPI } from '@utils/mapper/tag';\nimport { hasPermission } from '@utils/permissions';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type PaginatedResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport type {\n Tag,\n TagAPI,\n TagCreationData,\n TagData,\n TagSchema,\n} from '@/types/tag.types';\n\nexport type GetTagsParams = FiltersAndPagination<TagFiltersParams>;\nexport type GetTagsResult = PaginatedResponse<TagAPI>;\n\n/**\n * Retrieves a list of tags based on filters and pagination.\n */\nexport const getTags = async (\n request: FastifyRequest<{ Querystring: GetTagsParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, organization, roles } = request.session || {};\n const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } =\n getTagFiltersAndPagination(request);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_DEFINED'\n );\n }\n\n try {\n const tags = await tagService.findTags(\n filters,\n skip,\n pageSize,\n sortOptions\n );\n\n if (\n !hasPermission(\n roles || [],\n 'tag:read'\n )({\n ...request.session,\n targetTags: tags,\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n const totalItems = await tagService.countTags(filters);\n\n const formattedTags = mapTagsToAPI(tags);\n\n const responseData = formatPaginatedResponse<TagAPI>({\n data: formattedTags,\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type AddTagBody = TagCreationData;\nexport type AddTagResult = ResponseData<TagAPI>;\n\n/**\n * Adds a new tag to the database.\n */\nexport const addTag = async (\n request: FastifyRequest<{ Body: AddTagBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { organization, project, user, roles } = request.session || {};\n const tagData = request.body;\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_DEFINED'\n );\n }\n\n if (!project) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROJECT_NOT_DEFINED'\n );\n }\n\n if (!tagData) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROJECT_DATA_NOT_FOUND'\n );\n }\n\n const tag: TagData = {\n creatorId: user.id,\n organizationId: organization.id,\n projectId: project.id,\n ...tagData,\n };\n\n if (\n !hasPermission(\n roles || [],\n 'tag:admin'\n )({\n ...request.session,\n targetTags: [tag as Tag],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n try {\n const newTag = await tagService.createTag(tag);\n\n const formattedTag = mapTagToAPI(newTag);\n\n const responseData = formatResponse<TagAPI>({\n message: t({\n en: 'Tag created successfully',\n 'en-GB': 'Tag created successfully',\n fr: 'Tag créé avec succès',\n es: 'Tag creado con éxito',\n ru: 'Тег успешно создан',\n ja: 'タグが正常に作成されました',\n ko: '태그가 성공적으로 생성되었습니다',\n zh: '标签已成功创建',\n de: 'Tag erfolgreich erstellt',\n ar: 'تم إنشاء العلامة بنجاح',\n it: 'Tag creato con successo',\n pt: 'Tag criada com sucesso',\n hi: 'टैग सफलतापूर्वक बनाया गया',\n tr: 'Etiket başarıyla oluşturuldu',\n pl: 'Tag został pomyślnie utworzony',\n id: 'Tag berhasil dibuat',\n vi: 'Thẻ đã được tạo thành công',\n uk: 'Тег успішно створено',\n }),\n description: t({\n en: 'Your tag has been created successfully',\n 'en-GB': 'Your tag has been created successfully',\n fr: 'Votre tag a été créé avec succès',\n es: 'Su tag ha sido creado con éxito',\n ru: 'Ваш тег был успешно создан',\n ja: 'タグは正常に作成されました',\n ko: '태그가 성공적으로 생성되었습니다',\n zh: '您的标签已成功创建',\n de: 'Ihr Tag wurde erfolgreich erstellt',\n ar: 'لقد تم إنشاء علامتك بنجاح',\n it: 'Il tuo tag è stato creato con successo',\n pt: 'Sua tag foi criada com sucesso',\n hi: 'आपका टैग सफलतापूर्वक बना लिया गया है',\n tr: 'Etiketiniz başarıyla oluşturuldu',\n pl: 'Twój tag został pomyślnie utworzony',\n id: 'Tag Anda telah berhasil dibuat',\n vi: 'Thẻ của bạn đã được tạo thành công',\n uk: 'Ваш тег успішно створено',\n }),\n data: formattedTag,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UpdateTagParams = { tagId: string | Tag['id'] };\nexport type UpdateTagBody = Partial<TagData>;\nexport type UpdateTagResult = ResponseData<TagAPI>;\n\n/**\n * Updates an existing tag in the database.\n */\nexport const updateTag = async (\n request: FastifyRequest<{ Params: UpdateTagParams; Body: UpdateTagBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { tagId } = request.params;\n const { organization, user, roles } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_DEFINED'\n );\n }\n\n try {\n const tag = {\n _id: tagId,\n name: request.body.name,\n key: request.body.key,\n description: request.body.description,\n instructions: request.body.instructions,\n } as Partial<TagSchema> & { _id: Tag['id'] };\n\n const tagToDelete = await tagService.getTagById(tagId);\n\n if (\n !hasPermission(\n roles || [],\n 'tag:write'\n )({\n ...request.session,\n targetTags: [tagToDelete],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n if (String(tagToDelete.organizationId) !== String(organization.id)) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'TAG_NOT_IN_ORGANIZATION'\n );\n }\n\n const updatedTag = await tagService.updateTagById(tag._id, tag);\n\n const formattedTag = mapTagToAPI(updatedTag);\n\n const responseData = formatResponse<TagAPI>({\n message: t({\n en: 'Tag updated successfully',\n 'en-GB': 'Tag updated successfully',\n fr: 'Tag mis à jour avec succès',\n es: 'Tag actualizado con éxito',\n ru: 'Тег успешно обновлен',\n ja: 'タグが正常に更新されました',\n ko: '태그가 성공적으로 업데이트되었습니다',\n zh: '标签已成功更新',\n de: 'Tag erfolgreich aktualisiert',\n ar: 'تم تحديث العلامة بنجاح',\n it: 'Tag aggiornato con successo',\n pt: 'Tag atualizada com sucesso',\n hi: 'टैग सफलतापूर्वक अपडेट किया गया',\n tr: 'Etiket başarıyla güncellendi',\n pl: 'Tag został pomyślnie zaktualizowany',\n id: 'Tag berhasil diperbarui',\n vi: 'Thẻ đã được cập nhật thành công',\n uk: 'Тег успішно оновлено',\n }),\n description: t({\n en: 'Your tag has been updated successfully',\n 'en-GB': 'Your tag has been updated successfully',\n fr: 'Votre tag a été mis à jour avec succès',\n es: 'Su tag ha sido actualizado con éxito',\n ru: 'Ваш тег был успешно обновлен',\n ja: 'タグは正常に更新されました',\n ko: '태그가 성공적으로 업데이트되었습니다',\n zh: '您的标签已成功更新',\n de: 'Ihr Tag wurde erfolgreich aktualisiert',\n ar: 'لقد تم تحديث علامتك بنجاح',\n it: 'Il tuo tag è stato aggiornato con successo',\n pt: 'Sua tag foi atualizada com sucesso',\n hi: 'आपका टैग सफलतापूर्वक अपडेट कर दिया गया है',\n tr: 'Etiketiniz başarıyla güncellendi',\n pl: 'Twój tag został pomyślnie zaktualizowany',\n id: 'Tag Anda telah berhasil diperbarui',\n vi: 'Thẻ của bạn đã được cập nhật thành công',\n uk: 'Ваш тег успішно оновлено',\n }),\n data: formattedTag,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type DeleteTagParams = { tagId: string | Tag['id'] };\nexport type DeleteTagResult = ResponseData<TagAPI>;\n\n/**\n * Deletes a tag from the database by its ID.\n */\nexport const deleteTag = async (\n request: FastifyRequest<{ Params: DeleteTagParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, organization, roles } = request.session || {};\n const { tagId } = request.params;\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_DEFINED'\n );\n }\n\n if (!tagId) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'TAG_ID_NOT_FOUND');\n }\n\n try {\n const tagToDelete = await tagService.getTagById(tagId);\n\n if (\n !hasPermission(\n roles || [],\n 'tag:admin'\n )({\n ...request.session,\n targetTags: [tagToDelete],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n if (String(tagToDelete.organizationId) !== String(organization.id)) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'TAG_NOT_IN_ORGANIZATION'\n );\n }\n\n const deletedTag = await tagService.deleteTagById(tagId);\n\n if (!deletedTag) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'TAG_NOT_FOUND', {\n tagId,\n });\n }\n\n logger.info(`Tag deleted: ${String(deletedTag.id)}`);\n\n const formattedTag = mapTagToAPI(deletedTag);\n\n const responseData = formatResponse<TagAPI>({\n message: t({\n en: 'Tag deleted successfully',\n 'en-GB': 'Tag deleted successfully',\n fr: 'Tag supprimé avec succès',\n es: 'Tag eliminado con éxito',\n ru: 'Тег успешно удален',\n ja: 'タグが正常に削除されました',\n ko: '태그가 성공적으로 삭제되었습니다',\n zh: '标签已成功删除',\n de: 'Tag erfolgreich gelöscht',\n ar: 'تم حذف العلامة بنجاح',\n it: 'Tag eliminato con successo',\n pt: 'Tag excluída com sucesso',\n hi: 'टैग सफलतापूर्वक हटा दिया गया',\n tr: 'Etiket başarıyla silindi',\n pl: 'Tag został pomyślnie usunięty',\n id: 'Tag berhasil dihapus',\n vi: 'Thẻ đã được xóa thành công',\n uk: 'Тег успішно видалено',\n }),\n description: t({\n en: 'Your tag has been deleted successfully',\n 'en-GB': 'Your tag has been deleted successfully',\n fr: 'Votre tag a été supprimé avec succès',\n es: 'Su tag ha sido eliminado con éxito',\n ru: 'Ваш тег был успешно удален',\n ja: 'タグは正常に削除されました',\n ko: '태그가 성공적으로 삭제되었습니다',\n zh: '您的标签已成功删除',\n de: 'Ihr Tag wurde erfolgreich gelöscht',\n ar: 'لقد تم حذف علامتك بنجاح',\n it: 'Il tuo tag è stato eliminato con successo',\n pt: 'Sua tag foi excluída com sucesso',\n hi: 'आपका टैग सफलतापूर्वक हटा दिया गया है',\n tr: 'Etiketiniz başarıyla silindi',\n pl: 'Twój tag został pomyślnie usunięty',\n id: 'Tag Anda telah berhasil dihapus',\n vi: 'Thẻ của bạn đã được xóa thành công',\n uk: 'Ваш тег успішно видалено',\n }),\n data: formattedTag,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation.controller.mjs","names":["dictionaryService.findDictionaries"],"sources":["../../../src/controllers/translation.controller.ts"],"sourcesContent":["import { getMissingLocalesContentFromDictionary } from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport * as dictionaryService from '@services/dictionary.service';\nimport {\n addTranslationJob,\n getTranslationQueue,\n isTranslationJobPaused,\n translationCancelKey,\n translationPauseKey,\n} from '@services/translationQueue.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { getRedisClient } from '@utils/redis/connectRedis';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { Types } from 'mongoose';\n\nexport type TranslateDictionariesBody = {\n dictionaryIds: string[];\n targetLocales: Locale[];\n mode?: 'complete' | 'review';\n};\nexport type TranslateDictionariesResult = ResponseData<{ jobId: string }>;\n\nexport const translateDictionaries = async (\n request: FastifyRequest<{ Body: TranslateDictionariesBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { dictionaryIds, targetLocales, mode = 'complete' } = request.body;\n\n if (!project) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROJECT_NOT_DEFINED'\n );\n }\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const validIds = dictionaryIds.filter((id) => Types.ObjectId.isValid(id));\n\n if (validIds.length === 0) {\n return reply.send(\n formatResponse({\n data: null,\n message: 'No valid dictionary IDs provided',\n })\n );\n }\n\n const dictionaries = await dictionaryService.findDictionaries(\n { _id: { $in: validIds.map((id) => new Types.ObjectId(id)) } },\n 0,\n validIds.length,\n undefined,\n true\n );\n\n const dictionaryTargets: { dictionaryId: string; locales: Locale[] }[] = [];\n\n const projectLocales =\n project.configuration?.internationalization?.locales ?? [];\n\n for (const dictionary of dictionaries) {\n if (!dictionary.content) continue;\n\n const versionList = Array.from(dictionary.content.keys());\n const lastVersion = versionList[versionList.length - 1] || 'v1';\n const node = dictionary.content.get(lastVersion);\n\n if (!node) continue;\n\n // In 'complete' mode skip locales that already have full translations.\n // In 'review' mode translate everything regardless.\n let localesToTranslate: Locale[];\n if (mode === 'review') {\n localesToTranslate = targetLocales;\n } else {\n const missingLocales = getMissingLocalesContentFromDictionary(\n { key: dictionary.key, content: node.content, schema: undefined },\n projectLocales\n );\n localesToTranslate = targetLocales.filter((locale) =>\n missingLocales.includes(locale)\n );\n }\n\n if (localesToTranslate.length > 0) {\n dictionaryTargets.push({\n dictionaryId: String(dictionary._id),\n locales: localesToTranslate,\n });\n }\n }\n\n if (dictionaryTargets.length === 0) {\n return reply.send(\n formatResponse({\n data: null,\n message: 'All dictionaries are already translated',\n })\n );\n }\n\n const job = await addTranslationJob({\n dictionaryTargets,\n projectId: String(project.id),\n userId: String(user.id),\n mode,\n });\n\n return reply.send(\n formatResponse<{ jobId: string }>({\n data: { jobId: job.id! },\n message: 'Translation started',\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport const pauseTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.set(translationPauseKey(jobId), '1', 'EX', 86400);\n return reply.send(formatResponse({ data: { jobId }, message: 'Paused' }));\n};\n\nexport const resumeTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.del(translationPauseKey(jobId));\n return reply.send(formatResponse({ data: { jobId }, message: 'Resumed' }));\n};\n\nexport const stopTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.set(translationCancelKey(jobId), '1', 'EX', 86400);\n await redis.del(translationPauseKey(jobId));\n\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (job) {\n const state = await job.getState();\n if (state === 'waiting' || state === 'delayed') {\n await job.remove();\n }\n }\n return reply.send(formatResponse({ data: { jobId }, message: 'Stopped' }));\n};\n\nexport const retryTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (!job) return reply.status(404).send({ error: 'Job not found' });\n const redis = getRedisClient();\n await redis.del(translationCancelKey(jobId));\n await redis.del(translationPauseKey(jobId));\n await job.retry();\n return reply.send(formatResponse({ data: { jobId }, message: 'Retrying' }));\n};\n\nexport const restartTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (!job) return reply.status(404).send({ error: 'Job not found' });\n const newJob = await addTranslationJob(job.data);\n return reply.send(\n formatResponse({ data: { jobId: newJob.id! }, message: 'Restarted' })\n );\n};\n\nexport const getTranslationStatus = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n\n if (!user) {\n reply.raw.statusCode = 401;\n reply.raw.end();\n return;\n }\n\n reply.hijack();\n\n const headers = reply.getHeaders();\n Object.entries(headers).forEach(([key, value]) => {\n if (value !== undefined) {\n reply.raw.setHeader(key, value);\n }\n });\n\n const sseHeaders = {\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n };\n\n Object.entries(sseHeaders).forEach(([key, value]) => {\n reply.raw.setHeader(key, value);\n });\n reply.raw.flushHeaders?.();\n\n reply.raw.write(': connected\\n\\n');\n\n const send = (data: any) => {\n if (!reply.raw.writableEnded && !reply.raw.destroyed) {\n reply.raw.write(`data: ${JSON.stringify(data)}\\n\\n`);\n }\n };\n\n const projectId = project ? String(project.id) : null;\n const userId = String(user.id);\n\n const matchesSession = (job: any) =>\n projectId\n ? String(job.data.projectId) === projectId\n : String(job.data.userId) === userId;\n\n const sendJob = async (job: any) => {\n const state = await job.getState();\n const isPaused = await isTranslationJobPaused(job.id);\n send({\n jobId: job.id,\n state,\n isPaused,\n progress: job.progress,\n data: job.data,\n });\n };\n\n try {\n const translationQueue = getTranslationQueue();\n\n const getRelevantJobs = async () => {\n const jobs = await translationQueue.getJobs([\n 'active',\n 'waiting',\n 'delayed',\n 'completed',\n 'failed',\n ]);\n return jobs.filter(matchesSession);\n };\n\n const jobs = await getRelevantJobs();\n for (const job of jobs) {\n await sendJob(job);\n }\n\n const interval = setInterval(async () => {\n try {\n const currentJobs = await translationQueue.getJobs([\n 'active',\n 'waiting',\n 'delayed',\n 'completed',\n 'failed',\n ]);\n const relevantJobs = currentJobs.filter(matchesSession);\n for (const job of relevantJobs) {\n await sendJob(job);\n }\n } catch (error) {\n logger.error('Error polling translation status', error);\n }\n }, 2000);\n\n request.raw.on('close', () => {\n clearInterval(interval);\n });\n } catch (error) {\n logger.error('Error in translation status stream', error);\n if (!reply.raw.writableEnded && !reply.raw.destroyed) {\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify({ message: 'Internal Server Error' })}\\n\\n`\n );\n reply.raw.end();\n }\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"translation.controller.mjs","names":["dictionaryService.findDictionaries"],"sources":["../../../src/controllers/translation.controller.ts"],"sourcesContent":["import { getMissingLocalesContentFromDictionary } from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport * as dictionaryService from '@services/dictionary.service';\nimport {\n addTranslationJob,\n getTranslationQueue,\n isTranslationJobPaused,\n translationCancelKey,\n translationPauseKey,\n} from '@services/translationQueue.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { getRedisClient } from '@utils/redis/connectRedis';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { Types } from 'mongoose';\n\nexport type TranslateDictionariesBody = {\n dictionaryIds: string[];\n targetLocales: Locale[];\n mode?: 'complete' | 'review';\n};\nexport type TranslateDictionariesResult = ResponseData<{ jobId: string }>;\n\nexport const translateDictionaries = async (\n request: FastifyRequest<{ Body: TranslateDictionariesBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { dictionaryIds, targetLocales, mode = 'complete' } = request.body;\n\n if (!project) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROJECT_NOT_DEFINED'\n );\n }\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const validIds = dictionaryIds.filter((id) => Types.ObjectId.isValid(id));\n\n if (validIds.length === 0) {\n return reply.send(\n formatResponse({\n data: null,\n message: 'No valid dictionary IDs provided',\n })\n );\n }\n\n const dictionaries = await dictionaryService.findDictionaries(\n { _id: { $in: validIds.map((id) => new Types.ObjectId(id)) } },\n 0,\n validIds.length,\n undefined,\n true\n );\n\n const dictionaryTargets: { dictionaryId: string; locales: Locale[] }[] = [];\n\n const projectLocales =\n project.configuration?.internationalization?.locales ?? [];\n\n for (const dictionary of dictionaries) {\n if (!dictionary.content) continue;\n\n const versionList = Array.from(dictionary.content.keys());\n const lastVersion = versionList[versionList.length - 1] || 'v1';\n const node = dictionary.content.get(lastVersion);\n\n if (!node) continue;\n\n // In 'complete' mode skip locales that already have full translations.\n // In 'review' mode translate everything regardless.\n let localesToTranslate: Locale[];\n if (mode === 'review') {\n localesToTranslate = targetLocales;\n } else {\n const missingLocales = getMissingLocalesContentFromDictionary(\n { key: dictionary.key, content: node.content, schema: undefined },\n projectLocales\n );\n localesToTranslate = targetLocales.filter((locale) =>\n missingLocales.includes(locale)\n );\n }\n\n if (localesToTranslate.length > 0) {\n dictionaryTargets.push({\n dictionaryId: String(dictionary._id),\n locales: localesToTranslate,\n });\n }\n }\n\n if (dictionaryTargets.length === 0) {\n return reply.send(\n formatResponse({\n data: null,\n message: 'All dictionaries are already translated',\n })\n );\n }\n\n const job = await addTranslationJob({\n dictionaryTargets,\n projectId: String(project.id),\n userId: String(user.id),\n mode,\n });\n\n return reply.send(\n formatResponse<{ jobId: string }>({\n data: { jobId: job.id! },\n message: 'Translation started',\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport const pauseTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.set(translationPauseKey(jobId), '1', 'EX', 86400);\n return reply.send(formatResponse({ data: { jobId }, message: 'Paused' }));\n};\n\nexport const resumeTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.del(translationPauseKey(jobId));\n return reply.send(formatResponse({ data: { jobId }, message: 'Resumed' }));\n};\n\nexport const stopTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.set(translationCancelKey(jobId), '1', 'EX', 86400);\n await redis.del(translationPauseKey(jobId));\n\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (job) {\n const state = await job.getState();\n if (state === 'waiting' || state === 'delayed') {\n await job.remove();\n }\n }\n return reply.send(formatResponse({ data: { jobId }, message: 'Stopped' }));\n};\n\nexport const retryTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (!job) return reply.status(404).send({ error: 'Job not found' });\n const redis = getRedisClient();\n await redis.del(translationCancelKey(jobId));\n await redis.del(translationPauseKey(jobId));\n await job.retry();\n return reply.send(formatResponse({ data: { jobId }, message: 'Retrying' }));\n};\n\nexport const restartTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (!job) return reply.status(404).send({ error: 'Job not found' });\n const newJob = await addTranslationJob(job.data);\n return reply.send(\n formatResponse({ data: { jobId: newJob.id! }, message: 'Restarted' })\n );\n};\n\nexport const getTranslationStatus = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n\n if (!user) {\n reply.raw.statusCode = 401;\n reply.raw.end();\n return;\n }\n\n reply.hijack();\n\n const headers = reply.getHeaders();\n Object.entries(headers).forEach(([key, value]) => {\n if (value !== undefined) {\n reply.raw.setHeader(key, value);\n }\n });\n\n const sseHeaders = {\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n };\n\n Object.entries(sseHeaders).forEach(([key, value]) => {\n reply.raw.setHeader(key, value);\n });\n reply.raw.flushHeaders?.();\n\n reply.raw.write(': connected\\n\\n');\n\n const send = (data: any) => {\n if (!reply.raw.writableEnded && !reply.raw.destroyed) {\n reply.raw.write(`data: ${JSON.stringify(data)}\\n\\n`);\n }\n };\n\n const projectId = project ? String(project.id) : null;\n const userId = String(user.id);\n\n const matchesSession = (job: any) =>\n projectId\n ? String(job.data.projectId) === projectId\n : String(job.data.userId) === userId;\n\n const sendJob = async (job: any) => {\n const state = await job.getState();\n const isPaused = await isTranslationJobPaused(job.id);\n send({\n jobId: job.id,\n state,\n isPaused,\n progress: job.progress,\n data: job.data,\n });\n };\n\n try {\n const translationQueue = getTranslationQueue();\n\n const getRelevantJobs = async () => {\n const jobs = await translationQueue.getJobs([\n 'active',\n 'waiting',\n 'delayed',\n 'completed',\n 'failed',\n ]);\n return jobs.filter(matchesSession);\n };\n\n const jobs = await getRelevantJobs();\n for (const job of jobs) {\n await sendJob(job);\n }\n\n const interval = setInterval(async () => {\n try {\n const currentJobs = await translationQueue.getJobs([\n 'active',\n 'waiting',\n 'delayed',\n 'completed',\n 'failed',\n ]);\n const relevantJobs = currentJobs.filter(matchesSession);\n for (const job of relevantJobs) {\n await sendJob(job);\n }\n } catch (error) {\n logger.error('Error polling translation status', error);\n }\n }, 2000);\n\n request.raw.on('close', () => {\n clearInterval(interval);\n });\n } catch (error) {\n logger.error('Error in translation status stream', error);\n if (!reply.raw.writableEnded && !reply.raw.destroyed) {\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify({ message: 'Internal Server Error' })}\\n\\n`\n );\n reply.raw.end();\n }\n }\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.controller.mjs","names":["userService.createUser","userService.findUsers","userService.countUsers","userService.getUserById","userService.getUserByEmail","userService.updateUserById","userService.deleteUser"],"sources":["../../../src/controllers/user.controller.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { sendEmail } from '@services/email.service';\nimport {\n deleteUserAvatar,\n uploadUserAvatar,\n validateAvatarUpload,\n} from '@services/user/avatarUpload.service';\nimport * as userService from '@services/user.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport type { FiltersAndPagination } from '@utils/filtersAndPagination/getFiltersAndPaginationFromBody';\nimport {\n getUserFiltersAndPagination,\n type UserFiltersParam,\n} from '@utils/filtersAndPagination/getUserFiltersAndPagination';\nimport { mapUsersToAPI, mapUserToAPI } from '@utils/mapper/user';\nimport { hasPermission } from '@utils/permissions';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type PaginatedResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport type { User, UserAPI } from '@/types/user.types';\n\nexport type CreateUserBody = { email: string; password?: string };\nexport type CreateUserResult = ResponseData<UserAPI>;\n\n/**\n * Creates a new user.\n */\nexport const createUser = async (\n request: FastifyRequest<{ Body: User }>,\n reply: FastifyReply\n): Promise<void> => {\n const user: User | undefined = request.body;\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const newUser = await userService.createUser(user);\n\n await sendEmail({\n type: 'welcome',\n to: newUser.email,\n username: newUser.name,\n loginLink: `${process.env.APP_URL}/auth/login`,\n });\n\n const formattedUser = mapUserToAPI(newUser);\n\n const responseData = formatResponse<UserAPI>({\n message: t({\n en: 'User created',\n 'en-GB': 'User created',\n fr: 'Utilisateur créé',\n es: 'Usuario creado',\n ru: 'Пользователь создан',\n ja: 'ユーザーが作成されました',\n ko: '사용자가 생성되었습니다',\n zh: '用户已创建',\n de: 'Benutzer erstellt',\n ar: 'تم إنشاء المستخدم',\n it: 'Utente creato',\n pt: 'Usuário criado',\n hi: 'उपयोगकर्ता बनाया गया',\n tr: 'Kullanıcı oluşturuldu',\n pl: 'Użytkownik został utworzony',\n id: 'Pengguna dibuat',\n vi: 'Người dùng đã được tạo',\n uk: 'Користувача створено',\n }),\n description: t({\n en: 'User created successfully',\n 'en-GB': 'User created successfully',\n fr: 'Utilisateur créé avec succès',\n es: 'Usuario creado con éxito',\n ru: 'Пользователь успешно создан',\n ja: 'ユーザーが正常に作成されました',\n ko: '사용자가 성공적으로 생성되었습니다',\n zh: '用户已成功创建',\n de: 'Benutzer erfolgreich erstellt',\n ar: 'تم إنشاء المستخدم بنجاح',\n it: 'Utente creato con successo',\n pt: 'Usuário criado com sucesso',\n hi: 'उपयोगकर्ता सफलतापूर्वक बनाया गया',\n tr: 'Kullanıcı başarıyla oluşturuldu',\n pl: 'Użytkownik został pomyślnie utworzony',\n id: 'Pengguna berhasil dibuat',\n vi: 'Người dùng đã được tạo thành công',\n uk: 'Користувача успішно створено',\n }),\n data: formattedUser,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetUsersParams = FiltersAndPagination<UserFiltersParam>;\nexport type GetUsersResult = PaginatedResponse<UserAPI>;\n\n/**\n * Retrieves a list of users based on filters and pagination.\n */\nexport const getUsers = async (\n request: FastifyRequest<{ Querystring: GetUsersParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, roles } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } =\n getUserFiltersAndPagination(request);\n\n try {\n const users = await userService.findUsers(\n filters,\n skip,\n pageSize,\n sortOptions\n );\n\n // Skip permission check when there are no users to protect.\n // An empty result is safe to return without checking roles.\n if (\n users.length > 0 &&\n !hasPermission(\n roles || [],\n 'user:read'\n )({\n ...request.session,\n targetUsers: users,\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n const totalItems = await userService.countUsers(filters);\n\n const formattedUsers = mapUsersToAPI(users);\n\n const responseData = formatPaginatedResponse<UserAPI>({\n data: formattedUsers,\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetUserByIdParams = { userId: UserAPI['id'] };\nexport type GetUserByIdResult = ResponseData<UserAPI>;\n\nexport const getUserById = async (\n request: FastifyRequest<{ Params: GetUserByIdParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { userId } = request.params;\n const { user: sessionUser } = request.session || {};\n\n if (!sessionUser) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (\n String(sessionUser.id) !== String(userId) &&\n sessionUser.role !== 'admin'\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n try {\n const user = await userService.getUserById(userId);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const formattedUser = mapUserToAPI(user);\n const responseData = formatResponse<UserAPI>({ data: formattedUser });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetUserByEmailParams = { email: string };\nexport type GetUserByEmailResult = ResponseData<UserAPI>;\n\nexport const getUserByEmail = async (\n request: FastifyRequest<{ Params: GetUserByEmailParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { email } = request.params;\n const { user: sessionUser, roles } = request.session || {};\n\n if (!sessionUser) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const user = await userService.getUserByEmail(email);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (\n !hasPermission(\n roles || [],\n 'user:read'\n )({\n ...request.session,\n targetUsers: [user],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n const formattedUser = mapUserToAPI(user);\n const responseData = formatResponse<UserAPI>({ data: formattedUser });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UpdateUserBody = Partial<UserAPI>;\nexport type UpdateUserResult = ResponseData<UserAPI>;\n\n/**\n * Updates user information (phone number, date of birth).\n */\nexport const updateUser = async (\n request: FastifyRequest<{ Body: UpdateUserBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const userData = request.body;\n const { user, roles } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (typeof userData !== 'object') {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_DATA_NOT_FOUND'\n );\n }\n\n if (!userData.id) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_INVALID_FIELDS'\n );\n }\n\n const userDB = await userService.getUserById(userData.id);\n\n if (!userDB) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n if (\n !hasPermission(\n roles || [],\n 'user:write'\n )({\n ...request.session,\n targetUsers: [userDB],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n try {\n const updatedUser = await userService.updateUserById(userDB.id, userData);\n\n logger.info(\n `User updated: Name: ${updatedUser.name}, id: ${String(updatedUser.id)}`\n );\n\n const formattedUser = mapUserToAPI(updatedUser);\n const responseData = formatResponse<UserAPI>({\n message: t({\n en: 'User updated',\n 'en-GB': 'User updated',\n fr: 'Utilisateur mis à jour',\n es: 'Usuario actualizado',\n ru: 'Пользователь обновлен',\n ja: 'ユーザーが更新されました',\n ko: '사용자가 업데이트되었습니다',\n zh: '用户已更新',\n de: 'Benutzer aktualisiert',\n ar: 'تم تحديث المستخدم',\n it: 'Utente aggiornato',\n pt: 'Usuário atualizado',\n hi: 'उपयोगकर्ता अपडेट किया गया',\n tr: 'Kullanıcı güncellendi',\n pl: 'Użytkownik został zaktualizowany',\n id: 'Pengguna diperbarui',\n vi: 'Người dùng đã được cập nhật',\n uk: 'Користувача оновлено',\n }),\n description: t({\n en: 'User updated successfully',\n 'en-GB': 'User updated successfully',\n fr: 'Utilisateur mis à jour avec succès',\n es: 'Usuario actualizado con éxito',\n ru: 'Пользователь успешно обновлен',\n ja: 'ユーザーが正常に更新されました',\n ko: '사용자가 성공적으로 업데이트되었습니다',\n zh: '用户已成功更新',\n de: 'Benutzer erfolgreich aktualisiert',\n ar: 'تم تحديث المستخدم بنجاح',\n it: 'Utente aggiornato con successo',\n pt: 'Usuário atualizado com sucesso',\n hi: 'उपयोगकर्ता सफलतापूर्वक अपडेट किया गया',\n tr: 'Kullanıcı başarıyla güncellendi',\n pl: 'Użytkownik został pomyślnie zaktualizowany',\n id: 'Pengguna berhasil diperbarui',\n vi: 'Người dùng đã được cập nhật thành công',\n uk: 'Користувача успішно оновлено',\n }),\n data: formattedUser,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type DeleteUserParams = { userId: string };\nexport type DeleteUserResult = ResponseData<UserAPI>;\n\n/**\n * Deletes a user based on the provided ID.\n */\nexport const deleteUser = async (\n request: FastifyRequest<{ Params: DeleteUserParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { userId } = request.params;\n const { roles } = request.session || {};\n\n try {\n const user = await userService.getUserById(userId);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n if (\n !hasPermission(\n roles || [],\n 'user:admin'\n )({\n ...request.session,\n targetUsers: [user],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n await userService.deleteUser(userId);\n\n const formattedUser = mapUserToAPI(user);\n const responseData = formatResponse<UserAPI>({\n message: t({\n en: 'User deleted',\n 'en-GB': 'User deleted',\n fr: 'Utilisateur supprimé',\n es: 'Usuario eliminado',\n ru: 'Пользователь удален',\n ja: 'ユーザーが削除されました',\n ko: '사용자가 삭제되었습니다',\n zh: '用户已删除',\n de: 'Benutzer gelöscht',\n ar: 'تم حذف المستخدم',\n it: 'Utente eliminato',\n pt: 'Usuário excluído',\n hi: 'उपयोगकर्ता हटा दिया गया',\n tr: 'Kullanıcı silindi',\n pl: 'Użytkownik został usunięty',\n id: 'Pengguna dihapus',\n vi: 'Người dùng đã bị xóa',\n uk: 'Користувача видалено',\n }),\n description: t({\n en: 'User deleted successfully',\n 'en-GB': 'User deleted successfully',\n fr: 'Utilisateur supprimé avec succès',\n es: 'Usuario eliminado con éxito',\n ru: 'Пользователь успешно удален',\n ja: 'ユーザーが正常に削除されました',\n ko: '사용자가 성공적으로 삭제되었습니다',\n zh: '用户已成功删除',\n de: 'Benutzer erfolgreich gelöscht',\n ar: 'تم حذف المستخدم بنجاح',\n it: 'Utente eliminato con successo',\n pt: 'Usuário excluído com sucesso',\n hi: 'उपयोगकर्ता सफलतापूर्वक हटा दिया गया',\n tr: 'Kullanıcı başarıyla silindi',\n pl: 'Użytkownik został pomyślnie usunięty',\n id: 'Pengguna berhasil dihapus',\n vi: 'Người dùng đã được xóa thành công',\n uk: 'Користувача успішно видалено',\n }),\n data: formattedUser,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UploadUserAvatarResult = ResponseData<UserAPI>;\n\n/**\n * Uploads a new avatar for the authenticated user, stores it in S3, and\n * updates the user's image field. Deletes the previous avatar if it was\n * hosted on our own storage.\n */\nexport const uploadAvatar = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const { user, roles } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const rawContentType = request.headers['content-type'] ?? '';\n // Strip parameters like \"; charset=utf-8\" to get the bare MIME type\n const contentType = rawContentType.split(';')[0].trim() || 'image/jpeg';\n const contentLength = Number(request.headers['content-length'] ?? 0);\n\n logger.info('uploadAvatar request', {\n contentType,\n contentLength,\n userId: String(user.id),\n });\n\n const validationError = validateAvatarUpload(contentType, contentLength);\n if (validationError === 'UNSUPPORTED_TYPE') {\n logger.warn('uploadAvatar: unsupported type', { contentType });\n return reply.status(415).send(\n formatResponse({\n message: t({\n en: 'Unsupported image type. Allowed: JPEG, PNG, WebP, GIF.',\n fr: \"Type d'image non supporté. Formats acceptés : JPEG, PNG, WebP, GIF.\",\n es: 'Tipo de imagen no admitido. Permitidos: JPEG, PNG, WebP, GIF.',\n 'en-GB': 'Unsupported image type. Allowed: JPEG, PNG, WebP, GIF.',\n de: 'Nicht unterstützter Bildtyp. Erlaubt: JPEG, PNG, WebP, GIF.',\n ja: '対応していない画像形式です。使用可能: JPEG, PNG, WebP, GIF。',\n ko: '지원하지 않는 이미지 형식입니다. 허용: JPEG, PNG, WebP, GIF.',\n zh: '不支持的图片格式。允许:JPEG、PNG、WebP、GIF。',\n it: 'Tipo di immagine non supportato. Consentiti: JPEG, PNG, WebP, GIF.',\n pt: 'Tipo de imagem não suportado. Permitidos: JPEG, PNG, WebP, GIF.',\n hi: 'असमर्थित छवि प्रकार। अनुमत: JPEG, PNG, WebP, GIF।',\n ar: 'نوع صورة غير مدعوم. المسموح به: JPEG، PNG، WebP، GIF.',\n ru: 'Неподдерживаемый тип изображения. Разрешены: JPEG, PNG, WebP, GIF.',\n tr: 'Desteklenmeyen görüntü türü. İzin verilenler: JPEG, PNG, WebP, GIF.',\n pl: 'Nieobsługiwany typ obrazu. Dozwolone: JPEG, PNG, WebP, GIF.',\n id: 'Tipe gambar tidak didukung. Diizinkan: JPEG, PNG, WebP, GIF.',\n vi: 'Loại ảnh không được hỗ trợ. Được phép: JPEG, PNG, WebP, GIF.',\n uk: 'Непідтримуваний тип зображення. Дозволено: JPEG, PNG, WebP, GIF.',\n }),\n data: null,\n })\n );\n }\n\n if (validationError === 'TOO_LARGE') {\n logger.warn('uploadAvatar: file too large', { contentLength });\n return reply.status(413).send(\n formatResponse({\n message: t({\n en: 'File too large. Maximum size is 20 MB.',\n fr: 'Fichier trop volumineux. La taille maximale est de 20 Mo.',\n es: 'Archivo demasiado grande. El tamaño máximo es de 20 MB.',\n 'en-GB': 'File too large. Maximum size is 20 MB.',\n de: 'Datei zu groß. Maximale Größe: 20 MB.',\n ja: 'ファイルが大きすぎます。最大サイズは20MBです。',\n ko: '파일이 너무 큽니다. 최대 크기는 20MB입니다.',\n zh: '文件过大。最大大小为 20 MB。',\n it: 'File troppo grande. La dimensione massima è 20 MB.',\n pt: 'Arquivo muito grande. O tamanho máximo é 20 MB.',\n hi: 'फ़ाइल बहुत बड़ी है। अधिकतम आकार 20 MB है।',\n ar: 'الملف كبير جدًا. الحجم الأقصى هو 20 ميغابايت.',\n ru: 'Файл слишком большой. Максимальный размер: 20 МБ.',\n tr: 'Dosya çok büyük. Maksimum boyut 20 MB.',\n pl: 'Plik zbyt duży. Maksymalny rozmiar to 20 MB.',\n id: 'File terlalu besar. Ukuran maksimum adalah 20 MB.',\n vi: 'Tệp quá lớn. Kích thước tối đa là 20 MB.',\n uk: 'Файл завеликий. Максимальний розмір: 20 МБ.',\n }),\n data: null,\n })\n );\n }\n\n if (\n !hasPermission(\n roles || [],\n 'user:write'\n )({ ...request.session, targetUsers: [user] })\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n try {\n const buffer = request.body;\n const userId = String(user.id);\n\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n logger.error('uploadAvatar: body is not a Buffer', {\n bodyType: typeof buffer,\n isBuffer: Buffer.isBuffer(buffer),\n length: Buffer.isBuffer(buffer) ? buffer.length : 'N/A',\n contentType,\n });\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_INVALID_FIELDS'\n );\n }\n\n // Delete the old avatar from S3 if it was uploaded by us\n if (user.image) {\n await deleteUserAvatar(user.image).catch(() => {});\n }\n\n const imageUrl = await uploadUserAvatar(buffer, userId);\n\n const updatedUser = await userService.updateUserById(userId, {\n image: imageUrl,\n });\n\n logger.info(`Avatar uploaded for user ${userId}`);\n\n const formattedUser = mapUserToAPI(updatedUser);\n const responseData = formatResponse<UserAPI>({\n message: t({\n en: 'Avatar uploaded',\n 'en-GB': 'Avatar uploaded',\n fr: 'Avatar mis à jour',\n es: 'Avatar actualizado',\n ru: 'Аватар загружен',\n ja: 'アバターをアップロードしました',\n ko: '아바타가 업로드되었습니다',\n zh: '头像已上传',\n de: 'Avatar hochgeladen',\n ar: 'تم رفع الصورة الرمزية',\n it: 'Avatar caricato',\n pt: 'Avatar enviado',\n hi: 'अवतार अपलोड किया गया',\n tr: 'Avatar yüklendi',\n pl: 'Awatar przesłany',\n id: 'Avatar diunggah',\n vi: 'Ảnh đại diện đã được tải lên',\n uk: 'Аватар завантажено',\n }),\n data: formattedUser,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nlet clients: Array<{\n id: number;\n userId: string;\n res: { raw: FastifyReply['raw'] };\n}> = [];\n\nexport const sendVerificationUpdate = (user: User) => {\n const filteredClients = clients.filter(\n (client) => String(client.userId) === String(user.id)\n );\n\n for (const client of filteredClients) {\n if (user.emailVerified) {\n client.res.raw.write(\n `data: ${JSON.stringify({ userId: user.id, status: 'verified' })}\\n\\n`\n );\n }\n }\n};\n\nexport type VerifyEmailStatusSSEParams = { userId: string };\n\n/**\n * SSE to check the email verification status\n */\nexport const verifyEmailStatusSSE = async (\n request: FastifyRequest<{ Params: VerifyEmailStatusSSEParams }>,\n reply: FastifyReply\n) => {\n const { user: sessionUser, roles } = request.session || {};\n\n const { userId } = request.params; // Get user ID from params\n\n const user = await userService.getUserById(userId);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n // When a session exists, enforce ownership or admin access.\n // When there is no session the user just registered and is waiting for\n // email verification — BetterAuth does not create a session until the\n // email is verified, so we allow unauthenticated access for this endpoint.\n if (\n sessionUser &&\n String(sessionUser.id) !== String(userId) &&\n !hasPermission(\n roles || [],\n 'user:admin'\n )({ ...request.session, targetUsers: [user] })\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n // Set headers for SSE\n reply.raw.setHeader('Content-Type', 'text/event-stream;charset=utf-8');\n reply.raw.setHeader('Cache-Control', 'no-cache, no-transform');\n reply.raw.setHeader('Connection', 'keep-alive');\n reply.raw.setHeader('X-Accel-Buffering', 'no'); // For Nginx buffering\n\n // Send initial data to ensure the connection is open\n reply.raw.write(':\\n\\n'); // Comment to keep connection alive\n reply.raw.flushHeaders?.();\n\n const clientId = Date.now();\n\n // Add client to the list\n const newClient = { id: clientId, userId, res: { raw: reply.raw } };\n clients.push(newClient);\n\n sendVerificationUpdate(user);\n\n // Remove client on connection close\n request.raw.on('close', () => {\n clients = clients.filter((client) => client.id !== clientId);\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"user.controller.mjs","names":["userService.createUser","userService.findUsers","userService.countUsers","userService.getUserById","userService.getUserByEmail","userService.updateUserById","userService.deleteUser"],"sources":["../../../src/controllers/user.controller.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { sendEmail } from '@services/email.service';\nimport {\n deleteUserAvatar,\n uploadUserAvatar,\n validateAvatarUpload,\n} from '@services/user/avatarUpload.service';\nimport * as userService from '@services/user.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport type { FiltersAndPagination } from '@utils/filtersAndPagination/getFiltersAndPaginationFromBody';\nimport {\n getUserFiltersAndPagination,\n type UserFiltersParam,\n} from '@utils/filtersAndPagination/getUserFiltersAndPagination';\nimport { mapUsersToAPI, mapUserToAPI } from '@utils/mapper/user';\nimport { hasPermission } from '@utils/permissions';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type PaginatedResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport type { User, UserAPI } from '@/types/user.types';\n\nexport type CreateUserBody = { email: string; password?: string };\nexport type CreateUserResult = ResponseData<UserAPI>;\n\n/**\n * Creates a new user.\n */\nexport const createUser = async (\n request: FastifyRequest<{ Body: User }>,\n reply: FastifyReply\n): Promise<void> => {\n const user: User | undefined = request.body;\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const newUser = await userService.createUser(user);\n\n await sendEmail({\n type: 'welcome',\n to: newUser.email,\n username: newUser.name,\n loginLink: `${process.env.APP_URL}/auth/login`,\n });\n\n const formattedUser = mapUserToAPI(newUser);\n\n const responseData = formatResponse<UserAPI>({\n message: t({\n en: 'User created',\n 'en-GB': 'User created',\n fr: 'Utilisateur créé',\n es: 'Usuario creado',\n ru: 'Пользователь создан',\n ja: 'ユーザーが作成されました',\n ko: '사용자가 생성되었습니다',\n zh: '用户已创建',\n de: 'Benutzer erstellt',\n ar: 'تم إنشاء المستخدم',\n it: 'Utente creato',\n pt: 'Usuário criado',\n hi: 'उपयोगकर्ता बनाया गया',\n tr: 'Kullanıcı oluşturuldu',\n pl: 'Użytkownik został utworzony',\n id: 'Pengguna dibuat',\n vi: 'Người dùng đã được tạo',\n uk: 'Користувача створено',\n }),\n description: t({\n en: 'User created successfully',\n 'en-GB': 'User created successfully',\n fr: 'Utilisateur créé avec succès',\n es: 'Usuario creado con éxito',\n ru: 'Пользователь успешно создан',\n ja: 'ユーザーが正常に作成されました',\n ko: '사용자가 성공적으로 생성되었습니다',\n zh: '用户已成功创建',\n de: 'Benutzer erfolgreich erstellt',\n ar: 'تم إنشاء المستخدم بنجاح',\n it: 'Utente creato con successo',\n pt: 'Usuário criado com sucesso',\n hi: 'उपयोगकर्ता सफलतापूर्वक बनाया गया',\n tr: 'Kullanıcı başarıyla oluşturuldu',\n pl: 'Użytkownik został pomyślnie utworzony',\n id: 'Pengguna berhasil dibuat',\n vi: 'Người dùng đã được tạo thành công',\n uk: 'Користувача успішно створено',\n }),\n data: formattedUser,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetUsersParams = FiltersAndPagination<UserFiltersParam>;\nexport type GetUsersResult = PaginatedResponse<UserAPI>;\n\n/**\n * Retrieves a list of users based on filters and pagination.\n */\nexport const getUsers = async (\n request: FastifyRequest<{ Querystring: GetUsersParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user, roles } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } =\n getUserFiltersAndPagination(request);\n\n try {\n const users = await userService.findUsers(\n filters,\n skip,\n pageSize,\n sortOptions\n );\n\n // Skip permission check when there are no users to protect.\n // An empty result is safe to return without checking roles.\n if (\n users.length > 0 &&\n !hasPermission(\n roles || [],\n 'user:read'\n )({\n ...request.session,\n targetUsers: users,\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n const totalItems = await userService.countUsers(filters);\n\n const formattedUsers = mapUsersToAPI(users);\n\n const responseData = formatPaginatedResponse<UserAPI>({\n data: formattedUsers,\n page,\n pageSize,\n totalPages: getNumberOfPages(totalItems),\n totalItems,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetUserByIdParams = { userId: UserAPI['id'] };\nexport type GetUserByIdResult = ResponseData<UserAPI>;\n\nexport const getUserById = async (\n request: FastifyRequest<{ Params: GetUserByIdParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { userId } = request.params;\n const { user: sessionUser } = request.session || {};\n\n if (!sessionUser) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (\n String(sessionUser.id) !== String(userId) &&\n sessionUser.role !== 'admin'\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n try {\n const user = await userService.getUserById(userId);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const formattedUser = mapUserToAPI(user);\n const responseData = formatResponse<UserAPI>({ data: formattedUser });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetUserByEmailParams = { email: string };\nexport type GetUserByEmailResult = ResponseData<UserAPI>;\n\nexport const getUserByEmail = async (\n request: FastifyRequest<{ Params: GetUserByEmailParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { email } = request.params;\n const { user: sessionUser, roles } = request.session || {};\n\n if (!sessionUser) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const user = await userService.getUserByEmail(email);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (\n !hasPermission(\n roles || [],\n 'user:read'\n )({\n ...request.session,\n targetUsers: [user],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n const formattedUser = mapUserToAPI(user);\n const responseData = formatResponse<UserAPI>({ data: formattedUser });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UpdateUserBody = Partial<UserAPI>;\nexport type UpdateUserResult = ResponseData<UserAPI>;\n\n/**\n * Updates user information (phone number, date of birth).\n */\nexport const updateUser = async (\n request: FastifyRequest<{ Body: UpdateUserBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const userData = request.body;\n const { user, roles } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n if (typeof userData !== 'object') {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_DATA_NOT_FOUND'\n );\n }\n\n if (!userData.id) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_INVALID_FIELDS'\n );\n }\n\n const userDB = await userService.getUserById(userData.id);\n\n if (!userDB) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n if (\n !hasPermission(\n roles || [],\n 'user:write'\n )({\n ...request.session,\n targetUsers: [userDB],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n try {\n const updatedUser = await userService.updateUserById(userDB.id, userData);\n\n logger.info(\n `User updated: Name: ${updatedUser.name}, id: ${String(updatedUser.id)}`\n );\n\n const formattedUser = mapUserToAPI(updatedUser);\n const responseData = formatResponse<UserAPI>({\n message: t({\n en: 'User updated',\n 'en-GB': 'User updated',\n fr: 'Utilisateur mis à jour',\n es: 'Usuario actualizado',\n ru: 'Пользователь обновлен',\n ja: 'ユーザーが更新されました',\n ko: '사용자가 업데이트되었습니다',\n zh: '用户已更新',\n de: 'Benutzer aktualisiert',\n ar: 'تم تحديث المستخدم',\n it: 'Utente aggiornato',\n pt: 'Usuário atualizado',\n hi: 'उपयोगकर्ता अपडेट किया गया',\n tr: 'Kullanıcı güncellendi',\n pl: 'Użytkownik został zaktualizowany',\n id: 'Pengguna diperbarui',\n vi: 'Người dùng đã được cập nhật',\n uk: 'Користувача оновлено',\n }),\n description: t({\n en: 'User updated successfully',\n 'en-GB': 'User updated successfully',\n fr: 'Utilisateur mis à jour avec succès',\n es: 'Usuario actualizado con éxito',\n ru: 'Пользователь успешно обновлен',\n ja: 'ユーザーが正常に更新されました',\n ko: '사용자가 성공적으로 업데이트되었습니다',\n zh: '用户已成功更新',\n de: 'Benutzer erfolgreich aktualisiert',\n ar: 'تم تحديث المستخدم بنجاح',\n it: 'Utente aggiornato con successo',\n pt: 'Usuário atualizado com sucesso',\n hi: 'उपयोगकर्ता सफलतापूर्वक अपडेट किया गया',\n tr: 'Kullanıcı başarıyla güncellendi',\n pl: 'Użytkownik został pomyślnie zaktualizowany',\n id: 'Pengguna berhasil diperbarui',\n vi: 'Người dùng đã được cập nhật thành công',\n uk: 'Користувача успішно оновлено',\n }),\n data: formattedUser,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type DeleteUserParams = { userId: string };\nexport type DeleteUserResult = ResponseData<UserAPI>;\n\n/**\n * Deletes a user based on the provided ID.\n */\nexport const deleteUser = async (\n request: FastifyRequest<{ Params: DeleteUserParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { userId } = request.params;\n const { roles } = request.session || {};\n\n try {\n const user = await userService.getUserById(userId);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n if (\n !hasPermission(\n roles || [],\n 'user:admin'\n )({\n ...request.session,\n targetUsers: [user],\n })\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PERMISSION_DENIED'\n );\n }\n\n await userService.deleteUser(userId);\n\n const formattedUser = mapUserToAPI(user);\n const responseData = formatResponse<UserAPI>({\n message: t({\n en: 'User deleted',\n 'en-GB': 'User deleted',\n fr: 'Utilisateur supprimé',\n es: 'Usuario eliminado',\n ru: 'Пользователь удален',\n ja: 'ユーザーが削除されました',\n ko: '사용자가 삭제되었습니다',\n zh: '用户已删除',\n de: 'Benutzer gelöscht',\n ar: 'تم حذف المستخدم',\n it: 'Utente eliminato',\n pt: 'Usuário excluído',\n hi: 'उपयोगकर्ता हटा दिया गया',\n tr: 'Kullanıcı silindi',\n pl: 'Użytkownik został usunięty',\n id: 'Pengguna dihapus',\n vi: 'Người dùng đã bị xóa',\n uk: 'Користувача видалено',\n }),\n description: t({\n en: 'User deleted successfully',\n 'en-GB': 'User deleted successfully',\n fr: 'Utilisateur supprimé avec succès',\n es: 'Usuario eliminado con éxito',\n ru: 'Пользователь успешно удален',\n ja: 'ユーザーが正常に削除されました',\n ko: '사용자가 성공적으로 삭제되었습니다',\n zh: '用户已成功删除',\n de: 'Benutzer erfolgreich gelöscht',\n ar: 'تم حذف المستخدم بنجاح',\n it: 'Utente eliminato con successo',\n pt: 'Usuário excluído com sucesso',\n hi: 'उपयोगकर्ता सफलतापूर्वक हटा दिया गया',\n tr: 'Kullanıcı başarıyla silindi',\n pl: 'Użytkownik został pomyślnie usunięty',\n id: 'Pengguna berhasil dihapus',\n vi: 'Người dùng đã được xóa thành công',\n uk: 'Користувача успішно видалено',\n }),\n data: formattedUser,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type UploadUserAvatarResult = ResponseData<UserAPI>;\n\n/**\n * Uploads a new avatar for the authenticated user, stores it in S3, and\n * updates the user's image field. Deletes the previous avatar if it was\n * hosted on our own storage.\n */\nexport const uploadAvatar = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const { user, roles } = request.session || {};\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const rawContentType = request.headers['content-type'] ?? '';\n // Strip parameters like \"; charset=utf-8\" to get the bare MIME type\n const contentType = rawContentType.split(';')[0].trim() || 'image/jpeg';\n const contentLength = Number(request.headers['content-length'] ?? 0);\n\n logger.info('uploadAvatar request', {\n contentType,\n contentLength,\n userId: String(user.id),\n });\n\n const validationError = validateAvatarUpload(contentType, contentLength);\n if (validationError === 'UNSUPPORTED_TYPE') {\n logger.warn('uploadAvatar: unsupported type', { contentType });\n return reply.status(415).send(\n formatResponse({\n message: t({\n en: 'Unsupported image type. Allowed: JPEG, PNG, WebP, GIF.',\n fr: \"Type d'image non supporté. Formats acceptés : JPEG, PNG, WebP, GIF.\",\n es: 'Tipo de imagen no admitido. Permitidos: JPEG, PNG, WebP, GIF.',\n 'en-GB': 'Unsupported image type. Allowed: JPEG, PNG, WebP, GIF.',\n de: 'Nicht unterstützter Bildtyp. Erlaubt: JPEG, PNG, WebP, GIF.',\n ja: '対応していない画像形式です。使用可能: JPEG, PNG, WebP, GIF。',\n ko: '지원하지 않는 이미지 형식입니다. 허용: JPEG, PNG, WebP, GIF.',\n zh: '不支持的图片格式。允许:JPEG、PNG、WebP、GIF。',\n it: 'Tipo di immagine non supportato. Consentiti: JPEG, PNG, WebP, GIF.',\n pt: 'Tipo de imagem não suportado. Permitidos: JPEG, PNG, WebP, GIF.',\n hi: 'असमर्थित छवि प्रकार। अनुमत: JPEG, PNG, WebP, GIF।',\n ar: 'نوع صورة غير مدعوم. المسموح به: JPEG، PNG، WebP، GIF.',\n ru: 'Неподдерживаемый тип изображения. Разрешены: JPEG, PNG, WebP, GIF.',\n tr: 'Desteklenmeyen görüntü türü. İzin verilenler: JPEG, PNG, WebP, GIF.',\n pl: 'Nieobsługiwany typ obrazu. Dozwolone: JPEG, PNG, WebP, GIF.',\n id: 'Tipe gambar tidak didukung. Diizinkan: JPEG, PNG, WebP, GIF.',\n vi: 'Loại ảnh không được hỗ trợ. Được phép: JPEG, PNG, WebP, GIF.',\n uk: 'Непідтримуваний тип зображення. Дозволено: JPEG, PNG, WebP, GIF.',\n }),\n data: null,\n })\n );\n }\n\n if (validationError === 'TOO_LARGE') {\n logger.warn('uploadAvatar: file too large', { contentLength });\n return reply.status(413).send(\n formatResponse({\n message: t({\n en: 'File too large. Maximum size is 20 MB.',\n fr: 'Fichier trop volumineux. La taille maximale est de 20 Mo.',\n es: 'Archivo demasiado grande. El tamaño máximo es de 20 MB.',\n 'en-GB': 'File too large. Maximum size is 20 MB.',\n de: 'Datei zu groß. Maximale Größe: 20 MB.',\n ja: 'ファイルが大きすぎます。最大サイズは20MBです。',\n ko: '파일이 너무 큽니다. 최대 크기는 20MB입니다.',\n zh: '文件过大。最大大小为 20 MB。',\n it: 'File troppo grande. La dimensione massima è 20 MB.',\n pt: 'Arquivo muito grande. O tamanho máximo é 20 MB.',\n hi: 'फ़ाइल बहुत बड़ी है। अधिकतम आकार 20 MB है।',\n ar: 'الملف كبير جدًا. الحجم الأقصى هو 20 ميغابايت.',\n ru: 'Файл слишком большой. Максимальный размер: 20 МБ.',\n tr: 'Dosya çok büyük. Maksimum boyut 20 MB.',\n pl: 'Plik zbyt duży. Maksymalny rozmiar to 20 MB.',\n id: 'File terlalu besar. Ukuran maksimum adalah 20 MB.',\n vi: 'Tệp quá lớn. Kích thước tối đa là 20 MB.',\n uk: 'Файл завеликий. Максимальний розмір: 20 МБ.',\n }),\n data: null,\n })\n );\n }\n\n if (\n !hasPermission(\n roles || [],\n 'user:write'\n )({ ...request.session, targetUsers: [user] })\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n try {\n const buffer = request.body;\n const userId = String(user.id);\n\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n logger.error('uploadAvatar: body is not a Buffer', {\n bodyType: typeof buffer,\n isBuffer: Buffer.isBuffer(buffer),\n length: Buffer.isBuffer(buffer) ? buffer.length : 'N/A',\n contentType,\n });\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_INVALID_FIELDS'\n );\n }\n\n // Delete the old avatar from S3 if it was uploaded by us\n if (user.image) {\n await deleteUserAvatar(user.image).catch(() => {});\n }\n\n const imageUrl = await uploadUserAvatar(buffer, userId);\n\n const updatedUser = await userService.updateUserById(userId, {\n image: imageUrl,\n });\n\n logger.info(`Avatar uploaded for user ${userId}`);\n\n const formattedUser = mapUserToAPI(updatedUser);\n const responseData = formatResponse<UserAPI>({\n message: t({\n en: 'Avatar uploaded',\n 'en-GB': 'Avatar uploaded',\n fr: 'Avatar mis à jour',\n es: 'Avatar actualizado',\n ru: 'Аватар загружен',\n ja: 'アバターをアップロードしました',\n ko: '아바타가 업로드되었습니다',\n zh: '头像已上传',\n de: 'Avatar hochgeladen',\n ar: 'تم رفع الصورة الرمزية',\n it: 'Avatar caricato',\n pt: 'Avatar enviado',\n hi: 'अवतार अपलोड किया गया',\n tr: 'Avatar yüklendi',\n pl: 'Awatar przesłany',\n id: 'Avatar diunggah',\n vi: 'Ảnh đại diện đã được tải lên',\n uk: 'Аватар завантажено',\n }),\n data: formattedUser,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nlet clients: Array<{\n id: number;\n userId: string;\n res: { raw: FastifyReply['raw'] };\n}> = [];\n\nexport const sendVerificationUpdate = (user: User) => {\n const filteredClients = clients.filter(\n (client) => String(client.userId) === String(user.id)\n );\n\n for (const client of filteredClients) {\n if (user.emailVerified) {\n client.res.raw.write(\n `data: ${JSON.stringify({ userId: user.id, status: 'verified' })}\\n\\n`\n );\n }\n }\n};\n\nexport type VerifyEmailStatusSSEParams = { userId: string };\n\n/**\n * SSE to check the email verification status\n */\nexport const verifyEmailStatusSSE = async (\n request: FastifyRequest<{ Params: VerifyEmailStatusSSEParams }>,\n reply: FastifyReply\n) => {\n const { user: sessionUser, roles } = request.session || {};\n\n const { userId } = request.params; // Get user ID from params\n\n const user = await userService.getUserById(userId);\n\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n // When a session exists, enforce ownership or admin access.\n // When there is no session the user just registered and is waiting for\n // email verification — BetterAuth does not create a session until the\n // email is verified, so we allow unauthenticated access for this endpoint.\n if (\n sessionUser &&\n String(sessionUser.id) !== String(userId) &&\n !hasPermission(\n roles || [],\n 'user:admin'\n )({ ...request.session, targetUsers: [user] })\n ) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n // Set headers for SSE\n reply.raw.setHeader('Content-Type', 'text/event-stream;charset=utf-8');\n reply.raw.setHeader('Cache-Control', 'no-cache, no-transform');\n reply.raw.setHeader('Connection', 'keep-alive');\n reply.raw.setHeader('X-Accel-Buffering', 'no'); // For Nginx buffering\n\n // Send initial data to ensure the connection is open\n reply.raw.write(':\\n\\n'); // Comment to keep connection alive\n reply.raw.flushHeaders?.();\n\n const clientId = Date.now();\n\n // Add client to the list\n const newClient = { id: clientId, userId, res: { raw: reply.raw } };\n clients.push(newClient);\n\n sendVerificationUpdate(user);\n\n // Remove client on connection close\n request.raw.on('close', () => {\n clients = clients.filter((client) => client.id !== clientId);\n });\n};\n"],"mappings":"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"}
|