@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":"reviewer.controller.mjs","names":["reviewerService.findReviewerProfiles","reviewerService.countReviewerProfiles","reviewerService.getReviewerPriceDistribution","reviewerService.findReviewerById","reviewerService.findReviewerByUserId","reviewerService.createReviewerProfile","reviewerService.updateReviewerProfile","reviewerService.deleteReviewerProfile","reviewerService.findReviewerProfilesAdmin","reviewerService.countReviewerProfilesAdmin","userService.getUserById","reviewerService.findReviewsByReviewerId","reviewerService.countReviewsByReviewerId","missionService.findMissionById","reviewerService.findReviewByMissionId","reviewerService.createReview","reviewerService.updateRating","missionService.calculateMissionEstimate","missionService.createMission","missionService.findMissionsForReviewerProfile","missionService.findMissionsForUser","missionService.updateMissionStatus","reviewerService.incrementMissionCount","messageService.findMessagesByMissionId","messageService.markMessagesRead","messageService.createMessage","messageService.findNewMessagesSince"],"sources":["../../../src/controllers/reviewer.controller.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { sendEmail } from '@services/email.service';\nimport {\n deleteReviewerPicture,\n type ReviewerPictureKind,\n uploadReviewerPicture,\n validateReviewerPictureUpload,\n} from '@services/reviewer/pictureUpload.service';\nimport * as reviewerService from '@services/reviewer.service';\nimport * as messageService from '@services/reviewerMessage.service';\nimport * as missionService from '@services/reviewerMission.service';\nimport * as userService from '@services/user.service';\nimport { ErrorHandler } from '@utils/errors';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport type {\n MissionEstimate,\n MissionStatus,\n ReviewerCategory,\n ReviewerMessageAPI,\n ReviewerProfileAPI,\n ReviewerReviewAPI,\n TranslationMissionAPI,\n} from '@/types/reviewer.types';\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nconst toProfileAPI = (doc: any): ReviewerProfileAPI => doc.toJSON();\nconst toMissionAPI = (doc: any): TranslationMissionAPI => doc.toJSON();\nconst toReviewAPI = (doc: any): ReviewerReviewAPI => doc.toJSON();\nconst toMessageAPI = (doc: any): ReviewerMessageAPI => doc.toJSON();\n\n// ── Marketplace / Profile ────────────────────────────────────────────────────\n\nexport type GetMarketplaceQuery = {\n page?: number;\n pageSize?: number;\n fromLocale?: string;\n toLocale?: string;\n minRating?: number;\n maxPricePerHour?: number;\n minPricePerHour?: number;\n categories?: string | string[];\n};\n\nexport type PriceDistributionBucket = {\n min: number;\n max: number;\n count: number;\n};\n\nexport type PriceDistributionData = {\n buckets: PriceDistributionBucket[];\n globalMin: number;\n globalMax: number;\n};\n\nexport const getMarketplace = async (\n request: FastifyRequest<{ Querystring: GetMarketplaceQuery }>,\n reply: FastifyReply\n): Promise<void> => {\n const {\n page = 1,\n pageSize = 20,\n fromLocale,\n toLocale,\n minRating,\n maxPricePerHour,\n minPricePerHour,\n categories,\n } = request.query;\n\n const query: Record<string, any> = {};\n\n if (fromLocale || toLocale) {\n const pairFilter: Record<string, any> = {};\n if (fromLocale) pairFilter.from = fromLocale;\n if (toLocale) pairFilter.to = toLocale;\n query.languagePairs = { $elemMatch: pairFilter };\n }\n\n if (minRating !== undefined)\n query.averageRating = { $gte: Number(minRating) };\n\n if (minPricePerHour !== undefined || maxPricePerHour !== undefined) {\n query.pricePerHour = {};\n if (minPricePerHour !== undefined)\n query.pricePerHour.$gte = Number(minPricePerHour);\n if (maxPricePerHour !== undefined)\n query.pricePerHour.$lte = Number(maxPricePerHour);\n }\n\n if (categories) {\n const cats = Array.isArray(categories) ? categories : [categories];\n if (cats.length > 0) query.categories = { $in: cats };\n }\n\n const skip = (Number(page) - 1) * Number(pageSize);\n const [profiles, total] = await Promise.all([\n reviewerService.findReviewerProfiles(query, skip, Number(pageSize)),\n reviewerService.countReviewerProfiles(query),\n ]);\n\n const totalPages = Math.ceil(total / Number(pageSize));\n\n return reply.send(\n formatPaginatedResponse<ReviewerProfileAPI>({\n data: profiles.map(toProfileAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages,\n totalItems: total,\n message: t({\n en: 'Reviewer profiles fetched',\n fr: 'Profils récupérés',\n es: 'Perfiles obtenidos',\n de: 'Profile abgerufen',\n ar: 'تم جلب الملفات الشخصية',\n 'en-GB': 'Reviewer profiles fetched',\n ru: 'Профили получены',\n ja: 'プロフィールを取得しました',\n ko: '프로필을 가져왔습니다',\n zh: '已获取翻译员资料',\n it: 'Profili recuperati',\n pt: 'Perfis obtidos',\n hi: 'प्रोफाइल प्राप्त की गई',\n tr: 'Profiller getirildi',\n pl: 'Profile pobrane',\n id: 'Profil diambil',\n vi: 'Đã lấy hồ sơ',\n uk: 'Профілі отримано',\n }),\n })\n );\n};\n\nexport const getPriceDistribution = async (\n request: FastifyRequest<{\n Querystring: Pick<\n GetMarketplaceQuery,\n 'fromLocale' | 'toLocale' | 'minRating' | 'categories'\n >;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const { fromLocale, toLocale, minRating, categories } = request.query;\n\n const query: Record<string, any> = {};\n\n if (fromLocale || toLocale) {\n const pairFilter: Record<string, any> = {};\n if (fromLocale) pairFilter.from = fromLocale;\n if (toLocale) pairFilter.to = toLocale;\n query.languagePairs = { $elemMatch: pairFilter };\n }\n if (minRating !== undefined)\n query.averageRating = { $gte: Number(minRating) };\n if (categories) {\n const cats = Array.isArray(categories) ? categories : [categories];\n if (cats.length > 0) query.categories = { $in: cats };\n }\n\n try {\n const buckets = await reviewerService.getReviewerPriceDistribution(query);\n const globalMin = buckets.length > 0 ? buckets[0].min : 0;\n const globalMax =\n buckets.length > 0 ? buckets[buckets.length - 1].max : 500;\n\n return reply.send(\n formatResponse<PriceDistributionData>({\n data: { buckets, globalMin, globalMax },\n message: 'Price distribution fetched',\n })\n );\n } catch (_error) {\n return reply\n .status(500)\n .send({ error: 'Failed to fetch price distribution' });\n }\n};\n\nexport const getReviewerById = async (\n request: FastifyRequest<{ Params: { reviewerId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { reviewerId } = request.params;\n const profile = await reviewerService.findReviewerById(reviewerId);\n\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(profile),\n message: t({\n en: 'Reviewer profile fetched',\n fr: 'Profil récupéré',\n es: 'Perfil obtenido',\n de: 'Profil abgerufen',\n ar: 'تم جلب الملف الشخصي',\n 'en-GB': 'Reviewer profile fetched',\n ru: 'Профиль получен',\n ja: 'プロフィールを取得しました',\n ko: '프로필을 가져왔습니다',\n zh: '已获取翻译员资料',\n it: 'Profilo recuperato',\n pt: 'Perfil obtido',\n hi: 'प्रोफाइल प्राप्त की गई',\n tr: 'Profil getirildi',\n pl: 'Profil pobrany',\n id: 'Profil diambil',\n vi: 'Đã lấy hồ sơ',\n uk: 'Профіль отримано',\n }),\n })\n );\n};\n\nexport type RegisterReviewerBody = {\n bio?: string;\n languagePairs: { from: string; to: string }[];\n categories?: ReviewerCategory[];\n pricePerHour: number;\n socialLinks?: { github?: string; linkedin?: string; portfolio?: string };\n};\n\nexport const registerAsReviewer = async (\n request: FastifyRequest<{ Body: RegisterReviewerBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const existing = await reviewerService.findReviewerByUserId(String(user.id));\n if (existing) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_ALREADY_EXISTS'\n );\n }\n\n const { bio, languagePairs, categories, pricePerHour } = request.body;\n\n const profile = await reviewerService.createReviewerProfile({\n userId: user.id as any,\n bio,\n languagePairs: languagePairs ?? [],\n categories: (categories ?? []) as any,\n pricePerHour: pricePerHour ?? 0,\n });\n\n // Notify the Intlayer team that a new reviewer has applied\n await sendEmail({\n type: 'reviewerApplication',\n to: 'contact@intlayer.org',\n username: user.name ?? user.email,\n userEmail: user.email,\n profileLink: `${process.env.APP_URL}/admin/reviewers/${String(profile.id)}`,\n }).catch((err) =>\n logger.error('Failed to send reviewer application email', err)\n );\n\n return reply.status(201).send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(profile),\n message: t({\n en: 'Registered as reviewer',\n fr: 'Enregistré comme traducteur',\n es: 'Registrado como traductor',\n de: 'Als Übersetzer registriert',\n ar: 'تم التسجيل كمترجم',\n 'en-GB': 'Registered as reviewer',\n ru: 'Зарегистрирован как переводчик',\n ja: '翻訳者として登録されました',\n ko: '번역가로 등록되었습니다',\n zh: '已注册为翻译员',\n it: 'Registrato come traduttore',\n pt: 'Registado como tradutor',\n hi: 'अनुवादक के रूप में पंजीकृत',\n tr: 'Çevirmen olarak kaydedildi',\n pl: 'Zarejestrowany jako tłumacz',\n id: 'Terdaftar sebagai penerjemah',\n vi: 'Đã đăng ký là dịch giả',\n uk: 'Зареєстровано як перекладач',\n }),\n })\n );\n};\n\nexport type UpdateReviewerBody = Partial<RegisterReviewerBody> & {\n coverPicture?: string;\n isHidden?: boolean;\n};\n\nexport const updateReviewerProfile = async (\n request: FastifyRequest<{ Body: UpdateReviewerBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const profile = await reviewerService.findReviewerByUserId(String(user.id));\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n const updated = await reviewerService.updateReviewerProfile(\n String(profile.id),\n request.body\n );\n\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(updated),\n message: t({\n en: 'Profile updated',\n fr: 'Profil mis à jour',\n es: 'Perfil actualizado',\n de: 'Profil aktualisiert',\n ar: 'تم تحديث الملف الشخصي',\n 'en-GB': 'Profile updated',\n ru: 'Профиль обновлён',\n ja: 'プロフィールを更新しました',\n ko: '프로필이 업데이트되었습니다',\n zh: '资料已更新',\n it: 'Profilo aggiornato',\n pt: 'Perfil atualizado',\n hi: 'प्रोफाइल अपडेट की गई',\n tr: 'Profil güncellendi',\n pl: 'Profil zaktualizowany',\n id: 'Profil diperbarui',\n vi: 'Hồ sơ đã được cập nhật',\n uk: 'Профіль оновлено',\n }),\n })\n );\n};\n\nexport const deleteMyReviewerProfile = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const profile = await reviewerService.findReviewerByUserId(String(user.id));\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n await reviewerService.deleteReviewerProfile(String(profile.id));\n\n return reply.send(\n formatResponse<null>({\n data: null,\n message: t({\n en: 'Profile deleted',\n fr: 'Profil supprimé',\n es: 'Perfil eliminado',\n de: 'Profil gelöscht',\n ar: 'تم حذف الملف الشخصي',\n 'en-GB': 'Profile deleted',\n ru: 'Профиль удалён',\n ja: 'プロフィールを削除しました',\n ko: '프로필이 삭제되었습니다',\n zh: '资料已删除',\n it: 'Profilo eliminato',\n pt: 'Perfil excluído',\n hi: 'प्रोफाइल हटाई गई',\n tr: 'Profil silindi',\n pl: 'Profil usunięty',\n id: 'Profil dihapus',\n vi: 'Hồ sơ đã được xóa',\n uk: 'Профіль видалено',\n }),\n })\n );\n};\n\nexport const getMyReviewerProfile = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const profile = await reviewerService.findReviewerByUserId(String(user.id));\n\n return reply.send(\n formatResponse<ReviewerProfileAPI | null>({\n data: profile ? toProfileAPI(profile) : null,\n })\n );\n};\n\n// ── Admin: list all reviewers ───────────────────────────────────────────────\n\nexport const getAdminReviewers = async (\n request: FastifyRequest<{\n Querystring: {\n page?: number;\n pageSize?: number;\n status?: string;\n };\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const sessionUser = request.session?.user;\n if (sessionUser?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n const { page = 1, pageSize = 20, status } = request.query;\n const query: Record<string, any> = {};\n if (status) query.status = status;\n\n const skip = (Number(page) - 1) * Number(pageSize);\n const [profiles, total] = await Promise.all([\n reviewerService.findReviewerProfilesAdmin(query, skip, Number(pageSize)),\n reviewerService.countReviewerProfilesAdmin(query),\n ]);\n\n return reply.send(\n formatPaginatedResponse<ReviewerProfileAPI>({\n data: profiles.map(toProfileAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages: Math.ceil(total / Number(pageSize)),\n totalItems: total,\n })\n );\n};\n\n// ── Admin: validate reviewer profile ────────────────────────────────────────\n\nexport const validateReviewerProfile = async (\n request: FastifyRequest<{ Params: { reviewerId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const sessionUser = request.session?.user;\n if (sessionUser?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n const { reviewerId } = request.params;\n\n const profile = await reviewerService.findReviewerById(reviewerId);\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n if (profile.status === 'active') {\n // Already active — still return 200 so the admin can be idempotent\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(profile),\n message: t({\n en: 'Reviewer profile is already active',\n fr: 'Le profil du traducteur est déjà actif',\n es: 'El perfil del traductor ya está activo',\n de: 'Das Übersetzer-Profil ist bereits aktiv',\n ar: 'ملف المترجم نشط بالفعل',\n 'en-GB': 'Reviewer profile is already active',\n ru: 'Профиль переводчика уже активен',\n ja: '翻訳者プロフィールはすでにアクティブです',\n ko: '번역가 프로필이 이미 활성화되어 있습니다',\n zh: '译者档案已激活',\n it: 'Il profilo del traduttore è già attivo',\n pt: 'O perfil do tradutor já está ativo',\n hi: 'अनुवादक प्रोफ़ाइल पहले से सक्रिय है',\n tr: 'Çevirmen profili zaten aktif',\n pl: 'Profil tłumacza jest już aktywny',\n id: 'Profil penerjemah sudah aktif',\n vi: 'Hồ sơ dịch giả đã được kích hoạt',\n uk: 'Профіль перекладача вже активний',\n }),\n })\n );\n }\n\n const updated = await reviewerService.updateReviewerProfile(reviewerId, {\n status: 'active',\n });\n\n // Fetch the linked user to send the approval email\n const linkedUser = await userService.getUserById(String(profile.userId));\n if (linkedUser) {\n await sendEmail({\n type: 'reviewerApproved',\n to: linkedUser.email,\n locale: (linkedUser as any).locale,\n username: linkedUser.name ?? linkedUser.email,\n dashboardLink: `${process.env.APP_URL}/dashboard/reviewer`,\n }).catch((err) =>\n logger.error('Failed to send reviewer approved email', err)\n );\n }\n\n logger.info(\n `Reviewer profile ${reviewerId} validated by admin ${String(sessionUser.id)}`\n );\n\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(updated),\n message: t({\n en: 'Reviewer profile validated',\n fr: 'Profil traducteur validé',\n es: 'Perfil del traductor validado',\n de: 'Übersetzer-Profil validiert',\n ar: 'تم التحقق من ملف المترجم',\n 'en-GB': 'Reviewer profile validated',\n ru: 'Профиль переводчика подтверждён',\n ja: '翻訳者プロフィールを承認しました',\n ko: '번역가 프로필이 승인되었습니다',\n zh: '译者档案已验证',\n it: 'Profilo traduttore validato',\n pt: 'Perfil do tradutor validado',\n hi: 'अनुवादक प्रोफ़ाइल सत्यापित',\n tr: 'Çevirmen profili doğrulandı',\n pl: 'Profil tłumacza zwalidowany',\n id: 'Profil penerjemah divalidasi',\n vi: 'Hồ sơ dịch giả đã được xác nhận',\n uk: 'Профіль перекладача підтверджено',\n }),\n })\n );\n};\n\n// ── Picture upload ────────────────────────────────────────────────────────────\n\nexport type UploadReviewerPictureResult = ResponseData<ReviewerProfileAPI>;\n\nconst uploadReviewerPictureHandler =\n (kind: ReviewerPictureKind) =>\n async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const profile = await reviewerService.findReviewerByUserId(String(user.id));\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n const rawContentType = request.headers['content-type'] ?? '';\n const contentType = rawContentType.split(';')[0].trim() || 'image/jpeg';\n const contentLength = Number(request.headers['content-length'] ?? 0);\n\n const validationError = validateReviewerPictureUpload(\n contentType,\n contentLength\n );\n if (validationError === 'UNSUPPORTED_TYPE') {\n return reply.status(415).send(\n formatResponse({\n data: null,\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 })\n );\n }\n\n if (validationError === 'TOO_LARGE') {\n return reply.status(413).send(\n formatResponse({\n data: null,\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 })\n );\n }\n\n const buffer = request.body;\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_INVALID_FIELDS'\n );\n }\n\n const field = kind === 'main' ? 'mainPicture' : 'coverPicture';\n const existing = (profile as any)[field] as string | undefined;\n if (existing) {\n await deleteReviewerPicture(existing).catch(() => {});\n }\n\n const imageUrl = await uploadReviewerPicture(\n buffer,\n String(profile.id),\n kind\n );\n\n const updated = await reviewerService.updateReviewerProfile(\n String(profile.id),\n {\n [field]: imageUrl,\n }\n );\n\n logger.info(\n `Reviewer ${kind} picture uploaded for profile ${String(profile.id)}`\n );\n\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(updated),\n message: t({\n en: 'Picture uploaded',\n fr: 'Image mise à jour',\n es: 'Imagen subida',\n de: 'Bild hochgeladen',\n ar: 'تم رفع الصورة',\n 'en-GB': 'Picture uploaded',\n ru: 'Изображение загружено',\n ja: '画像をアップロードしました',\n ko: '이미지가 업로드되었습니다',\n zh: '图片已上传',\n it: 'Immagine caricata',\n pt: 'Imagem enviada',\n hi: 'चित्र अपलोड किया गया',\n tr: 'Resim yüklendi',\n pl: 'Zdjęcie przesłane',\n id: 'Gambar diunggah',\n vi: 'Ảnh đã được tải lên',\n uk: 'Зображення завантажено',\n }),\n })\n );\n };\n\nexport const uploadReviewerMainPicture = uploadReviewerPictureHandler('main');\nexport const uploadReviewerCoverPicture = uploadReviewerPictureHandler('cover');\n\n// ── Reviews ──────────────────────────────────────────────────────────────────\n\nexport const getReviewerReviews = async (\n request: FastifyRequest<{\n Params: { reviewerId: string };\n Querystring: { page?: number; pageSize?: number };\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const { reviewerId } = request.params;\n const { page = 1, pageSize = 20 } = request.query;\n\n const skip = (Number(page) - 1) * Number(pageSize);\n const [reviews, total] = await Promise.all([\n reviewerService.findReviewsByReviewerId(reviewerId, skip, Number(pageSize)),\n reviewerService.countReviewsByReviewerId(reviewerId),\n ]);\n\n return reply.send(\n formatPaginatedResponse<ReviewerReviewAPI>({\n data: reviews.map(toReviewAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages: Math.ceil(total / Number(pageSize)),\n totalItems: total,\n })\n );\n};\n\nexport type SubmitReviewBody = { rating: number; comment?: string };\n\nexport const submitReview = async (\n request: FastifyRequest<{\n Params: { missionId: string };\n Body: SubmitReviewBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n if (String(mission.clientUserId) !== String(user.id)) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n if (mission.status !== 'completed') {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_REVIEW_MISSION_NOT_COMPLETED'\n );\n }\n\n const existing = await reviewerService.findReviewByMissionId(\n request.params.missionId\n );\n if (existing) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_REVIEW_ALREADY_EXISTS'\n );\n }\n\n const review = await reviewerService.createReview({\n missionId: mission.id,\n reviewerId: mission.reviewerId,\n rating: request.body.rating,\n comment: request.body.comment,\n });\n\n await reviewerService.updateRating(\n String(mission.reviewerId),\n request.body.rating\n );\n\n return reply.status(201).send(\n formatResponse<ReviewerReviewAPI>({\n data: toReviewAPI(review),\n message: t({\n en: 'Review submitted',\n fr: 'Avis soumis',\n es: 'Reseña enviada',\n de: 'Bewertung eingereicht',\n ar: 'تم تقديم المراجعة',\n 'en-GB': 'Review submitted',\n ru: 'Отзыв отправлен',\n ja: 'レビューを送信しました',\n ko: '리뷰가 제출되었습니다',\n zh: '评价已提交',\n it: 'Recensione inviata',\n pt: 'Avaliação enviada',\n hi: 'समीक्षा सबमिट की गई',\n tr: 'İnceleme gönderildi',\n pl: 'Recenzja przesłana',\n id: 'Ulasan dikirim',\n vi: 'Đánh giá đã được gửi',\n uk: 'Відгук надіслано',\n }),\n })\n );\n};\n\n// ── Missions ─────────────────────────────────────────────────────────────────\n\nexport type EstimateMissionBody = {\n dictionaryIds: string[];\n sourceLocale: string;\n pricePerHour: number;\n};\n\nexport const estimateMission = async (\n request: FastifyRequest<{ Body: EstimateMissionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { dictionaryIds, sourceLocale, pricePerHour } = request.body;\n\n const estimate = await missionService.calculateMissionEstimate(\n dictionaryIds,\n sourceLocale,\n pricePerHour\n );\n\n return reply.send(formatResponse<MissionEstimate>({ data: estimate }));\n};\n\nexport type CreateMissionBody = {\n reviewerId: string;\n dictionaryIds: string[];\n sourceLocale: string;\n targetLocales: string[];\n projectId?: string;\n notes?: string;\n};\n\nexport const createMission = async (\n request: FastifyRequest<{ Body: CreateMissionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const reviewer = await reviewerService.findReviewerById(\n request.body.reviewerId\n );\n if (!reviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n const estimate = await missionService.calculateMissionEstimate(\n request.body.dictionaryIds,\n request.body.sourceLocale,\n reviewer.pricePerHour\n );\n\n const mission = await missionService.createMission({\n reviewerId: reviewer.id,\n clientUserId: user.id as any,\n projectId: request.body.projectId as any,\n dictionaryIds: request.body.dictionaryIds as any[],\n sourceLocale: request.body.sourceLocale,\n targetLocales: request.body.targetLocales,\n wordCount: estimate.wordCount,\n estimatedHours: estimate.estimatedHours,\n pricePerHour: reviewer.pricePerHour,\n totalPrice: estimate.totalPrice,\n currency: estimate.currency,\n notes: request.body.notes,\n });\n\n const missionLink = `${process.env.APP_URL}/find-reviewer/dashboard/mission/${String(mission.id)}`;\n\n // Notify the client that their request was sent\n sendEmail({\n type: 'missionRequestedClient',\n to: user.email,\n clientUsername: user.name ?? user.email,\n reviewerName: reviewer.name ?? 'the reviewer',\n missionLink,\n }).catch((err) =>\n logger.error('Failed to send missionRequestedClient email', err)\n );\n\n // Notify the reviewer that someone contacted them\n const reviewerUser = await userService.getUserById(String(reviewer.userId));\n if (reviewerUser?.email) {\n sendEmail({\n type: 'missionRequestedReviewer',\n to: reviewerUser.email,\n reviewerUsername: reviewerUser.name ?? reviewerUser.email,\n clientName: user.name ?? user.email,\n sourceLocale: request.body.sourceLocale,\n targetLocales: request.body.targetLocales,\n notes: request.body.notes,\n missionLink,\n }).catch((err) =>\n logger.error('Failed to send missionRequestedReviewer email', err)\n );\n }\n\n return reply.status(201).send(\n formatResponse<TranslationMissionAPI>({\n data: toMissionAPI(mission),\n message: t({\n en: 'Mission created',\n fr: 'Mission créée',\n es: 'Misión creada',\n de: 'Auftrag erstellt',\n ar: 'تم إنشاء المهمة',\n 'en-GB': 'Mission created',\n ru: 'Задание создано',\n ja: 'ミッションが作成されました',\n ko: '미션이 생성되었습니다',\n zh: '任务已创建',\n it: 'Missione creata',\n pt: 'Missão criada',\n hi: 'मिशन बनाया गया',\n tr: 'Görev oluşturuldu',\n pl: 'Misja utworzona',\n id: 'Misi dibuat',\n vi: 'Nhiệm vụ đã được tạo',\n uk: 'Завдання створено',\n }),\n })\n );\n};\n\nexport const getMyMissions = async (\n request: FastifyRequest<{\n Querystring: {\n role?: 'client' | 'reviewer';\n page?: number;\n pageSize?: number;\n };\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const { role = 'client', page = 1, pageSize = 20 } = request.query;\n const skip = (Number(page) - 1) * Number(pageSize);\n const userId = String(user.id);\n\n if (role === 'reviewer') {\n const profile = await reviewerService.findReviewerByUserId(userId);\n if (!profile) {\n return reply.send(\n formatPaginatedResponse<TranslationMissionAPI>({\n data: [],\n page: 1,\n pageSize: Number(pageSize),\n totalPages: 0,\n totalItems: 0,\n })\n );\n }\n const missions = await missionService.findMissionsForReviewerProfile(\n String(profile.id),\n skip,\n Number(pageSize)\n );\n return reply.send(\n formatPaginatedResponse<TranslationMissionAPI>({\n data: missions.map(toMissionAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages: 1,\n totalItems: missions.length,\n })\n );\n }\n\n const missions = await missionService.findMissionsForUser(\n userId,\n skip,\n Number(pageSize)\n );\n\n return reply.send(\n formatPaginatedResponse<TranslationMissionAPI>({\n data: missions.map(toMissionAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages: 1,\n totalItems: missions.length,\n })\n );\n};\n\nexport const getMissionById = async (\n request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n return reply.send(\n formatResponse<TranslationMissionAPI>({ data: toMissionAPI(mission) })\n );\n};\n\nexport type UpdateMissionStatusBody = { status: MissionStatus };\n\nexport const updateMissionStatus = async (\n request: FastifyRequest<{\n Params: { missionId: string };\n Body: UpdateMissionStatusBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n const newStatus = request.body.status;\n\n const updated = await missionService.updateMissionStatus(\n request.params.missionId,\n newStatus\n );\n\n if (newStatus === 'completed' && profile) {\n await reviewerService.incrementMissionCount(String(profile.id));\n }\n\n return reply.send(\n formatResponse<TranslationMissionAPI>({\n data: toMissionAPI(updated),\n message: t({\n en: 'Mission status updated',\n fr: 'Statut de la mission mis à jour',\n es: 'Estado de la misión actualizado',\n de: 'Auftragsstatus aktualisiert',\n ar: 'تم تحديث حالة المهمة',\n 'en-GB': 'Mission status updated',\n ru: 'Статус задания обновлён',\n ja: 'ミッションのステータスを更新しました',\n ko: '미션 상태가 업데이트되었습니다',\n zh: '任务状态已更新',\n it: 'Stato missione aggiornato',\n pt: 'Estado da missão atualizado',\n hi: 'मिशन स्थिति अपडेट की गई',\n tr: 'Görev durumu güncellendi',\n pl: 'Status misji zaktualizowany',\n id: 'Status misi diperbarui',\n vi: 'Trạng thái nhiệm vụ đã được cập nhật',\n uk: 'Статус завдання оновлено',\n }),\n })\n );\n};\n\n// ── Chat ─────────────────────────────────────────────────────────────────────\n\nexport const getChatHistory = async (\n request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n const messages = await messageService.findMessagesByMissionId(\n request.params.missionId\n );\n await messageService.markMessagesRead(request.params.missionId, userId);\n\n return reply.send(\n formatResponse<ReviewerMessageAPI[]>({ data: messages.map(toMessageAPI) })\n );\n};\n\nexport type SendMessageBody = { content: string };\n\nexport const sendMessage = async (\n request: FastifyRequest<{\n Params: { missionId: string };\n Body: SendMessageBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n const message = await messageService.createMessage(\n request.params.missionId,\n userId,\n request.body.content\n );\n\n return reply\n .status(201)\n .send(formatResponse<ReviewerMessageAPI>({ data: toMessageAPI(message) }));\n};\n\nexport const chatSSE = async (\n request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n reply.raw.statusCode = 401;\n reply.raw.end();\n return;\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n reply.raw.statusCode = 404;\n reply.raw.end();\n return;\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n reply.raw.statusCode = 403;\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) reply.raw.setHeader(key, value as string);\n });\n\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');\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 let lastChecked = new Date();\n\n const interval = setInterval(async () => {\n try {\n const newMessages = await messageService.findNewMessagesSince(\n request.params.missionId,\n lastChecked\n );\n if (newMessages.length > 0) {\n lastChecked = new Date();\n for (const msg of newMessages) {\n send(toMessageAPI(msg));\n }\n }\n } catch (error) {\n logger.error('Error polling chat messages', error);\n }\n }, 2000);\n\n request.raw.on('close', () => {\n clearInterval(interval);\n });\n};\n\n// ── Payment stubs ─────────────────────────────────────────────────────────────\n\nexport const createPaymentIntent = async (\n _request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n // TODO: create Stripe PaymentIntent for mission.totalPrice\n // Charge the client, hold funds in escrow until mission is completed\n return reply.send(formatResponse({ data: null, message: 'Not implemented' }));\n};\n\nexport const confirmPayment = async (\n _request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n // TODO: confirm Stripe payment and release funds to reviewer's Stripe account\n return reply.send(formatResponse({ data: null, message: 'Not implemented' }));\n};\n\nexport const requestPayout = async (\n _request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n // TODO: trigger payout to reviewer's Stripe Connect account\n return reply.send(formatResponse({ data: null, message: 'Not implemented' }));\n};\n\n// ── Contact ───────────────────────────────────────────────────────────────────\n\nexport const contactReviewer = async (\n request: FastifyRequest<{\n Params: { reviewerId: string };\n Body: { message: string };\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const { reviewerId } = request.params;\n const { message } = request.body;\n const user = (request as any).user;\n\n if (!message?.trim()) {\n return reply\n .status(400)\n .send(formatResponse({ data: null, message: 'Message is required' }));\n }\n\n const reviewer = await reviewerService.findReviewerById(reviewerId);\n if (!reviewer) {\n return reply\n .status(404)\n .send(formatResponse({ data: null, message: 'Reviewer not found' }));\n }\n\n const reviewerUser = await userService.getUserById(String(reviewer.userId));\n if (reviewerUser?.email) {\n sendEmail({\n type: 'reviewerContactInquiry',\n to: reviewerUser.email,\n reviewerUsername: reviewerUser.name ?? reviewerUser.email,\n clientName: user?.name ?? user?.email ?? 'A user',\n message: message.trim(),\n }).catch((err) =>\n logger.error('Failed to send reviewerContactInquiry email', err)\n );\n }\n\n return reply\n .status(200)\n .send(formatResponse({ data: null, message: 'Message sent' }));\n};\n"],"mappings":";;;;;;;;;;;;AAgCA,MAAM,gBAAgB,QAAiC,IAAI,OAAO;AAClE,MAAM,gBAAgB,QAAoC,IAAI,OAAO;AACrE,MAAM,eAAe,QAAgC,IAAI,OAAO;AAChE,MAAM,gBAAgB,QAAiC,IAAI,OAAO;AA2BlE,MAAa,iBAAiB,OAC5B,SACA,UACkB;CAClB,MAAM,EACJ,OAAO,GACP,WAAW,IACX,YACA,UACA,WACA,iBACA,iBACA,eACE,QAAQ;CAEZ,MAAM,QAA6B,CAAC;CAEpC,IAAI,cAAc,UAAU;EAC1B,MAAM,aAAkC,CAAC;EACzC,IAAI,YAAY,WAAW,OAAO;EAClC,IAAI,UAAU,WAAW,KAAK;EAC9B,MAAM,gBAAgB,EAAE,YAAY,WAAW;CACjD;CAEA,IAAI,cAAc,QAChB,MAAM,gBAAgB,EAAE,MAAM,OAAO,SAAS,EAAE;CAElD,IAAI,oBAAoB,UAAa,oBAAoB,QAAW;EAClE,MAAM,eAAe,CAAC;EACtB,IAAI,oBAAoB,QACtB,MAAM,aAAa,OAAO,OAAO,eAAe;EAClD,IAAI,oBAAoB,QACtB,MAAM,aAAa,OAAO,OAAO,eAAe;CACpD;CAEA,IAAI,YAAY;EACd,MAAM,OAAO,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC,UAAU;EACjE,IAAI,KAAK,SAAS,GAAG,MAAM,aAAa,EAAE,KAAK,KAAK;CACtD;CAEA,MAAM,QAAQ,OAAO,IAAI,IAAI,KAAK,OAAO,QAAQ;CACjD,MAAM,CAAC,UAAU,SAAS,MAAM,QAAQ,IAAI,CAC1CA,qBAAqC,OAAO,MAAM,OAAO,QAAQ,CAAC,GAClEC,sBAAsC,KAAK,CAC7C,CAAC;CAED,MAAM,aAAa,KAAK,KAAK,QAAQ,OAAO,QAAQ,CAAC;CAErD,OAAO,MAAM,KACX,wBAA4C;EAC1C,MAAM,SAAS,IAAI,YAAY;EAC/B,MAAM,OAAO,IAAI;EACjB,UAAU,OAAO,QAAQ;EACzB;EACA,YAAY;EACZ,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAEA,MAAa,uBAAuB,OAClC,SAMA,UACkB;CAClB,MAAM,EAAE,YAAY,UAAU,WAAW,eAAe,QAAQ;CAEhE,MAAM,QAA6B,CAAC;CAEpC,IAAI,cAAc,UAAU;EAC1B,MAAM,aAAkC,CAAC;EACzC,IAAI,YAAY,WAAW,OAAO;EAClC,IAAI,UAAU,WAAW,KAAK;EAC9B,MAAM,gBAAgB,EAAE,YAAY,WAAW;CACjD;CACA,IAAI,cAAc,QAChB,MAAM,gBAAgB,EAAE,MAAM,OAAO,SAAS,EAAE;CAClD,IAAI,YAAY;EACd,MAAM,OAAO,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC,UAAU;EACjE,IAAI,KAAK,SAAS,GAAG,MAAM,aAAa,EAAE,KAAK,KAAK;CACtD;CAEA,IAAI;EACF,MAAM,UAAU,MAAMC,6BAA6C,KAAK;EACxE,MAAM,YAAY,QAAQ,SAAS,IAAI,QAAQ,EAAE,CAAC,MAAM;EACxD,MAAM,YACJ,QAAQ,SAAS,IAAI,QAAQ,QAAQ,SAAS,EAAE,CAAC,MAAM;EAEzD,OAAO,MAAM,KACX,eAAsC;GACpC,MAAM;IAAE;IAAS;IAAW;GAAU;GACtC,SAAS;EACX,CAAC,CACH;CACF,SAAS,QAAQ;EACf,OAAO,MACJ,OAAO,GAAG,CAAC,CACX,KAAK,EAAE,OAAO,qCAAqC,CAAC;CACzD;AACF;AAEA,MAAa,kBAAkB,OAC7B,SACA,UACkB;CAClB,MAAM,EAAE,eAAe,QAAQ;CAC/B,MAAM,UAAU,MAAMC,iBAAiC,UAAU;CAEjE,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,OAAO,MAAM,KACX,eAAmC;EACjC,MAAM,aAAa,OAAO;EAC1B,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAUA,MAAa,qBAAqB,OAChC,SACA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAI1E,IAAI,MADmBC,qBAAqC,OAAO,KAAK,EAAE,CAAC,GAEzE,OAAO,aAAa,2BAClB,OACA,iCACF;CAGF,MAAM,EAAE,KAAK,eAAe,YAAY,iBAAiB,QAAQ;CAEjE,MAAM,UAAU,MAAMC,sBAAsC;EAC1D,QAAQ,KAAK;EACb;EACA,eAAe,iBAAiB,CAAC;EACjC,YAAa,cAAc,CAAC;EAC5B,cAAc,gBAAgB;CAChC,CAAC;CAGD,MAAM,UAAU;EACd,MAAM;EACN,IAAI;EACJ,UAAU,KAAK,QAAQ,KAAK;EAC5B,WAAW,KAAK;EAChB,aAAa,GAAG,QAAQ,IAAI,QAAQ,mBAAmB,OAAO,QAAQ,EAAE;CAC1E,CAAC,CAAC,CAAC,OAAO,QACR,OAAO,MAAM,6CAA6C,GAAG,CAC/D;CAEA,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KACvB,eAAmC;EACjC,MAAM,aAAa,OAAO;EAC1B,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAOA,MAAa,wBAAwB,OACnC,SACA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMD,qBAAqC,OAAO,KAAK,EAAE,CAAC;CAC1E,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,MAAM,UAAU,MAAME,wBACpB,OAAO,QAAQ,EAAE,GACjB,QAAQ,IACV;CAEA,OAAO,MAAM,KACX,eAAmC;EACjC,MAAM,aAAa,OAAO;EAC1B,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAEA,MAAa,0BAA0B,OACrC,SACA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMF,qBAAqC,OAAO,KAAK,EAAE,CAAC;CAC1E,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,MAAMG,sBAAsC,OAAO,QAAQ,EAAE,CAAC;CAE9D,OAAO,MAAM,KACX,eAAqB;EACnB,MAAM;EACN,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAEA,MAAa,uBAAuB,OAClC,SACA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMH,qBAAqC,OAAO,KAAK,EAAE,CAAC;CAE1E,OAAO,MAAM,KACX,eAA0C,EACxC,MAAM,UAAU,aAAa,OAAO,IAAI,KAC1C,CAAC,CACH;AACF;AAIA,MAAa,oBAAoB,OAC/B,SAOA,UACkB;CAElB,KADoB,QAAQ,SAAS,KACtB,EAAE,SAAS,SACxB,OAAO,aAAa,2BAA2B,OAAO,mBAAmB;CAG3E,MAAM,EAAE,OAAO,GAAG,WAAW,IAAI,WAAW,QAAQ;CACpD,MAAM,QAA6B,CAAC;CACpC,IAAI,QAAQ,MAAM,SAAS;CAE3B,MAAM,QAAQ,OAAO,IAAI,IAAI,KAAK,OAAO,QAAQ;CACjD,MAAM,CAAC,UAAU,SAAS,MAAM,QAAQ,IAAI,CAC1CI,0BAA0C,OAAO,MAAM,OAAO,QAAQ,CAAC,GACvEC,2BAA2C,KAAK,CAClD,CAAC;CAED,OAAO,MAAM,KACX,wBAA4C;EAC1C,MAAM,SAAS,IAAI,YAAY;EAC/B,MAAM,OAAO,IAAI;EACjB,UAAU,OAAO,QAAQ;EACzB,YAAY,KAAK,KAAK,QAAQ,OAAO,QAAQ,CAAC;EAC9C,YAAY;CACd,CAAC,CACH;AACF;AAIA,MAAa,0BAA0B,OACrC,SACA,UACkB;CAClB,MAAM,cAAc,QAAQ,SAAS;CACrC,IAAI,aAAa,SAAS,SACxB,OAAO,aAAa,2BAA2B,OAAO,mBAAmB;CAG3E,MAAM,EAAE,eAAe,QAAQ;CAE/B,MAAM,UAAU,MAAMN,iBAAiC,UAAU;CACjE,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,IAAI,QAAQ,WAAW,UAErB,OAAO,MAAM,KACX,eAAmC;EACjC,MAAM,aAAa,OAAO;EAC1B,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;CAGF,MAAM,UAAU,MAAMG,wBAAsC,YAAY,EACtE,QAAQ,SACV,CAAC;CAGD,MAAM,aAAa,MAAMI,YAAwB,OAAO,QAAQ,MAAM,CAAC;CACvE,IAAI,YACF,MAAM,UAAU;EACd,MAAM;EACN,IAAI,WAAW;EACf,QAAS,WAAmB;EAC5B,UAAU,WAAW,QAAQ,WAAW;EACxC,eAAe,GAAG,QAAQ,IAAI,QAAQ;CACxC,CAAC,CAAC,CAAC,OAAO,QACR,OAAO,MAAM,0CAA0C,GAAG,CAC5D;CAGF,OAAO,KACL,oBAAoB,WAAW,sBAAsB,OAAO,YAAY,EAAE,GAC5E;CAEA,OAAO,MAAM,KACX,eAAmC;EACjC,MAAM,aAAa,OAAO;EAC1B,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAMA,MAAM,gCACH,SACD,OAAO,SAAyB,UAAuC;CACrE,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMN,qBAAqC,OAAO,KAAK,EAAE,CAAC;CAC1E,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAOF,MAAM,kBAAkB,+BAJD,QAAQ,QAAQ,mBAAmB,GACxB,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,KAAK,cACrC,OAAO,QAAQ,QAAQ,qBAAqB,CAIpD,CACd;CACA,IAAI,oBAAoB,oBACtB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KACvB,eAAe;EACb,MAAM;EACN,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;CAGF,IAAI,oBAAoB,aACtB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KACvB,eAAe;EACb,MAAM;EACN,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;CAGF,MAAM,SAAS,QAAQ;CACvB,IAAI,CAAC,OAAO,SAAS,MAAM,KAAK,OAAO,WAAW,GAChD,OAAO,aAAa,2BAClB,OACA,qBACF;CAGF,MAAM,QAAQ,SAAS,SAAS,gBAAgB;CAChD,MAAM,WAAY,QAAgB;CAClC,IAAI,UACF,MAAM,sBAAsB,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC;CAGtD,MAAM,WAAW,MAAM,sBACrB,QACA,OAAO,QAAQ,EAAE,GACjB,IACF;CAEA,MAAM,UAAU,MAAME,wBACpB,OAAO,QAAQ,EAAE,GACjB,GACG,QAAQ,SACX,CACF;CAEA,OAAO,KACL,YAAY,KAAK,gCAAgC,OAAO,QAAQ,EAAE,GACpE;CAEA,OAAO,MAAM,KACX,eAAmC;EACjC,MAAM,aAAa,OAAO;EAC1B,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAEF,MAAa,4BAA4B,6BAA6B,MAAM;AAC5E,MAAa,6BAA6B,6BAA6B,OAAO;AAI9E,MAAa,qBAAqB,OAChC,SAIA,UACkB;CAClB,MAAM,EAAE,eAAe,QAAQ;CAC/B,MAAM,EAAE,OAAO,GAAG,WAAW,OAAO,QAAQ;CAE5C,MAAM,QAAQ,OAAO,IAAI,IAAI,KAAK,OAAO,QAAQ;CACjD,MAAM,CAAC,SAAS,SAAS,MAAM,QAAQ,IAAI,CACzCK,wBAAwC,YAAY,MAAM,OAAO,QAAQ,CAAC,GAC1EC,yBAAyC,UAAU,CACrD,CAAC;CAED,OAAO,MAAM,KACX,wBAA2C;EACzC,MAAM,QAAQ,IAAI,WAAW;EAC7B,MAAM,OAAO,IAAI;EACjB,UAAU,OAAO,QAAQ;EACzB,YAAY,KAAK,KAAK,QAAQ,OAAO,QAAQ,CAAC;EAC9C,YAAY;CACd,CAAC,CACH;AACF;AAIA,MAAa,eAAe,OAC1B,SAIA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMC,gBACpB,QAAQ,OAAO,SACjB;CACA,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,IAAI,OAAO,QAAQ,YAAY,MAAM,OAAO,KAAK,EAAE,GACjD,OAAO,aAAa,2BAClB,OACA,+BACF;CAGF,IAAI,QAAQ,WAAW,aACrB,OAAO,aAAa,2BAClB,OACA,uCACF;CAMF,IAAI,MAHmBC,sBACrB,QAAQ,OAAO,SACjB,GAEE,OAAO,aAAa,2BAClB,OACA,gCACF;CAGF,MAAM,SAAS,MAAMC,aAA6B;EAChD,WAAW,QAAQ;EACnB,YAAY,QAAQ;EACpB,QAAQ,QAAQ,KAAK;EACrB,SAAS,QAAQ,KAAK;CACxB,CAAC;CAED,MAAMC,aACJ,OAAO,QAAQ,UAAU,GACzB,QAAQ,KAAK,MACf;CAEA,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KACvB,eAAkC;EAChC,MAAM,YAAY,MAAM;EACxB,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAUA,MAAa,kBAAkB,OAC7B,SACA,UACkB;CAClB,MAAM,EAAE,eAAe,cAAc,iBAAiB,QAAQ;CAE9D,MAAM,WAAW,MAAMC,yBACrB,eACA,cACA,YACF;CAEA,OAAO,MAAM,KAAK,eAAgC,EAAE,MAAM,SAAS,CAAC,CAAC;AACvE;AAWA,MAAa,gBAAgB,OAC3B,SACA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,WAAW,MAAMd,iBACrB,QAAQ,KAAK,UACf;CACA,IAAI,CAAC,UACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,MAAM,WAAW,MAAMc,yBACrB,QAAQ,KAAK,eACb,QAAQ,KAAK,cACb,SAAS,YACX;CAEA,MAAM,UAAU,MAAMC,gBAA6B;EACjD,YAAY,SAAS;EACrB,cAAc,KAAK;EACnB,WAAW,QAAQ,KAAK;EACxB,eAAe,QAAQ,KAAK;EAC5B,cAAc,QAAQ,KAAK;EAC3B,eAAe,QAAQ,KAAK;EAC5B,WAAW,SAAS;EACpB,gBAAgB,SAAS;EACzB,cAAc,SAAS;EACvB,YAAY,SAAS;EACrB,UAAU,SAAS;EACnB,OAAO,QAAQ,KAAK;CACtB,CAAC;CAED,MAAM,cAAc,GAAG,QAAQ,IAAI,QAAQ,mCAAmC,OAAO,QAAQ,EAAE;CAG/F,UAAU;EACR,MAAM;EACN,IAAI,KAAK;EACT,gBAAgB,KAAK,QAAQ,KAAK;EAClC,cAAc,SAAS,QAAQ;EAC/B;CACF,CAAC,CAAC,CAAC,OAAO,QACR,OAAO,MAAM,+CAA+C,GAAG,CACjE;CAGA,MAAM,eAAe,MAAMR,YAAwB,OAAO,SAAS,MAAM,CAAC;CAC1E,IAAI,cAAc,OAChB,UAAU;EACR,MAAM;EACN,IAAI,aAAa;EACjB,kBAAkB,aAAa,QAAQ,aAAa;EACpD,YAAY,KAAK,QAAQ,KAAK;EAC9B,cAAc,QAAQ,KAAK;EAC3B,eAAe,QAAQ,KAAK;EAC5B,OAAO,QAAQ,KAAK;EACpB;CACF,CAAC,CAAC,CAAC,OAAO,QACR,OAAO,MAAM,iDAAiD,GAAG,CACnE;CAGF,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KACvB,eAAsC;EACpC,MAAM,aAAa,OAAO;EAC1B,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAEA,MAAa,gBAAgB,OAC3B,SAOA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,EAAE,OAAO,UAAU,OAAO,GAAG,WAAW,OAAO,QAAQ;CAC7D,MAAM,QAAQ,OAAO,IAAI,IAAI,KAAK,OAAO,QAAQ;CACjD,MAAM,SAAS,OAAO,KAAK,EAAE;CAE7B,IAAI,SAAS,YAAY;EACvB,MAAM,UAAU,MAAMN,qBAAqC,MAAM;EACjE,IAAI,CAAC,SACH,OAAO,MAAM,KACX,wBAA+C;GAC7C,MAAM,CAAC;GACP,MAAM;GACN,UAAU,OAAO,QAAQ;GACzB,YAAY;GACZ,YAAY;EACd,CAAC,CACH;EAEF,MAAM,WAAW,MAAMe,+BACrB,OAAO,QAAQ,EAAE,GACjB,MACA,OAAO,QAAQ,CACjB;EACA,OAAO,MAAM,KACX,wBAA+C;GAC7C,MAAM,SAAS,IAAI,YAAY;GAC/B,MAAM,OAAO,IAAI;GACjB,UAAU,OAAO,QAAQ;GACzB,YAAY;GACZ,YAAY,SAAS;EACvB,CAAC,CACH;CACF;CAEA,MAAM,WAAW,MAAMC,oBACrB,QACA,MACA,OAAO,QAAQ,CACjB;CAEA,OAAO,MAAM,KACX,wBAA+C;EAC7C,MAAM,SAAS,IAAI,YAAY;EAC/B,MAAM,OAAO,IAAI;EACjB,UAAU,OAAO,QAAQ;EACzB,YAAY;EACZ,YAAY,SAAS;CACvB,CAAC,CACH;AACF;AAEA,MAAa,iBAAiB,OAC5B,SACA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMP,gBACpB,QAAQ,OAAO,SACjB;CACA,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,MAAM,SAAS,OAAO,KAAK,EAAE;CAC7B,MAAM,UAAU,MAAMT,qBAAqC,MAAM;CACjE,MAAM,WAAW,OAAO,QAAQ,YAAY,MAAM;CAClD,MAAM,aACJ,WAAW,OAAO,QAAQ,UAAU,MAAM,OAAO,QAAQ,EAAE;CAE7D,IAAI,CAAC,YAAY,CAAC,YAChB,OAAO,aAAa,2BAClB,OACA,+BACF;CAGF,OAAO,MAAM,KACX,eAAsC,EAAE,MAAM,aAAa,OAAO,EAAE,CAAC,CACvE;AACF;AAIA,MAAa,sBAAsB,OACjC,SAIA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMS,gBACpB,QAAQ,OAAO,SACjB;CACA,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,MAAM,SAAS,OAAO,KAAK,EAAE;CAC7B,MAAM,UAAU,MAAMT,qBAAqC,MAAM;CACjE,MAAM,WAAW,OAAO,QAAQ,YAAY,MAAM;CAClD,MAAM,aACJ,WAAW,OAAO,QAAQ,UAAU,MAAM,OAAO,QAAQ,EAAE;CAE7D,IAAI,CAAC,YAAY,CAAC,YAChB,OAAO,aAAa,2BAClB,OACA,+BACF;CAGF,MAAM,YAAY,QAAQ,KAAK;CAE/B,MAAM,UAAU,MAAMiB,sBACpB,QAAQ,OAAO,WACf,SACF;CAEA,IAAI,cAAc,eAAe,SAC/B,MAAMC,sBAAsC,OAAO,QAAQ,EAAE,CAAC;CAGhE,OAAO,MAAM,KACX,eAAsC;EACpC,MAAM,aAAa,OAAO;EAC1B,SAAS,EAAE;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN,CAAC;CACH,CAAC,CACH;AACF;AAIA,MAAa,iBAAiB,OAC5B,SACA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMT,gBACpB,QAAQ,OAAO,SACjB;CACA,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,MAAM,SAAS,OAAO,KAAK,EAAE;CAC7B,MAAM,UAAU,MAAMT,qBAAqC,MAAM;CACjE,MAAM,WAAW,OAAO,QAAQ,YAAY,MAAM;CAClD,MAAM,aACJ,WAAW,OAAO,QAAQ,UAAU,MAAM,OAAO,QAAQ,EAAE;CAE7D,IAAI,CAAC,YAAY,CAAC,YAChB,OAAO,aAAa,2BAClB,OACA,+BACF;CAGF,MAAM,WAAW,MAAMmB,wBACrB,QAAQ,OAAO,SACjB;CACA,MAAMC,iBAAgC,QAAQ,OAAO,WAAW,MAAM;CAEtE,OAAO,MAAM,KACX,eAAqC,EAAE,MAAM,SAAS,IAAI,YAAY,EAAE,CAAC,CAC3E;AACF;AAIA,MAAa,cAAc,OACzB,SAIA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO,aAAa,2BAA2B,OAAO,kBAAkB;CAG1E,MAAM,UAAU,MAAMX,gBACpB,QAAQ,OAAO,SACjB;CACA,IAAI,CAAC,SACH,OAAO,aAAa,2BAClB,OACA,4BACF;CAGF,MAAM,SAAS,OAAO,KAAK,EAAE;CAC7B,MAAM,UAAU,MAAMT,qBAAqC,MAAM;CACjE,MAAM,WAAW,OAAO,QAAQ,YAAY,MAAM;CAClD,MAAM,aACJ,WAAW,OAAO,QAAQ,UAAU,MAAM,OAAO,QAAQ,EAAE;CAE7D,IAAI,CAAC,YAAY,CAAC,YAChB,OAAO,aAAa,2BAClB,OACA,+BACF;CAGF,MAAM,UAAU,MAAMqB,cACpB,QAAQ,OAAO,WACf,QACA,QAAQ,KAAK,OACf;CAEA,OAAO,MACJ,OAAO,GAAG,CAAC,CACX,KAAK,eAAmC,EAAE,MAAM,aAAa,OAAO,EAAE,CAAC,CAAC;AAC7E;AAEA,MAAa,UAAU,OACrB,SACA,UACkB;CAClB,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,CAAC,MAAM;EACT,MAAM,IAAI,aAAa;EACvB,MAAM,IAAI,IAAI;EACd;CACF;CAEA,MAAM,UAAU,MAAMZ,gBACpB,QAAQ,OAAO,SACjB;CACA,IAAI,CAAC,SAAS;EACZ,MAAM,IAAI,aAAa;EACvB,MAAM,IAAI,IAAI;EACd;CACF;CAEA,MAAM,SAAS,OAAO,KAAK,EAAE;CAC7B,MAAM,UAAU,MAAMT,qBAAqC,MAAM;CACjE,MAAM,WAAW,OAAO,QAAQ,YAAY,MAAM;CAClD,MAAM,aACJ,WAAW,OAAO,QAAQ,UAAU,MAAM,OAAO,QAAQ,EAAE;CAE7D,IAAI,CAAC,YAAY,CAAC,YAAY;EAC5B,MAAM,IAAI,aAAa;EACvB,MAAM,IAAI,IAAI;EACd;CACF;CAEA,MAAM,OAAO;CAEb,MAAM,UAAU,MAAM,WAAW;CACjC,OAAO,QAAQ,OAAO,CAAC,CAAC,SAAS,CAAC,KAAK,WAAW;EAChD,IAAI,UAAU,QAAW,MAAM,IAAI,UAAU,KAAK,KAAe;CACnE,CAAC;CAED,MAAM,IAAI,UAAU,gBAAgB,kCAAkC;CACtE,MAAM,IAAI,UAAU,iBAAiB,wBAAwB;CAC7D,MAAM,IAAI,UAAU,cAAc,YAAY;CAC9C,MAAM,IAAI,UAAU,qBAAqB,IAAI;CAC7C,MAAM,IAAI,eAAe;CAEzB,MAAM,IAAI,MAAM,iBAAiB;CAEjC,MAAM,QAAQ,SAAc;EAC1B,IAAI,CAAC,MAAM,IAAI,iBAAiB,CAAC,MAAM,IAAI,WACzC,MAAM,IAAI,MAAM,SAAS,KAAK,UAAU,IAAI,EAAE,KAAK;CAEvD;CAEA,IAAI,8BAAc,IAAI,KAAK;CAE3B,MAAM,WAAW,YAAY,YAAY;EACvC,IAAI;GACF,MAAM,cAAc,MAAMsB,qBACxB,QAAQ,OAAO,WACf,WACF;GACA,IAAI,YAAY,SAAS,GAAG;IAC1B,8BAAc,IAAI,KAAK;IACvB,KAAK,MAAM,OAAO,aAChB,KAAK,aAAa,GAAG,CAAC;GAE1B;EACF,SAAS,OAAO;GACd,OAAO,MAAM,+BAA+B,KAAK;EACnD;CACF,GAAG,GAAI;CAEP,QAAQ,IAAI,GAAG,eAAe;EAC5B,cAAc,QAAQ;CACxB,CAAC;AACH;AAIA,MAAa,sBAAsB,OACjC,UACA,UACkB;CAGlB,OAAO,MAAM,KAAK,eAAe;EAAE,MAAM;EAAM,SAAS;CAAkB,CAAC,CAAC;AAC9E;AAEA,MAAa,iBAAiB,OAC5B,UACA,UACkB;CAElB,OAAO,MAAM,KAAK,eAAe;EAAE,MAAM;EAAM,SAAS;CAAkB,CAAC,CAAC;AAC9E;AAEA,MAAa,gBAAgB,OAC3B,UACA,UACkB;CAElB,OAAO,MAAM,KAAK,eAAe;EAAE,MAAM;EAAM,SAAS;CAAkB,CAAC,CAAC;AAC9E;AAIA,MAAa,kBAAkB,OAC7B,SAIA,UACkB;CAClB,MAAM,EAAE,eAAe,QAAQ;CAC/B,MAAM,EAAE,YAAY,QAAQ;CAC5B,MAAM,OAAQ,QAAgB;CAE9B,IAAI,CAAC,SAAS,KAAK,GACjB,OAAO,MACJ,OAAO,GAAG,CAAC,CACX,KAAK,eAAe;EAAE,MAAM;EAAM,SAAS;CAAsB,CAAC,CAAC;CAGxE,MAAM,WAAW,MAAMvB,iBAAiC,UAAU;CAClE,IAAI,CAAC,UACH,OAAO,MACJ,OAAO,GAAG,CAAC,CACX,KAAK,eAAe;EAAE,MAAM;EAAM,SAAS;CAAqB,CAAC,CAAC;CAGvE,MAAM,eAAe,MAAMO,YAAwB,OAAO,SAAS,MAAM,CAAC;CAC1E,IAAI,cAAc,OAChB,UAAU;EACR,MAAM;EACN,IAAI,aAAa;EACjB,kBAAkB,aAAa,QAAQ,aAAa;EACpD,YAAY,MAAM,QAAQ,MAAM,SAAS;EACzC,SAAS,QAAQ,KAAK;CACxB,CAAC,CAAC,CAAC,OAAO,QACR,OAAO,MAAM,+CAA+C,GAAG,CACjE;CAGF,OAAO,MACJ,OAAO,GAAG,CAAC,CACX,KAAK,eAAe;EAAE,MAAM;EAAM,SAAS;CAAe,CAAC,CAAC;AACjE"}
|
|
1
|
+
{"version":3,"file":"reviewer.controller.mjs","names":["reviewerService.findReviewerProfiles","reviewerService.countReviewerProfiles","reviewerService.getReviewerPriceDistribution","reviewerService.findReviewerById","reviewerService.findReviewerByUserId","reviewerService.createReviewerProfile","reviewerService.updateReviewerProfile","reviewerService.deleteReviewerProfile","reviewerService.findReviewerProfilesAdmin","reviewerService.countReviewerProfilesAdmin","userService.getUserById","reviewerService.findReviewsByReviewerId","reviewerService.countReviewsByReviewerId","missionService.findMissionById","reviewerService.findReviewByMissionId","reviewerService.createReview","reviewerService.updateRating","missionService.calculateMissionEstimate","missionService.createMission","missionService.findMissionsForReviewerProfile","missionService.findMissionsForUser","missionService.updateMissionStatus","reviewerService.incrementMissionCount","messageService.findMessagesByMissionId","messageService.markMessagesRead","messageService.createMessage","messageService.findNewMessagesSince"],"sources":["../../../src/controllers/reviewer.controller.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { sendEmail } from '@services/email.service';\nimport {\n deleteReviewerPicture,\n type ReviewerPictureKind,\n uploadReviewerPicture,\n validateReviewerPictureUpload,\n} from '@services/reviewer/pictureUpload.service';\nimport * as reviewerService from '@services/reviewer.service';\nimport * as messageService from '@services/reviewerMessage.service';\nimport * as missionService from '@services/reviewerMission.service';\nimport * as userService from '@services/user.service';\nimport { ErrorHandler } from '@utils/errors';\nimport {\n formatPaginatedResponse,\n formatResponse,\n type ResponseData,\n} from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport type {\n MissionEstimate,\n MissionStatus,\n ReviewerCategory,\n ReviewerMessageAPI,\n ReviewerProfileAPI,\n ReviewerReviewAPI,\n TranslationMissionAPI,\n} from '@/types/reviewer.types';\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nconst toProfileAPI = (doc: any): ReviewerProfileAPI => doc.toJSON();\nconst toMissionAPI = (doc: any): TranslationMissionAPI => doc.toJSON();\nconst toReviewAPI = (doc: any): ReviewerReviewAPI => doc.toJSON();\nconst toMessageAPI = (doc: any): ReviewerMessageAPI => doc.toJSON();\n\n// ── Marketplace / Profile ────────────────────────────────────────────────────\n\nexport type GetMarketplaceQuery = {\n page?: number;\n pageSize?: number;\n fromLocale?: string;\n toLocale?: string;\n minRating?: number;\n maxPricePerHour?: number;\n minPricePerHour?: number;\n categories?: string | string[];\n};\n\nexport type PriceDistributionBucket = {\n min: number;\n max: number;\n count: number;\n};\n\nexport type PriceDistributionData = {\n buckets: PriceDistributionBucket[];\n globalMin: number;\n globalMax: number;\n};\n\nexport const getMarketplace = async (\n request: FastifyRequest<{ Querystring: GetMarketplaceQuery }>,\n reply: FastifyReply\n): Promise<void> => {\n const {\n page = 1,\n pageSize = 20,\n fromLocale,\n toLocale,\n minRating,\n maxPricePerHour,\n minPricePerHour,\n categories,\n } = request.query;\n\n const query: Record<string, any> = {};\n\n if (fromLocale || toLocale) {\n const pairFilter: Record<string, any> = {};\n if (fromLocale) pairFilter.from = fromLocale;\n if (toLocale) pairFilter.to = toLocale;\n query.languagePairs = { $elemMatch: pairFilter };\n }\n\n if (minRating !== undefined)\n query.averageRating = { $gte: Number(minRating) };\n\n if (minPricePerHour !== undefined || maxPricePerHour !== undefined) {\n query.pricePerHour = {};\n if (minPricePerHour !== undefined)\n query.pricePerHour.$gte = Number(minPricePerHour);\n if (maxPricePerHour !== undefined)\n query.pricePerHour.$lte = Number(maxPricePerHour);\n }\n\n if (categories) {\n const cats = Array.isArray(categories) ? categories : [categories];\n if (cats.length > 0) query.categories = { $in: cats };\n }\n\n const skip = (Number(page) - 1) * Number(pageSize);\n const [profiles, total] = await Promise.all([\n reviewerService.findReviewerProfiles(query, skip, Number(pageSize)),\n reviewerService.countReviewerProfiles(query),\n ]);\n\n const totalPages = Math.ceil(total / Number(pageSize));\n\n return reply.send(\n formatPaginatedResponse<ReviewerProfileAPI>({\n data: profiles.map(toProfileAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages,\n totalItems: total,\n message: t({\n en: 'Reviewer profiles fetched',\n fr: 'Profils récupérés',\n es: 'Perfiles obtenidos',\n de: 'Profile abgerufen',\n ar: 'تم جلب الملفات الشخصية',\n 'en-GB': 'Reviewer profiles fetched',\n ru: 'Профили получены',\n ja: 'プロフィールを取得しました',\n ko: '프로필을 가져왔습니다',\n zh: '已获取翻译员资料',\n it: 'Profili recuperati',\n pt: 'Perfis obtidos',\n hi: 'प्रोफाइल प्राप्त की गई',\n tr: 'Profiller getirildi',\n pl: 'Profile pobrane',\n id: 'Profil diambil',\n vi: 'Đã lấy hồ sơ',\n uk: 'Профілі отримано',\n }),\n })\n );\n};\n\nexport const getPriceDistribution = async (\n request: FastifyRequest<{\n Querystring: Pick<\n GetMarketplaceQuery,\n 'fromLocale' | 'toLocale' | 'minRating' | 'categories'\n >;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const { fromLocale, toLocale, minRating, categories } = request.query;\n\n const query: Record<string, any> = {};\n\n if (fromLocale || toLocale) {\n const pairFilter: Record<string, any> = {};\n if (fromLocale) pairFilter.from = fromLocale;\n if (toLocale) pairFilter.to = toLocale;\n query.languagePairs = { $elemMatch: pairFilter };\n }\n if (minRating !== undefined)\n query.averageRating = { $gte: Number(minRating) };\n if (categories) {\n const cats = Array.isArray(categories) ? categories : [categories];\n if (cats.length > 0) query.categories = { $in: cats };\n }\n\n try {\n const buckets = await reviewerService.getReviewerPriceDistribution(query);\n const globalMin = buckets.length > 0 ? buckets[0].min : 0;\n const globalMax =\n buckets.length > 0 ? buckets[buckets.length - 1].max : 500;\n\n return reply.send(\n formatResponse<PriceDistributionData>({\n data: { buckets, globalMin, globalMax },\n message: 'Price distribution fetched',\n })\n );\n } catch (_error) {\n return reply\n .status(500)\n .send({ error: 'Failed to fetch price distribution' });\n }\n};\n\nexport const getReviewerById = async (\n request: FastifyRequest<{ Params: { reviewerId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { reviewerId } = request.params;\n const profile = await reviewerService.findReviewerById(reviewerId);\n\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(profile),\n message: t({\n en: 'Reviewer profile fetched',\n fr: 'Profil récupéré',\n es: 'Perfil obtenido',\n de: 'Profil abgerufen',\n ar: 'تم جلب الملف الشخصي',\n 'en-GB': 'Reviewer profile fetched',\n ru: 'Профиль получен',\n ja: 'プロフィールを取得しました',\n ko: '프로필을 가져왔습니다',\n zh: '已获取翻译员资料',\n it: 'Profilo recuperato',\n pt: 'Perfil obtido',\n hi: 'प्रोफाइल प्राप्त की गई',\n tr: 'Profil getirildi',\n pl: 'Profil pobrany',\n id: 'Profil diambil',\n vi: 'Đã lấy hồ sơ',\n uk: 'Профіль отримано',\n }),\n })\n );\n};\n\nexport type RegisterReviewerBody = {\n bio?: string;\n languagePairs: { from: string; to: string }[];\n categories?: ReviewerCategory[];\n pricePerHour: number;\n socialLinks?: { github?: string; linkedin?: string; portfolio?: string };\n};\n\nexport const registerAsReviewer = async (\n request: FastifyRequest<{ Body: RegisterReviewerBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const existing = await reviewerService.findReviewerByUserId(String(user.id));\n if (existing) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_ALREADY_EXISTS'\n );\n }\n\n const { bio, languagePairs, categories, pricePerHour } = request.body;\n\n const profile = await reviewerService.createReviewerProfile({\n userId: user.id as any,\n bio,\n languagePairs: languagePairs ?? [],\n categories: (categories ?? []) as any,\n pricePerHour: pricePerHour ?? 0,\n });\n\n // Notify the Intlayer team that a new reviewer has applied\n await sendEmail({\n type: 'reviewerApplication',\n to: 'contact@intlayer.org',\n username: user.name ?? user.email,\n userEmail: user.email,\n profileLink: `${process.env.APP_URL}/admin/reviewers/${String(profile.id)}`,\n }).catch((err) =>\n logger.error('Failed to send reviewer application email', err)\n );\n\n return reply.status(201).send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(profile),\n message: t({\n en: 'Registered as reviewer',\n fr: 'Enregistré comme traducteur',\n es: 'Registrado como traductor',\n de: 'Als Übersetzer registriert',\n ar: 'تم التسجيل كمترجم',\n 'en-GB': 'Registered as reviewer',\n ru: 'Зарегистрирован как переводчик',\n ja: '翻訳者として登録されました',\n ko: '번역가로 등록되었습니다',\n zh: '已注册为翻译员',\n it: 'Registrato come traduttore',\n pt: 'Registado como tradutor',\n hi: 'अनुवादक के रूप में पंजीकृत',\n tr: 'Çevirmen olarak kaydedildi',\n pl: 'Zarejestrowany jako tłumacz',\n id: 'Terdaftar sebagai penerjemah',\n vi: 'Đã đăng ký là dịch giả',\n uk: 'Зареєстровано як перекладач',\n }),\n })\n );\n};\n\nexport type UpdateReviewerBody = Partial<RegisterReviewerBody> & {\n coverPicture?: string;\n isHidden?: boolean;\n};\n\nexport const updateReviewerProfile = async (\n request: FastifyRequest<{ Body: UpdateReviewerBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const profile = await reviewerService.findReviewerByUserId(String(user.id));\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n const updated = await reviewerService.updateReviewerProfile(\n String(profile.id),\n request.body\n );\n\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(updated),\n message: t({\n en: 'Profile updated',\n fr: 'Profil mis à jour',\n es: 'Perfil actualizado',\n de: 'Profil aktualisiert',\n ar: 'تم تحديث الملف الشخصي',\n 'en-GB': 'Profile updated',\n ru: 'Профиль обновлён',\n ja: 'プロフィールを更新しました',\n ko: '프로필이 업데이트되었습니다',\n zh: '资料已更新',\n it: 'Profilo aggiornato',\n pt: 'Perfil atualizado',\n hi: 'प्रोफाइल अपडेट की गई',\n tr: 'Profil güncellendi',\n pl: 'Profil zaktualizowany',\n id: 'Profil diperbarui',\n vi: 'Hồ sơ đã được cập nhật',\n uk: 'Профіль оновлено',\n }),\n })\n );\n};\n\nexport const deleteMyReviewerProfile = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const profile = await reviewerService.findReviewerByUserId(String(user.id));\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n await reviewerService.deleteReviewerProfile(String(profile.id));\n\n return reply.send(\n formatResponse<null>({\n data: null,\n message: t({\n en: 'Profile deleted',\n fr: 'Profil supprimé',\n es: 'Perfil eliminado',\n de: 'Profil gelöscht',\n ar: 'تم حذف الملف الشخصي',\n 'en-GB': 'Profile deleted',\n ru: 'Профиль удалён',\n ja: 'プロフィールを削除しました',\n ko: '프로필이 삭제되었습니다',\n zh: '资料已删除',\n it: 'Profilo eliminato',\n pt: 'Perfil excluído',\n hi: 'प्रोफाइल हटाई गई',\n tr: 'Profil silindi',\n pl: 'Profil usunięty',\n id: 'Profil dihapus',\n vi: 'Hồ sơ đã được xóa',\n uk: 'Профіль видалено',\n }),\n })\n );\n};\n\nexport const getMyReviewerProfile = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const profile = await reviewerService.findReviewerByUserId(String(user.id));\n\n return reply.send(\n formatResponse<ReviewerProfileAPI | null>({\n data: profile ? toProfileAPI(profile) : null,\n })\n );\n};\n\n// ── Admin: list all reviewers ───────────────────────────────────────────────\n\nexport const getAdminReviewers = async (\n request: FastifyRequest<{\n Querystring: {\n page?: number;\n pageSize?: number;\n status?: string;\n };\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const sessionUser = request.session?.user;\n if (sessionUser?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n const { page = 1, pageSize = 20, status } = request.query;\n const query: Record<string, any> = {};\n if (status) query.status = status;\n\n const skip = (Number(page) - 1) * Number(pageSize);\n const [profiles, total] = await Promise.all([\n reviewerService.findReviewerProfilesAdmin(query, skip, Number(pageSize)),\n reviewerService.countReviewerProfilesAdmin(query),\n ]);\n\n return reply.send(\n formatPaginatedResponse<ReviewerProfileAPI>({\n data: profiles.map(toProfileAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages: Math.ceil(total / Number(pageSize)),\n totalItems: total,\n })\n );\n};\n\n// ── Admin: validate reviewer profile ────────────────────────────────────────\n\nexport const validateReviewerProfile = async (\n request: FastifyRequest<{ Params: { reviewerId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const sessionUser = request.session?.user;\n if (sessionUser?.role !== 'admin') {\n return ErrorHandler.handleGenericErrorResponse(reply, 'PERMISSION_DENIED');\n }\n\n const { reviewerId } = request.params;\n\n const profile = await reviewerService.findReviewerById(reviewerId);\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n if (profile.status === 'active') {\n // Already active — still return 200 so the admin can be idempotent\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(profile),\n message: t({\n en: 'Reviewer profile is already active',\n fr: 'Le profil du traducteur est déjà actif',\n es: 'El perfil del traductor ya está activo',\n de: 'Das Übersetzer-Profil ist bereits aktiv',\n ar: 'ملف المترجم نشط بالفعل',\n 'en-GB': 'Reviewer profile is already active',\n ru: 'Профиль переводчика уже активен',\n ja: '翻訳者プロフィールはすでにアクティブです',\n ko: '번역가 프로필이 이미 활성화되어 있습니다',\n zh: '译者档案已激活',\n it: 'Il profilo del traduttore è già attivo',\n pt: 'O perfil do tradutor já está ativo',\n hi: 'अनुवादक प्रोफ़ाइल पहले से सक्रिय है',\n tr: 'Çevirmen profili zaten aktif',\n pl: 'Profil tłumacza jest już aktywny',\n id: 'Profil penerjemah sudah aktif',\n vi: 'Hồ sơ dịch giả đã được kích hoạt',\n uk: 'Профіль перекладача вже активний',\n }),\n })\n );\n }\n\n const updated = await reviewerService.updateReviewerProfile(reviewerId, {\n status: 'active',\n });\n\n // Fetch the linked user to send the approval email\n const linkedUser = await userService.getUserById(String(profile.userId));\n if (linkedUser) {\n await sendEmail({\n type: 'reviewerApproved',\n to: linkedUser.email,\n locale: (linkedUser as any).locale,\n username: linkedUser.name ?? linkedUser.email,\n dashboardLink: `${process.env.APP_URL}/dashboard/reviewer`,\n }).catch((err) =>\n logger.error('Failed to send reviewer approved email', err)\n );\n }\n\n logger.info(\n `Reviewer profile ${reviewerId} validated by admin ${String(sessionUser.id)}`\n );\n\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(updated),\n message: t({\n en: 'Reviewer profile validated',\n fr: 'Profil traducteur validé',\n es: 'Perfil del traductor validado',\n de: 'Übersetzer-Profil validiert',\n ar: 'تم التحقق من ملف المترجم',\n 'en-GB': 'Reviewer profile validated',\n ru: 'Профиль переводчика подтверждён',\n ja: '翻訳者プロフィールを承認しました',\n ko: '번역가 프로필이 승인되었습니다',\n zh: '译者档案已验证',\n it: 'Profilo traduttore validato',\n pt: 'Perfil do tradutor validado',\n hi: 'अनुवादक प्रोफ़ाइल सत्यापित',\n tr: 'Çevirmen profili doğrulandı',\n pl: 'Profil tłumacza zwalidowany',\n id: 'Profil penerjemah divalidasi',\n vi: 'Hồ sơ dịch giả đã được xác nhận',\n uk: 'Профіль перекладача підтверджено',\n }),\n })\n );\n};\n\n// ── Picture upload ────────────────────────────────────────────────────────────\n\nexport type UploadReviewerPictureResult = ResponseData<ReviewerProfileAPI>;\n\nconst uploadReviewerPictureHandler =\n (kind: ReviewerPictureKind) =>\n async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const profile = await reviewerService.findReviewerByUserId(String(user.id));\n if (!profile) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n const rawContentType = request.headers['content-type'] ?? '';\n const contentType = rawContentType.split(';')[0].trim() || 'image/jpeg';\n const contentLength = Number(request.headers['content-length'] ?? 0);\n\n const validationError = validateReviewerPictureUpload(\n contentType,\n contentLength\n );\n if (validationError === 'UNSUPPORTED_TYPE') {\n return reply.status(415).send(\n formatResponse({\n data: null,\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 })\n );\n }\n\n if (validationError === 'TOO_LARGE') {\n return reply.status(413).send(\n formatResponse({\n data: null,\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 })\n );\n }\n\n const buffer = request.body;\n if (!Buffer.isBuffer(buffer) || buffer.length === 0) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_INVALID_FIELDS'\n );\n }\n\n const field = kind === 'main' ? 'mainPicture' : 'coverPicture';\n const existing = (profile as any)[field] as string | undefined;\n if (existing) {\n await deleteReviewerPicture(existing).catch(() => {});\n }\n\n const imageUrl = await uploadReviewerPicture(\n buffer,\n String(profile.id),\n kind\n );\n\n const updated = await reviewerService.updateReviewerProfile(\n String(profile.id),\n {\n [field]: imageUrl,\n }\n );\n\n logger.info(\n `Reviewer ${kind} picture uploaded for profile ${String(profile.id)}`\n );\n\n return reply.send(\n formatResponse<ReviewerProfileAPI>({\n data: toProfileAPI(updated),\n message: t({\n en: 'Picture uploaded',\n fr: 'Image mise à jour',\n es: 'Imagen subida',\n de: 'Bild hochgeladen',\n ar: 'تم رفع الصورة',\n 'en-GB': 'Picture uploaded',\n ru: 'Изображение загружено',\n ja: '画像をアップロードしました',\n ko: '이미지가 업로드되었습니다',\n zh: '图片已上传',\n it: 'Immagine caricata',\n pt: 'Imagem enviada',\n hi: 'चित्र अपलोड किया गया',\n tr: 'Resim yüklendi',\n pl: 'Zdjęcie przesłane',\n id: 'Gambar diunggah',\n vi: 'Ảnh đã được tải lên',\n uk: 'Зображення завантажено',\n }),\n })\n );\n };\n\nexport const uploadReviewerMainPicture = uploadReviewerPictureHandler('main');\nexport const uploadReviewerCoverPicture = uploadReviewerPictureHandler('cover');\n\n// ── Reviews ──────────────────────────────────────────────────────────────────\n\nexport const getReviewerReviews = async (\n request: FastifyRequest<{\n Params: { reviewerId: string };\n Querystring: { page?: number; pageSize?: number };\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const { reviewerId } = request.params;\n const { page = 1, pageSize = 20 } = request.query;\n\n const skip = (Number(page) - 1) * Number(pageSize);\n const [reviews, total] = await Promise.all([\n reviewerService.findReviewsByReviewerId(reviewerId, skip, Number(pageSize)),\n reviewerService.countReviewsByReviewerId(reviewerId),\n ]);\n\n return reply.send(\n formatPaginatedResponse<ReviewerReviewAPI>({\n data: reviews.map(toReviewAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages: Math.ceil(total / Number(pageSize)),\n totalItems: total,\n })\n );\n};\n\nexport type SubmitReviewBody = { rating: number; comment?: string };\n\nexport const submitReview = async (\n request: FastifyRequest<{\n Params: { missionId: string };\n Body: SubmitReviewBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n if (String(mission.clientUserId) !== String(user.id)) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n if (mission.status !== 'completed') {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_REVIEW_MISSION_NOT_COMPLETED'\n );\n }\n\n const existing = await reviewerService.findReviewByMissionId(\n request.params.missionId\n );\n if (existing) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_REVIEW_ALREADY_EXISTS'\n );\n }\n\n const review = await reviewerService.createReview({\n missionId: mission.id,\n reviewerId: mission.reviewerId,\n rating: request.body.rating,\n comment: request.body.comment,\n });\n\n await reviewerService.updateRating(\n String(mission.reviewerId),\n request.body.rating\n );\n\n return reply.status(201).send(\n formatResponse<ReviewerReviewAPI>({\n data: toReviewAPI(review),\n message: t({\n en: 'Review submitted',\n fr: 'Avis soumis',\n es: 'Reseña enviada',\n de: 'Bewertung eingereicht',\n ar: 'تم تقديم المراجعة',\n 'en-GB': 'Review submitted',\n ru: 'Отзыв отправлен',\n ja: 'レビューを送信しました',\n ko: '리뷰가 제출되었습니다',\n zh: '评价已提交',\n it: 'Recensione inviata',\n pt: 'Avaliação enviada',\n hi: 'समीक्षा सबमिट की गई',\n tr: 'İnceleme gönderildi',\n pl: 'Recenzja przesłana',\n id: 'Ulasan dikirim',\n vi: 'Đánh giá đã được gửi',\n uk: 'Відгук надіслано',\n }),\n })\n );\n};\n\n// ── Missions ─────────────────────────────────────────────────────────────────\n\nexport type EstimateMissionBody = {\n dictionaryIds: string[];\n sourceLocale: string;\n pricePerHour: number;\n};\n\nexport const estimateMission = async (\n request: FastifyRequest<{ Body: EstimateMissionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { dictionaryIds, sourceLocale, pricePerHour } = request.body;\n\n const estimate = await missionService.calculateMissionEstimate(\n dictionaryIds,\n sourceLocale,\n pricePerHour\n );\n\n return reply.send(formatResponse<MissionEstimate>({ data: estimate }));\n};\n\nexport type CreateMissionBody = {\n reviewerId: string;\n dictionaryIds: string[];\n sourceLocale: string;\n targetLocales: string[];\n projectId?: string;\n notes?: string;\n};\n\nexport const createMission = async (\n request: FastifyRequest<{ Body: CreateMissionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const reviewer = await reviewerService.findReviewerById(\n request.body.reviewerId\n );\n if (!reviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_PROFILE_NOT_FOUND'\n );\n }\n\n const estimate = await missionService.calculateMissionEstimate(\n request.body.dictionaryIds,\n request.body.sourceLocale,\n reviewer.pricePerHour\n );\n\n const mission = await missionService.createMission({\n reviewerId: reviewer.id,\n clientUserId: user.id as any,\n projectId: request.body.projectId as any,\n dictionaryIds: request.body.dictionaryIds as any[],\n sourceLocale: request.body.sourceLocale,\n targetLocales: request.body.targetLocales,\n wordCount: estimate.wordCount,\n estimatedHours: estimate.estimatedHours,\n pricePerHour: reviewer.pricePerHour,\n totalPrice: estimate.totalPrice,\n currency: estimate.currency,\n notes: request.body.notes,\n });\n\n const missionLink = `${process.env.APP_URL}/find-reviewer/dashboard/mission/${String(mission.id)}`;\n\n // Notify the client that their request was sent\n sendEmail({\n type: 'missionRequestedClient',\n to: user.email,\n clientUsername: user.name ?? user.email,\n reviewerName: reviewer.name ?? 'the reviewer',\n missionLink,\n }).catch((err) =>\n logger.error('Failed to send missionRequestedClient email', err)\n );\n\n // Notify the reviewer that someone contacted them\n const reviewerUser = await userService.getUserById(String(reviewer.userId));\n if (reviewerUser?.email) {\n sendEmail({\n type: 'missionRequestedReviewer',\n to: reviewerUser.email,\n reviewerUsername: reviewerUser.name ?? reviewerUser.email,\n clientName: user.name ?? user.email,\n sourceLocale: request.body.sourceLocale,\n targetLocales: request.body.targetLocales,\n notes: request.body.notes,\n missionLink,\n }).catch((err) =>\n logger.error('Failed to send missionRequestedReviewer email', err)\n );\n }\n\n return reply.status(201).send(\n formatResponse<TranslationMissionAPI>({\n data: toMissionAPI(mission),\n message: t({\n en: 'Mission created',\n fr: 'Mission créée',\n es: 'Misión creada',\n de: 'Auftrag erstellt',\n ar: 'تم إنشاء المهمة',\n 'en-GB': 'Mission created',\n ru: 'Задание создано',\n ja: 'ミッションが作成されました',\n ko: '미션이 생성되었습니다',\n zh: '任务已创建',\n it: 'Missione creata',\n pt: 'Missão criada',\n hi: 'मिशन बनाया गया',\n tr: 'Görev oluşturuldu',\n pl: 'Misja utworzona',\n id: 'Misi dibuat',\n vi: 'Nhiệm vụ đã được tạo',\n uk: 'Завдання створено',\n }),\n })\n );\n};\n\nexport const getMyMissions = async (\n request: FastifyRequest<{\n Querystring: {\n role?: 'client' | 'reviewer';\n page?: number;\n pageSize?: number;\n };\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const { role = 'client', page = 1, pageSize = 20 } = request.query;\n const skip = (Number(page) - 1) * Number(pageSize);\n const userId = String(user.id);\n\n if (role === 'reviewer') {\n const profile = await reviewerService.findReviewerByUserId(userId);\n if (!profile) {\n return reply.send(\n formatPaginatedResponse<TranslationMissionAPI>({\n data: [],\n page: 1,\n pageSize: Number(pageSize),\n totalPages: 0,\n totalItems: 0,\n })\n );\n }\n const missions = await missionService.findMissionsForReviewerProfile(\n String(profile.id),\n skip,\n Number(pageSize)\n );\n return reply.send(\n formatPaginatedResponse<TranslationMissionAPI>({\n data: missions.map(toMissionAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages: 1,\n totalItems: missions.length,\n })\n );\n }\n\n const missions = await missionService.findMissionsForUser(\n userId,\n skip,\n Number(pageSize)\n );\n\n return reply.send(\n formatPaginatedResponse<TranslationMissionAPI>({\n data: missions.map(toMissionAPI),\n page: Number(page),\n pageSize: Number(pageSize),\n totalPages: 1,\n totalItems: missions.length,\n })\n );\n};\n\nexport const getMissionById = async (\n request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n return reply.send(\n formatResponse<TranslationMissionAPI>({ data: toMissionAPI(mission) })\n );\n};\n\nexport type UpdateMissionStatusBody = { status: MissionStatus };\n\nexport const updateMissionStatus = async (\n request: FastifyRequest<{\n Params: { missionId: string };\n Body: UpdateMissionStatusBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n const newStatus = request.body.status;\n\n const updated = await missionService.updateMissionStatus(\n request.params.missionId,\n newStatus\n );\n\n if (newStatus === 'completed' && profile) {\n await reviewerService.incrementMissionCount(String(profile.id));\n }\n\n return reply.send(\n formatResponse<TranslationMissionAPI>({\n data: toMissionAPI(updated),\n message: t({\n en: 'Mission status updated',\n fr: 'Statut de la mission mis à jour',\n es: 'Estado de la misión actualizado',\n de: 'Auftragsstatus aktualisiert',\n ar: 'تم تحديث حالة المهمة',\n 'en-GB': 'Mission status updated',\n ru: 'Статус задания обновлён',\n ja: 'ミッションのステータスを更新しました',\n ko: '미션 상태가 업데이트되었습니다',\n zh: '任务状态已更新',\n it: 'Stato missione aggiornato',\n pt: 'Estado da missão atualizado',\n hi: 'मिशन स्थिति अपडेट की गई',\n tr: 'Görev durumu güncellendi',\n pl: 'Status misji zaktualizowany',\n id: 'Status misi diperbarui',\n vi: 'Trạng thái nhiệm vụ đã được cập nhật',\n uk: 'Статус завдання оновлено',\n }),\n })\n );\n};\n\n// ── Chat ─────────────────────────────────────────────────────────────────────\n\nexport const getChatHistory = async (\n request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n const messages = await messageService.findMessagesByMissionId(\n request.params.missionId\n );\n await messageService.markMessagesRead(request.params.missionId, userId);\n\n return reply.send(\n formatResponse<ReviewerMessageAPI[]>({ data: messages.map(toMessageAPI) })\n );\n};\n\nexport type SendMessageBody = { content: string };\n\nexport const sendMessage = async (\n request: FastifyRequest<{\n Params: { missionId: string };\n Body: SendMessageBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_NOT_FOUND'\n );\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'REVIEWER_MISSION_UNAUTHORIZED'\n );\n }\n\n const message = await messageService.createMessage(\n request.params.missionId,\n userId,\n request.body.content\n );\n\n return reply\n .status(201)\n .send(formatResponse<ReviewerMessageAPI>({ data: toMessageAPI(message) }));\n};\n\nexport const chatSSE = async (\n request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const user = request.session?.user;\n if (!user) {\n reply.raw.statusCode = 401;\n reply.raw.end();\n return;\n }\n\n const mission = await missionService.findMissionById(\n request.params.missionId\n );\n if (!mission) {\n reply.raw.statusCode = 404;\n reply.raw.end();\n return;\n }\n\n const userId = String(user.id);\n const profile = await reviewerService.findReviewerByUserId(userId);\n const isClient = String(mission.clientUserId) === userId;\n const isReviewer =\n profile && String(mission.reviewerId) === String(profile.id);\n\n if (!isClient && !isReviewer) {\n reply.raw.statusCode = 403;\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) reply.raw.setHeader(key, value as string);\n });\n\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');\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 let lastChecked = new Date();\n\n const interval = setInterval(async () => {\n try {\n const newMessages = await messageService.findNewMessagesSince(\n request.params.missionId,\n lastChecked\n );\n if (newMessages.length > 0) {\n lastChecked = new Date();\n for (const msg of newMessages) {\n send(toMessageAPI(msg));\n }\n }\n } catch (error) {\n logger.error('Error polling chat messages', error);\n }\n }, 2000);\n\n request.raw.on('close', () => {\n clearInterval(interval);\n });\n};\n\n// ── Payment stubs ─────────────────────────────────────────────────────────────\n\nexport const createPaymentIntent = async (\n _request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n // TODO: create Stripe PaymentIntent for mission.totalPrice\n // Charge the client, hold funds in escrow until mission is completed\n return reply.send(formatResponse({ data: null, message: 'Not implemented' }));\n};\n\nexport const confirmPayment = async (\n _request: FastifyRequest<{ Params: { missionId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n // TODO: confirm Stripe payment and release funds to reviewer's Stripe account\n return reply.send(formatResponse({ data: null, message: 'Not implemented' }));\n};\n\nexport const requestPayout = async (\n _request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n // TODO: trigger payout to reviewer's Stripe Connect account\n return reply.send(formatResponse({ data: null, message: 'Not implemented' }));\n};\n\n// ── Contact ───────────────────────────────────────────────────────────────────\n\nexport const contactReviewer = async (\n request: FastifyRequest<{\n Params: { reviewerId: string };\n Body: { message: string };\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const { reviewerId } = request.params;\n const { message } = request.body;\n const user = (request as any).user;\n\n if (!message?.trim()) {\n return reply\n .status(400)\n .send(formatResponse({ data: null, message: 'Message is required' }));\n }\n\n const reviewer = await reviewerService.findReviewerById(reviewerId);\n if (!reviewer) {\n return reply\n .status(404)\n .send(formatResponse({ data: null, message: 'Reviewer not found' }));\n }\n\n const reviewerUser = await userService.getUserById(String(reviewer.userId));\n if (reviewerUser?.email) {\n sendEmail({\n type: 'reviewerContactInquiry',\n to: reviewerUser.email,\n reviewerUsername: reviewerUser.name ?? reviewerUser.email,\n clientName: user?.name ?? user?.email ?? 'A user',\n message: message.trim(),\n }).catch((err) =>\n logger.error('Failed to send reviewerContactInquiry email', err)\n );\n }\n\n return reply\n .status(200)\n .send(formatResponse({ data: null, message: 'Message sent' }));\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"searchDoc.controller.mjs","names":["askDocQuestionUtil.searchChunkReference"],"sources":["../../../src/controllers/searchDoc.controller.ts"],"sourcesContent":["import * as askDocQuestionUtil from '@utils/AI/askDocQuestion/askDocQuestion';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\n\nexport type SearchDocUtilParams = {\n input: string;\n limit?: string;\n returnContent?: string;\n};\n\nexport type SearchDocResult = {\n fileKey: string;\n chunkNumber: number;\n content?: string;\n docUrl: string;\n docName: string;\n};\n\nexport type SearchDocUtilResult = ResponseData<string[] | SearchDocResult[]>;\n\nexport const searchDocUtil = async (\n request: FastifyRequest<{ Querystring: SearchDocUtilParams }>,\n reply: FastifyReply\n) => {\n const { input, limit, returnContent } = request.query;\n\n const maxResults = limit ? Number.parseInt(limit, 10) : 30;\n const shouldReturnContent = returnContent === 'true';\n\n const response = await askDocQuestionUtil.searchChunkReference(\n input,\n maxResults,\n 0.4\n );\n\n if (shouldReturnContent) {\n const searchResults: SearchDocResult[] = response.map((doc) => ({\n fileKey: doc.fileKey,\n chunkNumber: doc.chunkNumber,\n content: doc.content,\n docUrl: doc.docUrl,\n docName: doc.docName,\n }));\n\n const responseData = formatResponse<SearchDocResult[]>({\n data: searchResults,\n });\n\n return reply.send(responseData);\n }\n\n const docFileList = response.map((doc) => doc.fileKey);\n\n const uniqueDocFileList = Array.from(new Set(docFileList));\n\n const responseData = formatResponse<string[]>({\n data: uniqueDocFileList,\n });\n\n return reply.send(responseData);\n};\n"],"mappings":";;;;AAoBA,MAAa,gBAAgB,OAC3B,SACA,UACG;CACH,MAAM,EAAE,OAAO,OAAO,kBAAkB,QAAQ;CAEhD,MAAM,aAAa,QAAQ,OAAO,SAAS,OAAO,
|
|
1
|
+
{"version":3,"file":"searchDoc.controller.mjs","names":["askDocQuestionUtil.searchChunkReference"],"sources":["../../../src/controllers/searchDoc.controller.ts"],"sourcesContent":["import * as askDocQuestionUtil from '@utils/AI/askDocQuestion/askDocQuestion';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\n\nexport type SearchDocUtilParams = {\n input: string;\n limit?: string;\n returnContent?: string;\n};\n\nexport type SearchDocResult = {\n fileKey: string;\n chunkNumber: number;\n content?: string;\n docUrl: string;\n docName: string;\n};\n\nexport type SearchDocUtilResult = ResponseData<string[] | SearchDocResult[]>;\n\nexport const searchDocUtil = async (\n request: FastifyRequest<{ Querystring: SearchDocUtilParams }>,\n reply: FastifyReply\n) => {\n const { input, limit, returnContent } = request.query;\n\n const maxResults = limit ? Number.parseInt(limit, 10) : 30;\n const shouldReturnContent = returnContent === 'true';\n\n const response = await askDocQuestionUtil.searchChunkReference(\n input,\n maxResults,\n 0.4\n );\n\n if (shouldReturnContent) {\n const searchResults: SearchDocResult[] = response.map((doc) => ({\n fileKey: doc.fileKey,\n chunkNumber: doc.chunkNumber,\n content: doc.content,\n docUrl: doc.docUrl,\n docName: doc.docName,\n }));\n\n const responseData = formatResponse<SearchDocResult[]>({\n data: searchResults,\n });\n\n return reply.send(responseData);\n }\n\n const docFileList = response.map((doc) => doc.fileKey);\n\n const uniqueDocFileList = Array.from(new Set(docFileList));\n\n const responseData = formatResponse<string[]>({\n data: uniqueDocFileList,\n });\n\n return reply.send(responseData);\n};\n"],"mappings":";;;;AAoBA,MAAa,gBAAgB,OAC3B,SACA,UACG;CACH,MAAM,EAAE,OAAO,OAAO,kBAAkB,QAAQ;CAEhD,MAAM,aAAa,QAAQ,OAAO,SAAS,OAAO,GAAG,GAAG;CACxD,MAAM,sBAAsB,kBAAkB;CAE9C,MAAM,WAAW,MAAMA,qBACrB,OACA,YACA,GACD;AAED,KAAI,qBAAqB;EASvB,MAAM,eAAe,eAAkC,EACrD,MATuC,SAAS,KAAK,SAAS;GAC9D,SAAS,IAAI;GACb,aAAa,IAAI;GACjB,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,SAAS,IAAI;GACd,EAGoB,EACpB,CAAC;AAEF,SAAO,MAAM,KAAK,aAAa;;CAGjC,MAAM,cAAc,SAAS,KAAK,QAAQ,IAAI,QAAQ;CAItD,MAAM,eAAe,eAAyB,EAC5C,MAHwB,MAAM,KAAK,IAAI,IAAI,YAAY,CAGhC,EACxB,CAAC;AAEF,QAAO,MAAM,KAAK,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"showcaseProject.controller.mjs","names":["showcaseProjectService.findShowcaseProjectByUrl","showcaseProjectService.createShowcaseProject","showcaseProjectService.findShowcaseProjects","showcaseProjectService.findShowcaseProjectById","showcaseProjectService.findOtherShowcaseProjects","showcaseProjectService.toggleShowcaseUpvote","showcaseProjectService.toggleShowcaseDownvote","showcaseProjectService.updateShowcaseProject","showcaseProjectService.deleteShowcaseProject","scanShowcaseProjectViaService"],"sources":["../../../src/controllers/showcaseProject.controller.ts"],"sourcesContent":["import { logger } from '@logger';\nimport * as showcaseProjectService from '@services/showcase/showcaseProject.service';\nimport { scanShowcaseProject as scanShowcaseProjectViaService } from '@services/showcase/showcaseScan.service';\nimport {\n deleteShowcaseScreenshot,\n uploadShowcaseScreenshot,\n} from '@services/showcase/showcaseUploadScreenshot.service';\nimport { verifyGithubRepo } from '@services/showcase/showcaseVerifyGithub.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { getFaviconUrl } from '@utils/getFaviconUrl';\nimport {\n mapShowcaseProjectsToAPI,\n mapShowcaseProjectToAPI,\n} from '@utils/mapper/showcaseProject';\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 { z } from 'zod';\nimport type { ShowcaseProjectAPI } from '@/types/showcaseProject.types';\n\nconst getUserId = (request: FastifyRequest): string | undefined =>\n request.session?.user\n ? String(request.session.user.id ?? (request.session.user as any)._id)\n : undefined;\n\nconst urlSchema = z\n .url()\n .optional()\n .or(z.literal(''))\n .transform((value) => (value === '' ? undefined : value));\n\nconst submitProjectSchema = z.object({\n name: z.string().min(1),\n url: z\n .url()\n .refine((val) => !/github\\.com|gitlab\\.com|bitbucket\\.org/.test(val), {\n message: 'Repository URLs should be placed in the GitHub URL field',\n }),\n githubUrl: urlSchema,\n useCases: z.array(z.string()).max(3).optional(),\n});\nexport type SubmitShowcaseProjectBody = z.input<typeof submitProjectSchema>;\nexport type SubmitShowcaseProjectResult = ResponseData<ShowcaseProjectAPI>;\n\n/**\n * POST /api/showcase-project/submit\n * Submits a new project to the showcase.\n */\nexport const submitShowcaseProject = async (\n request: FastifyRequest<{ Body: SubmitShowcaseProjectBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const parsed = submitProjectSchema.safeParse(request.body);\n\n if (!parsed.success) {\n const message = parsed.error.issues\n .map((e) => `${e.path.join('.')}: ${e.message}`)\n .join(', ');\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'INVALID_REQUEST_BODY',\n { message }\n );\n }\n\n const validatedData = parsed.data;\n\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n try {\n // Check for existing project with the same URL\n const existing = await showcaseProjectService.findShowcaseProjectByUrl(\n validatedData.url\n );\n\n if (existing) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SHOWCASE_PROJECT_URL_ALREADY_EXISTS',\n { url: validatedData.url }\n );\n }\n\n // Save project to DB immediately — scan is triggered separately by the owner\n // tagline and description will be populated automatically during the scan step\n const logoUrl = getFaviconUrl(validatedData.url);\n const newProject = await showcaseProjectService.createShowcaseProject({\n title: validatedData.name,\n description: '',\n websiteUrl: validatedData.url,\n githubUrl: validatedData.githubUrl,\n logoUrl: logoUrl ?? '',\n tags: validatedData.useCases || [],\n owner: userId,\n status: 'pending_scan',\n });\n\n const responseData = formatResponse<ShowcaseProjectAPI>({\n message: t({\n en: 'Project submitted successfully',\n 'en-GB': 'Project submitted successfully',\n fr: 'Projet soumis avec succès',\n es: 'Proyecto enviado con éxito',\n ru: 'Проект успешно отправлен',\n ja: 'プロジェクトが正常に送信されました',\n ko: '프로젝트가 성공적으로 제출되었습니다',\n zh: '项目已成功提交',\n de: 'Projekt erfolgreich eingereicht',\n ar: 'تم تقديم المشروع بنجاح',\n it: 'Progetto inviato con successo',\n pt: 'Projeto enviado con sucesso',\n hi: 'प्रोजेक्ट सफलतापूर्वक सबमिट किया गया',\n tr: 'Proje başarıyla gönderildi',\n pl: 'Projekt został pomyślnie przesłany',\n id: 'Proyek berhasil dikirim',\n vi: 'Dự án đã được gửi thành công',\n uk: 'Проєкт успішно надіслано',\n }),\n description: t({\n en: 'Your project has been added to the showcase. Use the Scan button to verify and enrich it.',\n 'en-GB':\n 'Your project has been added to the showcase. Use the Scan button to verify and enrich it.',\n fr: \"Votre projet a été ajouté à la vitrine. Utilisez le bouton Scan pour le vérifier et l'enrichir.\",\n es: 'Su proyecto ha sido añadido al showcase. Use el botón Scan para verificarlo y enriquecerlo.',\n ru: 'Ваш проект был добавлен в витрину. Используйте кнопку Сканировать, чтобы проверить и дополнить его.',\n ja: 'プロジェクトがショーケースに追加されました。「スキャン」ボタンを使用して、検証と充実を行ってください。',\n ko: '프로젝트가 쇼케이스에 추가되었습니다. 스캔 버튼을 사용하여 확인하고 보완하십시오.',\n zh: '您的项目已添加到展示墙。使用“扫描”按钮进行验证和完善。',\n de: 'Ihr Projekt wurde zum Showcase hinzugefügt. Verwenden Sie die Schaltfläche Scan, um es zu überprüfen und zu erweitern.',\n ar: 'تمت إضافة مشروعك إلى المعرض. استخدم زر المسح للتحقق منه وإثرائه.',\n it: 'Il tuo progetto è stato aggiunto alla vetrina. Usa il pulsante Scansiona per verificarlo e arricchirlo.',\n pt: 'Seu projeto foi adicionado ao showcase. Use o botão Scan para verificá-lo e enriquecê-lo.',\n hi: 'आपका प्रोजेक्ट शोकेस में जोड़ दिया गया है। इसे सत्यापित करने और समृद्ध करने के लिए स्कैन बटन का उपयोग करें।',\n tr: 'Projeniz vitrine eklendi. Doğrulamak ve zenginleştirmek için Tara düğmesini kullanın.',\n pl: 'Twój projekt został dodany do witryny. Użyj przycisku Skanuj, aby go zweryfikować i wzbogacić.',\n id: 'Proyek Anda telah ditambahkan ke showcase. Gunakan tombol Pindai untuk memverifikasi dan memperkayanya.',\n vi: 'Dự án của bạn đã được thêm vào showcase. Sử dụng nút Quét để xác minh và làm phong phú nó.',\n uk: 'Ваш проєкт додано до вітрини. Скористайтеся кнопкою Сканувати, щоб перевірити та доповнити його.',\n }),\n data: mapShowcaseProjectToAPI(newProject, userId),\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type GetShowcaseProjectsQuery = {\n page?: string;\n pageSize?: string;\n search?: string;\n selectedUseCases?: string | string[];\n isOpenSource?: string;\n};\nexport type GetShowcaseProjectsResult = PaginatedResponse<ShowcaseProjectAPI>;\n\n/**\n * GET /api/showcase-project\n */\nexport const getShowcaseProjects = async (\n request: FastifyRequest<{ Querystring: GetShowcaseProjectsQuery }>,\n reply: FastifyReply\n): Promise<void> => {\n const {\n page: pageStr = '1',\n pageSize: pageSizeStr = '20',\n search = '',\n selectedUseCases,\n isOpenSource: isOpenSourceStr = 'false',\n } = request.query;\n\n const page = parseInt(pageStr, 10) || 1;\n const pageSize = parseInt(pageSizeStr, 10) || 20;\n const isOpenSource = isOpenSourceStr === 'true';\n\n const useCasesArray = selectedUseCases\n ? Array.isArray(selectedUseCases)\n ? selectedUseCases\n : [selectedUseCases]\n : [];\n\n try {\n const { data, total_items, total_pages } =\n await showcaseProjectService.findShowcaseProjects({\n search,\n selectedUseCases: useCasesArray,\n isOpenSource,\n page,\n pageSize,\n });\n\n const userId = getUserId(request);\n\n const responseData = formatPaginatedResponse<ShowcaseProjectAPI>({\n data: mapShowcaseProjectsToAPI(data, userId),\n page,\n pageSize,\n totalPages: total_pages,\n totalItems: total_items,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type GetShowcaseProjectByIdParams = { projectId: string };\nexport type GetShowcaseProjectByIdResult = ResponseData<ShowcaseProjectAPI>;\n\n/**\n * GET /api/showcase-project/:projectId\n */\nexport const getShowcaseProjectById = async (\n request: FastifyRequest<{ Params: GetShowcaseProjectByIdParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { projectId } = request.params;\n\n try {\n const project =\n await showcaseProjectService.findShowcaseProjectById(projectId);\n\n const userId = getUserId(request);\n\n return reply.send(\n formatResponse<ShowcaseProjectAPI>({\n data: mapShowcaseProjectToAPI(project, userId),\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type GetOtherShowcaseProjectsQuery = {\n excludeId: string;\n limit?: string;\n};\nexport type GetOtherShowcaseProjectsResult = ResponseData<ShowcaseProjectAPI[]>;\n\n/**\n * GET /api/showcase-project/others?excludeId=...&limit=...\n */\nexport const getOtherShowcaseProjects = async (\n request: FastifyRequest<{ Querystring: GetOtherShowcaseProjectsQuery }>,\n reply: FastifyReply\n): Promise<void> => {\n const { excludeId, limit: limitStr } = request.query;\n const limit = limitStr ? parseInt(limitStr, 10) : 4;\n\n if (!excludeId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SHOWCASE_PROJECT_NOT_FOUND',\n { detail: 'excludeId is required' }\n );\n }\n\n try {\n const projects = await showcaseProjectService.findOtherShowcaseProjects(\n excludeId,\n limit\n );\n\n const userId = getUserId(request);\n\n return reply.send(\n formatResponse<ShowcaseProjectAPI[]>({\n data: mapShowcaseProjectsToAPI(projects, userId),\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type ToggleShowcaseUpvoteBody = { projectId: string };\nexport type ToggleShowcaseUpvoteResult = ResponseData<{\n upvotes: number;\n isUpVoted: boolean;\n downvotes: number;\n isDownVoted: boolean;\n}>;\n\n/**\n * POST /api/showcase-project/upvote\n * Requires authentication — userId is read from the session.\n */\nexport const toggleShowcaseUpvote = async (\n request: FastifyRequest<{ Body: ToggleShowcaseUpvoteBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n const { projectId } = request.body;\n\n if (!projectId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'INVALID_REQUEST_BODY',\n { detail: 'projectId is required' }\n );\n }\n\n try {\n const result = await showcaseProjectService.toggleShowcaseUpvote(\n projectId,\n userId\n );\n\n return reply.send(formatResponse({ data: result }));\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type ToggleShowcaseDownvoteBody = { projectId: string };\nexport type ToggleShowcaseDownvoteResult = ResponseData<{\n upvotes: number;\n isUpVoted: boolean;\n downvotes: number;\n isDownVoted: boolean;\n}>;\n\n/**\n * POST /api/showcase-project/downvote\n * Requires authentication — userId is read from the session.\n */\nexport const toggleShowcaseDownvote = async (\n request: FastifyRequest<{ Body: ToggleShowcaseDownvoteBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n const { projectId } = request.body;\n\n if (!projectId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'INVALID_REQUEST_BODY',\n { detail: 'projectId is required' }\n );\n }\n\n try {\n const result = await showcaseProjectService.toggleShowcaseDownvote(\n projectId,\n userId\n );\n\n return reply.send(formatResponse({ data: result }));\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nconst updateProjectSchema = z.object({\n name: z.string().min(1).max(255).optional(),\n url: z\n .url()\n .refine((val) => !/github\\.com|gitlab\\.com|bitbucket\\.org/.test(val), {\n message: 'Repository URLs should be placed in the GitHub URL field',\n })\n .optional(),\n githubUrl: z\n .string()\n .optional()\n .transform((value) => {\n if (!value) return null;\n if (value.startsWith('http://') || value.startsWith('https://'))\n return value;\n return `https://${value}`;\n }),\n tagline: z.string().min(1).max(500).optional(),\n description: z.string().optional(),\n useCases: z.array(z.string()).max(3).optional(),\n});\n\nexport type UpdateShowcaseProjectBody = z.input<typeof updateProjectSchema>;\nexport type UpdateShowcaseProjectParams = { projectId: string };\nexport type UpdateShowcaseProjectResult = ResponseData<ShowcaseProjectAPI>;\n\n/**\n * PATCH /api/showcase-project/:projectId\n * Updates an existing project. Only the owner can update.\n */\nexport const updateShowcaseProjectHandler = async (\n request: FastifyRequest<{\n Params: UpdateShowcaseProjectParams;\n Body: UpdateShowcaseProjectBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n const parsed = updateProjectSchema.safeParse(request.body);\n\n if (!parsed.success) {\n const message = parsed.error.issues\n .map((e) => `${e.path.join('.')}: ${e.message}`)\n .join(', ');\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'INVALID_REQUEST_BODY',\n { message }\n );\n }\n\n const { projectId } = request.params;\n\n try {\n const project =\n await showcaseProjectService.findShowcaseProjectById(projectId);\n\n if (String(project.owner) !== userId) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_ID_MISMATCH');\n }\n\n const { name, url, githubUrl, tagline, useCases } = parsed.data;\n const updates: Parameters<\n typeof showcaseProjectService.updateShowcaseProject\n >[1] = {};\n\n if (name !== undefined) updates.title = name;\n if (url !== undefined) updates.websiteUrl = url;\n if ('githubUrl' in parsed.data) updates.githubUrl = githubUrl;\n if (tagline !== undefined) updates.description = tagline;\n if (useCases !== undefined) updates.tags = useCases;\n\n const updated = await showcaseProjectService.updateShowcaseProject(\n projectId,\n updates\n );\n\n return reply.send(\n formatResponse<ShowcaseProjectAPI>({\n message: t({\n en: 'Project updated successfully',\n 'en-GB': 'Project updated successfully',\n fr: 'Projet mis à jour avec succès',\n es: 'Proyecto actualizado con éxito',\n ru: 'Проект успешно обновлен',\n ja: 'プロジェクトが正常に更新されました',\n ko: '프로젝트가 성공적으로 업데이트되었습니다',\n zh: '项目已成功更新',\n de: 'Projekt erfolgreich aktualisiert',\n ar: 'تم تحديث المشروع بنجاح',\n it: 'Progetto aggiornato con successo',\n pt: 'Projeto atualizado com sucesso',\n hi: 'प्रोजेक्ट सफलतापूर्वक अपडेट किया गया',\n tr: 'Proje başarıyla güncellendi',\n pl: 'Projekt został pomyślnie zaktualizowany',\n id: 'Proyek berhasil diperbarui',\n vi: 'Dự án đã được cập nhật thành công',\n uk: 'Проєкт успішно оновлено',\n }),\n data: mapShowcaseProjectToAPI(updated, userId),\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type DeleteShowcaseProjectParams = { projectId: string };\n\n/**\n * DELETE /api/showcase-project/:projectId\n * Deletes a project (DB + R2 screenshot). Only the owner can delete.\n */\nexport const deleteShowcaseProjectHandler = async (\n request: FastifyRequest<{ Params: DeleteShowcaseProjectParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n const { projectId } = request.params;\n\n try {\n const project =\n await showcaseProjectService.findShowcaseProjectById(projectId);\n\n if (String(project.owner) !== userId) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_ID_MISMATCH');\n }\n\n await Promise.allSettled([\n showcaseProjectService.deleteShowcaseProject(projectId),\n project.imageUrl\n ? deleteShowcaseScreenshot(project.imageUrl)\n : Promise.resolve(),\n ]);\n\n return reply.send(formatResponse({ data: { success: true } }));\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type ScanShowcaseProjectParams = { projectId: string };\n\n/**\n * GET /api/showcase-project/:projectId/scan\n * SSE endpoint — streams scan progress to the owner.\n * Requires authentication: only the project owner can trigger the scan.\n */\nexport const scanShowcaseProject = async (\n request: FastifyRequest<{ Params: ScanShowcaseProjectParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n reply.status(401).send({ error: { message: 'Authentication required' } });\n return;\n }\n\n const { projectId } = request.params;\n\n let project: Awaited<\n ReturnType<typeof showcaseProjectService.findShowcaseProjectById>\n >;\n try {\n project = await showcaseProjectService.findShowcaseProjectById(projectId);\n } catch {\n reply.status(404).send({ error: { message: 'Project not found' } });\n return;\n }\n\n if (project.owner !== userId) {\n reply.status(403).send({\n error: { message: 'Only the project owner can trigger a scan' },\n });\n return;\n }\n\n // Hijack the reply and write SSE manually\n reply.hijack();\n const raw = reply.raw;\n const origin = request.headers.origin ?? '*';\n raw.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'Access-Control-Allow-Origin': origin,\n 'Access-Control-Allow-Credentials': 'true',\n });\n\n const send = (data: Record<string, unknown>) => {\n raw.write(`data: ${JSON.stringify(data)}\\n\\n`);\n };\n\n // Track the uploaded screenshot URL so we can delete it on failure.\n let uploadedImageUrl: string | null = null;\n\n const cleanup = async (message: string) => {\n await Promise.allSettled([\n showcaseProjectService.deleteShowcaseProject(projectId),\n uploadedImageUrl\n ? deleteShowcaseScreenshot(uploadedImageUrl)\n : Promise.resolve(),\n ]);\n send({ step: 'ERROR', message });\n };\n\n try {\n // ── Step 1: Scan website + take screenshot (single browser session) ────────\n send({ step: 'SCANNING_START' });\n const scanResult = await scanShowcaseProjectViaService(project.websiteUrl);\n\n if (!scanResult.hasIntlayer && !scanResult.libsUsed.includes('intlayer')) {\n await cleanup('Intlayer not detected on this website');\n raw.end();\n return;\n }\n send({ step: 'SCANNING_SUCCESS' });\n\n // ── Step 2: Verify GitHub (if provided) ───────────────────────────────────\n let isOpenSource = false;\n let githubPackageDetails: Record<string, string> = {};\n\n if (project.githubUrl) {\n send({ step: 'VERIFY_GITHUB_START' });\n logger.info(\n `[scanShowcaseProject] Verifying GitHub ${project.githubUrl}...`\n );\n const githubResult = await verifyGithubRepo(project.githubUrl);\n if (githubResult?.isValid) {\n isOpenSource = true;\n githubPackageDetails = githubResult.packageDetails ?? {};\n }\n send({ step: 'VERIFY_GITHUB_SUCCESS' });\n }\n\n // ── Step 3: Upload the screenshot captured during scan ────────────────────\n send({ step: 'SCREENSHOT_START' });\n if (scanResult.screenshotBuffer) {\n uploadedImageUrl = await uploadShowcaseScreenshot(\n scanResult.screenshotBuffer,\n project.websiteUrl\n );\n }\n send({ step: 'SCREENSHOT_SUCCESS' });\n\n // ── Step 4: Merge & save ──────────────────────────────────────────────────\n const mergedPackageDetails: Record<string, string> = {\n ...(scanResult.packageDetails ?? {}),\n ...githubPackageDetails,\n };\n const mergedLibsUsed = Array.from(\n new Set([...scanResult.libsUsed, ...Object.keys(githubPackageDetails)])\n );\n const intlayerVersion =\n mergedPackageDetails.intlayer || scanResult.intlayerVersion;\n\n const updated = await showcaseProjectService.updateShowcaseProject(\n projectId,\n {\n intlayerVersion,\n libsUsed: mergedLibsUsed,\n packageDetails: mergedPackageDetails,\n scanDetails: scanResult.scanDetails,\n imageUrl: uploadedImageUrl ?? project.imageUrl,\n isOpenSource,\n status: 'active',\n lastScanDate: new Date(),\n // Populate tagline/description from page metadata if not already set\n ...((!project.description || project.description === '') &&\n scanResult.metaDescription\n ? { description: scanResult.metaDescription }\n : {}),\n }\n );\n\n send({\n step: 'SUCCESS',\n project: mapShowcaseProjectToAPI(updated, userId),\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Scan failed unexpectedly';\n logger.error('[scanShowcaseProject] Error:', error);\n await cleanup(message);\n }\n\n raw.end();\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"showcaseProject.controller.mjs","names":["showcaseProjectService.findShowcaseProjectByUrl","showcaseProjectService.createShowcaseProject","showcaseProjectService.findShowcaseProjects","showcaseProjectService.findShowcaseProjectById","showcaseProjectService.findOtherShowcaseProjects","showcaseProjectService.toggleShowcaseUpvote","showcaseProjectService.toggleShowcaseDownvote","showcaseProjectService.updateShowcaseProject","showcaseProjectService.deleteShowcaseProject","scanShowcaseProjectViaService"],"sources":["../../../src/controllers/showcaseProject.controller.ts"],"sourcesContent":["import { logger } from '@logger';\nimport * as showcaseProjectService from '@services/showcase/showcaseProject.service';\nimport { scanShowcaseProject as scanShowcaseProjectViaService } from '@services/showcase/showcaseScan.service';\nimport {\n deleteShowcaseScreenshot,\n uploadShowcaseScreenshot,\n} from '@services/showcase/showcaseUploadScreenshot.service';\nimport { verifyGithubRepo } from '@services/showcase/showcaseVerifyGithub.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { getFaviconUrl } from '@utils/getFaviconUrl';\nimport {\n mapShowcaseProjectsToAPI,\n mapShowcaseProjectToAPI,\n} from '@utils/mapper/showcaseProject';\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 { z } from 'zod';\nimport type { ShowcaseProjectAPI } from '@/types/showcaseProject.types';\n\nconst getUserId = (request: FastifyRequest): string | undefined =>\n request.session?.user\n ? String(request.session.user.id ?? (request.session.user as any)._id)\n : undefined;\n\nconst urlSchema = z\n .url()\n .optional()\n .or(z.literal(''))\n .transform((value) => (value === '' ? undefined : value));\n\nconst submitProjectSchema = z.object({\n name: z.string().min(1),\n url: z\n .url()\n .refine((val) => !/github\\.com|gitlab\\.com|bitbucket\\.org/.test(val), {\n message: 'Repository URLs should be placed in the GitHub URL field',\n }),\n githubUrl: urlSchema,\n useCases: z.array(z.string()).max(3).optional(),\n});\nexport type SubmitShowcaseProjectBody = z.input<typeof submitProjectSchema>;\nexport type SubmitShowcaseProjectResult = ResponseData<ShowcaseProjectAPI>;\n\n/**\n * POST /api/showcase-project/submit\n * Submits a new project to the showcase.\n */\nexport const submitShowcaseProject = async (\n request: FastifyRequest<{ Body: SubmitShowcaseProjectBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const parsed = submitProjectSchema.safeParse(request.body);\n\n if (!parsed.success) {\n const message = parsed.error.issues\n .map((e) => `${e.path.join('.')}: ${e.message}`)\n .join(', ');\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'INVALID_REQUEST_BODY',\n { message }\n );\n }\n\n const validatedData = parsed.data;\n\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n try {\n // Check for existing project with the same URL\n const existing = await showcaseProjectService.findShowcaseProjectByUrl(\n validatedData.url\n );\n\n if (existing) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SHOWCASE_PROJECT_URL_ALREADY_EXISTS',\n { url: validatedData.url }\n );\n }\n\n // Save project to DB immediately — scan is triggered separately by the owner\n // tagline and description will be populated automatically during the scan step\n const logoUrl = getFaviconUrl(validatedData.url);\n const newProject = await showcaseProjectService.createShowcaseProject({\n title: validatedData.name,\n description: '',\n websiteUrl: validatedData.url,\n githubUrl: validatedData.githubUrl,\n logoUrl: logoUrl ?? '',\n tags: validatedData.useCases || [],\n owner: userId,\n status: 'pending_scan',\n });\n\n const responseData = formatResponse<ShowcaseProjectAPI>({\n message: t({\n en: 'Project submitted successfully',\n 'en-GB': 'Project submitted successfully',\n fr: 'Projet soumis avec succès',\n es: 'Proyecto enviado con éxito',\n ru: 'Проект успешно отправлен',\n ja: 'プロジェクトが正常に送信されました',\n ko: '프로젝트가 성공적으로 제출되었습니다',\n zh: '项目已成功提交',\n de: 'Projekt erfolgreich eingereicht',\n ar: 'تم تقديم المشروع بنجاح',\n it: 'Progetto inviato con successo',\n pt: 'Projeto enviado con sucesso',\n hi: 'प्रोजेक्ट सफलतापूर्वक सबमिट किया गया',\n tr: 'Proje başarıyla gönderildi',\n pl: 'Projekt został pomyślnie przesłany',\n id: 'Proyek berhasil dikirim',\n vi: 'Dự án đã được gửi thành công',\n uk: 'Проєкт успішно надіслано',\n }),\n description: t({\n en: 'Your project has been added to the showcase. Use the Scan button to verify and enrich it.',\n 'en-GB':\n 'Your project has been added to the showcase. Use the Scan button to verify and enrich it.',\n fr: \"Votre projet a été ajouté à la vitrine. Utilisez le bouton Scan pour le vérifier et l'enrichir.\",\n es: 'Su proyecto ha sido añadido al showcase. Use el botón Scan para verificarlo y enriquecerlo.',\n ru: 'Ваш проект был добавлен в витрину. Используйте кнопку Сканировать, чтобы проверить и дополнить его.',\n ja: 'プロジェクトがショーケースに追加されました。「スキャン」ボタンを使用して、検証と充実を行ってください。',\n ko: '프로젝트가 쇼케이스에 추가되었습니다. 스캔 버튼을 사용하여 확인하고 보완하십시오.',\n zh: '您的项目已添加到展示墙。使用“扫描”按钮进行验证和完善。',\n de: 'Ihr Projekt wurde zum Showcase hinzugefügt. Verwenden Sie die Schaltfläche Scan, um es zu überprüfen und zu erweitern.',\n ar: 'تمت إضافة مشروعك إلى المعرض. استخدم زر المسح للتحقق منه وإثرائه.',\n it: 'Il tuo progetto è stato aggiunto alla vetrina. Usa il pulsante Scansiona per verificarlo e arricchirlo.',\n pt: 'Seu projeto foi adicionado ao showcase. Use o botão Scan para verificá-lo e enriquecê-lo.',\n hi: 'आपका प्रोजेक्ट शोकेस में जोड़ दिया गया है। इसे सत्यापित करने और समृद्ध करने के लिए स्कैन बटन का उपयोग करें।',\n tr: 'Projeniz vitrine eklendi. Doğrulamak ve zenginleştirmek için Tara düğmesini kullanın.',\n pl: 'Twój projekt został dodany do witryny. Użyj przycisku Skanuj, aby go zweryfikować i wzbogacić.',\n id: 'Proyek Anda telah ditambahkan ke showcase. Gunakan tombol Pindai untuk memverifikasi dan memperkayanya.',\n vi: 'Dự án của bạn đã được thêm vào showcase. Sử dụng nút Quét để xác minh và làm phong phú nó.',\n uk: 'Ваш проєкт додано до вітрини. Скористайтеся кнопкою Сканувати, щоб перевірити та доповнити його.',\n }),\n data: mapShowcaseProjectToAPI(newProject, userId),\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type GetShowcaseProjectsQuery = {\n page?: string;\n pageSize?: string;\n search?: string;\n selectedUseCases?: string | string[];\n isOpenSource?: string;\n};\nexport type GetShowcaseProjectsResult = PaginatedResponse<ShowcaseProjectAPI>;\n\n/**\n * GET /api/showcase-project\n */\nexport const getShowcaseProjects = async (\n request: FastifyRequest<{ Querystring: GetShowcaseProjectsQuery }>,\n reply: FastifyReply\n): Promise<void> => {\n const {\n page: pageStr = '1',\n pageSize: pageSizeStr = '20',\n search = '',\n selectedUseCases,\n isOpenSource: isOpenSourceStr = 'false',\n } = request.query;\n\n const page = parseInt(pageStr, 10) || 1;\n const pageSize = parseInt(pageSizeStr, 10) || 20;\n const isOpenSource = isOpenSourceStr === 'true';\n\n const useCasesArray = selectedUseCases\n ? Array.isArray(selectedUseCases)\n ? selectedUseCases\n : [selectedUseCases]\n : [];\n\n try {\n const { data, total_items, total_pages } =\n await showcaseProjectService.findShowcaseProjects({\n search,\n selectedUseCases: useCasesArray,\n isOpenSource,\n page,\n pageSize,\n });\n\n const userId = getUserId(request);\n\n const responseData = formatPaginatedResponse<ShowcaseProjectAPI>({\n data: mapShowcaseProjectsToAPI(data, userId),\n page,\n pageSize,\n totalPages: total_pages,\n totalItems: total_items,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type GetShowcaseProjectByIdParams = { projectId: string };\nexport type GetShowcaseProjectByIdResult = ResponseData<ShowcaseProjectAPI>;\n\n/**\n * GET /api/showcase-project/:projectId\n */\nexport const getShowcaseProjectById = async (\n request: FastifyRequest<{ Params: GetShowcaseProjectByIdParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const { projectId } = request.params;\n\n try {\n const project =\n await showcaseProjectService.findShowcaseProjectById(projectId);\n\n const userId = getUserId(request);\n\n return reply.send(\n formatResponse<ShowcaseProjectAPI>({\n data: mapShowcaseProjectToAPI(project, userId),\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type GetOtherShowcaseProjectsQuery = {\n excludeId: string;\n limit?: string;\n};\nexport type GetOtherShowcaseProjectsResult = ResponseData<ShowcaseProjectAPI[]>;\n\n/**\n * GET /api/showcase-project/others?excludeId=...&limit=...\n */\nexport const getOtherShowcaseProjects = async (\n request: FastifyRequest<{ Querystring: GetOtherShowcaseProjectsQuery }>,\n reply: FastifyReply\n): Promise<void> => {\n const { excludeId, limit: limitStr } = request.query;\n const limit = limitStr ? parseInt(limitStr, 10) : 4;\n\n if (!excludeId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SHOWCASE_PROJECT_NOT_FOUND',\n { detail: 'excludeId is required' }\n );\n }\n\n try {\n const projects = await showcaseProjectService.findOtherShowcaseProjects(\n excludeId,\n limit\n );\n\n const userId = getUserId(request);\n\n return reply.send(\n formatResponse<ShowcaseProjectAPI[]>({\n data: mapShowcaseProjectsToAPI(projects, userId),\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type ToggleShowcaseUpvoteBody = { projectId: string };\nexport type ToggleShowcaseUpvoteResult = ResponseData<{\n upvotes: number;\n isUpVoted: boolean;\n downvotes: number;\n isDownVoted: boolean;\n}>;\n\n/**\n * POST /api/showcase-project/upvote\n * Requires authentication — userId is read from the session.\n */\nexport const toggleShowcaseUpvote = async (\n request: FastifyRequest<{ Body: ToggleShowcaseUpvoteBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n const { projectId } = request.body;\n\n if (!projectId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'INVALID_REQUEST_BODY',\n { detail: 'projectId is required' }\n );\n }\n\n try {\n const result = await showcaseProjectService.toggleShowcaseUpvote(\n projectId,\n userId\n );\n\n return reply.send(formatResponse({ data: result }));\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type ToggleShowcaseDownvoteBody = { projectId: string };\nexport type ToggleShowcaseDownvoteResult = ResponseData<{\n upvotes: number;\n isUpVoted: boolean;\n downvotes: number;\n isDownVoted: boolean;\n}>;\n\n/**\n * POST /api/showcase-project/downvote\n * Requires authentication — userId is read from the session.\n */\nexport const toggleShowcaseDownvote = async (\n request: FastifyRequest<{ Body: ToggleShowcaseDownvoteBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n const { projectId } = request.body;\n\n if (!projectId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'INVALID_REQUEST_BODY',\n { detail: 'projectId is required' }\n );\n }\n\n try {\n const result = await showcaseProjectService.toggleShowcaseDownvote(\n projectId,\n userId\n );\n\n return reply.send(formatResponse({ data: result }));\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nconst updateProjectSchema = z.object({\n name: z.string().min(1).max(255).optional(),\n url: z\n .url()\n .refine((val) => !/github\\.com|gitlab\\.com|bitbucket\\.org/.test(val), {\n message: 'Repository URLs should be placed in the GitHub URL field',\n })\n .optional(),\n githubUrl: z\n .string()\n .optional()\n .transform((value) => {\n if (!value) return null;\n if (value.startsWith('http://') || value.startsWith('https://'))\n return value;\n return `https://${value}`;\n }),\n tagline: z.string().min(1).max(500).optional(),\n description: z.string().optional(),\n useCases: z.array(z.string()).max(3).optional(),\n});\n\nexport type UpdateShowcaseProjectBody = z.input<typeof updateProjectSchema>;\nexport type UpdateShowcaseProjectParams = { projectId: string };\nexport type UpdateShowcaseProjectResult = ResponseData<ShowcaseProjectAPI>;\n\n/**\n * PATCH /api/showcase-project/:projectId\n * Updates an existing project. Only the owner can update.\n */\nexport const updateShowcaseProjectHandler = async (\n request: FastifyRequest<{\n Params: UpdateShowcaseProjectParams;\n Body: UpdateShowcaseProjectBody;\n }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n const parsed = updateProjectSchema.safeParse(request.body);\n\n if (!parsed.success) {\n const message = parsed.error.issues\n .map((e) => `${e.path.join('.')}: ${e.message}`)\n .join(', ');\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'INVALID_REQUEST_BODY',\n { message }\n );\n }\n\n const { projectId } = request.params;\n\n try {\n const project =\n await showcaseProjectService.findShowcaseProjectById(projectId);\n\n if (String(project.owner) !== userId) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_ID_MISMATCH');\n }\n\n const { name, url, githubUrl, tagline, useCases } = parsed.data;\n const updates: Parameters<\n typeof showcaseProjectService.updateShowcaseProject\n >[1] = {};\n\n if (name !== undefined) updates.title = name;\n if (url !== undefined) updates.websiteUrl = url;\n if ('githubUrl' in parsed.data) updates.githubUrl = githubUrl;\n if (tagline !== undefined) updates.description = tagline;\n if (useCases !== undefined) updates.tags = useCases;\n\n const updated = await showcaseProjectService.updateShowcaseProject(\n projectId,\n updates\n );\n\n return reply.send(\n formatResponse<ShowcaseProjectAPI>({\n message: t({\n en: 'Project updated successfully',\n 'en-GB': 'Project updated successfully',\n fr: 'Projet mis à jour avec succès',\n es: 'Proyecto actualizado con éxito',\n ru: 'Проект успешно обновлен',\n ja: 'プロジェクトが正常に更新されました',\n ko: '프로젝트가 성공적으로 업데이트되었습니다',\n zh: '项目已成功更新',\n de: 'Projekt erfolgreich aktualisiert',\n ar: 'تم تحديث المشروع بنجاح',\n it: 'Progetto aggiornato con successo',\n pt: 'Projeto atualizado com sucesso',\n hi: 'प्रोजेक्ट सफलतापूर्वक अपडेट किया गया',\n tr: 'Proje başarıyla güncellendi',\n pl: 'Projekt został pomyślnie zaktualizowany',\n id: 'Proyek berhasil diperbarui',\n vi: 'Dự án đã được cập nhật thành công',\n uk: 'Проєкт успішно оновлено',\n }),\n data: mapShowcaseProjectToAPI(updated, userId),\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type DeleteShowcaseProjectParams = { projectId: string };\n\n/**\n * DELETE /api/showcase-project/:projectId\n * Deletes a project (DB + R2 screenshot). Only the owner can delete.\n */\nexport const deleteShowcaseProjectHandler = async (\n request: FastifyRequest<{ Params: DeleteShowcaseProjectParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'USER_NOT_AUTHENTICATED'\n );\n }\n\n const { projectId } = request.params;\n\n try {\n const project =\n await showcaseProjectService.findShowcaseProjectById(projectId);\n\n if (String(project.owner) !== userId) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_ID_MISMATCH');\n }\n\n await Promise.allSettled([\n showcaseProjectService.deleteShowcaseProject(projectId),\n project.imageUrl\n ? deleteShowcaseScreenshot(project.imageUrl)\n : Promise.resolve(),\n ]);\n\n return reply.send(formatResponse({ data: { success: true } }));\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type ScanShowcaseProjectParams = { projectId: string };\n\n/**\n * GET /api/showcase-project/:projectId/scan\n * SSE endpoint — streams scan progress to the owner.\n * Requires authentication: only the project owner can trigger the scan.\n */\nexport const scanShowcaseProject = async (\n request: FastifyRequest<{ Params: ScanShowcaseProjectParams }>,\n reply: FastifyReply\n): Promise<void> => {\n const userId = getUserId(request);\n\n if (!userId) {\n reply.status(401).send({ error: { message: 'Authentication required' } });\n return;\n }\n\n const { projectId } = request.params;\n\n let project: Awaited<\n ReturnType<typeof showcaseProjectService.findShowcaseProjectById>\n >;\n try {\n project = await showcaseProjectService.findShowcaseProjectById(projectId);\n } catch {\n reply.status(404).send({ error: { message: 'Project not found' } });\n return;\n }\n\n if (project.owner !== userId) {\n reply.status(403).send({\n error: { message: 'Only the project owner can trigger a scan' },\n });\n return;\n }\n\n // Hijack the reply and write SSE manually\n reply.hijack();\n const raw = reply.raw;\n const origin = request.headers.origin ?? '*';\n raw.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'Access-Control-Allow-Origin': origin,\n 'Access-Control-Allow-Credentials': 'true',\n });\n\n const send = (data: Record<string, unknown>) => {\n raw.write(`data: ${JSON.stringify(data)}\\n\\n`);\n };\n\n // Track the uploaded screenshot URL so we can delete it on failure.\n let uploadedImageUrl: string | null = null;\n\n const cleanup = async (message: string) => {\n await Promise.allSettled([\n showcaseProjectService.deleteShowcaseProject(projectId),\n uploadedImageUrl\n ? deleteShowcaseScreenshot(uploadedImageUrl)\n : Promise.resolve(),\n ]);\n send({ step: 'ERROR', message });\n };\n\n try {\n // ── Step 1: Scan website + take screenshot (single browser session) ────────\n send({ step: 'SCANNING_START' });\n const scanResult = await scanShowcaseProjectViaService(project.websiteUrl);\n\n if (!scanResult.hasIntlayer && !scanResult.libsUsed.includes('intlayer')) {\n await cleanup('Intlayer not detected on this website');\n raw.end();\n return;\n }\n send({ step: 'SCANNING_SUCCESS' });\n\n // ── Step 2: Verify GitHub (if provided) ───────────────────────────────────\n let isOpenSource = false;\n let githubPackageDetails: Record<string, string> = {};\n\n if (project.githubUrl) {\n send({ step: 'VERIFY_GITHUB_START' });\n logger.info(\n `[scanShowcaseProject] Verifying GitHub ${project.githubUrl}...`\n );\n const githubResult = await verifyGithubRepo(project.githubUrl);\n if (githubResult?.isValid) {\n isOpenSource = true;\n githubPackageDetails = githubResult.packageDetails ?? {};\n }\n send({ step: 'VERIFY_GITHUB_SUCCESS' });\n }\n\n // ── Step 3: Upload the screenshot captured during scan ────────────────────\n send({ step: 'SCREENSHOT_START' });\n if (scanResult.screenshotBuffer) {\n uploadedImageUrl = await uploadShowcaseScreenshot(\n scanResult.screenshotBuffer,\n project.websiteUrl\n );\n }\n send({ step: 'SCREENSHOT_SUCCESS' });\n\n // ── Step 4: Merge & save ──────────────────────────────────────────────────\n const mergedPackageDetails: Record<string, string> = {\n ...(scanResult.packageDetails ?? {}),\n ...githubPackageDetails,\n };\n const mergedLibsUsed = Array.from(\n new Set([...scanResult.libsUsed, ...Object.keys(githubPackageDetails)])\n );\n const intlayerVersion =\n mergedPackageDetails.intlayer || scanResult.intlayerVersion;\n\n const updated = await showcaseProjectService.updateShowcaseProject(\n projectId,\n {\n intlayerVersion,\n libsUsed: mergedLibsUsed,\n packageDetails: mergedPackageDetails,\n scanDetails: scanResult.scanDetails,\n imageUrl: uploadedImageUrl ?? project.imageUrl,\n isOpenSource,\n status: 'active',\n lastScanDate: new Date(),\n // Populate tagline/description from page metadata if not already set\n ...((!project.description || project.description === '') &&\n scanResult.metaDescription\n ? { description: scanResult.metaDescription }\n : {}),\n }\n );\n\n send({\n step: 'SUCCESS',\n project: mapShowcaseProjectToAPI(updated, userId),\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Scan failed unexpectedly';\n logger.error('[scanShowcaseProject] Error:', error);\n await cleanup(message);\n }\n\n raw.end();\n};\n"],"mappings":"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"}
|