Merge branch 'dev' into feat-blueprint-ui-on-dashboard

This commit is contained in:
Fred KISSIE
2025-10-29 03:31:51 +01:00
committed by GitHub
169 changed files with 14164 additions and 1207 deletions

View File

@@ -0,0 +1,136 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, idp, idpOidcConfig } from "@server/db";
import { roles, userOrgs, users } from "@server/db";
import { and, eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import { OpenAPITags, registry } from "@server/openApi";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { CheckOrgAccessPolicyResult } from "@server/lib/checkOrgAccessPolicy";
async function queryUser(orgId: string, userId: string) {
const [user] = await db
.select({
orgId: userOrgs.orgId,
userId: users.userId,
email: users.email,
username: users.username,
name: users.name,
type: users.type,
roleId: userOrgs.roleId,
roleName: roles.name,
isOwner: userOrgs.isOwner,
isAdmin: roles.isAdmin,
twoFactorEnabled: users.twoFactorEnabled,
autoProvisioned: userOrgs.autoProvisioned,
idpId: users.idpId,
idpName: idp.name,
idpType: idp.type,
idpVariant: idpOidcConfig.variant,
idpAutoProvision: idp.autoProvision
})
.from(userOrgs)
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
.leftJoin(users, eq(userOrgs.userId, users.userId))
.leftJoin(idp, eq(users.idpId, idp.idpId))
.leftJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId))
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.limit(1);
return user;
}
export type CheckOrgUserAccessResponse = CheckOrgAccessPolicyResult;
const paramsSchema = z.object({
userId: z.string(),
orgId: z.string()
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/user/{userId}/check",
description: "Check a user's access in an organization.",
tags: [OpenAPITags.Org, OpenAPITags.User],
request: {
params: paramsSchema
},
responses: {}
});
export async function checkOrgUserAccess(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, userId } = parsedParams.data;
if (userId !== req.user?.userId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You do not have permission to check this user's access"
)
);
}
let user;
user = await queryUser(orgId, userId);
if (!user) {
const [fullUser] = await db
.select()
.from(users)
.where(eq(users.email, userId))
.limit(1);
if (fullUser) {
user = await queryUser(orgId, fullUser.userId);
}
}
if (!user) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`User with ID ${userId} not found in org`
)
);
}
const policyCheck = await checkOrgAccessPolicy({
orgId,
userId,
session: req.session
});
// if we get here, the user has an org join, we just don't know if they pass the policies
return response<CheckOrgUserAccessResponse>(res, {
data: policyCheck,
success: true,
error: false,
message: "User access checked successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -17,7 +17,7 @@ const getOrgSchema = z
.strict();
export type GetOrgResponse = {
org: Org & { settings: { } | null };
org: Org;
};
registry.registerPath({
@@ -49,13 +49,13 @@ export async function getOrg(
const { orgId } = parsedParams.data;
const org = await db
const [org] = await db
.select()
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (org.length === 0) {
if (!org) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
@@ -64,23 +64,9 @@ export async function getOrg(
);
}
// Parse settings from JSON string back to object
let parsedSettings = null;
if (org[0].settings) {
try {
parsedSettings = JSON.parse(org[0].settings);
} catch (error) {
// If parsing fails, keep as string for backward compatibility
parsedSettings = org[0].settings;
}
}
return response<GetOrgResponse>(res, {
data: {
org: {
...org[0],
settings: parsedSettings
}
org
},
success: true,
error: false,

View File

@@ -7,3 +7,5 @@ export * from "./checkId";
export * from "./getOrgOverview";
export * from "./listOrgs";
export * from "./pickOrgDefaults";
export * from "./applyBlueprint";
export * from "./checkOrgUserAccess";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { orgs } from "@server/db";
import { orgs, users } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -9,18 +9,36 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { UserType } from "@server/types/UserTypes";
import license from "#dynamic/license/license";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
const updateOrgParamsSchema = z
.object({
orgId: z.string(),
orgId: z.string()
})
.strict();
const updateOrgBodySchema = z
.object({
name: z.string().min(1).max(255).optional(),
settings: z.object({
}).optional(),
requireTwoFactor: z.boolean().optional(),
maxSessionLengthHours: z.number().nullable().optional(),
passwordExpiryDays: z.number().nullable().optional(),
settingsLogRetentionDaysRequest: z
.number()
.min(build === "saas" ? 0 : -1)
.optional(),
settingsLogRetentionDaysAccess: z
.number()
.min(build === "saas" ? 0 : -1)
.optional(),
settingsLogRetentionDaysAction: z
.number()
.min(build === "saas" ? 0 : -1)
.optional()
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
@@ -73,13 +91,40 @@ export async function updateOrg(
const { orgId } = parsedParams.data;
const settings = parsedBody.data.settings ? JSON.stringify(parsedBody.data.settings) : undefined;
const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) {
parsedBody.data.requireTwoFactor = undefined;
parsedBody.data.maxSessionLengthHours = undefined;
parsedBody.data.passwordExpiryDays = undefined;
}
const { tier } = await getOrgTierData(orgId);
if (
tier != TierId.STANDARD &&
parsedBody.data.settingsLogRetentionDaysRequest &&
parsedBody.data.settingsLogRetentionDaysRequest > 30
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You are not allowed to set log retention days greater than 30 with your current subscription"
)
);
}
const updatedOrg = await db
.update(orgs)
.set({
name: parsedBody.data.name,
settings: settings
requireTwoFactor: parsedBody.data.requireTwoFactor,
maxSessionLengthHours: parsedBody.data.maxSessionLengthHours,
passwordExpiryDays: parsedBody.data.passwordExpiryDays,
settingsLogRetentionDaysRequest:
parsedBody.data.settingsLogRetentionDaysRequest,
settingsLogRetentionDaysAccess:
parsedBody.data.settingsLogRetentionDaysAccess,
settingsLogRetentionDaysAction:
parsedBody.data.settingsLogRetentionDaysAction
})
.where(eq(orgs.orgId, orgId))
.returning();
@@ -107,3 +152,22 @@ export async function updateOrg(
);
}
}
async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
if (build === "enterprise") {
const isUnlocked = await license.isUnlocked();
if (!isUnlocked) {
return false;
}
}
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return false;
}
}
return true;
}