mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-26 23:06:37 +00:00
Pull up downstream changes
This commit is contained in:
@@ -11,4 +11,4 @@ export * from "./requestPasswordReset";
|
||||
export * from "./resetPassword";
|
||||
export * from "./checkResourceSession";
|
||||
export * from "./setServerAdmin";
|
||||
export * from "./initialSetupComplete";
|
||||
export * from "./initialSetupComplete";
|
||||
@@ -12,6 +12,7 @@ import { createTOTPKeyURI } from "oslo/otp";
|
||||
import logger from "@server/logger";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||
import config from "@server/lib/config";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
export const requestTotpSecretBody = z
|
||||
@@ -73,7 +74,11 @@ export async function requestTotpSecret(
|
||||
|
||||
const hex = crypto.getRandomValues(new Uint8Array(20));
|
||||
const secret = encodeHex(hex);
|
||||
const uri = createTOTPKeyURI("Pangolin", user.email!, hex);
|
||||
const uri = createTOTPKeyURI(
|
||||
"Pangolin",
|
||||
user.email!,
|
||||
hex
|
||||
);
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
|
||||
@@ -57,8 +57,6 @@ export async function signup(
|
||||
|
||||
const { email, password, inviteToken, inviteId } = parsedBody.data;
|
||||
|
||||
logger.debug("signup", { email, password, inviteToken, inviteId });
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
const userId = generateId(15);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/lib";
|
||||
import { db } from "@server/db";
|
||||
import { db, userOrgs } from "@server/db";
|
||||
import { User, emailVerificationCodes, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { isWithinExpirationDate } from "oslo";
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function createClient(
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
@@ -208,7 +208,7 @@ export async function createClient(
|
||||
clientId: newClient.clientId
|
||||
});
|
||||
|
||||
if (req.userOrgRoleId != adminRole[0].roleId) {
|
||||
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
|
||||
// make sure the user can access the site
|
||||
trx.insert(userClients).values({
|
||||
userId: req.user?.userId!,
|
||||
|
||||
@@ -126,7 +126,7 @@ export async function listClients(
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (orgId && orgId !== req.userOrgId) {
|
||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
@@ -135,21 +135,29 @@ export async function listClients(
|
||||
);
|
||||
}
|
||||
|
||||
const accessibleClients = await db
|
||||
.select({
|
||||
clientId: sql<number>`COALESCE(${userClients.clientId}, ${roleClients.clientId})`
|
||||
})
|
||||
.from(userClients)
|
||||
.fullJoin(
|
||||
roleClients,
|
||||
eq(userClients.clientId, roleClients.clientId)
|
||||
)
|
||||
.where(
|
||||
or(
|
||||
eq(userClients.userId, req.user!.userId),
|
||||
eq(roleClients.roleId, req.userOrgRoleId!)
|
||||
let accessibleClients;
|
||||
if (req.user) {
|
||||
accessibleClients = await db
|
||||
.select({
|
||||
clientId: sql<number>`COALESCE(${userClients.clientId}, ${roleClients.clientId})`
|
||||
})
|
||||
.from(userClients)
|
||||
.fullJoin(
|
||||
roleClients,
|
||||
eq(userClients.clientId, roleClients.clientId)
|
||||
)
|
||||
);
|
||||
.where(
|
||||
or(
|
||||
eq(userClients.userId, req.user!.userId),
|
||||
eq(roleClients.roleId, req.userOrgRoleId!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
accessibleClients = await db
|
||||
.select({ clientId: clients.clientId })
|
||||
.from(clients)
|
||||
.where(eq(clients.orgId, orgId));
|
||||
}
|
||||
|
||||
const accessibleClientIds = accessibleClients.map(
|
||||
(client) => client.clientId
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateId } from "@server/auth/sessions/app";
|
||||
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
export type PickClientDefaultsResponse = {
|
||||
olmId: string;
|
||||
@@ -20,6 +21,17 @@ const pickClientDefaultsSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/site/{siteId}/pick-client-defaults",
|
||||
description: "Return pre-requisite data for creating a client.",
|
||||
tags: [OpenAPITags.Client, OpenAPITags.Site],
|
||||
request: {
|
||||
params: pickClientDefaultsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function pickClientDefaults(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
252
server/routers/domain/createOrgDomain.ts
Normal file
252
server/routers/domain/createOrgDomain.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, Domain, domains, OrgDomains, orgDomains } from "@server/db";
|
||||
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 { subdomainSchema } from "@server/lib/schemas";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { isValidDomain } from "@server/lib/validators";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
type: z.enum(["ns", "cname"]),
|
||||
baseDomain: subdomainSchema
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CreateDomainResponse = {
|
||||
domainId: string;
|
||||
nsRecords?: string[];
|
||||
cnameRecords?: { baseDomain: string; value: string }[];
|
||||
txtRecords?: { baseDomain: string; value: string }[];
|
||||
};
|
||||
|
||||
// Helper to check if a domain is a subdomain or equal to another domain
|
||||
function isSubdomainOrEqual(a: string, b: string): boolean {
|
||||
const aParts = a.toLowerCase().split(".");
|
||||
const bParts = b.toLowerCase().split(".");
|
||||
if (aParts.length < bParts.length) return false;
|
||||
return aParts.slice(-bParts.length).join(".") === bParts.join(".");
|
||||
}
|
||||
|
||||
export async function createOrgDomain(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
const { type, baseDomain } = parsedBody.data;
|
||||
|
||||
// Validate organization exists
|
||||
if (!isValidDomain(baseDomain)) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid domain format")
|
||||
);
|
||||
}
|
||||
|
||||
let numOrgDomains: OrgDomains[] | undefined;
|
||||
let cnameRecords: CreateDomainResponse["cnameRecords"];
|
||||
let txtRecords: CreateDomainResponse["txtRecords"];
|
||||
let nsRecords: CreateDomainResponse["nsRecords"];
|
||||
let returned: Domain | undefined;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const [existing] = await trx
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(
|
||||
and(
|
||||
eq(domains.baseDomain, baseDomain),
|
||||
eq(domains.type, type)
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
orgDomains,
|
||||
eq(orgDomains.domainId, domains.domainId)
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
const {
|
||||
domains: existingDomain,
|
||||
orgDomains: existingOrgDomain
|
||||
} = existing;
|
||||
|
||||
// user alrady added domain to this account
|
||||
// always reject
|
||||
if (existingOrgDomain?.orgId === orgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Domain is already added to this org"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// domain already exists elsewhere
|
||||
// check if it's already fully verified
|
||||
if (existingDomain.verified) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Domain is already verified to an org"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Domain overlap logic ---
|
||||
// Only consider existing verified domains
|
||||
const verifiedDomains = await trx
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(eq(domains.verified, true));
|
||||
|
||||
if (type === "cname") {
|
||||
// Block if a verified CNAME exists at the same name
|
||||
const cnameExists = verifiedDomains.some(
|
||||
(d) => d.type === "cname" && d.baseDomain === baseDomain
|
||||
);
|
||||
if (cnameExists) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`A CNAME record already exists for ${baseDomain}. Only one CNAME record is allowed per domain.`
|
||||
)
|
||||
);
|
||||
}
|
||||
// Block if a verified NS exists at or below (same or subdomain)
|
||||
const nsAtOrBelow = verifiedDomains.some(
|
||||
(d) =>
|
||||
d.type === "ns" &&
|
||||
(isSubdomainOrEqual(baseDomain, d.baseDomain) ||
|
||||
baseDomain === d.baseDomain)
|
||||
);
|
||||
if (nsAtOrBelow) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`A nameserver (NS) record exists at or below ${baseDomain}. You cannot create a CNAME record here.`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (type === "ns") {
|
||||
// Block if a verified NS exists at or below (same or subdomain)
|
||||
const nsAtOrBelow = verifiedDomains.some(
|
||||
(d) =>
|
||||
d.type === "ns" &&
|
||||
(isSubdomainOrEqual(baseDomain, d.baseDomain) ||
|
||||
baseDomain === d.baseDomain)
|
||||
);
|
||||
if (nsAtOrBelow) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`A nameserver (NS) record already exists at or below ${baseDomain}. You cannot create another NS record here.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const domainId = generateId(15);
|
||||
|
||||
const [insertedDomain] = await trx
|
||||
.insert(domains)
|
||||
.values({
|
||||
domainId,
|
||||
baseDomain,
|
||||
type
|
||||
})
|
||||
.returning();
|
||||
|
||||
returned = insertedDomain;
|
||||
|
||||
// add domain to account
|
||||
await trx
|
||||
.insert(orgDomains)
|
||||
.values({
|
||||
orgId,
|
||||
domainId
|
||||
})
|
||||
.returning();
|
||||
|
||||
// TODO: This needs to be cross region and not hardcoded
|
||||
if (type === "ns") {
|
||||
nsRecords = ["ns-east.fossorial.io", "ns-west.fossorial.io"];
|
||||
} else if (type === "cname") {
|
||||
cnameRecords = [
|
||||
{
|
||||
value: `${domainId}.cname.fossorial.io`,
|
||||
baseDomain: baseDomain
|
||||
},
|
||||
{
|
||||
value: `_acme-challenge.${domainId}.cname.fossorial.io`,
|
||||
baseDomain: `_acme-challenge.${baseDomain}`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
numOrgDomains = await trx
|
||||
.select()
|
||||
.from(orgDomains)
|
||||
.where(eq(orgDomains.orgId, orgId));
|
||||
});
|
||||
|
||||
if (!returned) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to create domain"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response<CreateDomainResponse>(res, {
|
||||
data: {
|
||||
domainId: returned.domainId,
|
||||
cnameRecords,
|
||||
txtRecords,
|
||||
nsRecords
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Domain created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
72
server/routers/domain/deleteOrgDomain.ts
Normal file
72
server/routers/domain/deleteOrgDomain.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, domains, OrgDomains, orgDomains } from "@server/db";
|
||||
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 { and, eq } from "drizzle-orm";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
domainId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type DeleteAccountDomainResponse = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
export async function deleteAccountDomain(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsed = paramsSchema.safeParse(req.params);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsed.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { domainId, orgId } = parsed.data;
|
||||
|
||||
let numOrgDomains: OrgDomains[] | undefined;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(orgDomains)
|
||||
.where(
|
||||
and(
|
||||
eq(orgDomains.orgId, orgId),
|
||||
eq(orgDomains.domainId, domainId)
|
||||
)
|
||||
);
|
||||
|
||||
await trx.delete(domains).where(eq(domains.domainId, domainId));
|
||||
|
||||
numOrgDomains = await trx
|
||||
.select()
|
||||
.from(orgDomains)
|
||||
.where(eq(orgDomains.orgId, orgId));
|
||||
});
|
||||
|
||||
return response<DeleteAccountDomainResponse>(res, {
|
||||
data: { success: true },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Domain deleted from account successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
export * from "./listDomains";
|
||||
export * from "./createOrgDomain";
|
||||
export * from "./deleteOrgDomain";
|
||||
export * from "./restartOrgDomain";
|
||||
@@ -37,7 +37,11 @@ async function queryDomains(orgId: string, limit: number, offset: number) {
|
||||
const res = await db
|
||||
.select({
|
||||
domainId: domains.domainId,
|
||||
baseDomain: domains.baseDomain
|
||||
baseDomain: domains.baseDomain,
|
||||
verified: domains.verified,
|
||||
type: domains.type,
|
||||
failed: domains.failed,
|
||||
tries: domains.tries,
|
||||
})
|
||||
.from(orgDomains)
|
||||
.where(eq(orgDomains.orgId, orgId))
|
||||
@@ -112,7 +116,7 @@ export async function listDomains(
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Users retrieved successfully",
|
||||
message: "Domains retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
57
server/routers/domain/restartOrgDomain.ts
Normal file
57
server/routers/domain/restartOrgDomain.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, domains } from "@server/db";
|
||||
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 { and, eq } from "drizzle-orm";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
domainId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type RestartOrgDomainResponse = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
export async function restartOrgDomain(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsed = paramsSchema.safeParse(req.params);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsed.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { domainId, orgId } = parsed.data;
|
||||
|
||||
await db
|
||||
.update(domains)
|
||||
.set({ failed: false, tries: 0 })
|
||||
.where(and(eq(domains.domainId, domainId)));
|
||||
|
||||
return response<RestartOrgDomainResponse>(res, {
|
||||
data: { success: true },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Domain restarted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -33,15 +33,16 @@ import {
|
||||
verifyClientAccess,
|
||||
verifyApiKeyAccess,
|
||||
createStore,
|
||||
verifyDomainAccess,
|
||||
verifyClientsEnabled,
|
||||
verifyUserHasAction,
|
||||
verifyUserIsOrgOwner
|
||||
} from "@server/middlewares";
|
||||
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import { verifyUserIsOrgOwner } from "../middlewares/verifyUserIsOrgOwner";
|
||||
import { createNewt, getNewtToken } from "./newt";
|
||||
import { getOlmToken } from "./olm";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import createHttpError from "http-errors";
|
||||
import { verifyClientsEnabled } from "@server/middlewares/verifyClintsEnabled";
|
||||
|
||||
// Root routes
|
||||
export const unauthenticated = Router();
|
||||
@@ -54,10 +55,7 @@ unauthenticated.get("/", (_, res) => {
|
||||
export const authenticated = Router();
|
||||
authenticated.use(verifySessionUserMiddleware);
|
||||
|
||||
authenticated.get(
|
||||
"/pick-org-defaults",
|
||||
org.pickOrgDefaults
|
||||
);
|
||||
authenticated.get("/pick-org-defaults", org.pickOrgDefaults);
|
||||
authenticated.get("/org/checkId", org.checkId);
|
||||
authenticated.put("/org", getUserOrgs, org.createOrg);
|
||||
|
||||
@@ -750,6 +748,29 @@ authenticated.get(
|
||||
apiKeys.getApiKey
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
`/org/:orgId/domain`,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createOrgDomain),
|
||||
domain.createOrgDomain
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/org/:orgId/domain/:domainId/restart`,
|
||||
verifyOrgAccess,
|
||||
verifyDomainAccess,
|
||||
verifyUserHasAction(ActionsEnum.restartOrgDomain),
|
||||
domain.restartOrgDomain
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/org/:orgId/domain/:domainId`,
|
||||
verifyOrgAccess,
|
||||
verifyDomainAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteOrgDomain),
|
||||
domain.deleteAccountDomain
|
||||
);
|
||||
|
||||
// Auth routes
|
||||
export const authRouter = Router();
|
||||
unauthenticated.use("/auth", authRouter);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { eq, and, lt, inArray } from "drizzle-orm";
|
||||
import { eq, and, lt, inArray, sql } from "drizzle-orm";
|
||||
import { sites } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
@@ -7,6 +7,9 @@ import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
|
||||
// Track sites that are already offline to avoid unnecessary queries
|
||||
const offlineSites = new Set<string>();
|
||||
|
||||
interface PeerBandwidth {
|
||||
publicKey: string;
|
||||
bytesIn: number;
|
||||
@@ -28,43 +31,62 @@ export const receiveBandwidth = async (
|
||||
const currentTime = new Date();
|
||||
const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago
|
||||
|
||||
logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
// First, handle sites that are actively reporting bandwidth
|
||||
const activePeers = bandwidthData.filter(peer => peer.bytesIn > 0 || peer.bytesOut > 0);
|
||||
|
||||
const activePeers = bandwidthData.filter(peer => peer.bytesIn > 0); // Bytesout will have data as it tries to send keep alive messages
|
||||
|
||||
if (activePeers.length > 0) {
|
||||
// Get all active sites in one query
|
||||
const activeSites = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(inArray(sites.pubKey, activePeers.map(p => p.publicKey)));
|
||||
// Remove any active peers from offline tracking since they're sending data
|
||||
activePeers.forEach(peer => offlineSites.delete(peer.publicKey));
|
||||
|
||||
// Create a map for quick lookup
|
||||
const siteMap = new Map();
|
||||
activeSites.forEach(site => {
|
||||
siteMap.set(site.pubKey, site);
|
||||
});
|
||||
// Aggregate usage data by organization
|
||||
const orgUsageMap = new Map<string, number>();
|
||||
const orgUptimeMap = new Map<string, number>();
|
||||
|
||||
// Update sites with actual bandwidth usage
|
||||
// Update all active sites with bandwidth data and get the site data in one operation
|
||||
const updatedSites = [];
|
||||
for (const peer of activePeers) {
|
||||
const site = siteMap.get(peer.publicKey);
|
||||
if (!site) continue;
|
||||
|
||||
await trx
|
||||
const updatedSite = await trx
|
||||
.update(sites)
|
||||
.set({
|
||||
megabytesOut: (site.megabytesOut || 0) + peer.bytesIn,
|
||||
megabytesIn: (site.megabytesIn || 0) + peer.bytesOut,
|
||||
megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`,
|
||||
megabytesIn: sql`${sites.megabytesIn} + ${peer.bytesOut}`,
|
||||
lastBandwidthUpdate: currentTime.toISOString(),
|
||||
online: true
|
||||
})
|
||||
.where(eq(sites.siteId, site.siteId));
|
||||
.where(eq(sites.pubKey, peer.publicKey))
|
||||
.returning({
|
||||
online: sites.online,
|
||||
orgId: sites.orgId,
|
||||
siteId: sites.siteId,
|
||||
lastBandwidthUpdate: sites.lastBandwidthUpdate,
|
||||
});
|
||||
|
||||
if (updatedSite.length > 0) {
|
||||
updatedSites.push({ ...updatedSite[0], peer });
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate org usage aggregations using the updated site data
|
||||
for (const { peer, ...site } of updatedSites) {
|
||||
// Aggregate bandwidth usage for the org
|
||||
const totalBandwidth = peer.bytesIn + peer.bytesOut;
|
||||
const currentOrgUsage = orgUsageMap.get(site.orgId) || 0;
|
||||
orgUsageMap.set(site.orgId, currentOrgUsage + totalBandwidth);
|
||||
|
||||
// Add 10 seconds of uptime for each active site
|
||||
const currentOrgUptime = orgUptimeMap.get(site.orgId) || 0;
|
||||
orgUptimeMap.set(site.orgId, currentOrgUptime + 10 / 60); // Store in minutes and jut add 10 seconds
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sites that reported zero bandwidth but need online status updated
|
||||
const zeroBandwidthPeers = bandwidthData.filter(peer => peer.bytesIn === 0 && peer.bytesOut === 0);
|
||||
|
||||
const zeroBandwidthPeers = bandwidthData.filter(peer =>
|
||||
peer.bytesIn === 0 && !offlineSites.has(peer.publicKey) // Bytesout will have data as it tries to send keep alive messages
|
||||
);
|
||||
|
||||
if (zeroBandwidthPeers.length > 0) {
|
||||
const zeroBandwidthSites = await trx
|
||||
.select()
|
||||
@@ -91,18 +113,14 @@ export const receiveBandwidth = async (
|
||||
await trx
|
||||
.update(sites)
|
||||
.set({
|
||||
lastBandwidthUpdate: currentTime.toISOString(),
|
||||
online: newOnlineStatus
|
||||
})
|
||||
.where(eq(sites.siteId, site.siteId));
|
||||
} else {
|
||||
// Just update the heartbeat timestamp
|
||||
await trx
|
||||
.update(sites)
|
||||
.set({
|
||||
lastBandwidthUpdate: currentTime.toISOString()
|
||||
})
|
||||
.where(eq(sites.siteId, site.siteId));
|
||||
|
||||
// If site went offline, add it to our tracking set
|
||||
if (!newOnlineStatus && site.pubKey) {
|
||||
offlineSites.add(site.pubKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
idpOidcConfig,
|
||||
idpOrg,
|
||||
orgs,
|
||||
Role,
|
||||
roles,
|
||||
userOrgs,
|
||||
users
|
||||
@@ -307,6 +308,8 @@ export async function validateOidcCallback(
|
||||
|
||||
let existingUserId = existingUser?.userId;
|
||||
|
||||
let orgUserCounts: { orgId: string; userCount: number }[] = [];
|
||||
|
||||
// sync the user with the orgs and roles
|
||||
await db.transaction(async (trx) => {
|
||||
let userId = existingUser?.userId;
|
||||
@@ -410,6 +413,19 @@ export async function validateOidcCallback(
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// Loop through all the orgs and get the total number of users from the userOrgs table
|
||||
for (const org of currentUserOrgs) {
|
||||
const userCount = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.orgId, org.orgId));
|
||||
|
||||
orgUserCounts.push({
|
||||
orgId: org.orgId,
|
||||
userCount: userCount.length
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const token = generateSessionToken();
|
||||
|
||||
@@ -87,7 +87,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
|
||||
let siteSubnet = oldSite.subnet;
|
||||
let exitNodeIdToQuery = oldSite.exitNodeId;
|
||||
if (exitNodeId && oldSite.exitNodeId !== exitNodeId) {
|
||||
if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) {
|
||||
// This effectively moves the exit node to the new one
|
||||
exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId
|
||||
|
||||
@@ -105,7 +105,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
.limit(1);
|
||||
|
||||
const blockSize = config.getRawConfig().gerbil.site_block_size;
|
||||
const subnets = sitesQuery.map((site) => site.subnet);
|
||||
const subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null);
|
||||
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
|
||||
const newSubnet = findNextAvailableCidr(
|
||||
subnets,
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function createNewt(
|
||||
|
||||
const { newtId, secret } = parsedBody.data;
|
||||
|
||||
if (!req.userOrgRoleId) {
|
||||
if (req.user && !req.userOrgRoleId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||
);
|
||||
|
||||
@@ -33,8 +33,6 @@ const createOrgSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
// const MAX_ORGS = 5;
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org",
|
||||
@@ -80,16 +78,6 @@ export async function createOrg(
|
||||
);
|
||||
}
|
||||
|
||||
// const userOrgIds = req.userOrgIds;
|
||||
// if (userOrgIds && userOrgIds.length > MAX_ORGS) {
|
||||
// return next(
|
||||
// createHttpError(
|
||||
// HttpCode.FORBIDDEN,
|
||||
// `Maximum number of organizations reached.`
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
|
||||
const { orgId, name, subnet } = parsedBody.data;
|
||||
|
||||
if (!isValidCIDR(subnet)) {
|
||||
@@ -147,7 +135,7 @@ export async function createOrg(
|
||||
.values({
|
||||
orgId,
|
||||
name,
|
||||
subnet,
|
||||
subnet
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -182,15 +170,13 @@ export async function createOrg(
|
||||
const actionIds = await trx.select().from(actions).execute();
|
||||
|
||||
if (actionIds.length > 0) {
|
||||
await trx
|
||||
.insert(roleActions)
|
||||
.values(
|
||||
actionIds.map((action) => ({
|
||||
roleId,
|
||||
actionId: action.actionId,
|
||||
orgId: newOrg[0].orgId
|
||||
}))
|
||||
);
|
||||
await trx.insert(roleActions).values(
|
||||
actionIds.map((action) => ({
|
||||
roleId,
|
||||
actionId: action.actionId,
|
||||
orgId: newOrg[0].orgId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (allDomains.length) {
|
||||
@@ -227,7 +213,7 @@ export async function createOrg(
|
||||
orgId: newOrg[0].orgId,
|
||||
roleId: roleId,
|
||||
isOwner: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const memberRole = await trx
|
||||
|
||||
@@ -89,6 +89,8 @@ export async function deleteOrg(
|
||||
.where(eq(sites.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
const deletedNewtIds: string[] = [];
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (sites) {
|
||||
for (const site of orgSites) {
|
||||
@@ -102,11 +104,7 @@ export async function deleteOrg(
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
.returning();
|
||||
if (deletedNewt) {
|
||||
const payload = {
|
||||
type: `newt/terminate`,
|
||||
data: {}
|
||||
};
|
||||
sendToClient(deletedNewt.newtId, payload);
|
||||
deletedNewtIds.push(deletedNewt.newtId);
|
||||
|
||||
// delete all of the sessions for the newt
|
||||
await trx
|
||||
@@ -131,6 +129,18 @@ export async function deleteOrg(
|
||||
await trx.delete(orgs).where(eq(orgs.orgId, orgId));
|
||||
});
|
||||
|
||||
// Send termination messages outside of transaction to prevent blocking
|
||||
for (const newtId of deletedNewtIds) {
|
||||
const payload = {
|
||||
type: `newt/terminate`,
|
||||
data: {}
|
||||
};
|
||||
// Don't await this to prevent blocking the response
|
||||
sendToClient(newtId, payload).catch(error => {
|
||||
logger.error("Failed to send termination message to newt:", error);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
|
||||
@@ -69,7 +69,8 @@ function queryResources(
|
||||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort,
|
||||
enabled: resources.enabled
|
||||
enabled: resources.enabled,
|
||||
domainId: resources.domainId
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
@@ -103,7 +104,8 @@ function queryResources(
|
||||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort,
|
||||
enabled: resources.enabled
|
||||
enabled: resources.enabled,
|
||||
domainId: resources.domainId
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
|
||||
@@ -38,7 +38,7 @@ const createSiteSchema = z
|
||||
subnet: z.string().optional(),
|
||||
newtId: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
// address: z.string().optional(),
|
||||
type: z.enum(["newt", "wireguard", "local"])
|
||||
})
|
||||
.strict()
|
||||
@@ -97,7 +97,7 @@ export async function createSite(
|
||||
subnet,
|
||||
newtId,
|
||||
secret,
|
||||
address
|
||||
// address
|
||||
} = parsedBody.data;
|
||||
|
||||
const parsedParams = createSiteParamsSchema.safeParse(req.params);
|
||||
@@ -129,58 +129,58 @@ export async function createSite(
|
||||
);
|
||||
}
|
||||
|
||||
let updatedAddress = null;
|
||||
if (address) {
|
||||
if (!isValidIP(address)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid subnet format. Please provide a valid CIDR notation."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isIpInCidr(address, org.subnet)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"IP is not in the CIDR range of the subnet."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
updatedAddress = `${address}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
|
||||
|
||||
// make sure the subnet is unique
|
||||
const addressExistsSites = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.address, updatedAddress))
|
||||
.limit(1);
|
||||
|
||||
if (addressExistsSites.length > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
`Subnet ${subnet} already exists`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const addressExistsClients = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.subnet, updatedAddress))
|
||||
.limit(1);
|
||||
if (addressExistsClients.length > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
`Subnet ${subnet} already exists`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
// let updatedAddress = null;
|
||||
// if (address) {
|
||||
// if (!isValidIP(address)) {
|
||||
// return next(
|
||||
// createHttpError(
|
||||
// HttpCode.BAD_REQUEST,
|
||||
// "Invalid subnet format. Please provide a valid CIDR notation."
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (!isIpInCidr(address, org.subnet)) {
|
||||
// return next(
|
||||
// createHttpError(
|
||||
// HttpCode.BAD_REQUEST,
|
||||
// "IP is not in the CIDR range of the subnet."
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// updatedAddress = `${address}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
|
||||
//
|
||||
// // make sure the subnet is unique
|
||||
// const addressExistsSites = await db
|
||||
// .select()
|
||||
// .from(sites)
|
||||
// .where(eq(sites.address, updatedAddress))
|
||||
// .limit(1);
|
||||
//
|
||||
// if (addressExistsSites.length > 0) {
|
||||
// return next(
|
||||
// createHttpError(
|
||||
// HttpCode.CONFLICT,
|
||||
// `Subnet ${subnet} already exists`
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// const addressExistsClients = await db
|
||||
// .select()
|
||||
// .from(sites)
|
||||
// .where(eq(sites.subnet, updatedAddress))
|
||||
// .limit(1);
|
||||
// if (addressExistsClients.length > 0) {
|
||||
// return next(
|
||||
// createHttpError(
|
||||
// HttpCode.CONFLICT,
|
||||
// `Subnet ${subnet} already exists`
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
const niceId = await getUniqueSiteName(orgId);
|
||||
|
||||
@@ -205,7 +205,7 @@ export async function createSite(
|
||||
exitNodeId,
|
||||
name,
|
||||
niceId,
|
||||
address: updatedAddress || null,
|
||||
// address: updatedAddress || null,
|
||||
subnet,
|
||||
type,
|
||||
dockerSocketEnabled: type == "newt",
|
||||
@@ -221,7 +221,7 @@ export async function createSite(
|
||||
orgId,
|
||||
name,
|
||||
niceId,
|
||||
address: updatedAddress || null,
|
||||
// address: updatedAddress || null,
|
||||
type,
|
||||
dockerSocketEnabled: type == "newt",
|
||||
subnet: "0.0.0.0/0"
|
||||
|
||||
@@ -62,6 +62,8 @@ export async function deleteSite(
|
||||
);
|
||||
}
|
||||
|
||||
let deletedNewtId: string | null = null;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (site.pubKey) {
|
||||
if (site.type == "wireguard") {
|
||||
@@ -73,11 +75,7 @@ export async function deleteSite(
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.returning();
|
||||
if (deletedNewt) {
|
||||
const payload = {
|
||||
type: `newt/terminate`,
|
||||
data: {}
|
||||
};
|
||||
sendToClient(deletedNewt.newtId, payload);
|
||||
deletedNewtId = deletedNewt.newtId;
|
||||
|
||||
// delete all of the sessions for the newt
|
||||
await trx
|
||||
@@ -90,6 +88,18 @@ export async function deleteSite(
|
||||
await trx.delete(sites).where(eq(sites.siteId, siteId));
|
||||
});
|
||||
|
||||
// Send termination message outside of transaction to prevent blocking
|
||||
if (deletedNewtId) {
|
||||
const payload = {
|
||||
type: `newt/terminate`,
|
||||
data: {}
|
||||
};
|
||||
// Don't await this to prevent blocking the response
|
||||
sendToClient(deletedNewtId, payload).catch(error => {
|
||||
logger.error("Failed to send termination message to newt:", error);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
|
||||
@@ -20,10 +20,10 @@ export type PickSiteDefaultsResponse = {
|
||||
name: string;
|
||||
listenPort: number;
|
||||
endpoint: string;
|
||||
subnet: string;
|
||||
subnet: string; // TODO: make optional?
|
||||
newtId: string;
|
||||
newtSecret: string;
|
||||
clientAddress: string;
|
||||
clientAddress?: string;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
@@ -86,7 +86,7 @@ export async function pickSiteDefaults(
|
||||
.where(eq(sites.exitNodeId, exitNode.exitNodeId));
|
||||
|
||||
// TODO: we need to lock this subnet for some time so someone else does not take it
|
||||
let subnets = sitesQuery.map((site) => site.subnet);
|
||||
let subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null);
|
||||
// exclude the exit node address by replacing after the / with a site block size
|
||||
subnets.push(
|
||||
exitNode.address.replace(
|
||||
@@ -108,17 +108,17 @@ export async function pickSiteDefaults(
|
||||
);
|
||||
}
|
||||
|
||||
const newClientAddress = await getNextAvailableClientSubnet(orgId);
|
||||
if (!newClientAddress) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"No available subnet found"
|
||||
)
|
||||
);
|
||||
}
|
||||
// const newClientAddress = await getNextAvailableClientSubnet(orgId);
|
||||
// if (!newClientAddress) {
|
||||
// return next(
|
||||
// createHttpError(
|
||||
// HttpCode.INTERNAL_SERVER_ERROR,
|
||||
// "No available subnet found"
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
|
||||
const clientAddress = newClientAddress.split("/")[0];
|
||||
// const clientAddress = newClientAddress.split("/")[0];
|
||||
|
||||
const newtId = generateId(15);
|
||||
const secret = generateId(48);
|
||||
@@ -133,7 +133,7 @@ export async function pickSiteDefaults(
|
||||
endpoint: exitNode.endpoint,
|
||||
// subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().gerbil.block_size}`, // we want the block size of the whole subnet
|
||||
subnet: newSubnet,
|
||||
clientAddress: clientAddress,
|
||||
// clientAddress: clientAddress,
|
||||
newtId,
|
||||
newtSecret: secret
|
||||
},
|
||||
|
||||
@@ -56,12 +56,8 @@ export async function traefikConfigProvider(
|
||||
.select({
|
||||
// Resource fields
|
||||
resourceId: resources.resourceId,
|
||||
subdomain: resources.subdomain,
|
||||
fullDomain: resources.fullDomain,
|
||||
ssl: resources.ssl,
|
||||
blockAccess: resources.blockAccess,
|
||||
sso: resources.sso,
|
||||
emailWhitelistEnabled: resources.emailWhitelistEnabled,
|
||||
http: resources.http,
|
||||
proxyPort: resources.proxyPort,
|
||||
protocol: resources.protocol,
|
||||
@@ -74,10 +70,6 @@ export async function traefikConfigProvider(
|
||||
subnet: sites.subnet,
|
||||
exitNodeId: sites.exitNodeId,
|
||||
},
|
||||
// Org fields
|
||||
org: {
|
||||
orgId: orgs.orgId
|
||||
},
|
||||
enabled: resources.enabled,
|
||||
stickySession: resources.stickySession,
|
||||
tlsServerName: resources.tlsServerName,
|
||||
@@ -85,7 +77,6 @@ export async function traefikConfigProvider(
|
||||
})
|
||||
.from(resources)
|
||||
.innerJoin(sites, eq(sites.siteId, resources.siteId))
|
||||
.innerJoin(orgs, eq(resources.orgId, orgs.orgId))
|
||||
.where(eq(sites.exitNodeId, currentExitNodeId));
|
||||
|
||||
// Get all resource IDs from the first query
|
||||
@@ -179,7 +170,6 @@ export async function traefikConfigProvider(
|
||||
for (const resource of allResources) {
|
||||
const targets = resource.targets as Target[];
|
||||
const site = resource.site;
|
||||
const org = resource.org;
|
||||
|
||||
const routerName = `${resource.resourceId}-router`;
|
||||
const serviceName = `${resource.resourceId}-service`;
|
||||
@@ -203,11 +193,6 @@ export async function traefikConfigProvider(
|
||||
continue;
|
||||
}
|
||||
|
||||
// HTTP configuration remains the same
|
||||
if (!resource.subdomain && !resource.isBaseDomain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// add routers and services empty objects if they don't exist
|
||||
if (!config_output.http.routers) {
|
||||
config_output.http.routers = {};
|
||||
@@ -299,7 +284,7 @@ export async function traefikConfigProvider(
|
||||
} else if (site.type === "newt") {
|
||||
if (
|
||||
!target.internalPort ||
|
||||
!target.method
|
||||
!target.method || !site.subnet
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -315,7 +300,7 @@ export async function traefikConfigProvider(
|
||||
url: `${target.method}://${target.ip}:${target.port}`
|
||||
};
|
||||
} else if (site.type === "newt") {
|
||||
const ip = site.subnet.split("/")[0];
|
||||
const ip = site.subnet!.split("/")[0];
|
||||
return {
|
||||
url: `${target.method}://${ip}:${target.internalPort}`
|
||||
};
|
||||
@@ -409,7 +394,7 @@ export async function traefikConfigProvider(
|
||||
return false;
|
||||
}
|
||||
} else if (site.type === "newt") {
|
||||
if (!target.internalPort) {
|
||||
if (!target.internalPort || !site.subnet) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -424,7 +409,7 @@ export async function traefikConfigProvider(
|
||||
address: `${target.ip}:${target.port}`
|
||||
};
|
||||
} else if (site.type === "newt") {
|
||||
const ip = site.subnet.split("/")[0];
|
||||
const ip = site.subnet!.split("/")[0];
|
||||
return {
|
||||
address: `${ip}:${target.internalPort}`
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, UserOrg } from "@server/db";
|
||||
import { roles, userInvites, userOrgs, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -92,6 +92,7 @@ export async function acceptInvite(
|
||||
}
|
||||
|
||||
let roleId: number;
|
||||
let totalUsers: UserOrg[] | undefined;
|
||||
// get the role to make sure it exists
|
||||
const existingRole = await db
|
||||
.select()
|
||||
@@ -122,6 +123,12 @@ export async function acceptInvite(
|
||||
await trx
|
||||
.delete(userInvites)
|
||||
.where(eq(userInvites.inviteId, inviteId));
|
||||
|
||||
// Get the total number of users in the org now
|
||||
totalUsers = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.orgId, existingInvite.orgId));
|
||||
});
|
||||
|
||||
return response<AcceptInviteResponse>(res, {
|
||||
|
||||
@@ -6,7 +6,7 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { db } from "@server/db";
|
||||
import { db, UserOrg } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
@@ -135,65 +135,76 @@ export async function createOrgUser(
|
||||
);
|
||||
}
|
||||
|
||||
const [existingUser] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, username));
|
||||
let orgUsers: UserOrg[] | undefined;
|
||||
|
||||
if (existingUser) {
|
||||
const [existingOrgUser] = await db
|
||||
await db.transaction(async (trx) => {
|
||||
const [existingUser] = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.orgId, orgId),
|
||||
eq(userOrgs.userId, existingUser.userId)
|
||||
)
|
||||
);
|
||||
.from(users)
|
||||
.where(eq(users.username, username));
|
||||
|
||||
if (existingOrgUser) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"User already exists in this organization"
|
||||
)
|
||||
);
|
||||
if (existingUser) {
|
||||
const [existingOrgUser] = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.orgId, orgId),
|
||||
eq(userOrgs.userId, existingUser.userId)
|
||||
)
|
||||
);
|
||||
|
||||
if (existingOrgUser) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"User already exists in this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await trx
|
||||
.insert(userOrgs)
|
||||
.values({
|
||||
orgId,
|
||||
userId: existingUser.userId,
|
||||
roleId: role.roleId
|
||||
})
|
||||
.returning();
|
||||
} else {
|
||||
const userId = generateId(15);
|
||||
|
||||
const [newUser] = await trx
|
||||
.insert(users)
|
||||
.values({
|
||||
userId: userId,
|
||||
email,
|
||||
username,
|
||||
name,
|
||||
type: "oidc",
|
||||
idpId,
|
||||
dateCreated: new Date().toISOString(),
|
||||
emailVerified: true
|
||||
})
|
||||
.returning();
|
||||
|
||||
await trx
|
||||
.insert(userOrgs)
|
||||
.values({
|
||||
orgId,
|
||||
userId: newUser.userId,
|
||||
roleId: role.roleId
|
||||
})
|
||||
.returning();
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(userOrgs)
|
||||
.values({
|
||||
orgId,
|
||||
userId: existingUser.userId,
|
||||
roleId: role.roleId
|
||||
})
|
||||
.returning();
|
||||
} else {
|
||||
const userId = generateId(15);
|
||||
// List all of the users in the org
|
||||
orgUsers = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.orgId, orgId));
|
||||
});
|
||||
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
userId: userId,
|
||||
email,
|
||||
username,
|
||||
name,
|
||||
type: "oidc",
|
||||
idpId,
|
||||
dateCreated: new Date().toISOString(),
|
||||
emailVerified: true
|
||||
})
|
||||
.returning();
|
||||
|
||||
await db
|
||||
.insert(userOrgs)
|
||||
.values({
|
||||
orgId,
|
||||
userId: newUser.userId,
|
||||
roleId: role.roleId
|
||||
})
|
||||
.returning();
|
||||
}
|
||||
} else {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
||||
|
||||
@@ -99,6 +99,7 @@ export async function inviteUser(
|
||||
regenerate
|
||||
} = parsedBody.data;
|
||||
|
||||
|
||||
// Check if the organization exists
|
||||
const org = await db
|
||||
.select()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resources, sites } from "@server/db";
|
||||
import { db, resources, sites, UserOrg } from "@server/db";
|
||||
import { userOrgs, userResources, users, userSites } from "@server/db";
|
||||
import { and, eq, exists } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -65,6 +65,8 @@ export async function removeUserOrg(
|
||||
);
|
||||
}
|
||||
|
||||
let userCount: UserOrg[] | undefined;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(userOrgs)
|
||||
@@ -108,6 +110,11 @@ export async function removeUserOrg(
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
userCount = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.orgId, orgId));
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
Reference in New Issue
Block a user