import { clients, clientSiteResources, domains, orgDomains, roles, roleSiteResources, Site, SiteResource, siteNetworks, siteResources, Transaction, userOrgs, users, userSiteResources, networks } from "@server/db"; import { sites } from "@server/db"; import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm"; import { Config } from "./types"; import logger from "@server/logger"; import { getNextAvailableAliasAddress } from "../ip"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; async function getDomainForSiteResource( siteResourceId: number | undefined, fullDomain: string, orgId: string, trx: Transaction ): Promise<{ subdomain: string | null; domainId: string }> { const [fullDomainExists] = await trx .select({ siteResourceId: siteResources.siteResourceId }) .from(siteResources) .where( and( eq(siteResources.fullDomain, fullDomain), eq(siteResources.orgId, orgId), siteResourceId ? ne(siteResources.siteResourceId, siteResourceId) : isNotNull(siteResources.siteResourceId) ) ) .limit(1); if (fullDomainExists) { throw new Error( `Site resource already exists with domain: ${fullDomain} in org ${orgId}` ); } 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) { throw new Error( `Domain not found for full-domain: ${fullDomain} in org ${orgId}` ); } 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) { throw new Error( `Domain not found for full-domain: ${fullDomain} in org ${orgId}` ); } const domainSelection = validDomains[0].domains; const baseDomain = domainSelection.baseDomain; let subdomain: string | null = null; if (fullDomain !== baseDomain) { subdomain = fullDomain.replace(`.${baseDomain}`, ""); } await createCertificate(domainSelection.domainId, fullDomain, trx); return { subdomain, domainId: domainSelection.domainId }; } export type ClientResourcesResults = { newSiteResource: SiteResource; oldSiteResource?: SiteResource; newSites: { siteId: number }[]; oldSites: { siteId: number }[]; }[]; export async function updateClientResources( orgId: string, config: Config, trx: Transaction, siteId?: number ): Promise { const results: ClientResourcesResults = []; for (const [resourceNiceId, resourceData] of Object.entries( config["client-resources"] )) { const [existingResource] = await trx .select() .from(siteResources) .where( and( eq(siteResources.orgId, orgId), eq(siteResources.niceId, resourceNiceId) ) ) .limit(1); const existingSiteIds = existingResource?.networkId ? await trx .select({ siteId: sites.siteId }) .from(siteNetworks) .where(eq(siteNetworks.networkId, existingResource.networkId)) : []; let allSites: { siteId: number }[] = []; if (resourceData.site) { let siteSingle; const resourceSiteId = resourceData.site; if (resourceSiteId) { // Look up site by niceId [siteSingle] = await trx .select({ siteId: sites.siteId }) .from(sites) .where( and( eq(sites.niceId, resourceSiteId), eq(sites.orgId, orgId) ) ) .limit(1); } else if (siteId) { // Use the provided siteId directly, but verify it belongs to the org [siteSingle] = 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 (!siteSingle) { throw new Error( `Site not found: ${resourceSiteId} in org ${orgId}` ); } allSites.push(siteSingle); } if (resourceData.sites) { for (const siteNiceId of resourceData.sites) { const [site] = await trx .select({ siteId: sites.siteId }) .from(sites) .where( and( eq(sites.niceId, siteNiceId), eq(sites.orgId, orgId) ) ) .limit(1); if (!site) { throw new Error( `Site not found: ${siteId} in org ${orgId}` ); } allSites.push(site); } } if (existingResource) { let domainInfo: | { subdomain: string | null; domainId: string } | undefined; if (resourceData["full-domain"] && resourceData.mode === "http") { domainInfo = await getDomainForSiteResource( existingResource.siteResourceId, resourceData["full-domain"], orgId, trx ); } // Update existing resource const [updatedResource] = await trx .update(siteResources) .set({ name: resourceData.name || resourceNiceId, mode: resourceData.mode, ssl: resourceData.ssl, scheme: resourceData.scheme, destination: resourceData.destination, destinationPort: resourceData["destination-port"], enabled: true, // hardcoded for now // enabled: resourceData.enabled ?? true, alias: resourceData.alias || null, disableIcmp: resourceData["disable-icmp"], tcpPortRangeString: resourceData["tcp-ports"], udpPortRangeString: resourceData["udp-ports"], fullDomain: resourceData["full-domain"] || null, subdomain: domainInfo ? domainInfo.subdomain : null, domainId: domainInfo ? domainInfo.domainId : null }) .where( eq( siteResources.siteResourceId, existingResource.siteResourceId ) ) .returning(); const siteResourceId = existingResource.siteResourceId; if (updatedResource.networkId) { await trx .delete(siteNetworks) .where( eq(siteNetworks.networkId, updatedResource.networkId) ); for (const site of allSites) { await trx.insert(siteNetworks).values({ siteId: site.siteId, networkId: updatedResource.networkId }); } } await trx .delete(clientSiteResources) .where(eq(clientSiteResources.siteResourceId, siteResourceId)); if (resourceData.machines.length > 0) { // get clientIds from niceIds const clientsToUpdate = await trx .select() .from(clients) .where( and( inArray(clients.niceId, resourceData.machines), eq(clients.orgId, orgId) ) ); const clientIds = clientsToUpdate.map( (client) => client.clientId ); await trx.insert(clientSiteResources).values( clientIds.map((clientId) => ({ clientId, siteResourceId })) ); } await trx .delete(userSiteResources) .where(eq(userSiteResources.siteResourceId, siteResourceId)); if (resourceData.users.length > 0) { // get userIds from username const usersToUpdate = await trx .select() .from(users) .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) .where( and( or( inArray(users.username, resourceData.users), inArray(users.email, resourceData.users) ), eq(userOrgs.orgId, orgId) ) ); const userIds = usersToUpdate.map((user) => user.user.userId); await trx .insert(userSiteResources) .values( userIds.map((userId) => ({ userId, siteResourceId })) ); } // Get all admin role IDs for this org to exclude from deletion const adminRoles = await trx .select() .from(roles) .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))); const adminRoleIds = adminRoles.map((role) => role.roleId); if (adminRoleIds.length > 0) { await trx.delete(roleSiteResources).where( and( eq(roleSiteResources.siteResourceId, siteResourceId), ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role ) ); } else { await trx .delete(roleSiteResources) .where( eq(roleSiteResources.siteResourceId, siteResourceId) ); } if (resourceData.roles.length > 0) { // Re-add specified roles but we need to get the roleIds from the role name in the array const rolesToUpdate = await trx .select() .from(roles) .where( and( eq(roles.orgId, orgId), inArray(roles.name, resourceData.roles) ) ); const roleIds = rolesToUpdate.map((role) => role.roleId); await trx .insert(roleSiteResources) .values( roleIds.map((roleId) => ({ roleId, siteResourceId })) ); } results.push({ newSiteResource: updatedResource, oldSiteResource: existingResource, newSites: allSites, oldSites: existingSiteIds }); } else { let aliasAddress: string | null = null; if (resourceData.mode === "host" || resourceData.mode === "http") { aliasAddress = await getNextAvailableAliasAddress(orgId); } let domainInfo: | { subdomain: string | null; domainId: string } | undefined; if (resourceData["full-domain"] && resourceData.mode === "http") { domainInfo = await getDomainForSiteResource( undefined, resourceData["full-domain"], orgId, trx ); } const [network] = await trx .insert(networks) .values({ scope: "resource", orgId: orgId }) .returning(); // Create new resource const [newResource] = await trx .insert(siteResources) .values({ orgId: orgId, niceId: resourceNiceId, networkId: network.networkId, defaultNetworkId: network.networkId, name: resourceData.name || resourceNiceId, mode: resourceData.mode, ssl: resourceData.ssl, scheme: resourceData.scheme, destination: resourceData.destination, destinationPort: resourceData["destination-port"], enabled: true, // hardcoded for now // enabled: resourceData.enabled ?? true, alias: resourceData.alias || null, aliasAddress: aliasAddress, disableIcmp: resourceData["disable-icmp"], tcpPortRangeString: resourceData["tcp-ports"], udpPortRangeString: resourceData["udp-ports"], fullDomain: resourceData["full-domain"] || null, subdomain: domainInfo ? domainInfo.subdomain : null, domainId: domainInfo ? domainInfo.domainId : null }) .returning(); const siteResourceId = newResource.siteResourceId; for (const site of allSites) { await trx.insert(siteNetworks).values({ siteId: site.siteId, networkId: network.networkId }); } 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 for org ${orgId}`); } await trx.insert(roleSiteResources).values({ roleId: adminRole.roleId, siteResourceId: siteResourceId }); if (resourceData.roles.length > 0) { // get roleIds from role names const rolesToUpdate = await trx .select() .from(roles) .where( and( eq(roles.orgId, orgId), inArray(roles.name, resourceData.roles) ) ); const roleIds = rolesToUpdate.map((role) => role.roleId); await trx .insert(roleSiteResources) .values( roleIds.map((roleId) => ({ roleId, siteResourceId })) ); } if (resourceData.users.length > 0) { // get userIds from username const usersToUpdate = await trx .select() .from(users) .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) .where( and( or( inArray(users.username, resourceData.users), inArray(users.email, resourceData.users) ), eq(userOrgs.orgId, orgId) ) ); const userIds = usersToUpdate.map((user) => user.user.userId); await trx .insert(userSiteResources) .values( userIds.map((userId) => ({ userId, siteResourceId })) ); } if (resourceData.machines.length > 0) { // get clientIds from niceIds const clientsToUpdate = await trx .select() .from(clients) .where( and( inArray(clients.niceId, resourceData.machines), eq(clients.orgId, orgId) ) ); const clientIds = clientsToUpdate.map( (client) => client.clientId ); await trx.insert(clientSiteResources).values( clientIds.map((clientId) => ({ clientId, siteResourceId })) ); } logger.info( `Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}` ); results.push({ newSiteResource: newResource, newSites: allSites, oldSites: existingSiteIds }); } } return results; }