feat(secret-sync): TeamCity App Connection & Secret Sync

This commit is contained in:
x
2025-04-23 17:58:59 -04:00
parent 4e10f51e50
commit d3ee30f5e6
91 changed files with 1515 additions and 53 deletions
@@ -0,0 +1,4 @@
export * from "./teamcity-connection-enums";
export * from "./teamcity-connection-fns";
export * from "./teamcity-connection-schemas";
export * from "./teamcity-connection-types";
@@ -0,0 +1,3 @@
export enum TeamCityConnectionMethod {
AccessToken = "access-token"
}
@@ -0,0 +1,64 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
import {
TTeamCityConnection,
TTeamCityConnectionConfig,
TTeamCityListProjectsResponse
} from "./teamcity-connection-types";
export const getTeamCityConnectionListItem = () => {
return {
name: "TeamCity" as const,
app: AppConnection.TeamCity as const,
methods: Object.values(TeamCityConnectionMethod) as [TeamCityConnectionMethod.AccessToken]
};
};
export const validateTeamCityConnectionCredentials = async (config: TTeamCityConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
const { accessToken } = config.credentials;
try {
await request.get(`${instanceUrl}/app/rest/server`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
});
} 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"
});
}
return config.credentials;
};
export const listTeamCityProjects = async (appConnection: TTeamCityConnection) => {
const instanceUrl = removeTrailingSlash(appConnection.credentials.instanceUrl);
const { accessToken } = appConnection.credentials;
const resp = await request.get<TTeamCityListProjectsResponse>(
`${instanceUrl}/app/rest/projects?fields=project(id,name,buildTypes(buildType(id,name)))`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
// Filter out the root project. Should not be seen by users.
return resp.data.project.filter((proj) => proj.id !== "_Root");
};
@@ -0,0 +1,70 @@
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 { TeamCityConnectionMethod } from "./teamcity-connection-enums";
export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({
accessToken: z
.string()
.trim()
.min(1, "Access Token required")
.describe(AppConnections.CREDENTIALS.TEAMCITY.accessToken),
instanceUrl: z
.string()
.trim()
.url("Invalid Instance URL")
.min(1, "Instance URL required")
.describe(AppConnections.CREDENTIALS.TEAMCITY.instanceUrl)
});
const BaseTeamCityConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.TeamCity) });
export const TeamCityConnectionSchema = BaseTeamCityConnectionSchema.extend({
method: z.literal(TeamCityConnectionMethod.AccessToken),
credentials: TeamCityConnectionAccessTokenCredentialsSchema
});
export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method", [
BaseTeamCityConnectionSchema.extend({
method: z.literal(TeamCityConnectionMethod.AccessToken),
credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
]);
export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(TeamCityConnectionMethod.AccessToken)
.describe(AppConnections.CREATE(AppConnection.TeamCity).method),
credentials: TeamCityConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.TeamCity).credentials
)
})
]);
export const CreateTeamCityConnectionSchema = ValidateTeamCityConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.TeamCity)
);
export const UpdateTeamCityConnectionSchema = z
.object({
credentials: TeamCityConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.TeamCity).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity));
export const TeamCityConnectionListItemSchema = z.object({
name: z.literal("TeamCity"),
app: z.literal(AppConnection.TeamCity),
methods: z.nativeEnum(TeamCityConnectionMethod).array()
});
@@ -0,0 +1,28 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listTeamCityProjects } from "./teamcity-connection-fns";
import { TTeamCityConnection } from "./teamcity-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TTeamCityConnection>;
export const teamcityConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.TeamCity, connectionId, actor);
try {
const projects = await listTeamCityProjects(appConnection);
return projects;
} catch (error) {
return [];
}
};
return {
listProjects
};
};
@@ -0,0 +1,43 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateTeamCityConnectionSchema,
TeamCityConnectionSchema,
ValidateTeamCityConnectionCredentialsSchema
} from "./teamcity-connection-schemas";
export type TTeamCityConnection = z.infer<typeof TeamCityConnectionSchema>;
export type TTeamCityConnectionInput = z.infer<typeof CreateTeamCityConnectionSchema> & {
app: AppConnection.TeamCity;
};
export type TValidateTeamCityConnectionCredentialsSchema = typeof ValidateTeamCityConnectionCredentialsSchema;
export type TTeamCityConnectionConfig = DiscriminativePick<
TTeamCityConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TTeamCityProject = {
id: string;
name: string;
};
export type TTeamCityProjectWithBuildTypes = TTeamCityProject & {
buildTypes: {
buildType: {
id: string;
name: string;
}[];
};
};
export type TTeamCityListProjectsResponse = {
project: TTeamCityProjectWithBuildTypes[];
};