Merge branch 'main' into ENG-3723

This commit is contained in:
x032205
2025-09-27 01:58:44 -04:00
162 changed files with 7033 additions and 424 deletions
+6 -1
View File
@@ -320,6 +320,7 @@ import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal";
import { totpServiceFactory } from "@app/services/totp/totp-service";
import { upgradePathServiceFactory } from "@app/services/upgrade-path/upgrade-path-service";
import { userDALFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service";
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -771,6 +772,8 @@ export const registerRoutes = async (
userAliasDAL
});
const upgradePathService = upgradePathServiceFactory({ keyStore });
const totpService = totpServiceFactory({
totpConfigDAL,
userDAL,
@@ -792,6 +795,7 @@ export const registerRoutes = async (
smtpService,
authDAL,
userDAL,
orgMembershipDAL,
totpConfigDAL
});
@@ -2277,7 +2281,8 @@ export const registerRoutes = async (
notification: notificationService,
pamFolder: pamFolderService,
pamResource: pamResourceService,
pamSession: pamSessionService
pamSession: pamSessionService,
upgradePath: upgradePathService
});
const cronJobs: CronJob[] = [];
@@ -93,6 +93,7 @@ import {
RailwayConnectionListItemSchema,
SanitizedRailwayConnectionSchema
} from "@app/services/app-connection/railway";
import { RedisConnectionListItemSchema, SanitizedRedisConnectionSchema } from "@app/services/app-connection/redis";
import {
RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema
@@ -156,7 +157,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedDigitalOceanConnectionSchema.options,
...SanitizedNetlifyConnectionSchema.options,
...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options
...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -197,7 +199,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
DigitalOceanConnectionListItemSchema,
NetlifyConnectionListItemSchema,
OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema
AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -31,6 +31,7 @@ import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRedisConnectionRouter } from "./redis-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerSupabaseConnectionRouter } from "./supabase-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
@@ -80,5 +81,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter
[AppConnection.Okta]: registerOktaConnectionRouter,
[AppConnection.Redis]: registerRedisConnectionRouter
};
@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateRedisConnectionSchema,
SanitizedRedisConnectionSchema,
UpdateRedisConnectionSchema
} from "@app/services/app-connection/redis";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerRedisConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Redis,
server,
sanitizedResponseSchema: SanitizedRedisConnectionSchema,
createSchema: CreateRedisConnectionSchema,
updateSchema: UpdateRedisConnectionSchema
});
};
+2
View File
@@ -58,6 +58,7 @@ import { registerSecretRequestsRouter } from "./secret-requests-router";
import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router";
import { registerSlackRouter } from "./slack-router";
import { registerUpgradePathRouter } from "./upgrade-path-router";
import { registerSsoRouter } from "./sso-router";
import { registerUserActionRouter } from "./user-action-router";
import { registerUserEngagementRouter } from "./user-engagement-router";
@@ -217,4 +218,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
);
await server.register(registerEventRouter, { prefix: "/events" });
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
};
@@ -0,0 +1,117 @@
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { publicEndpointLimit } from "@app/server/config/rateLimiter";
import { versionSchema } from "@app/services/upgrade-path/upgrade-path-schemas";
export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/versions",
config: {
rateLimit: publicEndpointLimit
},
schema: {
response: {
200: z.object({
versions: z.array(
z.object({
tagName: z.string(),
name: z.string(),
publishedAt: z.string(),
prerelease: z.boolean(),
draft: z.boolean()
})
)
})
}
},
handler: async (req) => {
try {
const versions = await req.server.services.upgradePath.getGitHubReleases();
return {
versions
};
} catch (error) {
logger.error(error, "Failed to fetch versions");
if (error instanceof z.ZodError) {
throw new BadRequestError({ message: "Invalid query parameters" });
}
throw new BadRequestError({ message: "Failed to fetch GitHub releases" });
}
}
});
server.route({
method: "POST",
url: "/calculate",
config: {
rateLimit: publicEndpointLimit
},
schema: {
body: z.object({
fromVersion: versionSchema,
toVersion: versionSchema
}),
response: {
200: z.object({
path: z.array(
z.object({
version: z.string(),
name: z.string(),
publishedAt: z.string(),
prerelease: z.boolean()
})
),
breakingChanges: z.array(
z.object({
version: z.string(),
changes: z.array(
z.object({
title: z.string(),
description: z.string(),
action: z.string()
})
)
})
),
features: z.array(
z.object({
version: z.string(),
name: z.string(),
body: z.string(),
publishedAt: z.string()
})
),
hasDbMigration: z.boolean(),
config: z.record(z.unknown())
})
}
},
handler: async (req) => {
try {
const { fromVersion, toVersion } = req.body;
const result = await req.server.services.upgradePath.calculateUpgradePath(fromVersion, toVersion);
logger.info(
{ pathLength: result.path.length, hasBreaking: result.breakingChanges.length > 0 },
"Upgrade path calculated"
);
return result;
} catch (error) {
logger.error(error, "Failed to calculate upgrade path");
if (error instanceof z.ZodError) {
throw new BadRequestError({ message: `Invalid input: ${error.errors.map((e) => e.message).join(", ")}` });
}
if (error instanceof Error) {
throw new BadRequestError({ message: error.message });
}
throw new BadRequestError({ message: "Failed to calculate upgrade path" });
}
}
});
};
+3 -1
View File
@@ -255,7 +255,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
totp: z.string()
}),
response: {
200: z.object({})
200: z.object({
recoveryCodes: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], {
+75 -32
View File
@@ -1,5 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { TUsers } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -7,11 +9,54 @@ import { mfaRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
const handleMfaVerification = async (
req: FastifyRequest & { mfa: { userId: string; orgId?: string; user: TUsers } },
res: FastifyReply,
server: FastifyZodProvider,
mfaToken: string,
mfaMethod: MfaMethod,
isRecoveryCode?: boolean
) => {
const userAgent = req.headers["user-agent"];
const mfaJwtToken = req.headers.authorization?.replace("Bearer ", "");
if (!userAgent) throw new Error("user agent header is required");
if (!mfaJwtToken) throw new Error("authorization header is required");
const appCfg = getConfig();
const { user, token } = await server.services.login.verifyMfaToken({
userAgent,
mfaJwtToken,
ip: req.realIp,
userId: req.mfa.userId,
orgId: req.mfa.orgId,
mfaToken,
mfaMethod,
isRecoveryCode
});
void res.setCookie("jid", token.refresh, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
...user,
token: token.access,
protectedKey: user.protectedKey || null,
protectedKeyIV: user.protectedKeyIV || null,
protectedKeyTag: user.protectedKeyTag || null
};
};
export const registerMfaRouter = async (server: FastifyZodProvider) => {
const cfg = getConfig();
server.decorateRequest("mfa", null);
server.addHook("preParsing", async (req, res) => {
server.addHook("preValidation", async (req, res) => {
const authorizationHeader = req.headers.authorization;
if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) {
@@ -109,38 +154,36 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
const mfaJwtToken = req.headers.authorization?.replace("Bearer ", "");
if (!userAgent) throw new Error("user agent header is required");
if (!mfaJwtToken) throw new Error("authorization header is required");
const appCfg = getConfig();
return handleMfaVerification(req, res, server, req.body.mfaToken, req.body.mfaMethod);
}
});
const { user, token } = await server.services.login.verifyMfaToken({
userAgent,
mfaJwtToken,
ip: req.realIp,
userId: req.mfa.userId,
orgId: req.mfa.orgId,
mfaToken: req.body.mfaToken,
mfaMethod: req.body.mfaMethod
});
void res.setCookie("jid", token.refresh, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
...user,
token: token.access,
protectedKey: user.protectedKey || null,
protectedKeyIV: user.protectedKeyIV || null,
protectedKeyTag: user.protectedKeyTag || null
};
server.route({
url: "/mfa/verify/recovery-code",
method: "POST",
config: {
rateLimit: mfaRateLimit
},
schema: {
body: z.object({
recoveryCode: z.string().trim().length(8, "Recovery code must be 8 characters")
}),
response: {
200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(),
protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullish(),
publicKey: z.string().nullish(),
encryptedPrivateKey: z.string().nullish(),
iv: z.string().nullish(),
tag: z.string().nullish(),
token: z.string()
})
}
},
handler: async (req, res) => {
return handleMfaVerification(req, res, server, req.body.recoveryCode, MfaMethod.TOTP, true);
}
});
};