mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge remote-tracking branch 'origin/main' into feat/camunda-app-connection-and-secret-sync
This commit is contained in:
@@ -594,6 +594,7 @@ export const scimServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await orgMembershipDAL.updateById(
|
||||
membership.id,
|
||||
{
|
||||
|
||||
@@ -262,13 +262,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
id: el.id,
|
||||
version: el.version,
|
||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||
isRotatedSecret: el.secret.isRotatedSecret,
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
secretValue: el.secret.isRotatedSecret
|
||||
? undefined
|
||||
: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: "",
|
||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||
secretValue:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
el.secret && el.secret.isRotatedSecret
|
||||
? undefined
|
||||
: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: "",
|
||||
@@ -615,7 +616,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => {
|
||||
const encryptedValue =
|
||||
!el.secret.isRotatedSecret && typeof el.encryptedValue !== "undefined"
|
||||
!el.secret?.isRotatedSecret && typeof el.encryptedValue !== "undefined"
|
||||
? {
|
||||
encryptedValue: el.encryptedValue as Buffer,
|
||||
references: el.encryptedValue
|
||||
|
||||
@@ -66,6 +66,17 @@ export const IDENTITIES = {
|
||||
},
|
||||
LIST: {
|
||||
orgId: "The ID of the organization to list identities."
|
||||
},
|
||||
SEARCH: {
|
||||
search: {
|
||||
desc: "The filters to apply to the search.",
|
||||
name: "The name of the identity to filter by.",
|
||||
role: "The organizational role of the identity to filter by."
|
||||
},
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
|
||||
limit: "The number of identities to return.",
|
||||
orderBy: "The column to order identities by.",
|
||||
orderDirection: "The direction to order identities in."
|
||||
}
|
||||
} as const;
|
||||
|
||||
@@ -1694,6 +1705,9 @@ export const AppConnections = {
|
||||
sslEnabled: "Whether or not to use SSL when connecting to the database.",
|
||||
sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.",
|
||||
sslCertificate: "The SSL certificate to use for connection."
|
||||
},
|
||||
VERCEL: {
|
||||
apiToken: "The API token used to authenticate with Vercel."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1813,6 +1827,13 @@ export const SecretSyncs = {
|
||||
org: "The ID of the Humanitec org to sync secrets to.",
|
||||
env: "The ID of the Humanitec environment to sync secrets to.",
|
||||
scope: "The Humanitec scope that secrets should be synced to."
|
||||
},
|
||||
VERCEL: {
|
||||
app: "The ID of the Vercel app to sync secrets to.",
|
||||
appName: "The name of the Vercel app to sync secrets to.",
|
||||
env: "The ID of the Vercel environment to sync secrets to.",
|
||||
branch: "The branch to sync preview secrets to.",
|
||||
teamId: "The ID of the Vercel team to sync secrets to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
141
backend/src/lib/search-resource/db.ts
Normal file
141
backend/src/lib/search-resource/db.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SearchResourceOperators, TSearchResourceOperator } from "./search";
|
||||
|
||||
const buildKnexQuery = (
|
||||
query: Knex.QueryBuilder,
|
||||
// when it's multiple table field means it's field1 or field2
|
||||
fields: string | string[],
|
||||
operator: SearchResourceOperators,
|
||||
value: unknown
|
||||
) => {
|
||||
switch (operator) {
|
||||
case SearchResourceOperators.$eq: {
|
||||
if (typeof value !== "string" && typeof value !== "number")
|
||||
throw new Error("Invalid value type for $eq operator");
|
||||
|
||||
if (typeof fields === "string") {
|
||||
return void query.where(fields, "=", value);
|
||||
}
|
||||
|
||||
return void query.where((qb) => {
|
||||
return fields.forEach((el, index) => {
|
||||
if (index === 0) {
|
||||
return void qb.where(el, "=", value);
|
||||
}
|
||||
return void qb.orWhere(el, "=", value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
case SearchResourceOperators.$neq: {
|
||||
if (typeof value !== "string" && typeof value !== "number")
|
||||
throw new Error("Invalid value type for $neq operator");
|
||||
|
||||
if (typeof fields === "string") {
|
||||
return void query.where(fields, "<>", value);
|
||||
}
|
||||
|
||||
return void query.where((qb) => {
|
||||
return fields.forEach((el, index) => {
|
||||
if (index === 0) {
|
||||
return void qb.where(el, "<>", value);
|
||||
}
|
||||
return void qb.orWhere(el, "<>", value);
|
||||
});
|
||||
});
|
||||
}
|
||||
case SearchResourceOperators.$in: {
|
||||
if (!Array.isArray(value)) throw new Error("Invalid value type for $in operator");
|
||||
|
||||
if (typeof fields === "string") {
|
||||
return void query.whereIn(fields, value);
|
||||
}
|
||||
|
||||
return void query.where((qb) => {
|
||||
return fields.forEach((el, index) => {
|
||||
if (index === 0) {
|
||||
return void qb.whereIn(el, value);
|
||||
}
|
||||
return void qb.orWhereIn(el, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
case SearchResourceOperators.$contains: {
|
||||
if (typeof value !== "string") throw new Error("Invalid value type for $contains operator");
|
||||
|
||||
if (typeof fields === "string") {
|
||||
return void query.whereILike(fields, `%${value}%`);
|
||||
}
|
||||
|
||||
return void query.where((qb) => {
|
||||
return fields.forEach((el, index) => {
|
||||
if (index === 0) {
|
||||
return void qb.whereILike(el, `%${value}%`);
|
||||
}
|
||||
return void qb.orWhereILike(el, `%${value}%`);
|
||||
});
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported operator: ${String(operator)}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const buildKnexFilterForSearchResource = <T extends { [K: string]: TSearchResourceOperator }, K extends keyof T>(
|
||||
rootQuery: Knex.QueryBuilder,
|
||||
searchFilter: T & { $or?: T[] },
|
||||
getAttributeField: (attr: K) => string | string[] | null
|
||||
) => {
|
||||
const { $or: orFilters = [] } = searchFilter;
|
||||
(Object.keys(searchFilter) as K[]).forEach((key) => {
|
||||
// akhilmhdh: yes, we could have split in top. This is done to satisfy ts type error
|
||||
if (key === "$or") return;
|
||||
|
||||
const dbField = getAttributeField(key);
|
||||
if (!dbField) throw new Error(`DB field not found for ${String(key)}`);
|
||||
|
||||
const dbValue = searchFilter[key];
|
||||
if (typeof dbValue === "string" || typeof dbValue === "number") {
|
||||
buildKnexQuery(rootQuery, dbField, SearchResourceOperators.$eq, dbValue);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(dbValue as Record<string, unknown>).forEach((el) => {
|
||||
buildKnexQuery(
|
||||
rootQuery,
|
||||
dbField,
|
||||
el as SearchResourceOperators,
|
||||
(dbValue as Record<SearchResourceOperators, unknown>)[el as SearchResourceOperators]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (orFilters.length) {
|
||||
void rootQuery.andWhere((andQb) => {
|
||||
return orFilters.forEach((orFilter) => {
|
||||
return void andQb.orWhere((qb) => {
|
||||
(Object.keys(orFilter) as K[]).forEach((key) => {
|
||||
const dbField = getAttributeField(key);
|
||||
if (!dbField) throw new Error(`DB field not found for ${String(key)}`);
|
||||
|
||||
const dbValue = orFilter[key];
|
||||
if (typeof dbValue === "string" || typeof dbValue === "number") {
|
||||
buildKnexQuery(qb, dbField, SearchResourceOperators.$eq, dbValue);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(dbValue as Record<string, unknown>).forEach((el) => {
|
||||
buildKnexQuery(
|
||||
qb,
|
||||
dbField,
|
||||
el as SearchResourceOperators,
|
||||
(dbValue as Record<SearchResourceOperators, unknown>)[el as SearchResourceOperators]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
43
backend/src/lib/search-resource/search.ts
Normal file
43
backend/src/lib/search-resource/search.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum SearchResourceOperators {
|
||||
$eq = "$eq",
|
||||
$neq = "$neq",
|
||||
$in = "$in",
|
||||
$contains = "$contains"
|
||||
}
|
||||
|
||||
export const SearchResourceOperatorSchema = z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z
|
||||
.object({
|
||||
[SearchResourceOperators.$eq]: z.string().optional(),
|
||||
[SearchResourceOperators.$neq]: z.string().optional(),
|
||||
[SearchResourceOperators.$in]: z.string().array().optional(),
|
||||
[SearchResourceOperators.$contains]: z.string().array().optional()
|
||||
})
|
||||
.partial()
|
||||
]);
|
||||
|
||||
export type TSearchResourceOperator = z.infer<typeof SearchResourceOperatorSchema>;
|
||||
|
||||
export type TSearchResource = {
|
||||
[k: string]: z.ZodOptional<
|
||||
z.ZodUnion<
|
||||
[
|
||||
z.ZodEffects<z.ZodString | z.ZodNumber>,
|
||||
z.ZodObject<{
|
||||
[SearchResourceOperators.$eq]?: z.ZodOptional<z.ZodEffects<z.ZodString | z.ZodNumber>>;
|
||||
[SearchResourceOperators.$neq]?: z.ZodOptional<z.ZodEffects<z.ZodString | z.ZodNumber>>;
|
||||
[SearchResourceOperators.$in]?: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString | z.ZodNumber>>>;
|
||||
[SearchResourceOperators.$contains]?: z.ZodOptional<z.ZodEffects<z.ZodString>>;
|
||||
}>
|
||||
]
|
||||
>
|
||||
>;
|
||||
};
|
||||
|
||||
export const buildSearchZodSchema = <T extends TSearchResource>(schema: z.ZodObject<T>) => {
|
||||
return schema.extend({ $or: schema.array().max(5).optional() }).optional();
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum CharacterType {
|
||||
Alphabets = "alphabets",
|
||||
Numbers = "numbers",
|
||||
@@ -101,3 +103,10 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => {
|
||||
return regex.test(input);
|
||||
};
|
||||
};
|
||||
|
||||
export const zodValidateCharacters = (allowedCharacters: CharacterType[]) => {
|
||||
const validator = characterValidator(allowedCharacters);
|
||||
return (schema: z.ZodString, fieldName: string) => {
|
||||
return schema.refine(validator, { message: `${fieldName} can only contain ${allowedCharacters.join(",")}` });
|
||||
};
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
|
||||
await server.register(fastifyErrHandler);
|
||||
|
||||
// Rate limiters and security headers
|
||||
if (appCfg.isProductionMode) {
|
||||
if (appCfg.isProductionMode && appCfg.isCloud) {
|
||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
||||
}
|
||||
|
||||
|
||||
@@ -45,4 +45,6 @@ export const BaseSecretNameSchema = z.string().trim().min(1);
|
||||
export const SecretNameSchema = BaseSecretNameSchema.refine(
|
||||
(el) => !el.includes(" "),
|
||||
"Secret name cannot contain spaces."
|
||||
).refine((el) => !el.includes(":"), "Secret name cannot contain colon.");
|
||||
)
|
||||
.refine((el) => !el.includes(":"), "Secret name cannot contain colon.")
|
||||
.refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
PostgresConnectionListItemSchema,
|
||||
SanitizedPostgresConnectionSchema
|
||||
} from "@app/services/app-connection/postgres";
|
||||
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// can't use discriminated due to multiple schemas for certain apps
|
||||
@@ -42,6 +43,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||
...SanitizedDatabricksConnectionSchema.options,
|
||||
...SanitizedHumanitecConnectionSchema.options,
|
||||
...SanitizedVercelConnectionSchema.options,
|
||||
...SanitizedPostgresConnectionSchema.options,
|
||||
...SanitizedMsSqlConnectionSchema.options,
|
||||
...SanitizedCamundaConnectionSchema.options
|
||||
@@ -55,6 +57,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
DatabricksConnectionListItemSchema,
|
||||
HumanitecConnectionListItemSchema,
|
||||
VercelConnectionListItemSchema,
|
||||
PostgresConnectionListItemSchema,
|
||||
MsSqlConnectionListItemSchema,
|
||||
CamundaConnectionListItemSchema
|
||||
|
||||
@@ -10,6 +10,7 @@ import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||
|
||||
export * from "./app-connection-router";
|
||||
|
||||
@@ -22,6 +23,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||
[AppConnection.Vercel]: registerVercelConnectionRouter,
|
||||
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
||||
[AppConnection.Camunda]: registerCamundaConnectionRouter
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateVercelConnectionSchema,
|
||||
SanitizedVercelConnectionSchema,
|
||||
UpdateVercelConnectionSchema,
|
||||
VercelOrgWithApps
|
||||
} from "@app/services/app-connection/vercel";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerVercelConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Vercel,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedVercelConnectionSchema,
|
||||
createSchema: CreateVercelConnectionSchema,
|
||||
updateSchema: UpdateVercelConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/projects`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
apps: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
envs: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
type: z.string(),
|
||||
target: z.array(z.string()).optional(),
|
||||
description: z.string().optional(),
|
||||
createdAt: z.number().optional(),
|
||||
updatedAt: z.number().optional()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
previewBranches: z.array(z.string()).optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const projects: VercelOrgWithApps[] = await server.services.appConnection.vercel.listProjects(
|
||||
connectionId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
return projects;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -3,15 +3,26 @@ import { z } from "zod";
|
||||
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { IDENTITIES } from "@app/lib/api-docs";
|
||||
import { buildSearchZodSchema, SearchResourceOperators } from "@app/lib/search-resource/search";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
|
||||
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
|
||||
const searchResourceZodValidate = zodValidateCharacters([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Spaces,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
]);
|
||||
|
||||
export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@@ -245,7 +256,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
@@ -289,6 +300,103 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/search",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Search identities",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
orderBy: z
|
||||
.nativeEnum(OrgIdentityOrderBy)
|
||||
.default(OrgIdentityOrderBy.Name)
|
||||
.describe(IDENTITIES.SEARCH.orderBy)
|
||||
.optional(),
|
||||
orderDirection: z
|
||||
.nativeEnum(OrderByDirection)
|
||||
.default(OrderByDirection.ASC)
|
||||
.describe(IDENTITIES.SEARCH.orderDirection)
|
||||
.optional(),
|
||||
limit: z.number().max(100).default(50).describe(IDENTITIES.SEARCH.limit),
|
||||
offset: z.number().default(0).describe(IDENTITIES.SEARCH.offset),
|
||||
search: buildSearchZodSchema(
|
||||
z
|
||||
.object({
|
||||
name: z
|
||||
.union([
|
||||
searchResourceZodValidate(z.string().max(255), "Name"),
|
||||
z
|
||||
.object({
|
||||
[SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Name $eq"),
|
||||
[SearchResourceOperators.$contains]: searchResourceZodValidate(
|
||||
z.string().max(255),
|
||||
"Name $contains"
|
||||
),
|
||||
[SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Name $in").array()
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
.describe(IDENTITIES.SEARCH.search.name),
|
||||
role: z
|
||||
.union([
|
||||
searchResourceZodValidate(z.string().max(255), "Role"),
|
||||
z
|
||||
.object({
|
||||
[SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Role $eq"),
|
||||
[SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Role $in").array()
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
.describe(IDENTITIES.SEARCH.search.role)
|
||||
})
|
||||
.describe(IDENTITIES.SEARCH.search.desc)
|
||||
.partial()
|
||||
)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identities: IdentityOrgMembershipsSchema.extend({
|
||||
customRole: OrgRolesSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
}).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityMemberships, totalCount } = await server.services.identity.searchOrgIdentities({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
searchFilter: req.body.search,
|
||||
orgId: req.permission.orgId,
|
||||
limit: req.body.limit,
|
||||
offset: req.body.offset,
|
||||
orderBy: req.body.orderBy,
|
||||
orderDirection: req.body.orderDirection
|
||||
});
|
||||
|
||||
return { identities: identityMemberships, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:identityId/identity-memberships",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
||||
|
||||
export * from "./secret-sync-router";
|
||||
|
||||
@@ -21,5 +22,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
||||
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
||||
[SecretSync.Camunda]: registerCamundaSyncRouter
|
||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||
[SecretSync.Vercel]: registerVercelSyncRouter
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
|
||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncSchema,
|
||||
@@ -33,7 +34,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AzureAppConfigurationSyncSchema,
|
||||
DatabricksSyncSchema,
|
||||
HumanitecSyncSchema,
|
||||
CamundaSyncSchema
|
||||
CamundaSyncSchema,
|
||||
VercelSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -45,7 +47,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
AzureAppConfigurationSyncListItemSchema,
|
||||
DatabricksSyncListItemSchema,
|
||||
HumanitecSyncListItemSchema,
|
||||
CamundaSyncListItemSchema
|
||||
CamundaSyncListItemSchema,
|
||||
VercelSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { CreateVercelSyncSchema, UpdateVercelSyncSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerVercelSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Vercel,
|
||||
server,
|
||||
responseSchema: VercelSyncSchema,
|
||||
createSchema: CreateVercelSyncSchema,
|
||||
updateSchema: UpdateVercelSyncSchema
|
||||
});
|
||||
@@ -351,4 +351,56 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
return { identityMembership };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/identity-memberships/:identityMembershipId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
identityMembershipId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityMembership: z.object({
|
||||
id: z.string(),
|
||||
identityId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
roles: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
role: z.string(),
|
||||
customRoleId: z.string().optional().nullable(),
|
||||
customRoleName: z.string().optional().nullable(),
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryMode: z.string().optional().nullable(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||
temporaryAccessEndTime: z.date().nullable().optional()
|
||||
})
|
||||
),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
}),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityMembership = await server.services.identityProject.getProjectIdentityByMembershipId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityMembershipId: req.params.identityMembershipId
|
||||
});
|
||||
return { identityMembership };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export enum AppConnection {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
Humanitec = "humanitec",
|
||||
Vercel = "vercel",
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql",
|
||||
Camunda = "camunda"
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
} from "./humanitec";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import { VercelConnectionMethod } from "./vercel";
|
||||
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [
|
||||
@@ -52,6 +54,7 @@ export const listAppConnectionOptions = () => {
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getDatabricksConnectionListItem(),
|
||||
getHumanitecConnectionListItem(),
|
||||
getVercelConnectionListItem(),
|
||||
getPostgresConnectionListItem(),
|
||||
getMsSqlConnectionListItem(),
|
||||
getCamundaConnectionListItem()
|
||||
@@ -111,7 +114,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnect
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
export const validateAppConnectionCredentials = async (
|
||||
@@ -137,6 +141,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case CamundaConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
case HumanitecConnectionMethod.ApiToken:
|
||||
case VercelConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
@@ -181,5 +186,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.Databricks]: "Databricks",
|
||||
[AppConnection.Humanitec]: "Humanitec",
|
||||
[AppConnection.Vercel]: "Vercel",
|
||||
[AppConnection.Postgres]: "PostgreSQL",
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda"
|
||||
|
||||
@@ -43,6 +43,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||
import { vercelConnectionService } from "./vercel/vercel-connection-service";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
@@ -60,6 +62,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
|
||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema
|
||||
@@ -434,6 +437,7 @@ export const appConnectionServiceFactory = ({
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
aws: awsConnectionService(connectAppConnectionById),
|
||||
humanitec: humanitecConnectionService(connectAppConnectionById),
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -57,6 +57,12 @@ import {
|
||||
TPostgresConnectionInput,
|
||||
TValidatePostgresConnectionCredentialsSchema
|
||||
} from "./postgres";
|
||||
import {
|
||||
TValidateVercelConnectionCredentialsSchema,
|
||||
TVercelConnection,
|
||||
TVercelConnectionConfig,
|
||||
TVercelConnectionInput
|
||||
} from "./vercel";
|
||||
|
||||
export type TAppConnection = { id: string } & (
|
||||
| TAwsConnection
|
||||
@@ -66,6 +72,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TAzureAppConfigurationConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TVercelConnection
|
||||
| TPostgresConnection
|
||||
| TMsSqlConnection
|
||||
| TCamundaConnection
|
||||
@@ -83,6 +90,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TAzureAppConfigurationConnectionInput
|
||||
| TDatabricksConnectionInput
|
||||
| THumanitecConnectionInput
|
||||
| TVercelConnectionInput
|
||||
| TPostgresConnectionInput
|
||||
| TMsSqlConnectionInput
|
||||
| TCamundaConnectionInput
|
||||
@@ -108,7 +116,8 @@ export type TAppConnectionConfig =
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
| TSqlConnectionConfig
|
||||
| TCamundaConnectionConfig;
|
||||
| TCamundaConnectionConfig
|
||||
| TVercelConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -120,7 +129,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateHumanitecConnectionCredentialsSchema
|
||||
| TValidatePostgresConnectionCredentialsSchema
|
||||
| TValidateMsSqlConnectionCredentialsSchema
|
||||
| TValidateCamundaConnectionCredentialsSchema;
|
||||
| TValidateCamundaConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
4
backend/src/services/app-connection/vercel/index.ts
Normal file
4
backend/src/services/app-connection/vercel/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./vercel-connection-enums";
|
||||
export * from "./vercel-connection-fns";
|
||||
export * from "./vercel-connection-schemas";
|
||||
export * from "./vercel-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum VercelConnectionMethod {
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TVercelBranches } from "@app/services/integration-auth/integration-auth-types";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { VercelConnectionMethod } from "./vercel-connection-enums";
|
||||
import {
|
||||
TVercelConnection,
|
||||
TVercelConnectionConfig,
|
||||
VercelApp,
|
||||
VercelEnvironment,
|
||||
VercelOrgWithApps
|
||||
} from "./vercel-connection-types";
|
||||
|
||||
export const getVercelConnectionListItem = () => {
|
||||
return {
|
||||
name: "Vercel" as const,
|
||||
app: AppConnection.Vercel as const,
|
||||
methods: Object.values(VercelConnectionMethod) as [VercelConnectionMethod.ApiToken]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => {
|
||||
const { credentials: inputCredentials } = config;
|
||||
|
||||
let response: AxiosResponse<VercelApp[]> | null = null;
|
||||
|
||||
try {
|
||||
response = await request.get<VercelApp[]>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${inputCredentials.apiToken}`
|
||||
}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection - verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
if (!response?.data) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get organizations: Response was empty"
|
||||
});
|
||||
}
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
interface ApiResponse<T> {
|
||||
pagination?: {
|
||||
count: number;
|
||||
next: number;
|
||||
};
|
||||
data: T[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
async function fetchAllPages<T>(
|
||||
apiUrl: string,
|
||||
apiToken: string,
|
||||
initialParams: Record<string, string | number> = {},
|
||||
dataPath?: string
|
||||
): Promise<T[]> {
|
||||
const allItems: T[] = [];
|
||||
let hasMoreItems = true;
|
||||
let params: Record<string, string | number> = { ...initialParams, limit: 100 };
|
||||
|
||||
while (hasMoreItems) {
|
||||
try {
|
||||
const response = await request.get<ApiResponse<T>>(apiUrl, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response?.data) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to fetch data from ${apiUrl}: Response was empty or malformed`
|
||||
});
|
||||
}
|
||||
|
||||
let itemsData: T[];
|
||||
|
||||
if (dataPath && dataPath in response.data) {
|
||||
itemsData = response.data[dataPath] as T[];
|
||||
} else {
|
||||
itemsData = response.data.data;
|
||||
}
|
||||
|
||||
if (!Array.isArray(itemsData)) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to fetch data from ${apiUrl}: Expected array but got ${typeof itemsData}`
|
||||
});
|
||||
}
|
||||
|
||||
allItems.push(...itemsData);
|
||||
|
||||
if (response.data.pagination?.next) {
|
||||
params = { ...params, since: response.data.pagination.next };
|
||||
} else {
|
||||
hasMoreItems = false;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to fetch data from ${apiUrl}: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return allItems;
|
||||
}
|
||||
|
||||
async function fetchOrgProjects(orgId: string, apiToken: string): Promise<VercelApp[]> {
|
||||
return fetchAllPages<VercelApp>(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects`,
|
||||
apiToken,
|
||||
{ teamId: orgId },
|
||||
"projects"
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchProjectEnvironments(
|
||||
projectId: string,
|
||||
teamId: string,
|
||||
apiToken: string
|
||||
): Promise<VercelEnvironment[]> {
|
||||
try {
|
||||
return await fetchAllPages<VercelEnvironment>(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${projectId}/custom-environments?teamId=${teamId}`,
|
||||
apiToken,
|
||||
{},
|
||||
"environments"
|
||||
);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPreviewBranches(projectId: string, apiToken: string): Promise<string[]> {
|
||||
try {
|
||||
const { data } = await request.get<TVercelBranches[]>(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v1/integrations/git-branches`,
|
||||
{
|
||||
params: {
|
||||
projectId
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.filter((b) => b.ref !== "main").map((b) => b.ref);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
type VercelTeam = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
type VercelUserResponse = {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const listProjects = async (appConnection: TVercelConnection): Promise<VercelOrgWithApps[]> => {
|
||||
const { credentials } = appConnection;
|
||||
const { apiToken } = credentials;
|
||||
|
||||
const orgs = await fetchAllPages<VercelTeam>(`${IntegrationUrls.VERCEL_API_URL}/v2/teams`, apiToken, {}, "teams");
|
||||
|
||||
const personalAccountResponse = await request.get<VercelUserResponse>(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (personalAccountResponse?.data?.user) {
|
||||
const { user } = personalAccountResponse.data;
|
||||
orgs.push({
|
||||
id: user.id,
|
||||
name: user.name || "Personal Account",
|
||||
slug: user.username || "personal"
|
||||
});
|
||||
}
|
||||
|
||||
const orgsWithApps: VercelOrgWithApps[] = [];
|
||||
|
||||
const orgPromises = orgs.map(async (org) => {
|
||||
try {
|
||||
const projects = await fetchOrgProjects(org.id, apiToken);
|
||||
|
||||
const enhancedProjectsPromises = projects.map(async (project) => {
|
||||
try {
|
||||
const [environments, previewBranches] = await Promise.all([
|
||||
fetchProjectEnvironments(project.name, org.id, apiToken),
|
||||
fetchPreviewBranches(project.id, apiToken)
|
||||
]);
|
||||
|
||||
return {
|
||||
name: project.name,
|
||||
id: project.id,
|
||||
envs: environments,
|
||||
previewBranches
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: project.name,
|
||||
id: project.id,
|
||||
envs: [],
|
||||
previewBranches: []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const enhancedProjects = await Promise.all(enhancedProjectsPromises);
|
||||
|
||||
return {
|
||||
...org,
|
||||
apps: enhancedProjects
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(orgPromises);
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result !== null) {
|
||||
orgsWithApps.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
return orgsWithApps;
|
||||
};
|
||||
|
||||
export const getProjectEnvironmentVariables = (project: VercelApp): Record<string, string> => {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
if (!project.envs) return envVars;
|
||||
|
||||
project.envs.forEach((env) => {
|
||||
if (env.slug && env.type !== "gitBranch") {
|
||||
const { id, slug } = env;
|
||||
envVars[id] = slug;
|
||||
}
|
||||
});
|
||||
|
||||
return envVars;
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { VercelConnectionMethod } from "./vercel-connection-enums";
|
||||
|
||||
export const VercelConnectionAccessTokenCredentialsSchema = z.object({
|
||||
apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.VERCEL.apiToken)
|
||||
});
|
||||
|
||||
const BaseVercelConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.Vercel)
|
||||
});
|
||||
|
||||
export const VercelConnectionSchema = BaseVercelConnectionSchema.extend({
|
||||
method: z.literal(VercelConnectionMethod.ApiToken),
|
||||
credentials: VercelConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedVercelConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseVercelConnectionSchema.extend({
|
||||
method: z.literal(VercelConnectionMethod.ApiToken),
|
||||
credentials: VercelConnectionAccessTokenCredentialsSchema.pick({})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateVercelConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(VercelConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.Vercel).method),
|
||||
credentials: VercelConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Vercel).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateVercelConnectionSchema = ValidateVercelConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Vercel)
|
||||
);
|
||||
|
||||
export const UpdateVercelConnectionSchema = z
|
||||
.object({
|
||||
credentials: VercelConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Vercel).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Vercel));
|
||||
|
||||
export const VercelConnectionListItemSchema = z.object({
|
||||
name: z.literal("Vercel"),
|
||||
app: z.literal(AppConnection.Vercel),
|
||||
methods: z.nativeEnum(VercelConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listProjects as getVercelProjects } from "./vercel-connection-fns";
|
||||
import { TVercelConnection } from "./vercel-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TVercelConnection>;
|
||||
|
||||
export const vercelConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Vercel, connectionId, actor);
|
||||
try {
|
||||
const projects = await getVercelProjects(appConnection);
|
||||
return projects;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to establish connection with Vercel");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listProjects
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateVercelConnectionSchema,
|
||||
ValidateVercelConnectionCredentialsSchema,
|
||||
VercelConnectionSchema
|
||||
} from "./vercel-connection-schemas";
|
||||
|
||||
export type TVercelConnection = z.infer<typeof VercelConnectionSchema>;
|
||||
|
||||
export type TVercelConnectionInput = z.infer<typeof CreateVercelConnectionSchema> & {
|
||||
app: AppConnection.Vercel;
|
||||
};
|
||||
|
||||
export type TValidateVercelConnectionCredentialsSchema = typeof ValidateVercelConnectionCredentialsSchema;
|
||||
|
||||
export type TVercelConnectionConfig = DiscriminativePick<TVercelConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type VercelTeam = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type VercelEnvironment = {
|
||||
id: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
target?: string[];
|
||||
gitBranch?: string;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
};
|
||||
|
||||
export type VercelAppMeta = {
|
||||
githubCommitRef?: string;
|
||||
githubCommitSha?: string;
|
||||
githubCommitMessage?: string;
|
||||
githubCommitAuthorName?: string;
|
||||
};
|
||||
|
||||
export type VercelDeployment = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
created: number;
|
||||
meta?: VercelAppMeta;
|
||||
target?: "production" | "preview" | "development";
|
||||
};
|
||||
|
||||
export type VercelApp = {
|
||||
name: string;
|
||||
id: string;
|
||||
envs?: VercelEnvironment[];
|
||||
previewBranches?: string[];
|
||||
};
|
||||
|
||||
export type VercelOrgWithApps = VercelTeam & {
|
||||
apps: VercelApp[];
|
||||
};
|
||||
|
||||
export type VercelUserResponse = {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TCreateProjectIdentityDTO,
|
||||
TDeleteProjectIdentityDTO,
|
||||
TGetProjectIdentityByIdentityIdDTO,
|
||||
TGetProjectIdentityByMembershipIdDTO,
|
||||
TListProjectIdentityDTO,
|
||||
TUpdateProjectIdentityDTO
|
||||
} from "./identity-project-types";
|
||||
@@ -370,11 +371,48 @@ export const identityProjectServiceFactory = ({
|
||||
return identityMembership;
|
||||
};
|
||||
|
||||
const getProjectIdentityByMembershipId = async ({
|
||||
identityMembershipId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetProjectIdentityByMembershipIdDTO) => {
|
||||
const membership = await identityProjectDAL.findOne({ id: identityMembershipId });
|
||||
|
||||
if (!membership) {
|
||||
throw new NotFoundError({
|
||||
message: `Project membership with ID '${identityMembershipId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: membership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: membership.identityId })
|
||||
);
|
||||
|
||||
const [identityMembership] = await identityProjectDAL.findByProjectId(membership.projectId, {
|
||||
identityId: membership.identityId
|
||||
});
|
||||
|
||||
return identityMembership;
|
||||
};
|
||||
|
||||
return {
|
||||
createProjectIdentity,
|
||||
updateProjectIdentity,
|
||||
deleteProjectIdentity,
|
||||
listProjectIdentities,
|
||||
getProjectIdentityByIdentityId
|
||||
getProjectIdentityByIdentityId,
|
||||
getProjectIdentityByMembershipId
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,6 +52,10 @@ export type TGetProjectIdentityByIdentityIdDTO = {
|
||||
identityId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectIdentityByMembershipIdDTO = {
|
||||
identityMembershipId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export enum ProjectIdentityOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
@@ -14,10 +14,15 @@ import {
|
||||
TIdentityUniversalAuths,
|
||||
TOrgRoles
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
||||
import {
|
||||
OrgIdentityOrderBy,
|
||||
TListOrgIdentitiesByOrgIdDTO,
|
||||
TSearchOrgIdentitiesByOrgIdDAL
|
||||
} from "@app/services/identity/identity-types";
|
||||
|
||||
import { buildAuthMethods } from "./identity-fns";
|
||||
|
||||
@@ -195,7 +200,6 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("id").withSchema("paginatedIdentity"),
|
||||
db.ref("role").withSchema("paginatedIdentity"),
|
||||
@@ -309,6 +313,214 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const searchIdentities = async (
|
||||
{
|
||||
limit,
|
||||
offset = 0,
|
||||
orderBy = OrgIdentityOrderBy.Name,
|
||||
orderDirection = OrderByDirection.ASC,
|
||||
searchFilter,
|
||||
orgId
|
||||
}: TSearchOrgIdentitiesByOrgIdDAL,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const searchQuery = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`)
|
||||
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
|
||||
.select(`${TableName.IdentityOrgMembership}.id`)
|
||||
.select<{ id: string; total_count: string }>(
|
||||
db.raw(
|
||||
`count(${TableName.IdentityOrgMembership}."identityId") OVER(PARTITION BY ${TableName.IdentityOrgMembership}."orgId") as total_count`
|
||||
)
|
||||
)
|
||||
.as("searchedIdentities");
|
||||
|
||||
if (searchFilter) {
|
||||
buildKnexFilterForSearchResource(searchQuery, searchFilter, (attr) => {
|
||||
switch (attr) {
|
||||
case "role":
|
||||
return [`${TableName.OrgRoles}.slug`, `${TableName.IdentityOrgMembership}.role`];
|
||||
case "name":
|
||||
return `${TableName.Identity}.name`;
|
||||
default:
|
||||
throw new BadRequestError({ message: `Invalid ${String(attr)} provided` });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
void searchQuery.offset(offset).limit(limit);
|
||||
}
|
||||
|
||||
type TSubquery = Awaited<typeof searchQuery>;
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
||||
.join<TSubquery>(searchQuery, `${TableName.IdentityOrgMembership}.id`, "searchedIdentities.id")
|
||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`)
|
||||
.andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||
})
|
||||
.leftJoin(
|
||||
TableName.IdentityUniversalAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityUniversalAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityGcpAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityAwsAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityAwsAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityOidcAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityOidcAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityAzureAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityAzureAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityTokenAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityJwtAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.IdentityOrgMembership),
|
||||
db.ref("total_count").withSchema("searchedIdentities"),
|
||||
db.ref("role").withSchema(TableName.IdentityOrgMembership),
|
||||
db.ref("roleId").withSchema(TableName.IdentityOrgMembership),
|
||||
db.ref("orgId").withSchema(TableName.IdentityOrgMembership),
|
||||
db.ref("createdAt").withSchema(TableName.IdentityOrgMembership),
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership),
|
||||
db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"),
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
|
||||
)
|
||||
// cr stands for custom role
|
||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||
);
|
||||
|
||||
if (orderBy === OrgIdentityOrderBy.Name) {
|
||||
void query.orderBy("identityName", orderDirection);
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
const formattedDocs = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: ({
|
||||
crId,
|
||||
crDescription,
|
||||
crSlug,
|
||||
crPermission,
|
||||
crName,
|
||||
identityId,
|
||||
identityName,
|
||||
role,
|
||||
roleId,
|
||||
total_count,
|
||||
id,
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
jwtId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}) => ({
|
||||
role,
|
||||
roleId,
|
||||
identityId,
|
||||
id,
|
||||
total_count: total_count as string,
|
||||
orgId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
customRole: roleId
|
||||
? {
|
||||
id: crId,
|
||||
name: crName,
|
||||
slug: crSlug,
|
||||
permissions: crPermission,
|
||||
description: crDescription
|
||||
}
|
||||
: undefined,
|
||||
identity: {
|
||||
id: identityId,
|
||||
name: identityName,
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
jwtId
|
||||
})
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "metadataId",
|
||||
label: "metadata" as const,
|
||||
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||
id: metadataId,
|
||||
key: metadataKey,
|
||||
value: metadataValue
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return { docs: formattedDocs, totalCount: Number(formattedDocs?.[0]?.total_count ?? 0) };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByOrgId" });
|
||||
}
|
||||
};
|
||||
|
||||
const countAllOrgIdentities = async (
|
||||
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
|
||||
tx?: Knex
|
||||
@@ -331,5 +543,5 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities };
|
||||
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities, searchIdentities };
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TGetIdentityByIdDTO,
|
||||
TListOrgIdentitiesByOrgIdDTO,
|
||||
TListProjectIdentitiesByIdentityIdDTO,
|
||||
TSearchOrgIdentitiesByOrgIdDTO,
|
||||
TUpdateIdentityDTO
|
||||
} from "./identity-types";
|
||||
|
||||
@@ -288,6 +289,33 @@ export const identityServiceFactory = ({
|
||||
return { identityMemberships, totalCount };
|
||||
};
|
||||
|
||||
const searchOrgIdentities = async ({
|
||||
orgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
searchFilter = {}
|
||||
}: TSearchOrgIdentitiesByOrgIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { totalCount, docs } = await identityOrgMembershipDAL.searchIdentities({
|
||||
orgId,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
searchFilter
|
||||
});
|
||||
|
||||
return { identityMemberships: docs, totalCount };
|
||||
};
|
||||
|
||||
const listProjectIdentitiesByIdentityId = async ({
|
||||
identityId,
|
||||
actor,
|
||||
@@ -317,6 +345,7 @@ export const identityServiceFactory = ({
|
||||
deleteIdentity,
|
||||
listOrgIdentities,
|
||||
getIdentityById,
|
||||
searchOrgIdentities,
|
||||
listProjectIdentitiesByIdentityId
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IPType } from "@app/lib/ip";
|
||||
import { TSearchResourceOperator } from "@app/lib/search-resource/search";
|
||||
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateIdentityDTO = {
|
||||
@@ -46,3 +47,17 @@ export enum OrgIdentityOrderBy {
|
||||
Name = "name"
|
||||
// Role = "role"
|
||||
}
|
||||
|
||||
export type TSearchOrgIdentitiesByOrgIdDAL = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: OrgIdentityOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
orgId: string;
|
||||
searchFilter?: Partial<{
|
||||
name: Omit<TSearchResourceOperator, "number">;
|
||||
role: Omit<TSearchResourceOperator, "number">;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TSearchOrgIdentitiesByOrgIdDTO = TSearchOrgIdentitiesByOrgIdDAL & TOrgPermission;
|
||||
|
||||
@@ -7,7 +7,8 @@ export enum SecretSync {
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
Databricks = "databricks",
|
||||
Humanitec = "humanitec",
|
||||
Camunda = "camunda"
|
||||
Camunda = "camunda",
|
||||
Vercel = "vercel"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
|
||||
|
||||
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
@@ -37,7 +38,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
|
||||
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
|
||||
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
||||
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION
|
||||
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
|
||||
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -128,6 +130,8 @@ export const SecretSyncFns = {
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.Vercel:
|
||||
return VercelSyncFns.syncSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -177,6 +181,8 @@ export const SecretSyncFns = {
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).getSecrets(secretSync);
|
||||
case SecretSync.Vercel:
|
||||
secretMap = await VercelSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
@@ -225,6 +231,8 @@ export const SecretSyncFns = {
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.Vercel:
|
||||
return VercelSyncFns.removeSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
@@ -10,7 +10,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[SecretSync.Databricks]: "Databricks",
|
||||
[SecretSync.Humanitec]: "Humanitec",
|
||||
[SecretSync.Camunda]: "Camunda"
|
||||
[SecretSync.Camunda]: "Camunda",
|
||||
[SecretSync.Vercel]: "Vercel"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -22,5 +23,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||
[SecretSync.Databricks]: AppConnection.Databricks,
|
||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||
[SecretSync.Camunda]: AppConnection.Camunda
|
||||
[SecretSync.Camunda]: AppConnection.Camunda,
|
||||
[SecretSync.Vercel]: AppConnection.Vercel
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
THumanitecSyncListItem,
|
||||
THumanitecSyncWithCredentials
|
||||
} from "./humanitec";
|
||||
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
|
||||
|
||||
export type TSecretSync =
|
||||
| TAwsParameterStoreSync
|
||||
@@ -65,7 +66,8 @@ export type TSecretSync =
|
||||
| TAzureAppConfigurationSync
|
||||
| TDatabricksSync
|
||||
| THumanitecSync
|
||||
| TCamundaSync;
|
||||
| TCamundaSync
|
||||
| TVercelSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -76,7 +78,8 @@ export type TSecretSyncWithCredentials =
|
||||
| TAzureAppConfigurationSyncWithCredentials
|
||||
| TDatabricksSyncWithCredentials
|
||||
| THumanitecSyncWithCredentials
|
||||
| TCamundaSyncWithCredentials;
|
||||
| TCamundaSyncWithCredentials
|
||||
| TVercelSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -87,7 +90,8 @@ export type TSecretSyncInput =
|
||||
| TAzureAppConfigurationSyncInput
|
||||
| TDatabricksSyncInput
|
||||
| THumanitecSyncInput
|
||||
| TCamundaSyncInput;
|
||||
| TCamundaSyncInput
|
||||
| TVercelSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -98,7 +102,8 @@ export type TSecretSyncListItem =
|
||||
| TAzureAppConfigurationSyncListItem
|
||||
| TDatabricksSyncListItem
|
||||
| THumanitecSyncListItem
|
||||
| TCamundaSyncListItem;
|
||||
| TCamundaSyncListItem
|
||||
| TVercelSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
5
backend/src/services/secret-sync/vercel/index.ts
Normal file
5
backend/src/services/secret-sync/vercel/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./vercel-sync-constants";
|
||||
export * from "./vercel-sync-enums";
|
||||
export * from "./vercel-sync-fns";
|
||||
export * from "./vercel-sync-schemas";
|
||||
export * from "./vercel-sync-types";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const VERCEL_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Vercel",
|
||||
destination: SecretSync.Vercel,
|
||||
connection: AppConnection.Vercel,
|
||||
canImportSecrets: true
|
||||
};
|
||||
12
backend/src/services/secret-sync/vercel/vercel-sync-enums.ts
Normal file
12
backend/src/services/secret-sync/vercel/vercel-sync-enums.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export enum VercelSyncScope {
|
||||
Application = "application",
|
||||
Environment = "environment"
|
||||
}
|
||||
|
||||
export const VercelEnvironmentType = {
|
||||
Development: "development",
|
||||
Preview: "preview",
|
||||
Production: "production"
|
||||
} as const;
|
||||
|
||||
export type VercelEnvironment = (typeof VercelEnvironmentType)[keyof typeof VercelEnvironmentType];
|
||||
313
backend/src/services/secret-sync/vercel/vercel-sync-fns.ts
Normal file
313
backend/src/services/secret-sync/vercel/vercel-sync-fns.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { VercelEnvironmentType } from "./vercel-sync-enums";
|
||||
import { DefaultVercelEnvType, TVercelSyncWithCredentials, VercelApiSecret } from "./vercel-sync-types";
|
||||
|
||||
function isVercelDefaultEnvType(value: string): value is DefaultVercelEnvType {
|
||||
return Object.values(VercelEnvironmentType).map(String).includes(value);
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
|
||||
const sleep = async () =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 60000);
|
||||
});
|
||||
|
||||
const getVercelSecretsWithRetries = async (
|
||||
secretSync: TVercelSyncWithCredentials,
|
||||
attempt = 0
|
||||
): Promise<VercelApiSecret[]> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const params: { [key: string]: string } = {
|
||||
decrypt: "true",
|
||||
...(destinationConfig.branch ? { gitBranch: destinationConfig.branch } : {})
|
||||
};
|
||||
try {
|
||||
const { data } = await request.get<{ envs: VercelApiSecret[] }>(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env?teamId=${destinationConfig.teamId}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.envs;
|
||||
} catch (error) {
|
||||
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
return await getVercelSecretsWithRetries(secretSync, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getDecryptedVercelSecret = async (
|
||||
secretSync: TVercelSyncWithCredentials,
|
||||
secret: VercelApiSecret,
|
||||
attempt = 0
|
||||
): Promise<VercelApiSecret> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const params: { [key: string]: string } = {
|
||||
decrypt: "true",
|
||||
...(destinationConfig.branch ? { gitBranch: destinationConfig.branch } : {})
|
||||
};
|
||||
|
||||
try {
|
||||
const { data: decryptedSecret } = await request.get(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${secret.id}?teamId=${destinationConfig.teamId}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return decryptedSecret as VercelApiSecret;
|
||||
} catch (error) {
|
||||
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
return await getDecryptedVercelSecret(secretSync, secret, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getVercelSecrets = async (secretSync: TVercelSyncWithCredentials): Promise<VercelApiSecret[]> => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
const secrets = await getVercelSecretsWithRetries(secretSync);
|
||||
|
||||
const filteredSecrets = secrets.filter((secret) => {
|
||||
if (!isVercelDefaultEnvType(destinationConfig.env)) {
|
||||
if (secret.customEnvironmentIds?.includes(destinationConfig.env)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (secret.target.includes(destinationConfig.env)) {
|
||||
// If it's preview environment with a branch specified
|
||||
if (
|
||||
destinationConfig.env === VercelEnvironmentType.Preview &&
|
||||
destinationConfig.branch &&
|
||||
secret.gitBranch &&
|
||||
secret.gitBranch !== destinationConfig.branch
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// For secrets of type "encrypted", we need to get their decrypted value
|
||||
const secretsWithValues = await Promise.all(
|
||||
filteredSecrets.map(async (secret) => {
|
||||
if (secret.type === "encrypted") {
|
||||
const decryptedSecret = await getDecryptedVercelSecret(secretSync, secret);
|
||||
return decryptedSecret;
|
||||
}
|
||||
return secret;
|
||||
})
|
||||
);
|
||||
|
||||
return secretsWithValues;
|
||||
};
|
||||
|
||||
const deleteSecret = async (
|
||||
secretSync: TVercelSyncWithCredentials,
|
||||
vercelSecret: VercelApiSecret,
|
||||
attempt = 0
|
||||
): Promise<void> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
try {
|
||||
await request.delete(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}?teamId=${destinationConfig.teamId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
return await deleteSecret(secretSync, vercelSecret, attempt + 1);
|
||||
}
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: vercelSecret.key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createSecret = async (
|
||||
secretSync: TVercelSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
key: string,
|
||||
attempt = 0
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${destinationConfig.app}/env?teamId=${destinationConfig.teamId}`,
|
||||
{
|
||||
key,
|
||||
value: secretMap[key].value,
|
||||
type: "encrypted",
|
||||
target: isVercelDefaultEnvType(destinationConfig.env) ? [destinationConfig.env] : [],
|
||||
customEnvironmentIds: !isVercelDefaultEnvType(destinationConfig.env) ? [destinationConfig.env] : [],
|
||||
...(destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch
|
||||
? { gitBranch: destinationConfig.branch }
|
||||
: {})
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
return await createSecret(secretSync, secretMap, key, attempt + 1);
|
||||
}
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateSecret = async (
|
||||
secretSync: TVercelSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
vercelSecret: VercelApiSecret,
|
||||
attempt = 0
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiToken }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
let target = [...vercelSecret.target];
|
||||
if (isVercelDefaultEnvType(destinationConfig.env) && !vercelSecret.target.includes(destinationConfig.env)) {
|
||||
target = [...target, destinationConfig.env];
|
||||
}
|
||||
let customEnvironmentIds = [...(vercelSecret.customEnvironmentIds || [])];
|
||||
if (
|
||||
!isVercelDefaultEnvType(destinationConfig.env) &&
|
||||
!vercelSecret.customEnvironmentIds?.includes(destinationConfig.env)
|
||||
) {
|
||||
customEnvironmentIds = [...customEnvironmentIds, destinationConfig.env];
|
||||
}
|
||||
|
||||
await request.patch(
|
||||
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${destinationConfig.app}/env/${vercelSecret.id}?teamId=${destinationConfig.teamId}`,
|
||||
{
|
||||
...(vercelSecret.type !== "sensitive" && { key: vercelSecret.key }),
|
||||
value: secretMap[vercelSecret.key].value,
|
||||
type: vercelSecret.type,
|
||||
target,
|
||||
customEnvironmentIds,
|
||||
...(destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch
|
||||
? { gitBranch: destinationConfig.branch }
|
||||
: {})
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as { response: { status: number } }).response.status === 429 && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
return await updateSecret(secretSync, secretMap, vercelSecret, attempt + 1);
|
||||
}
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: vercelSecret.key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const VercelSyncFns = {
|
||||
syncSecrets: async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const vercelSecrets = await getVercelSecrets(secretSync);
|
||||
const vercelSecretsMap = new Map(vercelSecrets.map((s) => [s.key, s]));
|
||||
|
||||
// Create or update secrets
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
const existingSecret = vercelSecretsMap.get(key);
|
||||
|
||||
if (!existingSecret) {
|
||||
await createSecret(secretSync, secretMap, key);
|
||||
} else if (existingSecret.value !== secretMap[key].value) {
|
||||
await updateSecret(secretSync, secretMap, existingSecret);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete secrets if disableSecretDeletion is not set
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const vercelSecret of vercelSecrets) {
|
||||
if (!secretMap[vercelSecret.key]) {
|
||||
await deleteSecret(secretSync, vercelSecret);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getSecrets: async (secretSync: TVercelSyncWithCredentials): Promise<TSecretMap> => {
|
||||
const vercelSecrets = await getVercelSecrets(secretSync);
|
||||
return Object.fromEntries(vercelSecrets.map((s) => [s.key, { value: s.value ?? "" }]));
|
||||
},
|
||||
|
||||
removeSecrets: async (secretSync: TVercelSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const vercelSecrets = await getVercelSecrets(secretSync);
|
||||
|
||||
for await (const vercelSecret of vercelSecrets) {
|
||||
if (vercelSecret.key in secretMap) {
|
||||
await deleteSecret(secretSync, vercelSecret);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { VercelEnvironmentType } from "./vercel-sync-enums";
|
||||
|
||||
const VercelSyncDestinationConfigSchema = z.object({
|
||||
app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.app),
|
||||
appName: z.string().min(1, "App Name is required").describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.appName),
|
||||
env: z.nativeEnum(VercelEnvironmentType).or(z.string()).describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.env),
|
||||
branch: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.branch),
|
||||
teamId: z.string().describe(SecretSyncs.DESTINATION_CONFIG.VERCEL.teamId)
|
||||
});
|
||||
|
||||
const VercelSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const VercelSyncSchema = BaseSecretSyncSchema(SecretSync.Vercel, VercelSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Vercel),
|
||||
destinationConfig: VercelSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateVercelSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Vercel,
|
||||
VercelSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: VercelSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateVercelSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Vercel,
|
||||
VercelSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: VercelSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const VercelSyncListItemSchema = z.object({
|
||||
name: z.literal("Vercel"),
|
||||
connection: z.literal(AppConnection.Vercel),
|
||||
destination: z.literal(SecretSync.Vercel),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
40
backend/src/services/secret-sync/vercel/vercel-sync-types.ts
Normal file
40
backend/src/services/secret-sync/vercel/vercel-sync-types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TVercelConnection } from "@app/services/app-connection/vercel";
|
||||
|
||||
import { VercelEnvironmentType } from "./vercel-sync-enums";
|
||||
import { CreateVercelSyncSchema, VercelSyncListItemSchema, VercelSyncSchema } from "./vercel-sync-schemas";
|
||||
|
||||
export type TVercelSyncListItem = z.infer<typeof VercelSyncListItemSchema>;
|
||||
|
||||
export type TVercelSync = z.infer<typeof VercelSyncSchema>;
|
||||
|
||||
export type TVercelSyncInput = z.infer<typeof CreateVercelSyncSchema>;
|
||||
|
||||
export type TVercelSyncWithCredentials = TVercelSync & {
|
||||
connection: TVercelConnection;
|
||||
};
|
||||
|
||||
export type VercelSecret = {
|
||||
description: string;
|
||||
is_secret: boolean;
|
||||
key: string;
|
||||
source: "app" | "env";
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface VercelApiSecret {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
target: string[];
|
||||
customEnvironmentIds?: string[];
|
||||
gitBranch?: string;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
configurationId?: string;
|
||||
system?: boolean;
|
||||
}
|
||||
|
||||
export type DefaultVercelEnvType = (typeof VercelEnvironmentType)[keyof typeof VercelEnvironmentType];
|
||||
Reference in New Issue
Block a user