diff --git a/messages/en-US.json b/messages/en-US.json index ecef7605..35e88375 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1428,6 +1428,7 @@ "billingSites": "Sites", "billingUsers": "Users", "billingDomains": "Domains", + "billingOrganizations": "Orgs", "billingRemoteExitNodes": "Remote Nodes", "billingNoLimitConfigured": "No limit configured", "billingEstimatedPeriod": "Estimated Billing Period", diff --git a/server/lib/billing/usageService.ts b/server/lib/billing/usageService.ts index cde6cd2a..a7786c76 100644 --- a/server/lib/billing/usageService.ts +++ b/server/lib/billing/usageService.ts @@ -130,18 +130,22 @@ export class UsageService { featureId, orgId, meterId, - instantaneousValue: value, - latestValue: value, + instantaneousValue: value || 0, + latestValue: value || 0, updatedAt: Math.floor(Date.now() / 1000) }) .onConflictDoUpdate({ target: usage.usageId, set: { - instantaneousValue: sql`${usage.instantaneousValue} + ${value}` + instantaneousValue: sql`COALESCE(${usage.instantaneousValue}, 0) + ${value}` } }) .returning(); + logger.debug( + `Added usage for org ${orgId} feature ${featureId}: +${value}, new instantaneousValue: ${returnUsage.instantaneousValue}` + ); + return returnUsage; } diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index 45771492..e58f8207 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -171,16 +171,7 @@ export async function createOrg( } } - if (build == "saas") { - if (!billingOrgIdForNewOrg) { - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Billing org not found for user. Cannot create new organization." - ) - ); - } - + if (build == "saas" && billingOrgIdForNewOrg) { const usage = await usageService.getUsage(billingOrgIdForNewOrg, FeatureId.ORGINIZATIONS); if (!usage) { return next( diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index 3d28da6f..eb5f8a8d 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -8,7 +8,10 @@ import { userOrgs, resourcePassword, resourcePincode, - resourceWhitelist + resourceWhitelist, + siteResources, + userSiteResources, + roleSiteResources } from "@server/db"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -57,9 +60,21 @@ export async function getUserResources( .from(roleResources) .where(eq(roleResources.roleId, userRoleId)); - const [directResources, roleResourceResults] = await Promise.all([ + 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 [directResources, roleResourceResults, directSiteResourceResults, roleSiteResourceResults] = await Promise.all([ directResourcesQuery, - roleResourcesQuery + roleResourcesQuery, + directSiteResourcesQuery, + roleSiteResourcesQuery ]); // Combine all accessible resource IDs @@ -68,18 +83,25 @@ export async function getUserResources( ...roleResourceResults.map((r) => r.resourceId) ]; - if (accessibleResourceIds.length === 0) { - return response(res, { - data: { resources: [] }, - success: true, - error: false, - message: "No resources found", - status: HttpCode.OK - }); - } + // Combine all accessible site resource IDs + const accessibleSiteResourceIds = [ + ...directSiteResourceResults.map((r) => r.siteResourceId), + ...roleSiteResourceResults.map((r) => r.siteResourceId) + ]; // Get resource details for accessible resources - const resourcesData = await db + let resourcesData: Array<{ + resourceId: number; + name: string; + fullDomain: string | null; + ssl: boolean; + enabled: boolean; + sso: boolean; + protocol: string; + emailWhitelistEnabled: boolean; + }> = []; + if (accessibleResourceIds.length > 0) { + resourcesData = await db .select({ resourceId: resources.resourceId, name: resources.name, @@ -98,6 +120,40 @@ export async function getUserResources( eq(resources.enabled, true) ) ); + } + + // Get site resource details for accessible site resources + let siteResourcesData: Array<{ + siteResourceId: number; + name: string; + destination: string; + mode: string; + protocol: string | null; + enabled: boolean; + alias: string | null; + aliasAddress: string | null; + }> = []; + if (accessibleSiteResourceIds.length > 0) { + siteResourcesData = await db + .select({ + siteResourceId: siteResources.siteResourceId, + name: siteResources.name, + destination: siteResources.destination, + mode: siteResources.mode, + protocol: siteResources.protocol, + enabled: siteResources.enabled, + alias: siteResources.alias, + aliasAddress: siteResources.aliasAddress + }) + .from(siteResources) + .where( + and( + inArray(siteResources.siteResourceId, accessibleSiteResourceIds), + eq(siteResources.orgId, orgId), + eq(siteResources.enabled, true) + ) + ); + } // Check for password, pincode, and whitelist protection for each resource const resourcesWithAuth = await Promise.all( @@ -161,8 +217,26 @@ export async function getUserResources( }) ); + // Format site resources + const siteResourcesFormatted = siteResourcesData.map((siteResource) => { + return { + siteResourceId: siteResource.siteResourceId, + name: siteResource.name, + destination: siteResource.destination, + mode: siteResource.mode, + protocol: siteResource.protocol, + enabled: siteResource.enabled, + alias: siteResource.alias, + aliasAddress: siteResource.aliasAddress, + type: 'site' as const + }; + }); + return response(res, { - data: { resources: resourcesWithAuth }, + data: { + resources: resourcesWithAuth, + siteResources: siteResourcesFormatted + }, success: true, error: false, message: "User resources retrieved successfully", @@ -190,5 +264,16 @@ export type GetUserResourcesResponse = { protected: boolean; protocol: string; }>; + siteResources: Array<{ + siteResourceId: number; + name: string; + destination: string; + mode: string; + protocol: string | null; + enabled: boolean; + alias: string | null; + aliasAddress: string | null; + type: 'site'; + }>; }; }; diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index cdb9d3ba..58757253 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -100,6 +100,8 @@ export async function deleteSite( } } + await trx.delete(sites).where(eq(sites.siteId, siteId)); + await usageService.add(site.orgId, FeatureId.SITES, -1, trx); }); diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index 5f608e55..9d672902 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -768,7 +768,7 @@ export default function BillingPage() {
+ Web applications and services accessible via browser +
++ Internal network resources accessible via client +
++ {siteResource.name} +
+