@intlayer/backend 7.5.9 → 7.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/assets/utils/AI/askDocQuestion/PROMPT.md +1 -1
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/cli/ci.json +3080 -0
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/cli/list_projects.json +1 -0
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/intlayer_with_fastify.json +9 -0
- package/dist/esm/controllers/ai.controller.mjs +95 -128
- package/dist/esm/controllers/ai.controller.mjs.map +1 -1
- package/dist/esm/controllers/bitbucket.controller.mjs +77 -0
- package/dist/esm/controllers/bitbucket.controller.mjs.map +1 -0
- package/dist/esm/controllers/dictionary.controller.mjs +106 -198
- package/dist/esm/controllers/dictionary.controller.mjs.map +1 -1
- package/dist/esm/controllers/eventListener.controller.mjs +13 -19
- package/dist/esm/controllers/eventListener.controller.mjs.map +1 -1
- package/dist/esm/controllers/github.controller.mjs +77 -0
- package/dist/esm/controllers/github.controller.mjs.map +1 -0
- package/dist/esm/controllers/gitlab.controller.mjs +77 -0
- package/dist/esm/controllers/gitlab.controller.mjs.map +1 -0
- package/dist/esm/controllers/newsletter.controller.mjs +30 -60
- package/dist/esm/controllers/newsletter.controller.mjs.map +1 -1
- package/dist/esm/controllers/oAuth2.controller.mjs +11 -8
- package/dist/esm/controllers/oAuth2.controller.mjs.map +1 -1
- package/dist/esm/controllers/organization.controller.mjs +100 -225
- package/dist/esm/controllers/organization.controller.mjs.map +1 -1
- package/dist/esm/controllers/project.controller.mjs +194 -204
- package/dist/esm/controllers/project.controller.mjs.map +1 -1
- package/dist/esm/controllers/projectAccessKey.controller.mjs +38 -71
- package/dist/esm/controllers/projectAccessKey.controller.mjs.map +1 -1
- package/dist/esm/controllers/search.controller.mjs +3 -3
- package/dist/esm/controllers/search.controller.mjs.map +1 -1
- package/dist/esm/controllers/stripe.controller.mjs +34 -67
- package/dist/esm/controllers/stripe.controller.mjs.map +1 -1
- package/dist/esm/controllers/tag.controller.mjs +51 -113
- package/dist/esm/controllers/tag.controller.mjs.map +1 -1
- package/dist/esm/controllers/user.controller.mjs +64 -113
- package/dist/esm/controllers/user.controller.mjs.map +1 -1
- package/dist/esm/export.mjs +4 -1
- package/dist/esm/index.mjs +105 -41
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/middlewares/oAuth2.middleware.mjs +19 -14
- package/dist/esm/middlewares/oAuth2.middleware.mjs.map +1 -1
- package/dist/esm/middlewares/sessionAuth.middleware.mjs +6 -7
- package/dist/esm/middlewares/sessionAuth.middleware.mjs.map +1 -1
- package/dist/esm/routes/ai.routes.mjs +19 -15
- package/dist/esm/routes/ai.routes.mjs.map +1 -1
- package/dist/esm/routes/bitbucket.routes.mjs +43 -0
- package/dist/esm/routes/bitbucket.routes.mjs.map +1 -0
- package/dist/esm/routes/dictionary.routes.mjs +10 -10
- package/dist/esm/routes/dictionary.routes.mjs.map +1 -1
- package/dist/esm/routes/eventListener.routes.mjs +3 -3
- package/dist/esm/routes/eventListener.routes.mjs.map +1 -1
- package/dist/esm/routes/github.routes.mjs +43 -0
- package/dist/esm/routes/github.routes.mjs.map +1 -0
- package/dist/esm/routes/gitlab.routes.mjs +43 -0
- package/dist/esm/routes/gitlab.routes.mjs.map +1 -0
- package/dist/esm/routes/newsletter.routes.mjs +5 -5
- package/dist/esm/routes/newsletter.routes.mjs.map +1 -1
- package/dist/esm/routes/organization.routes.mjs +11 -11
- package/dist/esm/routes/organization.routes.mjs.map +1 -1
- package/dist/esm/routes/project.routes.mjs +38 -14
- package/dist/esm/routes/project.routes.mjs.map +1 -1
- package/dist/esm/routes/search.routes.mjs +3 -3
- package/dist/esm/routes/search.routes.mjs.map +1 -1
- package/dist/esm/routes/stripe.routes.mjs +5 -5
- package/dist/esm/routes/stripe.routes.mjs.map +1 -1
- package/dist/esm/routes/tags.routes.mjs +6 -6
- package/dist/esm/routes/tags.routes.mjs.map +1 -1
- package/dist/esm/routes/user.routes.mjs +9 -9
- package/dist/esm/routes/user.routes.mjs.map +1 -1
- package/dist/esm/schemas/project.schema.mjs +70 -1
- package/dist/esm/schemas/project.schema.mjs.map +1 -1
- package/dist/esm/services/bitbucket.service.mjs +173 -0
- package/dist/esm/services/bitbucket.service.mjs.map +1 -0
- package/dist/esm/services/ci.service.mjs +134 -0
- package/dist/esm/services/ci.service.mjs.map +1 -0
- package/dist/esm/services/email.service.mjs +1 -1
- package/dist/esm/services/email.service.mjs.map +1 -1
- package/dist/esm/services/github.service.mjs +218 -0
- package/dist/esm/services/github.service.mjs.map +1 -0
- package/dist/esm/services/gitlab.service.mjs +217 -0
- package/dist/esm/services/gitlab.service.mjs.map +1 -0
- package/dist/esm/services/oAuth2.service.mjs +1 -1
- package/dist/esm/services/subscription.service.mjs +1 -1
- package/dist/esm/services/subscription.service.mjs.map +1 -1
- package/dist/esm/services/webhook.service.mjs +164 -0
- package/dist/esm/services/webhook.service.mjs.map +1 -0
- package/dist/esm/utils/auth/getAuth.mjs +28 -16
- package/dist/esm/utils/auth/getAuth.mjs.map +1 -1
- package/dist/esm/utils/cors.mjs +15 -5
- package/dist/esm/utils/cors.mjs.map +1 -1
- package/dist/esm/utils/errors/ErrorHandler.mjs +32 -4
- package/dist/esm/utils/errors/ErrorHandler.mjs.map +1 -1
- package/dist/esm/utils/errors/ErrorsClass.mjs +1 -1
- package/dist/esm/utils/errors/ErrorsClass.mjs.map +1 -1
- package/dist/esm/utils/errors/errorCodes.mjs +234 -0
- package/dist/esm/utils/errors/errorCodes.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getDictionaryFiltersAndPagination.mjs +3 -2
- package/dist/esm/utils/filtersAndPagination/getDictionaryFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getDiscussionFiltersAndPagination.mjs +1 -1
- package/dist/esm/utils/filtersAndPagination/getDiscussionFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getFiltersAndPaginationFromBody.mjs +1 -1
- package/dist/esm/utils/filtersAndPagination/getFiltersAndPaginationFromBody.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getOrganizationFiltersAndPagination.mjs +3 -2
- package/dist/esm/utils/filtersAndPagination/getOrganizationFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getProjectFiltersAndPagination.mjs +3 -2
- package/dist/esm/utils/filtersAndPagination/getProjectFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getTagFiltersAndPagination.mjs +3 -2
- package/dist/esm/utils/filtersAndPagination/getTagFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/filtersAndPagination/getUserFiltersAndPagination.mjs +3 -2
- package/dist/esm/utils/filtersAndPagination/getUserFiltersAndPagination.mjs.map +1 -1
- package/dist/esm/utils/mapper/project.mjs +28 -1
- package/dist/esm/utils/mapper/project.mjs.map +1 -1
- package/dist/esm/utils/mongoDB/connectDB.mjs +1 -1
- package/dist/esm/utils/rateLimiter.mjs +40 -30
- package/dist/esm/utils/rateLimiter.mjs.map +1 -1
- package/dist/esm/webhooks/stripe.webhook.mjs +2 -2
- package/dist/esm/webhooks/stripe.webhook.mjs.map +1 -1
- package/dist/types/controllers/ai.controller.d.ts +29 -12
- package/dist/types/controllers/ai.controller.d.ts.map +1 -1
- package/dist/types/controllers/bitbucket.controller.d.ts +62 -0
- package/dist/types/controllers/bitbucket.controller.d.ts.map +1 -0
- package/dist/types/controllers/dictionary.controller.d.ts +23 -13
- package/dist/types/controllers/dictionary.controller.d.ts.map +1 -1
- package/dist/types/controllers/eventListener.controller.d.ts +4 -2
- package/dist/types/controllers/eventListener.controller.d.ts.map +1 -1
- package/dist/types/controllers/github.controller.d.ts +63 -0
- package/dist/types/controllers/github.controller.d.ts.map +1 -0
- package/dist/types/controllers/gitlab.controller.d.ts +67 -0
- package/dist/types/controllers/gitlab.controller.d.ts.map +1 -0
- package/dist/types/controllers/newsletter.controller.d.ts +8 -7
- package/dist/types/controllers/newsletter.controller.d.ts.map +1 -1
- package/dist/types/controllers/oAuth2.controller.d.ts +4 -2
- package/dist/types/controllers/oAuth2.controller.d.ts.map +1 -1
- package/dist/types/controllers/organization.controller.d.ts +28 -12
- package/dist/types/controllers/organization.controller.d.ts.map +1 -1
- package/dist/types/controllers/project.controller.d.ts +60 -17
- package/dist/types/controllers/project.controller.d.ts.map +1 -1
- package/dist/types/controllers/projectAccessKey.controller.d.ts +10 -5
- package/dist/types/controllers/projectAccessKey.controller.d.ts.map +1 -1
- package/dist/types/controllers/search.controller.d.ts +4 -2
- package/dist/types/controllers/search.controller.d.ts.map +1 -1
- package/dist/types/controllers/stripe.controller.d.ts +11 -12
- package/dist/types/controllers/stripe.controller.d.ts.map +1 -1
- package/dist/types/controllers/tag.controller.d.ts +14 -9
- package/dist/types/controllers/tag.controller.d.ts.map +1 -1
- package/dist/types/controllers/user.controller.d.ts +22 -9
- package/dist/types/controllers/user.controller.d.ts.map +1 -1
- package/dist/types/emails/InviteUserEmail.d.ts +4 -4
- package/dist/types/emails/MagicLinkEmail.d.ts +4 -4
- package/dist/types/emails/OAuthTokenCreatedEmail.d.ts +4 -4
- package/dist/types/emails/OAuthTokenCreatedEmail.d.ts.map +1 -1
- package/dist/types/emails/PasswordChangeConfirmation.d.ts +4 -4
- package/dist/types/emails/ResetUserPassword.d.ts +4 -4
- package/dist/types/emails/ResetUserPassword.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentCancellation.d.ts +4 -4
- package/dist/types/emails/SubscriptionPaymentError.d.ts +4 -4
- package/dist/types/emails/SubscriptionPaymentSuccess.d.ts +4 -4
- package/dist/types/emails/ValidateUserEmail.d.ts +4 -4
- package/dist/types/emails/Welcome.d.ts +4 -4
- package/dist/types/export.d.ts +11 -5
- package/dist/types/middlewares/oAuth2.middleware.d.ts +9 -4
- package/dist/types/middlewares/oAuth2.middleware.d.ts.map +1 -1
- package/dist/types/middlewares/sessionAuth.middleware.d.ts +13 -3
- package/dist/types/middlewares/sessionAuth.middleware.d.ts.map +1 -1
- package/dist/types/models/discussion.model.d.ts +3 -3
- package/dist/types/models/oAuth2.model.d.ts +3 -3
- package/dist/types/routes/ai.routes.d.ts +2 -2
- package/dist/types/routes/ai.routes.d.ts.map +1 -1
- package/dist/types/routes/bitbucket.routes.d.ts +35 -0
- package/dist/types/routes/bitbucket.routes.d.ts.map +1 -0
- package/dist/types/routes/dictionary.routes.d.ts +2 -2
- package/dist/types/routes/dictionary.routes.d.ts.map +1 -1
- package/dist/types/routes/eventListener.routes.d.ts +2 -2
- package/dist/types/routes/eventListener.routes.d.ts.map +1 -1
- package/dist/types/routes/github.routes.d.ts +35 -0
- package/dist/types/routes/github.routes.d.ts.map +1 -0
- package/dist/types/routes/gitlab.routes.d.ts +35 -0
- package/dist/types/routes/gitlab.routes.d.ts.map +1 -0
- package/dist/types/routes/newsletter.routes.d.ts +2 -2
- package/dist/types/routes/newsletter.routes.d.ts.map +1 -1
- package/dist/types/routes/organization.routes.d.ts +2 -2
- package/dist/types/routes/organization.routes.d.ts.map +1 -1
- package/dist/types/routes/project.routes.d.ts +22 -2
- package/dist/types/routes/project.routes.d.ts.map +1 -1
- package/dist/types/routes/search.routes.d.ts +2 -2
- package/dist/types/routes/search.routes.d.ts.map +1 -1
- package/dist/types/routes/stripe.routes.d.ts +2 -2
- package/dist/types/routes/stripe.routes.d.ts.map +1 -1
- package/dist/types/routes/tags.routes.d.ts +2 -2
- package/dist/types/routes/tags.routes.d.ts.map +1 -1
- package/dist/types/routes/user.routes.d.ts +2 -2
- package/dist/types/routes/user.routes.d.ts.map +1 -1
- package/dist/types/schemas/dictionary.schema.d.ts +6 -6
- package/dist/types/schemas/discussion.schema.d.ts +6 -6
- package/dist/types/schemas/oAuth2.schema.d.ts +5 -5
- package/dist/types/schemas/oAuth2.schema.d.ts.map +1 -1
- package/dist/types/schemas/plans.schema.d.ts +6 -6
- package/dist/types/schemas/project.schema.d.ts +6 -6
- package/dist/types/schemas/project.schema.d.ts.map +1 -1
- package/dist/types/schemas/session.schema.d.ts +6 -6
- package/dist/types/schemas/tag.schema.d.ts +6 -6
- package/dist/types/schemas/user.schema.d.ts +6 -6
- package/dist/types/schemas/user.schema.d.ts.map +1 -1
- package/dist/types/services/bitbucket.service.d.ts +71 -0
- package/dist/types/services/bitbucket.service.d.ts.map +1 -0
- package/dist/types/services/ci.service.d.ts +27 -0
- package/dist/types/services/ci.service.d.ts.map +1 -0
- package/dist/types/services/github.service.d.ts +40 -0
- package/dist/types/services/github.service.d.ts.map +1 -0
- package/dist/types/services/gitlab.service.d.ts +58 -0
- package/dist/types/services/gitlab.service.d.ts.map +1 -0
- package/dist/types/services/webhook.service.d.ts +19 -0
- package/dist/types/services/webhook.service.d.ts.map +1 -0
- package/dist/types/types/project.types.d.ts +46 -5
- package/dist/types/types/project.types.d.ts.map +1 -1
- package/dist/types/types/session.types.d.ts +1 -1
- package/dist/types/types/user.types.d.ts +1 -1
- package/dist/types/utils/AI/auditTag/index.d.ts +1 -1
- package/dist/types/utils/auth/getAuth.d.ts.map +1 -1
- package/dist/types/utils/cors.d.ts +2 -2
- package/dist/types/utils/errors/ErrorHandler.d.ts +31 -3
- package/dist/types/utils/errors/ErrorHandler.d.ts.map +1 -1
- package/dist/types/utils/errors/ErrorsClass.d.ts +1 -1
- package/dist/types/utils/errors/errorCodes.d.ts +234 -0
- package/dist/types/utils/errors/errorCodes.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getDictionaryFiltersAndPagination.d.ts +8 -4
- package/dist/types/utils/filtersAndPagination/getDictionaryFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getDiscussionFiltersAndPagination.d.ts +6 -3
- package/dist/types/utils/filtersAndPagination/getDiscussionFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getFiltersAndPaginationFromBody.d.ts +6 -2
- package/dist/types/utils/filtersAndPagination/getFiltersAndPaginationFromBody.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getOrganizationFiltersAndPagination.d.ts +8 -4
- package/dist/types/utils/filtersAndPagination/getOrganizationFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getProjectFiltersAndPagination.d.ts +6 -2
- package/dist/types/utils/filtersAndPagination/getProjectFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +8 -4
- package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getUserFiltersAndPagination.d.ts +6 -2
- package/dist/types/utils/filtersAndPagination/getUserFiltersAndPagination.d.ts.map +1 -1
- package/dist/types/utils/mapper/project.d.ts.map +1 -1
- package/dist/types/utils/permissions.d.ts +1 -1
- package/dist/types/utils/rateLimiter.d.ts +4 -2
- package/dist/types/utils/rateLimiter.d.ts.map +1 -1
- package/package.json +24 -28
- package/dist/esm/middlewares/request.middleware.mjs +0 -17
- package/dist/esm/middlewares/request.middleware.mjs.map +0 -1
- package/dist/types/middlewares/request.middleware.d.ts +0 -7
- package/dist/types/middlewares/request.middleware.d.ts.map +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { logger } from "../logger/index.mjs";
|
|
2
|
+
import { getDBClient } from "../utils/mongoDB/connectDB.mjs";
|
|
3
|
+
import { configurationFilesCandidates } from "@intlayer/config";
|
|
4
|
+
import { ObjectId } from "mongodb";
|
|
5
|
+
import { Octokit } from "@octokit/rest";
|
|
6
|
+
|
|
7
|
+
//#region src/services/github.service.ts
|
|
8
|
+
const getAuthorizationUrl = (redirectUri, login) => {
|
|
9
|
+
const clientId = process.env.GITHUB_CLIENT_ID;
|
|
10
|
+
if (!clientId) throw new Error("GitHub Client ID is not configured");
|
|
11
|
+
const params = new URLSearchParams({
|
|
12
|
+
client_id: clientId,
|
|
13
|
+
scope: "repo",
|
|
14
|
+
state: "github_oauth",
|
|
15
|
+
redirect_uri: redirectUri
|
|
16
|
+
});
|
|
17
|
+
if (login) params.append("login", login);
|
|
18
|
+
return `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
19
|
+
};
|
|
20
|
+
const exchangeCodeForToken = async (code) => {
|
|
21
|
+
const clientId = process.env.GITHUB_CLIENT_ID;
|
|
22
|
+
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
|
|
23
|
+
if (!clientId || !clientSecret) throw new Error("GitHub OAuth credentials are not configured");
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
Accept: "application/json"
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
client_id: clientId,
|
|
33
|
+
client_secret: clientSecret,
|
|
34
|
+
code
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) throw new Error(`GitHub token exchange failed: ${response.statusText}`);
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
if (data.error) throw new Error(`GitHub token error: ${data.error_description}`);
|
|
40
|
+
return data.access_token;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.error("Error exchanging GitHub code for token:", error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const getUserRepos = async (accessToken) => {
|
|
47
|
+
try {
|
|
48
|
+
const { data } = await new Octokit({ auth: accessToken }).rest.repos.listForAuthenticatedUser({
|
|
49
|
+
sort: "updated",
|
|
50
|
+
per_page: 100,
|
|
51
|
+
visibility: "all"
|
|
52
|
+
});
|
|
53
|
+
return data;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.error("Error fetching GitHub repositories:", error);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Check if valid intlayer configuration files exist in a repository (Recursively).
|
|
61
|
+
* Returns an array of file paths found (e.g. ['intlayer.config.ts', 'apps/web/intlayer.config.js']).
|
|
62
|
+
*/
|
|
63
|
+
const checkIntlayerConfig = async (accessToken, owner, repo, branch = "main") => {
|
|
64
|
+
try {
|
|
65
|
+
const { data } = await new Octokit({ auth: accessToken }).rest.git.getTree({
|
|
66
|
+
owner,
|
|
67
|
+
repo,
|
|
68
|
+
tree_sha: branch,
|
|
69
|
+
recursive: "true"
|
|
70
|
+
});
|
|
71
|
+
if (!data.tree || !Array.isArray(data.tree)) return [];
|
|
72
|
+
return data.tree.filter((item) => {
|
|
73
|
+
if (item.type !== "blob" || !item.path) return false;
|
|
74
|
+
return configurationFilesCandidates.some((candidate) => item.path?.endsWith(candidate));
|
|
75
|
+
}).map((item) => item.path);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.status === 404 || error.status === 409) return [];
|
|
78
|
+
logger.error("Error checking intlayer configuration:", error);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Get repository file contents and decode it
|
|
84
|
+
*/
|
|
85
|
+
const getRepositoryFileContents = async (accessToken, owner, repo, path, branch = "main") => {
|
|
86
|
+
try {
|
|
87
|
+
const { data } = await new Octokit({ auth: accessToken }).rest.repos.getContent({
|
|
88
|
+
owner,
|
|
89
|
+
repo,
|
|
90
|
+
path,
|
|
91
|
+
ref: branch
|
|
92
|
+
});
|
|
93
|
+
if (Array.isArray(data) || !("content" in data)) throw new Error("Path points to a directory, not a file");
|
|
94
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.status === 404) return null;
|
|
97
|
+
logger.error("Error fetching repository file contents:", error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const getGitHubTokenFromUser = async (userId) => {
|
|
102
|
+
try {
|
|
103
|
+
const db = getDBClient().db();
|
|
104
|
+
let account = await db.collection("account").findOne({
|
|
105
|
+
userId,
|
|
106
|
+
providerId: "github"
|
|
107
|
+
});
|
|
108
|
+
if (!account && ObjectId.isValid(userId)) account = await db.collection("account").findOne({
|
|
109
|
+
userId: new ObjectId(userId),
|
|
110
|
+
providerId: "github"
|
|
111
|
+
});
|
|
112
|
+
if (!account) account = await db.collection("accounts").findOne({
|
|
113
|
+
userId,
|
|
114
|
+
providerId: "github"
|
|
115
|
+
});
|
|
116
|
+
if (!account && ObjectId.isValid(userId)) account = await db.collection("accounts").findOne({
|
|
117
|
+
userId: new ObjectId(userId),
|
|
118
|
+
providerId: "github"
|
|
119
|
+
});
|
|
120
|
+
if (!account) return null;
|
|
121
|
+
return account.accessToken || account.access_token || null;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.error("Error retrieving GitHub token from DB:", error);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const triggerGithubDispatch = async ({ project, eventType = "intlayer_cms_update", payload = {} }) => {
|
|
128
|
+
const { repository, oAuth2Access } = project;
|
|
129
|
+
if (!repository || repository.provider !== "github") throw new Error("Project is not connected to a GitHub repository.");
|
|
130
|
+
const accessToken = (oAuth2Access?.[0])?.accessToken?.[0];
|
|
131
|
+
if (!accessToken) throw new Error("No valid OAuth2 access token found for GitHub.");
|
|
132
|
+
const { owner, repository: repoName } = repository;
|
|
133
|
+
const url = `https://api.github.com/repos/${owner}/${repoName}/dispatches`;
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
Authorization: `Bearer ${accessToken}`,
|
|
139
|
+
Accept: "application/vnd.github+json",
|
|
140
|
+
"Content-Type": "application/json"
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
event_type: eventType,
|
|
144
|
+
client_payload: {
|
|
145
|
+
...payload,
|
|
146
|
+
projectId: project.id,
|
|
147
|
+
timestamp: Date.now()
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const errorText = await response.text();
|
|
153
|
+
throw new Error(`GitHub API Error: ${response.status} - ${errorText}`);
|
|
154
|
+
}
|
|
155
|
+
logger.info(`Successfully triggered GitHub Action '${eventType}' for ${owner}/${repoName}`);
|
|
156
|
+
return true;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error(error);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Check if a GitHub workflow file exists
|
|
164
|
+
*/
|
|
165
|
+
const checkWorkflowFileExists = async (accessToken, owner, repo, filename, branch = "main") => {
|
|
166
|
+
try {
|
|
167
|
+
await new Octokit({ auth: accessToken }).rest.repos.getContent({
|
|
168
|
+
owner,
|
|
169
|
+
repo,
|
|
170
|
+
path: filename,
|
|
171
|
+
ref: branch
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error.status === 404) return false;
|
|
176
|
+
logger.error("Error checking workflow file existence:", error);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Create or update a GitHub workflow file
|
|
182
|
+
*/
|
|
183
|
+
const createWorkflowFile = async (accessToken, owner, repo, filename, content, branch = "main", message = "Add Intlayer CI workflow") => {
|
|
184
|
+
try {
|
|
185
|
+
const octokit = new Octokit({ auth: accessToken });
|
|
186
|
+
let sha;
|
|
187
|
+
try {
|
|
188
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
189
|
+
owner,
|
|
190
|
+
repo,
|
|
191
|
+
path: filename,
|
|
192
|
+
ref: branch
|
|
193
|
+
});
|
|
194
|
+
if (Array.isArray(data) || !("sha" in data)) throw new Error("Path points to a directory, not a file");
|
|
195
|
+
sha = data.sha;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error.status !== 404) throw error;
|
|
198
|
+
}
|
|
199
|
+
const encodedContent = Buffer.from(content, "utf-8").toString("base64");
|
|
200
|
+
await octokit.rest.repos.createOrUpdateFileContents({
|
|
201
|
+
owner,
|
|
202
|
+
repo,
|
|
203
|
+
path: filename,
|
|
204
|
+
message,
|
|
205
|
+
content: encodedContent,
|
|
206
|
+
branch,
|
|
207
|
+
...sha && { sha }
|
|
208
|
+
});
|
|
209
|
+
logger.info(`Successfully ${sha ? "updated" : "created"} workflow file ${filename} for ${owner}/${repo}`);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error("Error creating/updating workflow file:", error);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
export { checkIntlayerConfig, checkWorkflowFileExists, createWorkflowFile, exchangeCodeForToken, getAuthorizationUrl, getGitHubTokenFromUser, getRepositoryFileContents, getUserRepos, triggerGithubDispatch };
|
|
218
|
+
//# sourceMappingURL=github.service.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.service.mjs","names":["error: any","sha: string | undefined"],"sources":["../../../src/services/github.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport type { RestEndpointMethodTypes } from '@octokit/rest';\nimport { Octokit } from '@octokit/rest';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\nimport type { Project } from '@/types/project.types';\n\nexport type GitHubRepository =\n RestEndpointMethodTypes['repos']['listForAuthenticatedUser']['response']['data'][0];\nexport type GitHubFileContent =\n RestEndpointMethodTypes['repos']['getContent']['response']['data'];\n\nexport const getAuthorizationUrl = (\n redirectUri: string,\n login?: string\n): string => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('GitHub Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n scope: 'repo',\n state: 'github_oauth',\n redirect_uri: redirectUri,\n });\n\n if (login) {\n params.append('login', login);\n }\n\n return `https://github.com/login/oauth/authorize?${params.toString()}`;\n};\n\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n const clientSecret = process.env.GITHUB_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitHub OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(\n 'https://github.com/login/oauth/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n }),\n }\n );\n\n if (!response.ok) {\n throw new Error(`GitHub token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitHub token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitHub code for token:', error);\n throw error;\n }\n};\n\nexport const getUserRepos = async (\n accessToken: string\n): Promise<GitHubRepository[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.listForAuthenticatedUser({\n sort: 'updated',\n per_page: 100,\n visibility: 'all',\n });\n\n return data;\n } catch (error) {\n logger.error('Error fetching GitHub repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a repository (Recursively).\n * Returns an array of file paths found (e.g. ['intlayer.config.ts', 'apps/web/intlayer.config.js']).\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n owner: string,\n repo: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Use Git Tree API to get all files recursively\n // This allows finding configs in monorepos/subfolders\n const { data } = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: branch,\n recursive: 'true',\n });\n\n if (!data.tree || !Array.isArray(data.tree)) {\n return [];\n }\n\n // Filter files that match the configuration candidates\n // We check if the path ends with one of the candidate filenames\n const foundFiles = data.tree\n .filter((item) => {\n if (item.type !== 'blob' || !item.path) return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path?.endsWith(candidate)\n );\n })\n .map((item) => item.path as string); // Return the full path (e.g., 'packages/app/intlayer.config.ts')\n\n return foundFiles;\n } catch (error: any) {\n // If branch doesn't exist or repo is empty\n if (error.status === 404 || error.status === 409) return [];\n\n logger.error('Error checking intlayer configuration:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n owner: string,\n repo: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path,\n ref: branch,\n });\n\n // Octokit types are union types (file | dir | submodule), we need to check if it's a file\n if (Array.isArray(data) || !('content' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n // GitHub returns content in base64, we must decode it to read the actual code\n const decodedContent = Buffer.from(data.content, 'base64').toString(\n 'utf-8'\n );\n\n return decodedContent;\n } catch (error: any) {\n if (error.status === 404) return null;\n\n logger.error('Error fetching repository file contents:', error);\n throw error;\n }\n};\n\nexport const getGitHubTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'github',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'github',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'github',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitHub token from DB:', error);\n return null;\n }\n};\n\ntype DispatchEventOptions = {\n project: Project;\n eventType?: string;\n payload?: Record<string, any>;\n};\n\nexport const triggerGithubDispatch = async ({\n project,\n eventType = 'intlayer_cms_update',\n payload = {},\n}: DispatchEventOptions) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository || repository.provider !== 'github') {\n throw new Error('Project is not connected to a GitHub repository.');\n }\n\n // Get the valid Access Token\n // Assuming the first token is the active one, or implement logic to find the specific user's token\n const tokenData = oAuth2Access?.[0];\n const accessToken = tokenData?.accessToken?.[0]; // Assuming array of tokens\n\n if (!accessToken) {\n throw new Error('No valid OAuth2 access token found for GitHub.');\n }\n\n const { owner, repository: repoName } = repository;\n const url = `https://api.github.com/repos/${owner}/${repoName}/dispatches`;\n\n try {\n // 2. Send the Dispatch Event\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/vnd.github+json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n event_type: eventType,\n client_payload: {\n ...payload,\n projectId: project.id,\n timestamp: Date.now(),\n },\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`GitHub API Error: ${response.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitHub Action '${eventType}' for ${owner}/${repoName}`\n );\n return true;\n } catch (error) {\n logger.error(error);\n throw error;\n }\n};\n\n/**\n * Check if a GitHub workflow file exists\n */\nexport const checkWorkflowFileExists = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n });\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking workflow file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitHub workflow file\n */\nexport const createWorkflowFile = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n content: string,\n branch: string = 'main',\n message: string = 'Add Intlayer CI workflow'\n): Promise<void> => {\n try {\n const octokit = new Octokit({ auth: accessToken });\n\n // Check if file exists to get SHA for update\n let sha: string | undefined;\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n });\n\n if (Array.isArray(data) || !('sha' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n sha = data.sha;\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n // Encode content to base64\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filename,\n message,\n content: encodedContent,\n branch,\n ...(sha && { sha }), // Include SHA if updating existing file\n });\n\n logger.info(\n `Successfully ${sha ? 'updated' : 'created'} workflow file ${filename} for ${owner}/${repo}`\n );\n } catch (error) {\n logger.error('Error creating/updating workflow file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { logger } from "../logger/index.mjs";
|
|
2
|
+
import { getDBClient } from "../utils/mongoDB/connectDB.mjs";
|
|
3
|
+
import { configurationFilesCandidates } from "@intlayer/config";
|
|
4
|
+
import { ObjectId } from "mongodb";
|
|
5
|
+
|
|
6
|
+
//#region src/services/gitlab.service.ts
|
|
7
|
+
const GITLAB_DEFAULT_URL = "https://gitlab.com";
|
|
8
|
+
/**
|
|
9
|
+
* Get GitLab authorization URL for OAuth flow
|
|
10
|
+
*/
|
|
11
|
+
const getAuthorizationUrl = (redirectUri, instanceUrl, login) => {
|
|
12
|
+
const clientId = process.env.GITLAB_CLIENT_ID;
|
|
13
|
+
const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;
|
|
14
|
+
if (!clientId) throw new Error("GitLab Client ID is not configured");
|
|
15
|
+
const params = new URLSearchParams({
|
|
16
|
+
client_id: clientId,
|
|
17
|
+
redirect_uri: redirectUri,
|
|
18
|
+
response_type: "code",
|
|
19
|
+
scope: "api read_repository",
|
|
20
|
+
state: "gitlab_oauth"
|
|
21
|
+
});
|
|
22
|
+
if (login) params.append("login_hint", login);
|
|
23
|
+
return `${baseUrl}/oauth/authorize?${params.toString()}`;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Exchange GitLab authorization code for access token
|
|
27
|
+
*/
|
|
28
|
+
const exchangeCodeForToken = async (code, redirectUri, instanceUrl) => {
|
|
29
|
+
const clientId = process.env.GITLAB_CLIENT_ID;
|
|
30
|
+
const clientSecret = process.env.GITLAB_CLIENT_SECRET;
|
|
31
|
+
const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;
|
|
32
|
+
if (!clientId || !clientSecret) throw new Error("GitLab OAuth credentials are not configured");
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(`${baseUrl}/oauth/token`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
Accept: "application/json"
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
client_id: clientId,
|
|
42
|
+
client_secret: clientSecret,
|
|
43
|
+
code,
|
|
44
|
+
grant_type: "authorization_code",
|
|
45
|
+
redirect_uri: redirectUri
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) throw new Error(`GitLab token exchange failed: ${response.statusText}`);
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
if (data.error) throw new Error(`GitLab token error: ${data.error_description}`);
|
|
51
|
+
return data.access_token;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.error("Error exchanging GitLab code for token:", error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Get user's GitLab projects/repositories
|
|
59
|
+
*/
|
|
60
|
+
const getUserProjects = async (accessToken, instanceUrl) => {
|
|
61
|
+
const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(`${baseUrl}/api/v4/projects?membership=true&order_by=last_activity_at&per_page=100`, { headers: {
|
|
64
|
+
Authorization: `Bearer ${accessToken}`,
|
|
65
|
+
Accept: "application/json"
|
|
66
|
+
} });
|
|
67
|
+
if (!response.ok) throw new Error(`Failed to fetch GitLab projects: ${response.statusText}`);
|
|
68
|
+
return await response.json();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error("Error fetching GitLab projects:", error);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Check if valid intlayer configuration files exist in a GitLab repository (Recursively).
|
|
76
|
+
* Returns an array of file paths found.
|
|
77
|
+
*/
|
|
78
|
+
const checkIntlayerConfig = async (accessToken, projectId, branch = "main", instanceUrl) => {
|
|
79
|
+
const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(`${baseUrl}/api/v4/projects/${projectId}/repository/tree?ref=${encodeURIComponent(branch)}&recursive=true&per_page=10000`, { headers: {
|
|
82
|
+
Authorization: `Bearer ${accessToken}`,
|
|
83
|
+
Accept: "application/json"
|
|
84
|
+
} });
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
if (response.status === 404) return [];
|
|
87
|
+
throw new Error(`Failed to fetch repository tree: ${response.statusText}`);
|
|
88
|
+
}
|
|
89
|
+
return (await response.json()).filter((item) => {
|
|
90
|
+
if (item.type !== "blob") return false;
|
|
91
|
+
return configurationFilesCandidates.some((candidate) => item.path.endsWith(candidate));
|
|
92
|
+
}).map((item) => item.path);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.status === 404) return [];
|
|
95
|
+
logger.error("Error checking intlayer configuration on GitLab:", error);
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Get repository file contents from GitLab and decode it
|
|
101
|
+
*/
|
|
102
|
+
const getRepositoryFileContents = async (accessToken, projectId, path, branch = "main", instanceUrl) => {
|
|
103
|
+
const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;
|
|
104
|
+
try {
|
|
105
|
+
const encodedPath = encodeURIComponent(path);
|
|
106
|
+
const response = await fetch(`${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`, { headers: {
|
|
107
|
+
Authorization: `Bearer ${accessToken}`,
|
|
108
|
+
Accept: "application/json"
|
|
109
|
+
} });
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
if (response.status === 404) return null;
|
|
112
|
+
throw new Error(`Failed to fetch file contents: ${response.statusText}`);
|
|
113
|
+
}
|
|
114
|
+
return await response.text();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.status === 404) return null;
|
|
117
|
+
logger.error("Error fetching GitLab file contents:", error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Get GitLab access token from user's linked account
|
|
123
|
+
*/
|
|
124
|
+
const getGitLabTokenFromUser = async (userId) => {
|
|
125
|
+
try {
|
|
126
|
+
const db = getDBClient().db();
|
|
127
|
+
let account = await db.collection("account").findOne({
|
|
128
|
+
userId,
|
|
129
|
+
providerId: "gitlab"
|
|
130
|
+
});
|
|
131
|
+
if (!account && ObjectId.isValid(userId)) account = await db.collection("account").findOne({
|
|
132
|
+
userId: new ObjectId(userId),
|
|
133
|
+
providerId: "gitlab"
|
|
134
|
+
});
|
|
135
|
+
if (!account) account = await db.collection("accounts").findOne({
|
|
136
|
+
userId,
|
|
137
|
+
providerId: "gitlab"
|
|
138
|
+
});
|
|
139
|
+
if (!account && ObjectId.isValid(userId)) account = await db.collection("accounts").findOne({
|
|
140
|
+
userId: new ObjectId(userId),
|
|
141
|
+
providerId: "gitlab"
|
|
142
|
+
});
|
|
143
|
+
if (!account) return null;
|
|
144
|
+
return account.accessToken || account.access_token || null;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error("Error retrieving GitLab token from DB:", error);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Check if a GitLab CI pipeline file exists
|
|
152
|
+
*/
|
|
153
|
+
const checkPipelineFileExists = async (accessToken, projectId, filename = ".gitlab-ci.yml", branch = "main", instanceUrl) => {
|
|
154
|
+
const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;
|
|
155
|
+
try {
|
|
156
|
+
const encodedPath = encodeURIComponent(filename);
|
|
157
|
+
const response = await fetch(`${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`, { headers: {
|
|
158
|
+
Authorization: `Bearer ${accessToken}`,
|
|
159
|
+
Accept: "application/json"
|
|
160
|
+
} });
|
|
161
|
+
if (response.status === 404) return false;
|
|
162
|
+
if (!response.ok) throw new Error(`Failed to check file existence: ${response.statusText}`);
|
|
163
|
+
return true;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error.status === 404) return false;
|
|
166
|
+
logger.error("Error checking pipeline file existence:", error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Create or update a GitLab CI pipeline file
|
|
172
|
+
*/
|
|
173
|
+
const createPipelineFile = async (accessToken, projectId, filename = ".gitlab-ci.yml", content, branch = "main", instanceUrl, message = "Add Intlayer CI pipeline") => {
|
|
174
|
+
const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;
|
|
175
|
+
try {
|
|
176
|
+
const encodedPath = encodeURIComponent(filename);
|
|
177
|
+
const encodedContent = Buffer.from(content, "utf-8").toString("base64");
|
|
178
|
+
let existingContentSha;
|
|
179
|
+
try {
|
|
180
|
+
const checkResponse = await fetch(`${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`, { headers: {
|
|
181
|
+
Authorization: `Bearer ${accessToken}`,
|
|
182
|
+
Accept: "application/json"
|
|
183
|
+
} });
|
|
184
|
+
if (checkResponse.ok) existingContentSha = (await checkResponse.json()).content_sha256;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error.status !== 404) throw error;
|
|
187
|
+
}
|
|
188
|
+
const body = {
|
|
189
|
+
branch,
|
|
190
|
+
content: encodedContent,
|
|
191
|
+
commit_message: message,
|
|
192
|
+
encoding: "base64"
|
|
193
|
+
};
|
|
194
|
+
if (existingContentSha) body.last_commit_id = existingContentSha;
|
|
195
|
+
const response = await fetch(`${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}`, {
|
|
196
|
+
method: "PUT",
|
|
197
|
+
headers: {
|
|
198
|
+
Authorization: `Bearer ${accessToken}`,
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
Accept: "application/json"
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify(body)
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const errorText = await response.text();
|
|
206
|
+
throw new Error(`Failed to create/update pipeline file: ${errorText}`);
|
|
207
|
+
}
|
|
208
|
+
logger.info(`Successfully ${existingContentSha ? "updated" : "created"} pipeline file ${filename} for project ${projectId}`);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
logger.error("Error creating/updating pipeline file:", error);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
export { checkIntlayerConfig, checkPipelineFileExists, createPipelineFile, exchangeCodeForToken, getAuthorizationUrl, getGitLabTokenFromUser, getRepositoryFileContents, getUserProjects };
|
|
217
|
+
//# sourceMappingURL=gitlab.service.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab.service.mjs","names":["error: any","existingContentSha: string | undefined","body: any"],"sources":["../../../src/services/gitlab.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config';\nimport { logger } from '@logger';\nimport { getDBClient } from '@utils/mongoDB/connectDB';\nimport { ObjectId } from 'mongodb';\n\nconst GITLAB_DEFAULT_URL = 'https://gitlab.com';\n\nexport type GitLabProject = {\n id: number;\n name: string;\n path_with_namespace: string;\n web_url: string;\n default_branch: string;\n visibility: string;\n last_activity_at: string;\n namespace: {\n id: number;\n name: string;\n path: string;\n };\n};\n\nexport type GitLabTreeItem = {\n id: string;\n name: string;\n type: 'tree' | 'blob';\n path: string;\n mode: string;\n};\n\n/**\n * Get GitLab authorization URL for OAuth flow\n */\nexport const getAuthorizationUrl = (\n redirectUri: string,\n instanceUrl?: string,\n login?: string\n): string => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId) {\n throw new Error('GitLab Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n scope: 'api read_repository',\n state: 'gitlab_oauth',\n });\n\n if (login) {\n params.append('login_hint', login);\n }\n\n return `${baseUrl}/oauth/authorize?${params.toString()}`;\n};\n\n/**\n * Exchange GitLab authorization code for access token\n */\nexport const exchangeCodeForToken = async (\n code: string,\n redirectUri: string,\n instanceUrl?: string\n): Promise<string> => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const clientSecret = process.env.GITLAB_CLIENT_SECRET;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitLab OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(`${baseUrl}/oauth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n grant_type: 'authorization_code',\n redirect_uri: redirectUri,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`GitLab token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitLab token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitLab code for token:', error);\n throw error;\n }\n};\n\n/**\n * Get user's GitLab projects/repositories\n */\nexport const getUserProjects = async (\n accessToken: string,\n instanceUrl?: string\n): Promise<GitLabProject[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const response = await fetch(\n `${baseUrl}/api/v4/projects?membership=true&order_by=last_activity_at&per_page=100`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch GitLab projects: ${response.statusText}`\n );\n }\n\n const projects: GitLabProject[] = await response.json();\n return projects;\n } catch (error) {\n logger.error('Error fetching GitLab projects:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a GitLab repository (Recursively).\n * Returns an array of file paths found.\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n projectId: number,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n // Use GitLab's repository tree API with recursive option\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/tree?ref=${encodeURIComponent(branch)}&recursive=true&per_page=10000`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return [];\n throw new Error(\n `Failed to fetch repository tree: ${response.statusText}`\n );\n }\n\n const tree: GitLabTreeItem[] = await response.json();\n\n // Filter files that match the configuration candidates\n const foundFiles = tree\n .filter((item) => {\n if (item.type !== 'blob') return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path.endsWith(candidate)\n );\n })\n .map((item) => item.path);\n\n return foundFiles;\n } catch (error: any) {\n if (error.status === 404) return [];\n logger.error('Error checking intlayer configuration on GitLab:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents from GitLab and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n projectId: number,\n path: string,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string | null> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(path);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return null;\n throw new Error(`Failed to fetch file contents: ${response.statusText}`);\n }\n\n const content = await response.text();\n return content;\n } catch (error: any) {\n if (error.status === 404) return null;\n logger.error('Error fetching GitLab file contents:', error);\n throw error;\n }\n};\n\n/**\n * Get GitLab access token from user's linked account\n */\nexport const getGitLabTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const client = getDBClient();\n const db = client.db();\n\n let account = await db.collection('account').findOne({\n userId: userId,\n providerId: 'gitlab',\n });\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('account').findOne({\n userId: new ObjectId(userId),\n providerId: 'gitlab',\n });\n }\n\n if (!account) {\n account = await db.collection('accounts').findOne({\n userId: userId,\n providerId: 'gitlab',\n });\n }\n\n if (!account && ObjectId.isValid(userId)) {\n account = await db.collection('accounts').findOne({\n userId: new ObjectId(userId),\n providerId: 'gitlab',\n });\n }\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitLab token from DB:', error);\n return null;\n }\n};\n\n/**\n * Check if a GitLab CI pipeline file exists\n */\nexport const checkPipelineFileExists = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n branch: string = 'main',\n instanceUrl?: string\n): Promise<boolean> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new Error(`Failed to check file existence: ${response.statusText}`);\n }\n\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking pipeline file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitLab CI pipeline file\n */\nexport const createPipelineFile = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n content: string,\n branch: string = 'main',\n instanceUrl?: string,\n message: string = 'Add Intlayer CI pipeline'\n): Promise<void> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n // Check if file exists to get content_sha256 for update\n let existingContentSha: string | undefined;\n try {\n const checkResponse = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (checkResponse.ok) {\n const fileData = await checkResponse.json();\n existingContentSha = fileData.content_sha256;\n }\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n const body: any = {\n branch,\n content: encodedContent,\n commit_message: message,\n encoding: 'base64',\n };\n\n if (existingContentSha) {\n body.last_commit_id = existingContentSha;\n }\n\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}`,\n {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(body),\n }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to create/update pipeline file: ${errorText}`);\n }\n\n logger.info(\n `Successfully ${existingContentSha ? 'updated' : 'created'} pipeline file ${filename} for project ${projectId}`\n );\n } catch (error) {\n logger.error('Error creating/updating pipeline file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
|
|
@@ -2,11 +2,11 @@ import { ensureMongoDocumentToObject } from "../utils/ensureMongoDocumentToObjec
|
|
|
2
2
|
import { GenericError } from "../utils/errors/ErrorsClass.mjs";
|
|
3
3
|
import { getOrganizationById } from "./organization.service.mjs";
|
|
4
4
|
import { ProjectModel } from "../models/project.model.mjs";
|
|
5
|
+
import { OAuth2AccessTokenModel } from "../models/oAuth2.model.mjs";
|
|
5
6
|
import { getUserById } from "./user.service.mjs";
|
|
6
7
|
import { mapUserToAPI } from "../utils/mapper/user.mjs";
|
|
7
8
|
import { mapOrganizationToAPI } from "../utils/mapper/organization.mjs";
|
|
8
9
|
import { mapProjectToAPI } from "../utils/mapper/project.mjs";
|
|
9
|
-
import { OAuth2AccessTokenModel } from "../models/oAuth2.model.mjs";
|
|
10
10
|
import { getTokenExpireAt } from "../utils/oAuth2.mjs";
|
|
11
11
|
import { randomBytes } from "node:crypto";
|
|
12
12
|
|
|
@@ -79,7 +79,7 @@ const changeSubscriptionStatus = async (subscriptionId, status, userId, organiza
|
|
|
79
79
|
email: user.email,
|
|
80
80
|
planName: organization.plan.type,
|
|
81
81
|
date: (/* @__PURE__ */ new Date()).toLocaleDateString(),
|
|
82
|
-
link: `${process.env.
|
|
82
|
+
link: `${process.env.APP_URL}/dashboard`
|
|
83
83
|
};
|
|
84
84
|
switch (status) {
|
|
85
85
|
case "active":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.service.mjs","names":["Stripe","discountType: 'amount' | 'percentage' | null","results: PricingResult"],"sources":["../../../src/services/subscription.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { GenericError } from '@utils/errors';\nimport { retrievePlanInformation } from '@utils/plan';\nimport Stripe from 'stripe';\nimport type { Organization } from '@/types/organization.types';\nimport type { Plan } from '@/types/plan.types';\nimport { sendEmail } from './email.service';\nimport { getOrganizationById, updatePlan } from './organization.service';\nimport { getUserById } from './user.service';\n\nexport const addOrUpdateSubscription = async (\n subscriptionId: string,\n priceId: string,\n customerId: string,\n userId: string,\n organization: Organization,\n status: Plan['status']\n): Promise<Plan | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n });\n }\n\n if (String(user.customerId) !== customerId) {\n (user.customerId as unknown as string) = customerId;\n await user.save();\n }\n\n const planInfo = retrievePlanInformation(priceId);\n\n const subscriptions = await stripe.subscriptions.list({\n customer: customerId,\n status: 'active',\n limit: 1,\n });\n\n if (subscriptions.data.length >= 1) {\n // Active subscription exists; update it to the new plan\n const otherSubscriptionArray = subscriptions.data.filter(\n (subscription) => subscription.id !== subscriptionId\n );\n\n for (const subscription of otherSubscriptionArray) {\n await stripe.subscriptions.cancel(subscription.id);\n }\n }\n\n const updatedOrganization = await updatePlan(organization, {\n creatorId: user.id,\n priceId,\n customerId,\n subscriptionId,\n type: planInfo.type,\n period: planInfo.period,\n status,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Plan updated for organization ${organization.id} - ${planInfo.type} - ${planInfo.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const cancelSubscription = async (\n subscriptionId: string | Organization['id'],\n organizationId: Organization['id'] | string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n subscriptionId,\n });\n }\n\n if (!subscriptionId) {\n throw new GenericError('NO_SUBSCRIPTION_ID_PROVIDED');\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status: 'canceled',\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Cancelled plan for organization ${updatedOrganization.id} - ${updatedOrganization.plan?.type} - ${updatedOrganization.plan?.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const changeSubscriptionStatus = async (\n subscriptionId: string,\n status: Plan['status'],\n userId: string,\n organizationId: string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n userId,\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status,\n subscriptionId,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n logger.info(\n `Updated plan status for organization ${organization.id} - Status: ${status}`\n );\n\n const emailData = {\n to: user.email,\n username: user.name,\n email: user.email,\n planName: organization.plan.type,\n date: new Date().toLocaleDateString(),\n link: `${process.env.CLIENT_URL}/dashboard`,\n };\n\n switch (status) {\n case 'active':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentSuccess',\n organizationName: organization.name,\n subscriptionStartDate: emailData.date,\n manageSubscriptionLink: emailData.link,\n });\n break;\n case 'canceled':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentCancellation',\n organizationName: organization.name,\n cancellationDate: emailData.date,\n reactivateLink: emailData.link,\n });\n break;\n case 'incomplete':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentError',\n organizationName: organization.name,\n errorDate: emailData.date,\n retryPaymentLink: emailData.link,\n });\n break;\n default:\n logger.warn(`Unhandled subscription status: ${status}`);\n }\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const getCouponId = async (\n promoCode: string\n): Promise<string | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // Retrieve the coupon details by name\n const coupons = await stripe.coupons.list();\n const matchingCoupon = coupons.data.find(\n (coupon) => coupon.name === promoCode\n );\n\n return matchingCoupon ? matchingCoupon.id : null;\n } catch (error) {\n console.error('Error retrieving coupon:', error);\n return null;\n }\n};\n\nexport type PricingResult = Record<\n string,\n {\n originalTotal: number;\n discountApplied: number;\n discountType: 'amount' | 'percentage' | null;\n finalTotal: number;\n currency: string;\n }\n>;\n\nexport const getPricing = async (\n priceIds: string[],\n promoCode?: string\n): Promise<PricingResult> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // 1. Fetch all price objects\n const pricePromises = priceIds.map((priceId) =>\n stripe.prices.retrieve(priceId)\n );\n const prices = await Promise.all(pricePromises);\n\n // Calculate the total amount before discount (to help with proportional distribution if needed)\n const totalAmount = prices.reduce(\n (sum, price) => sum + (price.unit_amount ?? 0),\n 0\n );\n\n // 2. Retrieve the discount (if promo code is provided)\n let discountAmount = 0;\n let discountType: 'amount' | 'percentage' | null = null;\n\n if (promoCode) {\n const coupons = await stripe.coupons.list();\n const matchingCoupons = coupons.data.find(\n (coupon) => coupon.name === promoCode\n );\n if (matchingCoupons) {\n if (matchingCoupons.amount_off) {\n discountAmount = matchingCoupons.amount_off;\n discountType = 'amount';\n } else if (matchingCoupons.percent_off) {\n // For a percentage discount, we won't store discountAmount as a raw number\n // because each price line is discounted individually by the same percentage.\n discountAmount = matchingCoupons.percent_off;\n discountType = 'percentage';\n }\n }\n }\n\n // 3. Build the result for each priceId\n const results: PricingResult = {};\n\n for (const price of prices) {\n if (!price.id || !price.unit_amount) {\n continue; // Skip any invalid price\n }\n\n const originalTotal = price.unit_amount;\n let appliedDiscount = 0;\n let finalTotal = originalTotal;\n\n // Apply discount based on the discount type\n if (discountType === 'percentage' && discountAmount > 0) {\n // percentage-based discount\n appliedDiscount = (originalTotal * discountAmount) / 100;\n finalTotal = originalTotal - appliedDiscount;\n } else if (\n discountType === 'amount' &&\n totalAmount > 0 &&\n discountAmount > 0\n ) {\n // fixed amount discount - distribute proportionally\n const proportion = originalTotal / totalAmount;\n appliedDiscount = discountAmount * proportion;\n finalTotal = originalTotal - appliedDiscount;\n }\n\n // Prevent final total from going negative due to rounding\n finalTotal = Math.max(finalTotal, 0);\n\n results[price.id] = {\n originalTotal: originalTotal,\n discountApplied: appliedDiscount,\n discountType,\n finalTotal: finalTotal,\n currency: price.currency,\n };\n }\n\n return results;\n } catch (error) {\n console.error('Error calculating pricing per priceId:', error);\n throw new Error('Failed to calculate pricing breakdown.');\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"subscription.service.mjs","names":["Stripe","discountType: 'amount' | 'percentage' | null","results: PricingResult"],"sources":["../../../src/services/subscription.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { GenericError } from '@utils/errors';\nimport { retrievePlanInformation } from '@utils/plan';\nimport Stripe from 'stripe';\nimport type { Organization } from '@/types/organization.types';\nimport type { Plan } from '@/types/plan.types';\nimport { sendEmail } from './email.service';\nimport { getOrganizationById, updatePlan } from './organization.service';\nimport { getUserById } from './user.service';\n\nexport const addOrUpdateSubscription = async (\n subscriptionId: string,\n priceId: string,\n customerId: string,\n userId: string,\n organization: Organization,\n status: Plan['status']\n): Promise<Plan | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n });\n }\n\n if (String(user.customerId) !== customerId) {\n (user.customerId as unknown as string) = customerId;\n await user.save();\n }\n\n const planInfo = retrievePlanInformation(priceId);\n\n const subscriptions = await stripe.subscriptions.list({\n customer: customerId,\n status: 'active',\n limit: 1,\n });\n\n if (subscriptions.data.length >= 1) {\n // Active subscription exists; update it to the new plan\n const otherSubscriptionArray = subscriptions.data.filter(\n (subscription) => subscription.id !== subscriptionId\n );\n\n for (const subscription of otherSubscriptionArray) {\n await stripe.subscriptions.cancel(subscription.id);\n }\n }\n\n const updatedOrganization = await updatePlan(organization, {\n creatorId: user.id,\n priceId,\n customerId,\n subscriptionId,\n type: planInfo.type,\n period: planInfo.period,\n status,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Plan updated for organization ${organization.id} - ${planInfo.type} - ${planInfo.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const cancelSubscription = async (\n subscriptionId: string | Organization['id'],\n organizationId: Organization['id'] | string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n subscriptionId,\n });\n }\n\n if (!subscriptionId) {\n throw new GenericError('NO_SUBSCRIPTION_ID_PROVIDED');\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status: 'canceled',\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Cancelled plan for organization ${updatedOrganization.id} - ${updatedOrganization.plan?.type} - ${updatedOrganization.plan?.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const changeSubscriptionStatus = async (\n subscriptionId: string,\n status: Plan['status'],\n userId: string,\n organizationId: string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n userId,\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status,\n subscriptionId,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n logger.info(\n `Updated plan status for organization ${organization.id} - Status: ${status}`\n );\n\n const emailData = {\n to: user.email,\n username: user.name,\n email: user.email,\n planName: organization.plan.type,\n date: new Date().toLocaleDateString(),\n link: `${process.env.APP_URL}/dashboard`,\n };\n\n switch (status) {\n case 'active':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentSuccess',\n organizationName: organization.name,\n subscriptionStartDate: emailData.date,\n manageSubscriptionLink: emailData.link,\n });\n break;\n case 'canceled':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentCancellation',\n organizationName: organization.name,\n cancellationDate: emailData.date,\n reactivateLink: emailData.link,\n });\n break;\n case 'incomplete':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentError',\n organizationName: organization.name,\n errorDate: emailData.date,\n retryPaymentLink: emailData.link,\n });\n break;\n default:\n logger.warn(`Unhandled subscription status: ${status}`);\n }\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const getCouponId = async (\n promoCode: string\n): Promise<string | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // Retrieve the coupon details by name\n const coupons = await stripe.coupons.list();\n const matchingCoupon = coupons.data.find(\n (coupon) => coupon.name === promoCode\n );\n\n return matchingCoupon ? matchingCoupon.id : null;\n } catch (error) {\n console.error('Error retrieving coupon:', error);\n return null;\n }\n};\n\nexport type PricingResult = Record<\n string,\n {\n originalTotal: number;\n discountApplied: number;\n discountType: 'amount' | 'percentage' | null;\n finalTotal: number;\n currency: string;\n }\n>;\n\nexport const getPricing = async (\n priceIds: string[],\n promoCode?: string\n): Promise<PricingResult> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // 1. Fetch all price objects\n const pricePromises = priceIds.map((priceId) =>\n stripe.prices.retrieve(priceId)\n );\n const prices = await Promise.all(pricePromises);\n\n // Calculate the total amount before discount (to help with proportional distribution if needed)\n const totalAmount = prices.reduce(\n (sum, price) => sum + (price.unit_amount ?? 0),\n 0\n );\n\n // 2. Retrieve the discount (if promo code is provided)\n let discountAmount = 0;\n let discountType: 'amount' | 'percentage' | null = null;\n\n if (promoCode) {\n const coupons = await stripe.coupons.list();\n const matchingCoupons = coupons.data.find(\n (coupon) => coupon.name === promoCode\n );\n if (matchingCoupons) {\n if (matchingCoupons.amount_off) {\n discountAmount = matchingCoupons.amount_off;\n discountType = 'amount';\n } else if (matchingCoupons.percent_off) {\n // For a percentage discount, we won't store discountAmount as a raw number\n // because each price line is discounted individually by the same percentage.\n discountAmount = matchingCoupons.percent_off;\n discountType = 'percentage';\n }\n }\n }\n\n // 3. Build the result for each priceId\n const results: PricingResult = {};\n\n for (const price of prices) {\n if (!price.id || !price.unit_amount) {\n continue; // Skip any invalid price\n }\n\n const originalTotal = price.unit_amount;\n let appliedDiscount = 0;\n let finalTotal = originalTotal;\n\n // Apply discount based on the discount type\n if (discountType === 'percentage' && discountAmount > 0) {\n // percentage-based discount\n appliedDiscount = (originalTotal * discountAmount) / 100;\n finalTotal = originalTotal - appliedDiscount;\n } else if (\n discountType === 'amount' &&\n totalAmount > 0 &&\n discountAmount > 0\n ) {\n // fixed amount discount - distribute proportionally\n const proportion = originalTotal / totalAmount;\n appliedDiscount = discountAmount * proportion;\n finalTotal = originalTotal - appliedDiscount;\n }\n\n // Prevent final total from going negative due to rounding\n finalTotal = Math.max(finalTotal, 0);\n\n results[price.id] = {\n originalTotal: originalTotal,\n discountApplied: appliedDiscount,\n discountType,\n finalTotal: finalTotal,\n currency: price.currency,\n };\n }\n\n return results;\n } catch (error) {\n console.error('Error calculating pricing per priceId:', error);\n throw new Error('Failed to calculate pricing breakdown.');\n }\n};\n"],"mappings":"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"}
|