mirror of
https://github.com/fosrl/pangolin.git
synced 2025-12-14 20:16:38 +00:00
Site resources for the blueprint
This commit is contained in:
863
server/lib/blueprints/proxyResources.ts
Normal file
863
server/lib/blueprints/proxyResources.ts
Normal file
@@ -0,0 +1,863 @@
|
||||
import {
|
||||
domains,
|
||||
orgDomains,
|
||||
Resource,
|
||||
resourcePincode,
|
||||
resourceRules,
|
||||
resourceWhitelist,
|
||||
roleResources,
|
||||
roles,
|
||||
Target,
|
||||
Transaction,
|
||||
userOrgs,
|
||||
userResources,
|
||||
users
|
||||
} from "@server/db";
|
||||
import { resources, targets, sites } from "@server/db";
|
||||
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
||||
import {
|
||||
Config,
|
||||
ConfigSchema,
|
||||
isTargetsOnlyResource,
|
||||
TargetData
|
||||
} from "./types";
|
||||
import logger from "@server/logger";
|
||||
import { pickPort } from "@server/routers/target/helpers";
|
||||
import { resourcePassword } from "@server/db";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||
|
||||
export type ProxyResourcesResults = {
|
||||
proxyResource: Resource;
|
||||
targetsToUpdate: Target[];
|
||||
}[];
|
||||
|
||||
export async function updateProxyResources(
|
||||
orgId: string,
|
||||
config: Config,
|
||||
trx: Transaction,
|
||||
siteId?: number
|
||||
): Promise<ProxyResourcesResults> {
|
||||
const results: ProxyResourcesResults = [];
|
||||
|
||||
for (const [resourceNiceId, resourceData] of Object.entries(
|
||||
config["proxy-resources"]
|
||||
)) {
|
||||
const targetsToUpdate: Target[] = [];
|
||||
let resource: Resource;
|
||||
|
||||
async function createTarget( // reusable function to create a target
|
||||
resourceId: number,
|
||||
targetData: TargetData
|
||||
) {
|
||||
const targetSiteId = targetData.site;
|
||||
let site;
|
||||
|
||||
if (targetSiteId) {
|
||||
// Look up site by niceId
|
||||
[site] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.niceId, targetSiteId),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
} else if (siteId) {
|
||||
// Use the provided siteId directly, but verify it belongs to the org
|
||||
[site] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||
)
|
||||
.limit(1);
|
||||
} else {
|
||||
throw new Error(`Target site is required`);
|
||||
}
|
||||
|
||||
if (!site) {
|
||||
throw new Error(
|
||||
`Site not found: ${targetSiteId} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
let internalPortToCreate;
|
||||
if (!targetData["internal-port"]) {
|
||||
const { internalPort, targetIps } = await pickPort(
|
||||
site.siteId!,
|
||||
trx
|
||||
);
|
||||
internalPortToCreate = internalPort;
|
||||
} else {
|
||||
internalPortToCreate = targetData["internal-port"];
|
||||
}
|
||||
|
||||
// Create target
|
||||
const [newTarget] = await trx
|
||||
.insert(targets)
|
||||
.values({
|
||||
resourceId: resourceId,
|
||||
siteId: site.siteId,
|
||||
ip: targetData.hostname,
|
||||
method: targetData.method,
|
||||
port: targetData.port,
|
||||
enabled: targetData.enabled,
|
||||
internalPort: internalPortToCreate,
|
||||
path: targetData.path,
|
||||
pathMatchType: targetData.pathMatchType
|
||||
})
|
||||
.returning();
|
||||
|
||||
targetsToUpdate.push(newTarget);
|
||||
}
|
||||
|
||||
// Find existing resource by niceId and orgId
|
||||
const [existingResource] = await trx
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.niceId, resourceNiceId),
|
||||
eq(resources.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const http = resourceData.protocol == "http";
|
||||
const protocol =
|
||||
resourceData.protocol == "http" ? "tcp" : resourceData.protocol;
|
||||
const resourceEnabled =
|
||||
resourceData.enabled == undefined || resourceData.enabled == null
|
||||
? true
|
||||
: resourceData.enabled;
|
||||
const resourceSsl =
|
||||
resourceData.ssl == undefined || resourceData.ssl == null
|
||||
? true
|
||||
: resourceData.ssl;
|
||||
let headers = "";
|
||||
for (const headerObj of resourceData.headers || []) {
|
||||
for (const [key, value] of Object.entries(headerObj)) {
|
||||
headers += `${key}: ${value},`;
|
||||
}
|
||||
}
|
||||
// if there are headers, remove the trailing comma
|
||||
if (headers.endsWith(",")) {
|
||||
headers = headers.slice(0, -1);
|
||||
}
|
||||
|
||||
if (existingResource) {
|
||||
let domain;
|
||||
if (http) {
|
||||
domain = await getDomain(
|
||||
existingResource.resourceId,
|
||||
resourceData["full-domain"]!,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// check if the only key in the resource is targets, if so, skip the update
|
||||
if (isTargetsOnlyResource(resourceData)) {
|
||||
logger.debug(
|
||||
`Skipping update for resource ${existingResource.resourceId} as only targets are provided`
|
||||
);
|
||||
resource = existingResource;
|
||||
} else {
|
||||
// Update existing resource
|
||||
[resource] = await trx
|
||||
.update(resources)
|
||||
.set({
|
||||
name: resourceData.name || "Unnamed Resource",
|
||||
protocol: protocol || "http",
|
||||
http: http,
|
||||
proxyPort: http ? null : resourceData["proxy-port"],
|
||||
fullDomain: http ? resourceData["full-domain"] : null,
|
||||
subdomain: domain ? domain.subdomain : null,
|
||||
domainId: domain ? domain.domainId : null,
|
||||
enabled: resourceEnabled,
|
||||
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||
ssl: resourceSsl,
|
||||
setHostHeader: resourceData["host-header"] || null,
|
||||
tlsServerName: resourceData["tls-server-name"] || null,
|
||||
emailWhitelistEnabled: resourceData.auth?.[
|
||||
"whitelist-users"
|
||||
]
|
||||
? resourceData.auth["whitelist-users"].length > 0
|
||||
: false,
|
||||
headers: headers || null,
|
||||
applyRules:
|
||||
resourceData.rules && resourceData.rules.length > 0
|
||||
})
|
||||
.where(
|
||||
eq(resources.resourceId, existingResource.resourceId)
|
||||
)
|
||||
.returning();
|
||||
|
||||
await trx
|
||||
.delete(resourcePassword)
|
||||
.where(
|
||||
eq(
|
||||
resourcePassword.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
);
|
||||
if (resourceData.auth?.password) {
|
||||
const passwordHash = await hashPassword(
|
||||
resourceData.auth.password
|
||||
);
|
||||
|
||||
await trx.insert(resourcePassword).values({
|
||||
resourceId: existingResource.resourceId,
|
||||
passwordHash
|
||||
});
|
||||
}
|
||||
|
||||
await trx
|
||||
.delete(resourcePincode)
|
||||
.where(
|
||||
eq(
|
||||
resourcePincode.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
);
|
||||
if (resourceData.auth?.pincode) {
|
||||
const pincodeHash = await hashPassword(
|
||||
resourceData.auth.pincode.toString()
|
||||
);
|
||||
|
||||
await trx.insert(resourcePincode).values({
|
||||
resourceId: existingResource.resourceId,
|
||||
pincodeHash,
|
||||
digitLength: 6
|
||||
});
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["sso-roles"]) {
|
||||
const ssoRoles = resourceData.auth?.["sso-roles"];
|
||||
await syncRoleResources(
|
||||
existingResource.resourceId,
|
||||
ssoRoles,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["sso-users"]) {
|
||||
const ssoUsers = resourceData.auth?.["sso-users"];
|
||||
await syncUserResources(
|
||||
existingResource.resourceId,
|
||||
ssoUsers,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["whitelist-users"]) {
|
||||
const whitelistUsers =
|
||||
resourceData.auth?.["whitelist-users"];
|
||||
await syncWhitelistUsers(
|
||||
existingResource.resourceId,
|
||||
whitelistUsers,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const existingResourceTargets = await trx
|
||||
.select()
|
||||
.from(targets)
|
||||
.where(eq(targets.resourceId, existingResource.resourceId))
|
||||
.orderBy(asc(targets.targetId));
|
||||
|
||||
// Create new targets
|
||||
for (const [index, targetData] of resourceData.targets.entries()) {
|
||||
if (
|
||||
!targetData ||
|
||||
(typeof targetData === "object" &&
|
||||
Object.keys(targetData).length === 0)
|
||||
) {
|
||||
// If targetData is null or an empty object, we can skip it
|
||||
continue;
|
||||
}
|
||||
const existingTarget = existingResourceTargets[index];
|
||||
|
||||
if (existingTarget) {
|
||||
const targetSiteId = targetData.site;
|
||||
let site;
|
||||
|
||||
if (targetSiteId) {
|
||||
// Look up site by niceId
|
||||
[site] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.niceId, targetSiteId),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
} else if (siteId) {
|
||||
// Use the provided siteId directly, but verify it belongs to the org
|
||||
[site] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.siteId, siteId),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
} else {
|
||||
throw new Error(`Target site is required`);
|
||||
}
|
||||
|
||||
if (!site) {
|
||||
throw new Error(
|
||||
`Site not found: ${targetSiteId} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
// update this target
|
||||
const [updatedTarget] = await trx
|
||||
.update(targets)
|
||||
.set({
|
||||
siteId: site.siteId,
|
||||
ip: targetData.hostname,
|
||||
method: http ? targetData.method : null,
|
||||
port: targetData.port,
|
||||
enabled: targetData.enabled,
|
||||
path: targetData.path,
|
||||
pathMatchType: targetData.pathMatchType
|
||||
})
|
||||
.where(eq(targets.targetId, existingTarget.targetId))
|
||||
.returning();
|
||||
|
||||
if (checkIfTargetChanged(existingTarget, updatedTarget)) {
|
||||
let internalPortToUpdate;
|
||||
if (!targetData["internal-port"]) {
|
||||
const { internalPort, targetIps } = await pickPort(
|
||||
site.siteId!,
|
||||
trx
|
||||
);
|
||||
internalPortToUpdate = internalPort;
|
||||
} else {
|
||||
internalPortToUpdate = targetData["internal-port"];
|
||||
}
|
||||
|
||||
const [finalUpdatedTarget] = await trx // this double is so we can check the whole target before and after
|
||||
.update(targets)
|
||||
.set({
|
||||
internalPort: internalPortToUpdate
|
||||
})
|
||||
.where(
|
||||
eq(targets.targetId, existingTarget.targetId)
|
||||
)
|
||||
.returning();
|
||||
|
||||
targetsToUpdate.push(finalUpdatedTarget);
|
||||
}
|
||||
} else {
|
||||
await createTarget(existingResource.resourceId, targetData);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingResourceTargets.length > resourceData.targets.length) {
|
||||
const targetsToDelete = existingResourceTargets.slice(
|
||||
resourceData.targets.length
|
||||
);
|
||||
logger.debug(
|
||||
`Targets to delete: ${JSON.stringify(targetsToDelete)}`
|
||||
);
|
||||
for (const target of targetsToDelete) {
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
if (siteId && target.siteId !== siteId) {
|
||||
logger.debug(
|
||||
`Skipping target ${target.targetId} for deletion. Site ID does not match filter.`
|
||||
);
|
||||
continue; // only delete targets for the specified siteId
|
||||
}
|
||||
logger.debug(`Deleting target ${target.targetId}`);
|
||||
await trx
|
||||
.delete(targets)
|
||||
.where(eq(targets.targetId, target.targetId));
|
||||
}
|
||||
}
|
||||
|
||||
const existingRules = await trx
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(
|
||||
eq(resourceRules.resourceId, existingResource.resourceId)
|
||||
)
|
||||
.orderBy(resourceRules.priority);
|
||||
|
||||
// Sync rules
|
||||
for (const [index, rule] of resourceData.rules?.entries() || []) {
|
||||
const existingRule = existingRules[index];
|
||||
if (existingRule) {
|
||||
if (
|
||||
existingRule.action !== rule.action ||
|
||||
existingRule.match !== rule.match ||
|
||||
existingRule.value !== rule.value
|
||||
) {
|
||||
await trx
|
||||
.update(resourceRules)
|
||||
.set({
|
||||
action: rule.action,
|
||||
match: rule.match,
|
||||
value: rule.value
|
||||
})
|
||||
.where(
|
||||
eq(resourceRules.ruleId, existingRule.ruleId)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (existingRules.length > (resourceData.rules?.length || 0)) {
|
||||
const rulesToDelete = existingRules.slice(
|
||||
resourceData.rules?.length || 0
|
||||
);
|
||||
for (const rule of rulesToDelete) {
|
||||
await trx
|
||||
.delete(resourceRules)
|
||||
.where(eq(resourceRules.ruleId, rule.ruleId));
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Updated resource ${existingResource.resourceId}`);
|
||||
} else {
|
||||
// create a brand new resource
|
||||
let domain;
|
||||
if (http) {
|
||||
domain = await getDomain(
|
||||
undefined,
|
||||
resourceData["full-domain"]!,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// Create new resource
|
||||
const [newResource] = await trx
|
||||
.insert(resources)
|
||||
.values({
|
||||
orgId,
|
||||
niceId: resourceNiceId,
|
||||
name: resourceData.name || "Unnamed Resource",
|
||||
protocol: resourceData.protocol || "http",
|
||||
http: http,
|
||||
proxyPort: http ? null : resourceData["proxy-port"],
|
||||
fullDomain: http ? resourceData["full-domain"] : null,
|
||||
subdomain: domain ? domain.subdomain : null,
|
||||
domainId: domain ? domain.domainId : null,
|
||||
enabled: resourceEnabled,
|
||||
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||
setHostHeader: resourceData["host-header"] || null,
|
||||
tlsServerName: resourceData["tls-server-name"] || null,
|
||||
ssl: resourceSsl,
|
||||
headers: headers || null,
|
||||
applyRules:
|
||||
resourceData.rules && resourceData.rules.length > 0
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (resourceData.auth?.password) {
|
||||
const passwordHash = await hashPassword(
|
||||
resourceData.auth.password
|
||||
);
|
||||
|
||||
await trx.insert(resourcePassword).values({
|
||||
resourceId: newResource.resourceId,
|
||||
passwordHash
|
||||
});
|
||||
}
|
||||
|
||||
if (resourceData.auth?.pincode) {
|
||||
const pincodeHash = await hashPassword(
|
||||
resourceData.auth.pincode.toString()
|
||||
);
|
||||
|
||||
await trx.insert(resourcePincode).values({
|
||||
resourceId: newResource.resourceId,
|
||||
pincodeHash,
|
||||
digitLength: 6
|
||||
});
|
||||
}
|
||||
|
||||
resource = newResource;
|
||||
|
||||
const [adminRole] = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (!adminRole) {
|
||||
throw new Error(`Admin role not found`);
|
||||
}
|
||||
|
||||
await trx.insert(roleResources).values({
|
||||
roleId: adminRole.roleId,
|
||||
resourceId: newResource.resourceId
|
||||
});
|
||||
|
||||
if (resourceData.auth?.["sso-roles"]) {
|
||||
const ssoRoles = resourceData.auth?.["sso-roles"];
|
||||
await syncRoleResources(
|
||||
newResource.resourceId,
|
||||
ssoRoles,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["sso-users"]) {
|
||||
const ssoUsers = resourceData.auth?.["sso-users"];
|
||||
await syncUserResources(
|
||||
newResource.resourceId,
|
||||
ssoUsers,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["whitelist-users"]) {
|
||||
const whitelistUsers = resourceData.auth?.["whitelist-users"];
|
||||
await syncWhitelistUsers(
|
||||
newResource.resourceId,
|
||||
whitelistUsers,
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// Create new targets
|
||||
for (const targetData of resourceData.targets) {
|
||||
if (!targetData) {
|
||||
// If targetData is null or an empty object, we can skip it
|
||||
continue;
|
||||
}
|
||||
await createTarget(newResource.resourceId, targetData);
|
||||
}
|
||||
|
||||
for (const [index, rule] of resourceData.rules?.entries() || []) {
|
||||
if (rule.match === "cidr") {
|
||||
if (!isValidCIDR(rule.value)) {
|
||||
throw new Error(`Invalid CIDR provided: ${rule.value}`);
|
||||
}
|
||||
} else if (rule.match === "ip") {
|
||||
if (!isValidIP(rule.value)) {
|
||||
throw new Error(`Invalid IP provided: ${rule.value}`);
|
||||
}
|
||||
} else if (rule.match === "path") {
|
||||
if (!isValidUrlGlobPattern(rule.value)) {
|
||||
throw new Error(
|
||||
`Invalid URL glob pattern: ${rule.value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
await trx.insert(resourceRules).values({
|
||||
resourceId: newResource.resourceId,
|
||||
action: rule.action,
|
||||
match: rule.match,
|
||||
value: rule.value,
|
||||
priority: index + 1 // start priorities at 1
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(`Created resource ${newResource.resourceId}`);
|
||||
}
|
||||
|
||||
results.push({
|
||||
proxyResource: resource,
|
||||
targetsToUpdate
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function syncRoleResources(
|
||||
resourceId: number,
|
||||
ssoRoles: string[],
|
||||
orgId: string,
|
||||
trx: Transaction
|
||||
) {
|
||||
const existingRoleResources = await trx
|
||||
.select()
|
||||
.from(roleResources)
|
||||
.where(eq(roleResources.resourceId, resourceId));
|
||||
|
||||
for (const roleName of ssoRoles) {
|
||||
if (roleName === "Admin") {
|
||||
continue; // never add admin access
|
||||
}
|
||||
|
||||
const [role] = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.name, roleName), eq(roles.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (!role) {
|
||||
throw new Error(`Role not found: ${roleName} in org ${orgId}`);
|
||||
}
|
||||
|
||||
const existingRoleResource = existingRoleResources.find(
|
||||
(rr) => rr.roleId === role.roleId
|
||||
);
|
||||
|
||||
if (!existingRoleResource) {
|
||||
await trx.insert(roleResources).values({
|
||||
roleId: role.roleId,
|
||||
resourceId: resourceId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingRoleResource of existingRoleResources) {
|
||||
const [role] = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(eq(roles.roleId, existingRoleResource.roleId))
|
||||
.limit(1);
|
||||
|
||||
if (role.isAdmin) {
|
||||
continue; // never remove admin access
|
||||
}
|
||||
|
||||
if (role && !ssoRoles.includes(role.name)) {
|
||||
await trx
|
||||
.delete(roleResources)
|
||||
.where(
|
||||
and(
|
||||
eq(roleResources.roleId, existingRoleResource.roleId),
|
||||
eq(roleResources.resourceId, resourceId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function syncUserResources(
|
||||
resourceId: number,
|
||||
ssoUsers: string[],
|
||||
orgId: string,
|
||||
trx: Transaction
|
||||
) {
|
||||
const existingUserResources = await trx
|
||||
.select()
|
||||
.from(userResources)
|
||||
.where(eq(userResources.resourceId, resourceId));
|
||||
|
||||
for (const email of ssoUsers) {
|
||||
const [user] = await trx
|
||||
.select()
|
||||
.from(users)
|
||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||
.where(and(eq(users.email, email), eq(userOrgs.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`User not found: ${email} in org ${orgId}`);
|
||||
}
|
||||
|
||||
const existingUserResource = existingUserResources.find(
|
||||
(rr) => rr.userId === user.user.userId
|
||||
);
|
||||
|
||||
if (!existingUserResource) {
|
||||
await trx.insert(userResources).values({
|
||||
userId: user.user.userId,
|
||||
resourceId: resourceId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingUserResource of existingUserResources) {
|
||||
const [user] = await trx
|
||||
.select()
|
||||
.from(users)
|
||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||
.where(
|
||||
and(
|
||||
eq(users.userId, existingUserResource.userId),
|
||||
eq(userOrgs.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (user && user.user.email && !ssoUsers.includes(user.user.email)) {
|
||||
await trx
|
||||
.delete(userResources)
|
||||
.where(
|
||||
and(
|
||||
eq(userResources.userId, existingUserResource.userId),
|
||||
eq(userResources.resourceId, resourceId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function syncWhitelistUsers(
|
||||
resourceId: number,
|
||||
whitelistUsers: string[],
|
||||
orgId: string,
|
||||
trx: Transaction
|
||||
) {
|
||||
const existingWhitelist = await trx
|
||||
.select()
|
||||
.from(resourceWhitelist)
|
||||
.where(eq(resourceWhitelist.resourceId, resourceId));
|
||||
|
||||
for (const email of whitelistUsers) {
|
||||
const [user] = await trx
|
||||
.select()
|
||||
.from(users)
|
||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||
.where(and(eq(users.email, email), eq(userOrgs.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`User not found: ${email} in org ${orgId}`);
|
||||
}
|
||||
|
||||
const existingWhitelistEntry = existingWhitelist.find(
|
||||
(w) => w.email === email
|
||||
);
|
||||
|
||||
if (!existingWhitelistEntry) {
|
||||
await trx.insert(resourceWhitelist).values({
|
||||
email,
|
||||
resourceId: resourceId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingWhitelistEntry of existingWhitelist) {
|
||||
if (!whitelistUsers.includes(existingWhitelistEntry.email)) {
|
||||
await trx
|
||||
.delete(resourceWhitelist)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceWhitelist.resourceId, resourceId),
|
||||
eq(
|
||||
resourceWhitelist.email,
|
||||
existingWhitelistEntry.email
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfTargetChanged(
|
||||
existing: Target | undefined,
|
||||
incoming: Target | undefined
|
||||
): boolean {
|
||||
if (!existing && incoming) return true;
|
||||
if (existing && !incoming) return true;
|
||||
if (!existing || !incoming) return false;
|
||||
|
||||
if (existing.ip !== incoming.ip) return true;
|
||||
if (existing.port !== incoming.port) return true;
|
||||
if (existing.siteId !== incoming.siteId) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getDomain(
|
||||
resourceId: number | undefined,
|
||||
fullDomain: string,
|
||||
orgId: string,
|
||||
trx: Transaction
|
||||
) {
|
||||
const [fullDomainExists] = await trx
|
||||
.select({ resourceId: resources.resourceId })
|
||||
.from(resources)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.fullDomain, fullDomain),
|
||||
eq(resources.orgId, orgId),
|
||||
resourceId
|
||||
? ne(resources.resourceId, resourceId)
|
||||
: isNotNull(resources.resourceId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (fullDomainExists) {
|
||||
throw new Error(
|
||||
`Resource already exists: ${fullDomain} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
const domain = await getDomainId(orgId, fullDomain, trx);
|
||||
|
||||
if (!domain) {
|
||||
throw new Error(
|
||||
`Domain not found for full-domain: ${fullDomain} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
async function getDomainId(
|
||||
orgId: string,
|
||||
fullDomain: string,
|
||||
trx: Transaction
|
||||
): Promise<{ subdomain: string | null; domainId: string } | null> {
|
||||
const possibleDomains = await trx
|
||||
.select()
|
||||
.from(domains)
|
||||
.innerJoin(orgDomains, eq(domains.domainId, orgDomains.domainId))
|
||||
.where(and(eq(orgDomains.orgId, orgId), eq(domains.verified, true)))
|
||||
.execute();
|
||||
|
||||
if (possibleDomains.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validDomains = possibleDomains.filter((domain) => {
|
||||
if (domain.domains.type == "ns" || domain.domains.type == "wildcard") {
|
||||
return (
|
||||
fullDomain === domain.domains.baseDomain ||
|
||||
fullDomain.endsWith(`.${domain.domains.baseDomain}`)
|
||||
);
|
||||
} else if (domain.domains.type == "cname") {
|
||||
return fullDomain === domain.domains.baseDomain;
|
||||
}
|
||||
});
|
||||
|
||||
if (validDomains.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const domainSelection = validDomains[0].domains;
|
||||
const baseDomain = domainSelection.baseDomain;
|
||||
|
||||
// remove the base domain of the domain
|
||||
let subdomain = null;
|
||||
if (domainSelection.type == "ns") {
|
||||
if (fullDomain != baseDomain) {
|
||||
subdomain = fullDomain.replace(`.${baseDomain}`, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Return the first valid domain
|
||||
return {
|
||||
subdomain: subdomain,
|
||||
domainId: domainSelection.domainId
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user