add createOrgUser endpoint

This commit is contained in:
miloschwartz 2025-04-23 13:26:38 -04:00
parent feb558cfa8
commit 6f59d0cd2d
No known key found for this signature in database
9 changed files with 302 additions and 81 deletions

View file

@ -6,6 +6,7 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export enum ActionsEnum {
createOrgUser = "createOrgUser",
listOrgs = "listOrgs",
listUserOrgs = "listUserOrgs",
createOrg = "createOrg",

View file

@ -26,7 +26,12 @@ import {
verifyUserAccess,
getUserOrgs,
verifyUserIsServerAdmin,
<<<<<<< Updated upstream
verifyIsLoggedInUser
=======
verifyIsLoggedInUser,
verifyClientAccess
>>>>>>> Stashed changes
} from "@server/middlewares";
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
import { ActionsEnum } from "@server/auth/actions";
@ -46,6 +51,10 @@ unauthenticated.get("/", (_, res) => {
export const authenticated = Router();
authenticated.use(verifySessionUserMiddleware);
<<<<<<< Updated upstream
=======
authenticated.get("/pick-org-defaults", org.pickOrgDefaults);
>>>>>>> Stashed changes
authenticated.get("/org/checkId", org.checkId);
authenticated.put("/org", getUserOrgs, org.createOrg);
@ -448,7 +457,15 @@ authenticated.delete(
user.adminRemoveUser
);
authenticated.put(
"/org/:orgId/user",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createOrgUser),
user.createOrgUser
);
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
authenticated.get(
"/org/:orgId/users",
verifyOrgAccess,

View file

@ -5,11 +5,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import {
idp,
idpOidcConfig,
users
} from "@server/db/schemas";
import { idp, idpOidcConfig, users } from "@server/db/schemas";
import { and, eq } from "drizzle-orm";
import * as arctic from "arctic";
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
@ -17,6 +13,12 @@ import jmespath from "jmespath";
import jsonwebtoken from "jsonwebtoken";
import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import {
createSession,
generateSessionToken,
serializeSessionCookie
} from "@server/auth/sessions/app";
import { response } from "@server/lib";
const paramsSchema = z
.object({
@ -213,31 +215,31 @@ export async function validateOidcCallback(
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"User not found in the IdP"
"User not provisioned in the system"
)
);
}
//
// const token = generateSessionToken();
// const sess = await createSession(token, existingUser.userId);
// const isSecure = req.protocol === "https";
// const cookie = serializeSessionCookie(
// token,
// isSecure,
// new Date(sess.expiresAt)
// );
//
// res.appendHeader("Set-Cookie", cookie);
//
// return response<ValidateOidcUrlCallbackResponse>(res, {
// data: {
// redirectUrl: postAuthRedirectUrl
// },
// success: true,
// error: false,
// message: "OIDC callback validated successfully",
// status: HttpCode.CREATED
// });
const token = generateSessionToken();
const sess = await createSession(token, existingUser.userId);
const isSecure = req.protocol === "https";
const cookie = serializeSessionCookie(
token,
isSecure,
new Date(sess.expiresAt)
);
res.appendHeader("Set-Cookie", cookie);
return response<ValidateOidcUrlCallbackResponse>(res, {
data: {
redirectUrl: postAuthRedirectUrl
},
success: true,
error: false,
message: "OIDC callback validated successfully",
status: HttpCode.CREATED
});
}
} catch (error) {
logger.error(error);
@ -246,13 +248,3 @@ export async function validateOidcCallback(
);
}
}
function hydrateOrgMapping(
orgMapping: string | null,
orgId: string
): string | undefined {
if (!orgMapping) {
return undefined;
}
return orgMapping.split("{{orgId}}").join(orgId);
}

View file

@ -0,0 +1,207 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 db from "@server/db";
import { and, eq } from "drizzle-orm";
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db/schemas";
import { generateId } from "@server/auth/sessions/app";
const paramsSchema = z
.object({
orgId: z.string().nonempty()
})
.strict();
const bodySchema = z
.object({
email: z.string().email().optional(),
username: z.string().nonempty(),
name: z.string().optional(),
type: z.enum(["internal", "oidc"]).optional(),
idpId: z.number().optional(),
roleId: z.number()
})
.strict();
export type CreateOrgUserResponse = {};
registry.registerPath({
method: "put",
path: "/org/{orgId}/user",
description: "Create an organization user.",
tags: [OpenAPITags.User, OpenAPITags.Org],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
export async function createOrgUser(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const { username, email, name, type, idpId, roleId } = parsedBody.data;
const [role] = await db
.select()
.from(roles)
.where(eq(roles.roleId, roleId));
if (!role) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Role ID not found")
);
}
if (type === "internal") {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Internal users are not supported yet"
)
);
} else if (type === "oidc") {
if (!idpId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"IDP ID is required for OIDC users"
)
);
}
const [idpRes] = await db
.select()
.from(idp)
.innerJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId))
.where(eq(idp.idpId, idpId));
if (!idpRes) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "IDP ID not found")
);
}
if (idpRes.idp.type !== "oidc") {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"IDP ID is not of type OIDC"
)
);
}
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.username, username));
if (existingUser) {
const [existingOrgUser] = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.orgId, orgId),
eq(userOrgs.userId, existingUser.userId)
)
);
if (existingOrgUser) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"User already exists in this organization"
)
);
}
await db
.insert(userOrgs)
.values({
orgId,
userId: existingUser.userId,
roleId: role.roleId
})
.returning();
} else {
const userId = generateId(15);
const [newUser] = await db
.insert(users)
.values({
userId: userId,
email,
username,
name,
type: "oidc",
idpId,
dateCreated: new Date().toISOString(),
emailVerified: true
})
.returning();
await db
.insert(userOrgs)
.values({
orgId,
userId: newUser.userId,
roleId: role.roleId
})
.returning();
}
} else {
return next(
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
);
}
return response<CreateOrgUserResponse>(res, {
data: {},
success: true,
error: false,
message: "Org user created successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -9,3 +9,4 @@ export * from "./adminListUsers";
export * from "./adminRemoveUser";
export * from "./listInvitations";
export * from "./removeInvitation";
export * from "./createOrgUser";