mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-31 15:06:42 +00:00
Merge branch 'multi-role' of https://github.com/fosrl/pangolin into multi-role
This commit is contained in:
@@ -1,4 +1,12 @@
|
|||||||
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs, roles } from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
loginPage,
|
||||||
|
LoginPage,
|
||||||
|
loginPageOrg,
|
||||||
|
Org,
|
||||||
|
orgs,
|
||||||
|
roles
|
||||||
|
} from "@server/db";
|
||||||
import {
|
import {
|
||||||
Resource,
|
Resource,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
@@ -12,14 +20,12 @@ import {
|
|||||||
resources,
|
resources,
|
||||||
roleResources,
|
roleResources,
|
||||||
sessions,
|
sessions,
|
||||||
userOrgRoles,
|
|
||||||
userOrgs,
|
|
||||||
userResources,
|
userResources,
|
||||||
users,
|
users,
|
||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
resourceHeaderAuthExtendedCompatibility
|
resourceHeaderAuthExtendedCompatibility
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
export type ResourceWithAuth = {
|
export type ResourceWithAuth = {
|
||||||
resource: Resource | null;
|
resource: Resource | null;
|
||||||
@@ -121,7 +127,7 @@ export async function getRoleName(roleId: number): Promise<string | null> {
|
|||||||
*/
|
*/
|
||||||
export async function getRoleResourceAccess(
|
export async function getRoleResourceAccess(
|
||||||
resourceId: number,
|
resourceId: number,
|
||||||
roleId: number
|
roleIds: number[]
|
||||||
) {
|
) {
|
||||||
const roleResourceAccess = await db
|
const roleResourceAccess = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -129,12 +135,11 @@ export async function getRoleResourceAccess(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(roleResources.resourceId, resourceId),
|
eq(roleResources.resourceId, resourceId),
|
||||||
eq(roleResources.roleId, roleId)
|
inArray(roleResources.roleId, roleIds)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
|
return roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const configSchema = z
|
|||||||
.default(3001)
|
.default(3001)
|
||||||
.transform(stoi)
|
.transform(stoi)
|
||||||
.pipe(portSchema),
|
.pipe(portSchema),
|
||||||
|
badger_override: z.string().optional(),
|
||||||
next_port: portSchema
|
next_port: portSchema
|
||||||
.optional()
|
.optional()
|
||||||
.default(3002)
|
.default(3002)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { db, userOrgRoles } from "@server/db";
|
import { db, roles, userOrgRoles } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,3 +20,17 @@ export async function getUserOrgRoleIds(
|
|||||||
);
|
);
|
||||||
return rows.map((r) => r.roleId);
|
return rows.map((r) => r.roleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserOrgRoles(
|
||||||
|
userId: string,
|
||||||
|
orgId: string
|
||||||
|
): Promise<{ roleId: number; roleName: string }[]> {
|
||||||
|
const rows = await db
|
||||||
|
.select({ roleId: userOrgRoles.roleId, roleName: roles.name })
|
||||||
|
.from(userOrgRoles)
|
||||||
|
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||||
|
.where(
|
||||||
|
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,6 +124,23 @@ const getRoleResourceAccessParamsSchema = z.strictObject({
|
|||||||
.pipe(z.int().positive("Resource ID must be a positive integer"))
|
.pipe(z.int().positive("Resource ID must be a positive integer"))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getResourceAccessParamsSchema = z.strictObject({
|
||||||
|
resourceId: z
|
||||||
|
.string()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.int().positive("Resource ID must be a positive integer"))
|
||||||
|
});
|
||||||
|
|
||||||
|
const getResourceAccessQuerySchema = z.strictObject({
|
||||||
|
roleIds: z
|
||||||
|
.union([z.array(z.string()), z.string()])
|
||||||
|
.transform((val) =>
|
||||||
|
(Array.isArray(val) ? val : [val])
|
||||||
|
.map(Number)
|
||||||
|
.filter((n) => !isNaN(n))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
const getUserResourceAccessParamsSchema = z.strictObject({
|
const getUserResourceAccessParamsSchema = z.strictObject({
|
||||||
userId: z.string().min(1, "User ID is required"),
|
userId: z.string().min(1, "User ID is required"),
|
||||||
resourceId: z
|
resourceId: z
|
||||||
@@ -769,7 +786,7 @@ hybridRouter.get(
|
|||||||
|
|
||||||
// Get user organization role
|
// Get user organization role
|
||||||
hybridRouter.get(
|
hybridRouter.get(
|
||||||
"/user/:userId/org/:orgId/role",
|
"/user/:userId/org/:orgId/roles",
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const parsedParams = getUserOrgRoleParamsSchema.safeParse(
|
const parsedParams = getUserOrgRoleParamsSchema.safeParse(
|
||||||
@@ -805,6 +822,82 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userOrgRoleRows = await db
|
||||||
|
.select({ roleId: userOrgRoles.roleId, roleName: roles.name })
|
||||||
|
.from(userOrgRoles)
|
||||||
|
.innerJoin(roles, eq(roles.roleId, userOrgRoles.roleId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgRoles.userId, userId),
|
||||||
|
eq(userOrgRoles.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(`User ${userId} has roles in org ${orgId}:`, userOrgRoleRows);
|
||||||
|
|
||||||
|
return response<{ roleId: number, roleName: string }[]>(res, {
|
||||||
|
data: userOrgRoleRows,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message:
|
||||||
|
userOrgRoleRows.length > 0
|
||||||
|
? "User org roles retrieved successfully"
|
||||||
|
: "User has no roles in this organization",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get user org role"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// DEPRICATED Get user organization role
|
||||||
|
// used for backward compatibility with old remote nodes
|
||||||
|
hybridRouter.get(
|
||||||
|
"/user/:userId/org/:orgId/role", // <- note the missing s
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedParams = getUserOrgRoleParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, orgId } = parsedParams.data;
|
||||||
|
const remoteExitNode = req.remoteExitNode;
|
||||||
|
|
||||||
|
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Remote exit node not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.UNAUTHORIZED,
|
||||||
|
"User is not authorized to access this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the roles on the user
|
||||||
|
|
||||||
const userOrgRoleRows = await db
|
const userOrgRoleRows = await db
|
||||||
.select({ roleId: userOrgRoles.roleId })
|
.select({ roleId: userOrgRoles.roleId })
|
||||||
.from(userOrgRoles)
|
.from(userOrgRoles)
|
||||||
@@ -817,8 +910,35 @@ hybridRouter.get(
|
|||||||
|
|
||||||
const roleIds = userOrgRoleRows.map((r) => r.roleId);
|
const roleIds = userOrgRoleRows.map((r) => r.roleId);
|
||||||
|
|
||||||
return response<number[]>(res, {
|
let roleId: number | null = null;
|
||||||
data: roleIds,
|
|
||||||
|
if (userOrgRoleRows.length === 0) {
|
||||||
|
// User has no roles in this organization
|
||||||
|
roleId = null;
|
||||||
|
} else if (userOrgRoleRows.length === 1) {
|
||||||
|
// User has exactly one role, return it
|
||||||
|
roleId = userOrgRoleRows[0].roleId;
|
||||||
|
} else {
|
||||||
|
// User has multiple roles
|
||||||
|
// Check if any of these roles are also assigned to a resource
|
||||||
|
// If we find a match, prefer that role; otherwise return the first role
|
||||||
|
// Get all resources that have any of these roles assigned
|
||||||
|
const roleResourceMatches = await db
|
||||||
|
.select({ roleId: roleResources.roleId })
|
||||||
|
.from(roleResources)
|
||||||
|
.where(inArray(roleResources.roleId, roleIds))
|
||||||
|
.limit(1);
|
||||||
|
if (roleResourceMatches.length > 0) {
|
||||||
|
// Return the first role that's also on a resource
|
||||||
|
roleId = roleResourceMatches[0].roleId;
|
||||||
|
} else {
|
||||||
|
// No resource match found, return the first role
|
||||||
|
roleId = userOrgRoleRows[0].roleId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<{ roleId: number | null }>(res, {
|
||||||
|
data: { roleId },
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message:
|
message:
|
||||||
@@ -939,7 +1059,9 @@ hybridRouter.get(
|
|||||||
data: role?.name ?? null,
|
data: role?.name ?? null,
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: role ? "Role name retrieved successfully" : "Role not found",
|
message: role
|
||||||
|
? "Role name retrieved successfully"
|
||||||
|
: "Role not found",
|
||||||
status: HttpCode.OK
|
status: HttpCode.OK
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1039,6 +1161,101 @@ hybridRouter.get(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if role has access to resource
|
||||||
|
hybridRouter.get(
|
||||||
|
"/resource/:resourceId/access",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedParams = getResourceAccessParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourceId } = parsedParams.data;
|
||||||
|
const parsedQuery = getResourceAccessQuerySchema.safeParse(
|
||||||
|
req.query
|
||||||
|
);
|
||||||
|
const roleIds = parsedQuery.success ? parsedQuery.data.roleIds : [];
|
||||||
|
|
||||||
|
const remoteExitNode = req.remoteExitNode;
|
||||||
|
|
||||||
|
if (!remoteExitNode?.exitNodeId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Remote exit node not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [resource] = await db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(eq(resources.resourceId, resourceId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (
|
||||||
|
await checkExitNodeOrg(
|
||||||
|
remoteExitNode.exitNodeId,
|
||||||
|
resource.orgId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// If the exit node is not allowed for the org, return an error
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Exit node not allowed for this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleResourceAccess = await db
|
||||||
|
.select({
|
||||||
|
resourceId: roleResources.resourceId,
|
||||||
|
roleId: roleResources.roleId
|
||||||
|
})
|
||||||
|
.from(roleResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleResources.resourceId, resourceId),
|
||||||
|
inArray(roleResources.roleId, roleIds)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result =
|
||||||
|
roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
||||||
|
|
||||||
|
return response<{ resourceId: number; roleId: number }[] | null>(
|
||||||
|
res,
|
||||||
|
{
|
||||||
|
data: result,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: result
|
||||||
|
? "Role resource access retrieved successfully"
|
||||||
|
: "Role resource access not found",
|
||||||
|
status: HttpCode.OK
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get role resource access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has direct access to resource
|
// Check if user has direct access to resource
|
||||||
hybridRouter.get(
|
hybridRouter.get(
|
||||||
"/user/:userId/resource/:resourceId/access",
|
"/user/:userId/resource/:resourceId/access",
|
||||||
@@ -1937,7 +2154,8 @@ hybridRouter.post(
|
|||||||
// userAgent: data.userAgent, // TODO: add this
|
// userAgent: data.userAgent, // TODO: add this
|
||||||
// headers: data.body.headers,
|
// headers: data.body.headers,
|
||||||
// query: data.body.query,
|
// query: data.body.query,
|
||||||
originalRequestURL: sanitizeString(logEntry.originalRequestURL) ?? "",
|
originalRequestURL:
|
||||||
|
sanitizeString(logEntry.originalRequestURL) ?? "",
|
||||||
scheme: sanitizeString(logEntry.scheme) ?? "",
|
scheme: sanitizeString(logEntry.scheme) ?? "",
|
||||||
host: sanitizeString(logEntry.host) ?? "",
|
host: sanitizeString(logEntry.host) ?? "",
|
||||||
path: sanitizeString(logEntry.path) ?? "",
|
path: sanitizeString(logEntry.path) ?? "",
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToke
|
|||||||
import {
|
import {
|
||||||
getResourceByDomain,
|
getResourceByDomain,
|
||||||
getResourceRules,
|
getResourceRules,
|
||||||
getRoleName,
|
|
||||||
getRoleResourceAccess,
|
getRoleResourceAccess,
|
||||||
getUserResourceAccess,
|
getUserResourceAccess,
|
||||||
getOrgLoginPage,
|
getOrgLoginPage,
|
||||||
getUserSessionWithUser
|
getUserSessionWithUser
|
||||||
} from "@server/db/queries/verifySessionQueries";
|
} from "@server/db/queries/verifySessionQueries";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoles } from "@server/lib/userOrgRoles";
|
||||||
import {
|
import {
|
||||||
LoginPage,
|
LoginPage,
|
||||||
Org,
|
Org,
|
||||||
@@ -31,7 +30,6 @@ import { z } from "zod";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { getCountryCodeForIp } from "@server/lib/geoip";
|
import { getCountryCodeForIp } from "@server/lib/geoip";
|
||||||
import { getAsnForIp } from "@server/lib/asn";
|
import { getAsnForIp } from "@server/lib/asn";
|
||||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
|
||||||
import { verifyPassword } from "@server/auth/password";
|
import { verifyPassword } from "@server/auth/password";
|
||||||
import {
|
import {
|
||||||
checkOrgAccessPolicy,
|
checkOrgAccessPolicy,
|
||||||
@@ -798,7 +796,8 @@ async function notAllowed(
|
|||||||
) {
|
) {
|
||||||
let loginPage: LoginPage | null = null;
|
let loginPage: LoginPage | null = null;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
const subscribed = await isSubscribed( // this is fine because the org login page is only a saas feature
|
const subscribed = await isSubscribed(
|
||||||
|
// this is fine because the org login page is only a saas feature
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix.loginPageDomain
|
tierMatrix.loginPageDomain
|
||||||
);
|
);
|
||||||
@@ -855,7 +854,10 @@ async function headerAuthChallenged(
|
|||||||
) {
|
) {
|
||||||
let loginPage: LoginPage | null = null;
|
let loginPage: LoginPage | null = null;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain); // this is fine because the org login page is only a saas feature
|
const subscribed = await isSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix.loginPageDomain
|
||||||
|
); // this is fine because the org login page is only a saas feature
|
||||||
if (subscribed) {
|
if (subscribed) {
|
||||||
loginPage = await getOrgLoginPage(orgId);
|
loginPage = await getOrgLoginPage(orgId);
|
||||||
}
|
}
|
||||||
@@ -917,9 +919,9 @@ async function isUserAllowedToAccessResource(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userOrgRoleIds = await getUserOrgRoleIds(user.userId, resource.orgId);
|
const userOrgRoles = await getUserOrgRoles(user.userId, resource.orgId);
|
||||||
|
|
||||||
if (!userOrgRoleIds.length) {
|
if (!userOrgRoles.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,23 +937,16 @@ async function isUserAllowedToAccessResource(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleNames: string[] = [];
|
|
||||||
for (const roleId of userOrgRoleIds) {
|
|
||||||
const roleResourceAccess = await getRoleResourceAccess(
|
const roleResourceAccess = await getRoleResourceAccess(
|
||||||
resource.resourceId,
|
resource.resourceId,
|
||||||
roleId
|
userOrgRoles.map((r) => r.roleId)
|
||||||
);
|
);
|
||||||
if (roleResourceAccess) {
|
if (roleResourceAccess && roleResourceAccess.length > 0) {
|
||||||
const roleName = await getRoleName(roleId);
|
|
||||||
if (roleName) roleNames.push(roleName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (roleNames.length > 0) {
|
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: roleNames.join(", ")
|
role: userOrgRoles.map((r) => r.roleName).join(", ")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,15 +956,11 @@ async function isUserAllowedToAccessResource(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (userResourceAccess) {
|
if (userResourceAccess) {
|
||||||
const names = await Promise.all(
|
|
||||||
userOrgRoleIds.map((id) => getRoleName(id))
|
|
||||||
);
|
|
||||||
const role = names.filter(Boolean).join(", ") || "";
|
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role
|
role: userOrgRoles.map((r) => r.roleName).join(", ")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,13 @@ export async function traefikConfigProvider(
|
|||||||
traefikConfig.http.middlewares[badgerMiddlewareName] = {
|
traefikConfig.http.middlewares[badgerMiddlewareName] = {
|
||||||
plugin: {
|
plugin: {
|
||||||
[badgerMiddlewareName]: {
|
[badgerMiddlewareName]: {
|
||||||
apiBaseUrl: new URL(
|
apiBaseUrl:
|
||||||
|
config.getRawConfig().server.badger_override ||
|
||||||
|
new URL(
|
||||||
"/api/v1",
|
"/api/v1",
|
||||||
`http://${
|
`http://${
|
||||||
config.getRawConfig().server.internal_hostname
|
config.getRawConfig().server
|
||||||
|
.internal_hostname
|
||||||
}:${config.getRawConfig().server.internal_port}`
|
}:${config.getRawConfig().server.internal_port}`
|
||||||
).href,
|
).href,
|
||||||
userSessionCookieName:
|
userSessionCookieName:
|
||||||
@@ -61,7 +64,7 @@ export async function traefikConfigProvider(
|
|||||||
|
|
||||||
return res.status(HttpCode.OK).json(traefikConfig);
|
return res.status(HttpCode.OK).json(traefikConfig);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to build Traefik config: ${e}`);
|
logger.error(e);
|
||||||
return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
|
return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
|
||||||
error: "Failed to build Traefik config"
|
error: "Failed to build Traefik config"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import m12 from "./scriptsPg/1.15.0";
|
|||||||
import m13 from "./scriptsPg/1.15.3";
|
import m13 from "./scriptsPg/1.15.3";
|
||||||
import m14 from "./scriptsPg/1.15.4";
|
import m14 from "./scriptsPg/1.15.4";
|
||||||
import m15 from "./scriptsPg/1.16.0";
|
import m15 from "./scriptsPg/1.16.0";
|
||||||
|
import m16 from "./scriptsPg/1.17.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -41,7 +42,8 @@ const migrations = [
|
|||||||
{ version: "1.15.0", run: m12 },
|
{ version: "1.15.0", run: m12 },
|
||||||
{ version: "1.15.3", run: m13 },
|
{ version: "1.15.3", run: m13 },
|
||||||
{ version: "1.15.4", run: m14 },
|
{ version: "1.15.4", run: m14 },
|
||||||
{ version: "1.16.0", run: m15 }
|
{ version: "1.16.0", run: m15 },
|
||||||
|
{ version: "1.17.0", run: m16 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import m33 from "./scriptsSqlite/1.15.0";
|
|||||||
import m34 from "./scriptsSqlite/1.15.3";
|
import m34 from "./scriptsSqlite/1.15.3";
|
||||||
import m35 from "./scriptsSqlite/1.15.4";
|
import m35 from "./scriptsSqlite/1.15.4";
|
||||||
import m36 from "./scriptsSqlite/1.16.0";
|
import m36 from "./scriptsSqlite/1.16.0";
|
||||||
|
import m37 from "./scriptsSqlite/1.17.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -75,7 +76,8 @@ const migrations = [
|
|||||||
{ version: "1.15.0", run: m33 },
|
{ version: "1.15.0", run: m33 },
|
||||||
{ version: "1.15.3", run: m34 },
|
{ version: "1.15.3", run: m34 },
|
||||||
{ version: "1.15.4", run: m35 },
|
{ version: "1.15.4", run: m35 },
|
||||||
{ version: "1.16.0", run: m36 }
|
{ version: "1.16.0", run: m36 },
|
||||||
|
{ version: "1.17.0", run: m37 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
73
server/setup/scriptsPg/1.17.0.ts
Normal file
73
server/setup/scriptsPg/1.17.0.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
const version = "1.17.0";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
// Query existing roleId data from userOrgs before the transaction destroys it
|
||||||
|
const existingRolesQuery = await db.execute(
|
||||||
|
sql`SELECT "userId", "orgId", "roleId" FROM "userOrgs" WHERE "roleId" IS NOT NULL`
|
||||||
|
);
|
||||||
|
const existingUserOrgRoles = existingRolesQuery.rows as {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
roleId: number;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found ${existingUserOrgRoles.length} existing userOrgs role assignment(s) to migrate`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE "userOrgRoles" (
|
||||||
|
"userId" varchar NOT NULL,
|
||||||
|
"orgId" varchar NOT NULL,
|
||||||
|
"roleId" integer NOT NULL,
|
||||||
|
CONSTRAINT "userOrgRoles_userId_orgId_roleId_unique" UNIQUE("userId","orgId","roleId")
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
await db.execute(sql`ALTER TABLE "userOrgs" DROP CONSTRAINT "userOrgs_roleId_roles_roleId_fk";`);
|
||||||
|
await db.execute(sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`);
|
||||||
|
await db.execute(sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`);
|
||||||
|
await db.execute(sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;`);
|
||||||
|
await db.execute(sql`ALTER TABLE "userOrgs" DROP COLUMN "roleId";`);
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Unable to migrate database");
|
||||||
|
console.log(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-insert the preserved role assignments into the new userOrgRoles table
|
||||||
|
if (existingUserOrgRoles.length > 0) {
|
||||||
|
try {
|
||||||
|
for (const row of existingUserOrgRoles) {
|
||||||
|
await db.execute(sql`
|
||||||
|
INSERT INTO "userOrgRoles" ("userId", "orgId", "roleId")
|
||||||
|
VALUES (${row.userId}, ${row.orgId}, ${row.roleId})
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Migrated ${existingUserOrgRoles.length} role assignment(s) into userOrgRoles`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
"Error while migrating role assignments into userOrgRoles:",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
96
server/setup/scriptsSqlite/1.17.0.ts
Normal file
96
server/setup/scriptsSqlite/1.17.0.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const version = "1.17.0";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
const db = new Database(location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.pragma("foreign_keys = OFF");
|
||||||
|
|
||||||
|
// Query existing roleId data from userOrgs before the transaction destroys it
|
||||||
|
const existingUserOrgRoles = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT "userId", "orgId", "roleId" FROM 'userOrgs' WHERE "roleId" IS NOT NULL`
|
||||||
|
)
|
||||||
|
.all() as { userId: string; orgId: string; roleId: number }[];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found ${existingUserOrgRoles.length} existing userOrgs role assignment(s) to migrate`
|
||||||
|
);
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
CREATE TABLE 'userOrgRoles' (
|
||||||
|
'userId' text NOT NULL,
|
||||||
|
'orgId' text NOT NULL,
|
||||||
|
'roleId' integer NOT NULL,
|
||||||
|
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`CREATE UNIQUE INDEX 'userOrgRoles_userId_orgId_roleId_unique' ON 'userOrgRoles' ('userId','orgId','roleId');`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
CREATE TABLE '__new_userOrgs' (
|
||||||
|
'userId' text NOT NULL,
|
||||||
|
'orgId' text NOT NULL,
|
||||||
|
'isOwner' integer DEFAULT false NOT NULL,
|
||||||
|
'autoProvisioned' integer DEFAULT false,
|
||||||
|
'pamUsername' text,
|
||||||
|
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`INSERT INTO '__new_userOrgs'("userId", "orgId", "isOwner", "autoProvisioned", "pamUsername") SELECT "userId", "orgId", "isOwner", "autoProvisioned", "pamUsername" FROM 'userOrgs';`
|
||||||
|
).run();
|
||||||
|
db.prepare(`DROP TABLE 'userOrgs';`).run();
|
||||||
|
db.prepare(
|
||||||
|
`ALTER TABLE '__new_userOrgs' RENAME TO 'userOrgs';`
|
||||||
|
).run();
|
||||||
|
})();
|
||||||
|
|
||||||
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
|
// Re-insert the preserved role assignments into the new userOrgRoles table
|
||||||
|
if (existingUserOrgRoles.length > 0) {
|
||||||
|
const insertUserOrgRole = db.prepare(
|
||||||
|
`INSERT OR IGNORE INTO 'userOrgRoles' ("userId", "orgId", "roleId") VALUES (?, ?, ?)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const insertAll = db.transaction(() => {
|
||||||
|
for (const row of existingUserOrgRoles) {
|
||||||
|
insertUserOrgRole.run(row.userId, row.orgId, row.roleId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
insertAll();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Migrated ${existingUserOrgRoles.length} role assignment(s) into userOrgRoles`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Migrated database`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user