mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-25 20:16:40 +00:00
Merge branch 'dev' into multi-role
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
136
server/routers/user/getOrgUserByUsername.ts
Normal file
136
server/routers/user/getOrgUserByUsername.ts
Normal 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user