Merge branch 'dev' into multi-role

This commit is contained in:
miloschwartz
2026-03-24 22:01:13 -07:00
266 changed files with 7813 additions and 5880 deletions

View File

@@ -37,7 +37,7 @@ registry.registerPath({
method: "put",
path: "/org/{orgId}/user",
description: "Create an organization user.",
tags: [OpenAPITags.User, OpenAPITags.Org],
tags: [OpenAPITags.User],
request: {
params: paramsSchema,
body: {

View File

@@ -11,7 +11,7 @@ import { fromError } from "zod-validation-error";
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import { OpenAPITags, registry } from "@server/openApi";
async function queryUser(orgId: string, userId: string) {
export async function queryUser(orgId: string, userId: string) {
const [userRow] = await db
.select({
orgId: userOrgs.orgId,
@@ -79,7 +79,7 @@ registry.registerPath({
method: "get",
path: "/org/{orgId}/user/{userId}",
description: "Get a user in an organization.",
tags: [OpenAPITags.Org, OpenAPITags.User],
tags: [OpenAPITags.User],
request: {
params: getOrgUserParamsSchema
},

View File

@@ -0,0 +1,136 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { 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 { OpenAPITags, registry } from "@server/openApi";
import { queryUser, type GetOrgUserResponse } from "./getOrgUser";
const getOrgUserByUsernameParamsSchema = z.strictObject({
orgId: z.string()
});
const getOrgUserByUsernameQuerySchema = z.strictObject({
username: z.string().min(1, "username is required"),
idpId: z
.string()
.optional()
.transform((v) =>
v === undefined || v === "" ? undefined : parseInt(v, 10)
)
.refine(
(v) =>
v === undefined || (Number.isInteger(v) && (v as number) > 0),
{ message: "idpId must be a positive integer" }
)
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/user-by-username",
description:
"Get a user in an organization by username. When idpId is not passed, only internal users are searched (username is globally unique for them). For external (OIDC) users, pass idpId to search by username within that identity provider.",
tags: [OpenAPITags.User],
request: {
params: getOrgUserByUsernameParamsSchema,
query: getOrgUserByUsernameQuerySchema
},
responses: {}
});
export async function getOrgUserByUsername(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = getOrgUserByUsernameParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedQuery = getOrgUserByUsernameQuerySchema.safeParse(
req.query
);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const { username, idpId } = parsedQuery.data;
const conditions = [
eq(userOrgs.orgId, orgId),
eq(users.username, username)
];
if (idpId !== undefined) {
conditions.push(eq(users.idpId, idpId));
} else {
conditions.push(eq(users.type, "internal"));
}
const candidates = await db
.select({ userId: users.userId })
.from(userOrgs)
.innerJoin(users, eq(userOrgs.userId, users.userId))
.where(and(...conditions));
if (candidates.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`User with username '${username}' not found in organization`
)
);
}
if (candidates.length > 1) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Multiple users with this username (external users from different identity providers). Specify idpId (identity provider ID) to disambiguate. When not specified, this searches for internal users only."
)
);
}
const user = await queryUser(orgId, candidates[0].userId);
if (!user) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`User with username '${username}' not found in organization`
)
);
}
return response<GetOrgUserResponse>(res, {
data: user,
success: true,
error: false,
message: "User retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -6,6 +6,7 @@ export * from "./removeUserRole";
export * from "./inviteUser";
export * from "./acceptInvite";
export * from "./getOrgUser";
export * from "./getOrgUserByUsername";
export * from "./adminListUsers";
export * from "./adminRemoveUser";
export * from "./adminGetUser";

View File

@@ -19,7 +19,7 @@ import { UserType } from "@server/types/UserTypes";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import { build } from "@server/build";
import cache from "@server/lib/cache";
import cache from "#dynamic/lib/cache";
const inviteUserParamsSchema = z.strictObject({
orgId: z.string()
@@ -44,7 +44,7 @@ registry.registerPath({
method: "post",
path: "/org/{orgId}/create-invite",
description: "Invite a user to join an organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.Invitation],
request: {
params: inviteUserParamsSchema,
body: {
@@ -191,7 +191,7 @@ export async function inviteUser(
}
if (existingInvite.length) {
const attempts = cache.get<number>(email) || 0;
const attempts = (await cache.get<number>(email)) || 0;
if (attempts >= 3) {
return next(
createHttpError(
@@ -201,7 +201,7 @@ export async function inviteUser(
);
}
cache.set(email, attempts + 1);
await cache.set("regenerateInvite:" + email, attempts + 1, 3600);
const inviteId = existingInvite[0].inviteId; // Retrieve the original inviteId
const token = generateRandomString(

View File

@@ -54,7 +54,7 @@ registry.registerPath({
method: "get",
path: "/org/{orgId}/invitations",
description: "List invitations in an organization.",
tags: [OpenAPITags.Org, OpenAPITags.Invitation],
tags: [OpenAPITags.Invitation],
request: {
params: listInvitationsParamsSchema,
query: listInvitationsQuerySchema

View File

@@ -94,7 +94,7 @@ registry.registerPath({
method: "get",
path: "/org/{orgId}/users",
description: "List users in an organization.",
tags: [OpenAPITags.Org, OpenAPITags.User],
tags: [OpenAPITags.User],
request: {
params: listUsersParamsSchema,
query: listUsersSchema

View File

@@ -19,7 +19,7 @@ registry.registerPath({
method: "delete",
path: "/org/{orgId}/invitations/{inviteId}",
description: "Remove an open invitation from an organization",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.Invitation],
request: {
params: removeInvitationParamsSchema
},

View File

@@ -33,7 +33,7 @@ registry.registerPath({
method: "delete",
path: "/org/{orgId}/user/{userId}",
description: "Remove a user from an organization.",
tags: [OpenAPITags.Org, OpenAPITags.User],
tags: [OpenAPITags.User],
request: {
params: removeUserSchema
},

View File

@@ -26,7 +26,7 @@ registry.registerPath({
method: "post",
path: "/org/{orgId}/user/{userId}",
description: "Update a user in an org.",
tags: [OpenAPITags.Org, OpenAPITags.User],
tags: [OpenAPITags.Org],
request: {
params: paramsSchema,
body: {