first pass

This commit is contained in:
miloschwartz
2026-02-24 17:58:11 -08:00
parent 848d4d91e6
commit 20e547a0f6
60 changed files with 1023 additions and 399 deletions

View File

@@ -208,7 +208,7 @@ export async function listAccessTokens(
.where(
or(
eq(userResources.userId, req.user!.userId),
eq(roleResources.roleId, req.userOrgRoleId!)
inArray(roleResources.roleId, req.userOrgRoleIds!)
)
);
} else {

View File

@@ -3,12 +3,13 @@ import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToke
import {
getResourceByDomain,
getResourceRules,
getRoleName,
getRoleResourceAccess,
getUserOrgRole,
getUserResourceAccess,
getOrgLoginPage,
getUserSessionWithUser
} from "@server/db/queries/verifySessionQueries";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import {
LoginPage,
Org,
@@ -916,9 +917,9 @@ async function isUserAllowedToAccessResource(
return null;
}
const userOrgRole = await getUserOrgRole(user.userId, resource.orgId);
const userOrgRoleIds = await getUserOrgRoleIds(user.userId, resource.orgId);
if (!userOrgRole) {
if (!userOrgRoleIds.length) {
return null;
}
@@ -934,17 +935,23 @@ async function isUserAllowedToAccessResource(
return null;
}
const roleResourceAccess = await getRoleResourceAccess(
resource.resourceId,
userOrgRole.roleId
);
if (roleResourceAccess) {
const roleNames: string[] = [];
for (const roleId of userOrgRoleIds) {
const roleResourceAccess = await getRoleResourceAccess(
resource.resourceId,
roleId
);
if (roleResourceAccess) {
const roleName = await getRoleName(roleId);
if (roleName) roleNames.push(roleName);
}
}
if (roleNames.length > 0) {
return {
username: user.username,
email: user.email,
name: user.name,
role: userOrgRole.roleName
role: roleNames.join(", ")
};
}
@@ -954,11 +961,15 @@ async function isUserAllowedToAccessResource(
);
if (userResourceAccess) {
const names = await Promise.all(
userOrgRoleIds.map((id) => getRoleName(id))
);
const role = names.filter(Boolean).join(", ") || "";
return {
username: user.username,
email: user.email,
name: user.name,
role: userOrgRole.roleName
role
};
}

View File

@@ -92,7 +92,7 @@ export async function createClient(
const { orgId } = parsedParams.data;
if (req.user && !req.userOrgRoleId) {
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
@@ -234,7 +234,7 @@ export async function createClient(
clientId: newClient.clientId
});
if (req.user && req.userOrgRoleId != adminRole.roleId) {
if (req.user && !req.userOrgRoleIds?.includes(adminRole.roleId)) {
// make sure the user can access the client
trx.insert(userClients).values({
userId: req.user.userId,

View File

@@ -297,7 +297,7 @@ export async function listClients(
.where(
or(
eq(userClients.userId, req.user!.userId),
eq(roleClients.roleId, req.userOrgRoleId!)
inArray(roleClients.roleId, req.userOrgRoleIds!)
)
);
} else {

View File

@@ -316,7 +316,7 @@ export async function listUserDevices(
.where(
or(
eq(userClients.userId, req.user!.userId),
eq(roleClients.roleId, req.userOrgRoleId!)
inArray(roleClients.roleId, req.userOrgRoleIds!)
)
);
} else {

View File

@@ -654,6 +654,16 @@ authenticated.post(
user.addUserRole
);
authenticated.delete(
"/role/:roleId/remove/:userId",
verifyRoleAccess,
verifyUserAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.removeUserRole),
logActionAudit(ActionsEnum.removeUserRole),
user.removeUserRole
);
authenticated.post(
"/resource/:resourceId/roles",
verifyResourceAccess,

View File

@@ -13,6 +13,7 @@ import {
orgs,
Role,
roles,
userOrgRoles,
userOrgs,
users
} from "@server/db";
@@ -570,32 +571,28 @@ export async function validateOidcCallback(
}
}
// Update roles for existing auto-provisioned orgs where the role has changed
const orgsToUpdate = autoProvisionedOrgs.filter(
(currentOrg) => {
const newOrg = userOrgInfo.find(
(newOrg) => newOrg.orgId === currentOrg.orgId
);
return newOrg && newOrg.roleId !== currentOrg.roleId;
}
);
if (orgsToUpdate.length > 0) {
for (const org of orgsToUpdate) {
const newRole = userOrgInfo.find(
(newOrg) => newOrg.orgId === org.orgId
);
if (newRole) {
await trx
.update(userOrgs)
.set({ roleId: newRole.roleId })
.where(
and(
eq(userOrgs.userId, userId!),
eq(userOrgs.orgId, org.orgId)
)
);
}
// Ensure IDP-provided role exists for existing auto-provisioned orgs (add only; never delete other roles)
const userRolesInOrgs = await trx
.select()
.from(userOrgRoles)
.where(eq(userOrgRoles.userId, userId!));
for (const currentOrg of autoProvisionedOrgs) {
const newRole = userOrgInfo.find(
(newOrg) => newOrg.orgId === currentOrg.orgId
);
if (!newRole) continue;
const currentRolesInOrg = userRolesInOrgs.filter(
(r) => r.orgId === currentOrg.orgId
);
const hasIdpRole = currentRolesInOrg.some(
(r) => r.roleId === newRole.roleId
);
if (!hasIdpRole) {
await trx.insert(userOrgRoles).values({
userId: userId!,
orgId: currentOrg.orgId,
roleId: newRole.roleId
});
}
}
@@ -619,9 +616,9 @@ export async function validateOidcCallback(
{
orgId: org.orgId,
userId: userId!,
roleId: org.roleId,
autoProvisioned: true,
},
org.roleId,
trx
);
}

View File

@@ -532,6 +532,16 @@ authenticated.post(
user.addUserRole
);
authenticated.delete(
"/role/:roleId/remove/:userId",
verifyApiKeyRoleAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.removeUserRole),
logActionAudit(ActionsEnum.removeUserRole),
user.removeUserRole
);
authenticated.post(
"/resource/:resourceId/roles",
verifyApiKeyResourceAccess,

View File

@@ -46,7 +46,7 @@ export async function createNewt(
const { newtId, secret } = parsedBody.data;
if (req.user && !req.userOrgRoleId) {
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);

View File

@@ -46,7 +46,7 @@ export async function createNewt(
const { newtId, secret } = parsedBody.data;
if (req.user && !req.userOrgRoleId) {
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);

View File

@@ -1,7 +1,7 @@
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 { roles, userOrgRoles, userOrgs, users } from "@server/db";
import { and, eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -14,7 +14,7 @@ import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { CheckOrgAccessPolicyResult } from "@server/lib/checkOrgAccessPolicy";
async function queryUser(orgId: string, userId: string) {
const [user] = await db
const [userRow] = await db
.select({
orgId: userOrgs.orgId,
userId: users.userId,
@@ -22,10 +22,7 @@ async function queryUser(orgId: string, userId: string) {
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,
@@ -35,13 +32,40 @@ async function queryUser(orgId: string, userId: string) {
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;
if (!userRow) return undefined;
const roleRows = await db
.select({
roleId: userOrgRoles.roleId,
roleName: roles.name,
isAdmin: roles.isAdmin
})
.from(userOrgRoles)
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
);
const isAdmin = roleRows.some((r) => r.isAdmin);
return {
...userRow,
isAdmin,
roleIds: roleRows.map((r) => r.roleId),
roles: roleRows.map((r) => ({
roleId: r.roleId,
name: r.roleName ?? ""
}))
};
}
export type CheckOrgUserAccessResponse = CheckOrgAccessPolicyResult;

View File

@@ -9,6 +9,7 @@ import {
orgs,
roleActions,
roles,
userOrgRoles,
userOrgs,
users,
actions
@@ -312,9 +313,13 @@ export async function createOrg(
await trx.insert(userOrgs).values({
userId: req.user!.userId,
orgId: newOrg[0].orgId,
roleId: roleId,
isOwner: true
});
await trx.insert(userOrgRoles).values({
userId: req.user!.userId,
orgId: newOrg[0].orgId,
roleId
});
ownerUserId = req.user!.userId;
} else {
// if org created by root api key, set the server admin as the owner
@@ -332,9 +337,13 @@ export async function createOrg(
await trx.insert(userOrgs).values({
userId: serverAdmin.userId,
orgId: newOrg[0].orgId,
roleId: roleId,
isOwner: true
});
await trx.insert(userOrgRoles).values({
userId: serverAdmin.userId,
orgId: newOrg[0].orgId,
roleId
});
ownerUserId = serverAdmin.userId;
}

View File

@@ -117,20 +117,26 @@ export async function getOrgOverview(
.from(userOrgs)
.where(eq(userOrgs.orgId, orgId));
const [role] = await db
.select()
.from(roles)
.where(eq(roles.roleId, req.userOrg.roleId));
const roleIds = req.userOrgRoleIds ?? [];
const roleRows =
roleIds.length > 0
? await db
.select({ name: roles.name, isAdmin: roles.isAdmin })
.from(roles)
.where(inArray(roles.roleId, roleIds))
: [];
const userRoleName = roleRows.map((r) => r.name ?? "").join(", ") ?? "";
const isAdmin = roleRows.some((r) => r.isAdmin === true);
return response<GetOrgOverviewResponse>(res, {
data: {
orgName: org[0].name,
orgId: org[0].orgId,
userRoleName: role.name,
userRoleName,
numSites,
numUsers,
numResources,
isAdmin: role.isAdmin || false,
isAdmin,
isOwner: req.userOrg?.isOwner || false
},
success: true,

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, roles } from "@server/db";
import { Org, orgs, userOrgs } from "@server/db";
import { Org, orgs, userOrgRoles, userOrgs } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -82,10 +82,7 @@ export async function listUserOrgs(
const { userId } = parsedParams.data;
const userOrganizations = await db
.select({
orgId: userOrgs.orgId,
roleId: userOrgs.roleId
})
.select({ orgId: userOrgs.orgId })
.from(userOrgs)
.where(eq(userOrgs.userId, userId));
@@ -116,10 +113,27 @@ export async function listUserOrgs(
userOrgs,
and(eq(userOrgs.orgId, orgs.orgId), eq(userOrgs.userId, userId))
)
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
.limit(limit)
.offset(offset);
const roleRows = await db
.select({
orgId: userOrgRoles.orgId,
isAdmin: roles.isAdmin
})
.from(userOrgRoles)
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(
and(
eq(userOrgRoles.userId, userId),
inArray(userOrgRoles.orgId, userOrgIds)
)
);
const orgHasAdmin = new Set(
roleRows.filter((r) => r.isAdmin).map((r) => r.orgId)
);
const totalCountResult = await db
.select({ count: sql<number>`cast(count(*) as integer)` })
.from(orgs)
@@ -133,8 +147,8 @@ export async function listUserOrgs(
if (val.userOrgs && val.userOrgs.isOwner) {
res.isOwner = val.userOrgs.isOwner;
}
if (val.roles && val.roles.isAdmin) {
res.isAdmin = val.roles.isAdmin;
if (val.orgs && orgHasAdmin.has(val.orgs.orgId)) {
res.isAdmin = true;
}
if (val.userOrgs?.isOwner && val.orgs?.isBillingOrg) {
res.isPrimaryOrg = val.orgs.isBillingOrg;

View File

@@ -112,7 +112,7 @@ export async function createResource(
const { orgId } = parsedParams.data;
if (req.user && !req.userOrgRoleId) {
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
@@ -278,7 +278,7 @@ async function createHttpResource(
resourceId: newResource[0].resourceId
});
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) {
// make sure the user can access the resource
await trx.insert(userResources).values({
userId: req.user?.userId!,
@@ -371,7 +371,7 @@ async function createRawResource(
resourceId: newResource[0].resourceId
});
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) {
// make sure the user can access the resource
await trx.insert(userResources).values({
userId: req.user?.userId!,

View File

@@ -5,6 +5,7 @@ import {
resources,
userResources,
roleResources,
userOrgRoles,
userOrgs,
resourcePassword,
resourcePincode,
@@ -32,22 +33,29 @@ export async function getUserResources(
);
}
// First get the user's role in the organization
const userOrgResult = await db
.select({
roleId: userOrgs.roleId
})
// Check user is in organization and get their role IDs
const [userOrg] = await db
.select()
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.limit(1);
if (userOrgResult.length === 0) {
if (!userOrg) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User not in organization")
);
}
const userRoleId = userOrgResult[0].roleId;
const userRoleIds = await db
.select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
)
.then((rows) => rows.map((r) => r.roleId));
// Get resources accessible through direct assignment or role assignment
const directResourcesQuery = db
@@ -55,20 +63,28 @@ export async function getUserResources(
.from(userResources)
.where(eq(userResources.userId, userId));
const roleResourcesQuery = db
.select({ resourceId: roleResources.resourceId })
.from(roleResources)
.where(eq(roleResources.roleId, userRoleId));
const roleResourcesQuery =
userRoleIds.length > 0
? db
.select({ resourceId: roleResources.resourceId })
.from(roleResources)
.where(inArray(roleResources.roleId, userRoleIds))
: Promise.resolve([]);
const directSiteResourcesQuery = db
.select({ siteResourceId: userSiteResources.siteResourceId })
.from(userSiteResources)
.where(eq(userSiteResources.userId, userId));
const roleSiteResourcesQuery = db
.select({ siteResourceId: roleSiteResources.siteResourceId })
.from(roleSiteResources)
.where(eq(roleSiteResources.roleId, userRoleId));
const roleSiteResourcesQuery =
userRoleIds.length > 0
? db
.select({
siteResourceId: roleSiteResources.siteResourceId
})
.from(roleSiteResources)
.where(inArray(roleSiteResources.roleId, userRoleIds))
: Promise.resolve([]);
const [directResources, roleResourceResults, directSiteResourceResults, roleSiteResourceResults] = await Promise.all([
directResourcesQuery,

View File

@@ -276,7 +276,7 @@ export async function listResources(
.where(
or(
eq(userResources.userId, req.user!.userId),
eq(roleResources.roleId, req.userOrgRoleId!)
inArray(roleResources.roleId, req.userOrgRoleIds!)
)
);
} else {

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { roles, userOrgs } from "@server/db";
import { roles, userOrgRoles } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -114,11 +114,11 @@ export async function deleteRole(
}
await db.transaction(async (trx) => {
// move all users from the userOrgs table with roleId to newRoleId
// move all users from userOrgRoles with roleId to newRoleId
await trx
.update(userOrgs)
.update(userOrgRoles)
.set({ roleId: newRoleId })
.where(eq(userOrgs.roleId, roleId));
.where(eq(userOrgRoles.roleId, roleId));
// delete the old role
await trx.delete(roles).where(eq(roles.roleId, roleId));

View File

@@ -111,7 +111,7 @@ export async function createSite(
const { orgId } = parsedParams.data;
if (req.user && !req.userOrgRoleId) {
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
@@ -399,7 +399,7 @@ export async function createSite(
siteId: newSite.siteId
});
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) {
// make sure the user can access the site
trx.insert(userSites).values({
userId: req.user?.userId!,

View File

@@ -235,7 +235,7 @@ export async function listSites(
.where(
or(
eq(userSites.userId, req.user!.userId),
eq(roleSites.roleId, req.userOrgRoleId!)
inArray(roleSites.roleId, req.userOrgRoleIds!)
)
);
} else {

View File

@@ -165,9 +165,9 @@ export async function acceptInvite(
org,
{
userId: existingUser[0].userId,
orgId: existingInvite.orgId,
roleId: existingInvite.roleId
orgId: existingInvite.orgId
},
existingInvite.roleId,
trx
);

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { clients, db, UserOrg } from "@server/db";
import { userOrgs, roles } from "@server/db";
import { clients, db } from "@server/db";
import { userOrgRoles, userOrgs, roles } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -111,20 +111,23 @@ export async function addUserRole(
);
}
let newUserRole: UserOrg | null = null;
let newUserRole: { userId: string; orgId: string; roleId: number } | null =
null;
await db.transaction(async (trx) => {
[newUserRole] = await trx
.update(userOrgs)
.set({ roleId })
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, role.orgId)
)
)
const inserted = await trx
.insert(userOrgRoles)
.values({
userId,
orgId: role.orgId,
roleId
})
.onConflictDoNothing()
.returning();
// get the client associated with this user in this org
if (inserted.length > 0) {
newUserRole = inserted[0];
}
const orgClients = await trx
.select()
.from(clients)
@@ -133,17 +136,15 @@ export async function addUserRole(
eq(clients.userId, userId),
eq(clients.orgId, role.orgId)
)
)
.limit(1);
);
for (const orgClient of orgClients) {
// we just changed the user's role, so we need to rebuild client associations and what they have access to
await rebuildClientAssociationsFromClient(orgClient, trx);
}
});
return response(res, {
data: newUserRole,
data: newUserRole ?? { userId, orgId: role.orgId, roleId },
success: true,
error: false,
message: "Role added to user successfully",

View File

@@ -221,12 +221,16 @@ export async function createOrgUser(
);
}
await assignUserToOrg(org, {
orgId,
userId: existingUser.userId,
roleId: role.roleId,
autoProvisioned: false
}, trx);
await assignUserToOrg(
org,
{
orgId,
userId: existingUser.userId,
autoProvisioned: false,
},
role.roleId,
trx
);
} else {
userId = generateId(15);
@@ -244,12 +248,16 @@ export async function createOrgUser(
})
.returning();
await assignUserToOrg(org, {
orgId,
userId: newUser.userId,
roleId: role.roleId,
autoProvisioned: false
}, trx);
await assignUserToOrg(
org,
{
orgId,
userId: newUser.userId,
autoProvisioned: false,
},
role.roleId,
trx
);
}
await calculateUserClientsForOrgs(userId, trx);

View File

@@ -1,7 +1,7 @@
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 { roles, userOrgRoles, userOrgs, users } from "@server/db";
import { and, eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -12,7 +12,7 @@ import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import { OpenAPITags, registry } from "@server/openApi";
async function queryUser(orgId: string, userId: string) {
const [user] = await db
const [userRow] = await db
.select({
orgId: userOrgs.orgId,
userId: users.userId,
@@ -20,10 +20,7 @@ async function queryUser(orgId: string, userId: string) {
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,
@@ -33,13 +30,40 @@ async function queryUser(orgId: string, userId: string) {
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;
if (!userRow) return undefined;
const roleRows = await db
.select({
roleId: userOrgRoles.roleId,
roleName: roles.name,
isAdmin: roles.isAdmin
})
.from(userOrgRoles)
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
);
const isAdmin = roleRows.some((r) => r.isAdmin);
return {
...userRow,
isAdmin,
roleIds: roleRows.map((r) => r.roleId),
roles: roleRows.map((r) => ({
roleId: r.roleId,
name: r.roleName ?? ""
}))
};
}
export type GetOrgUserResponse = NonNullable<

View File

@@ -2,6 +2,7 @@ export * from "./getUser";
export * from "./removeUserOrg";
export * from "./listUsers";
export * from "./addUserRole";
export * from "./removeUserRole";
export * from "./inviteUser";
export * from "./acceptInvite";
export * from "./getOrgUser";

View File

@@ -1,11 +1,11 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, idpOidcConfig } from "@server/db";
import { idp, roles, userOrgs, users } from "@server/db";
import { idp, roles, userOrgRoles, userOrgs, users } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { and, sql } from "drizzle-orm";
import { sql } from "drizzle-orm";
import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
@@ -31,7 +31,7 @@ const listUsersSchema = z.strictObject({
});
async function queryUsers(orgId: string, limit: number, offset: number) {
return await db
const rows = await db
.select({
id: users.userId,
email: users.email,
@@ -41,8 +41,6 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
username: users.username,
name: users.name,
type: users.type,
roleId: userOrgs.roleId,
roleName: roles.name,
isOwner: userOrgs.isOwner,
idpName: idp.name,
idpId: users.idpId,
@@ -52,12 +50,39 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
})
.from(users)
.leftJoin(userOrgs, eq(users.userId, userOrgs.userId))
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
.leftJoin(idp, eq(users.idpId, idp.idpId))
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId))
.where(eq(userOrgs.orgId, orgId))
.limit(limit)
.offset(offset);
const roleRows = await db
.select({
userId: userOrgRoles.userId,
roleId: userOrgRoles.roleId,
roleName: roles.name
})
.from(userOrgRoles)
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(eq(userOrgRoles.orgId, orgId));
const rolesByUser = new Map<
string,
{ roleId: number; roleName: string }[]
>();
for (const r of roleRows) {
const list = rolesByUser.get(r.userId) ?? [];
list.push({ roleId: r.roleId, roleName: r.roleName ?? "" });
rolesByUser.set(r.userId, list);
}
return rows.map((row) => {
const userRoles = rolesByUser.get(row.id) ?? [];
return {
...row,
roles: userRoles
};
});
}
export type ListUsersResponse = {

View File

@@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from "express";
import { db, Olm, olms, orgs, userOrgs } from "@server/db";
import { db, Olm, olms, orgs, userOrgRoles, userOrgs } from "@server/db";
import { idp, users } from "@server/db";
import { and, eq } from "drizzle-orm";
import response from "@server/lib/response";
@@ -84,16 +84,31 @@ export async function myDevice(
.from(olms)
.where(and(eq(olms.userId, userId), eq(olms.olmId, olmId)));
const userOrganizations = await db
const userOrgRows = await db
.select({
orgId: userOrgs.orgId,
orgName: orgs.name,
roleId: userOrgs.roleId
orgName: orgs.name
})
.from(userOrgs)
.where(eq(userOrgs.userId, userId))
.innerJoin(orgs, eq(userOrgs.orgId, orgs.orgId));
const roleRows = await db
.select({
orgId: userOrgRoles.orgId,
roleId: userOrgRoles.roleId
})
.from(userOrgRoles)
.where(eq(userOrgRoles.userId, userId));
const roleByOrg = new Map(
roleRows.map((r) => [r.orgId, r.roleId])
);
const userOrganizations = userOrgRows.map((row) => ({
...row,
roleId: roleByOrg.get(row.orgId) ?? 0
}));
return response<MyDeviceResponse>(res, {
data: {
user,

View File

@@ -0,0 +1,157 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { userOrgRoles, userOrgs, roles, clients } from "@server/db";
import { eq, and } 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 stoi from "@server/lib/stoi";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
const removeUserRoleParamsSchema = z.strictObject({
userId: z.string(),
roleId: z.string().transform(stoi).pipe(z.number())
});
registry.registerPath({
method: "delete",
path: "/role/{roleId}/remove/{userId}",
description: "Remove a role from a user. User must have at least one role left in the org.",
tags: [OpenAPITags.Role, OpenAPITags.User],
request: {
params: removeUserRoleParamsSchema
},
responses: {}
});
export async function removeUserRole(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = removeUserRoleParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { userId, roleId } = parsedParams.data;
if (req.user && !req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You do not have access to this organization"
)
);
}
const [role] = await db
.select()
.from(roles)
.where(eq(roles.roleId, roleId))
.limit(1);
if (!role) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid role ID")
);
}
const [existingUser] = await db
.select()
.from(userOrgs)
.where(
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, role.orgId))
)
.limit(1);
if (!existingUser) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"User not found or does not belong to the specified organization"
)
);
}
if (existingUser.isOwner) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Cannot change the roles of the owner of the organization"
)
);
}
const remainingRoles = await db
.select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, role.orgId)
)
);
if (remainingRoles.length <= 1) {
const hasThisRole = remainingRoles.some((r) => r.roleId === roleId);
if (hasThisRole) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User must have at least one role in the organization. Remove the last role is not allowed."
)
);
}
}
await db.transaction(async (trx) => {
await trx
.delete(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, role.orgId),
eq(userOrgRoles.roleId, roleId)
)
);
const orgClients = await trx
.select()
.from(clients)
.where(
and(
eq(clients.userId, userId),
eq(clients.orgId, role.orgId)
)
);
for (const orgClient of orgClients) {
await rebuildClientAssociationsFromClient(orgClient, trx);
}
});
return response(res, {
data: { userId, orgId: role.orgId, roleId },
success: true,
error: false,
message: "Role removed from user successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}