Update api endpoints for new association

This commit is contained in:
Owen 2025-03-31 16:21:01 -04:00
parent 1baa02de89
commit 56e1684e2e
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
7 changed files with 172 additions and 133 deletions

View file

@ -64,6 +64,7 @@ export enum ActionsEnum {
updateResourceRule = "updateResourceRule", updateResourceRule = "updateResourceRule",
createClient = "createClient", createClient = "createClient",
deleteClient = "deleteClient", deleteClient = "deleteClient",
updateClient = "updateClient",
listClients = "listClients", listClients = "listClients",
listOrgDomains = "listOrgDomains", listOrgDomains = "listOrgDomains",
} }

View file

@ -3,15 +3,12 @@ import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { import {
roles, roles,
userSites,
sites,
roleSites,
Site,
Client, Client,
clients, clients,
roleClients, roleClients,
userClients, userClients,
olms olms,
clientSites
} from "@server/db/schema"; } from "@server/db/schema";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -21,21 +18,19 @@ import { eq, and } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import moment from "moment"; import moment from "moment";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { getNextAvailableClientSubnet } from "@server/lib/ip";
import config from "@server/lib/config";
const createClientParamsSchema = z const createClientParamsSchema = z
.object({ .object({
siteId: z orgId: z.string()
.string()
.transform((val) => parseInt(val))
.pipe(z.number())
}) })
.strict(); .strict();
const createClientSchema = z const createClientSchema = z
.object({ .object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
siteId: z.number().int().positive(), siteIds: z.array(z.string().transform(Number).pipe(z.number())),
subnet: z.string(),
olmId: z.string(), olmId: z.string(),
secret: z.string(), secret: z.string(),
type: z.enum(["olm"]) type: z.enum(["olm"])
@ -62,7 +57,7 @@ export async function createClient(
); );
} }
const { name, type, siteId, subnet, olmId, secret } = const { name, type, siteIds, olmId, secret } =
parsedBody.data; parsedBody.data;
const parsedParams = createClientParamsSchema.safeParse(req.params); const parsedParams = createClientParamsSchema.safeParse(req.params);
@ -75,16 +70,7 @@ export async function createClient(
); );
} }
const { siteId: paramSiteId } = parsedParams.data; const { orgId } = parsedParams.data;
if (siteId != paramSiteId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Site ID in body does not match site ID in URL"
)
);
}
if (!req.userOrgRoleId) { if (!req.userOrgRoleId) {
return next( return next(
@ -92,21 +78,16 @@ export async function createClient(
); );
} }
const [site] = await db const newSubnet = await getNextAvailableClientSubnet(orgId);
.select()
.from(sites)
.where(eq(sites.siteId, siteId));
if (!site) { const subnet = `${newSubnet.split("/")[0]}/${config.getRawConfig().orgs.block_size}` // we want the block size of the whole org
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
}
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
const adminRole = await trx const adminRole = await trx
.select() .select()
.from(roles) .from(roles)
.where( .where(
and(eq(roles.isAdmin, true), eq(roles.orgId, site.orgId)) and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))
) )
.limit(1); .limit(1);
@ -120,7 +101,7 @@ export async function createClient(
const [newClient] = await trx const [newClient] = await trx
.insert(clients) .insert(clients)
.values({ .values({
orgId: site.orgId, orgId,
name, name,
subnet, subnet,
type type
@ -140,6 +121,16 @@ export async function createClient(
}); });
} }
// Create site to client associations
if (siteIds && siteIds.length > 0) {
await trx.insert(clientSites).values(
siteIds.map(siteId => ({
clientId: newClient.clientId,
siteId
}))
);
}
const secretHash = await hashPassword(secret); const secretHash = await hashPassword(secret);
await trx.insert(olms).values({ await trx.insert(olms).values({
@ -163,4 +154,4 @@ export async function createClient(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );
} }
} }

View file

@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { clients, sites } from "@server/db/schema"; import { clients, clientSites } from "@server/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -48,7 +48,17 @@ export async function deleteClient(
); );
} }
await db.delete(clients).where(eq(clients.clientId, clientId)); await db.transaction(async (trx) => {
// Delete the client-site associations first
await trx
.delete(clientSites)
.where(eq(clientSites.clientId, clientId));
// Then delete the client itself
await trx
.delete(clients)
.where(eq(clients.clientId, clientId));
});
return response(res, { return response(res, {
data: null, data: null,
@ -63,4 +73,4 @@ export async function deleteClient(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
); );
} }
} }

View file

@ -2,3 +2,4 @@ export * from "./pickClientDefaults";
export * from "./createClient"; export * from "./createClient";
export * from "./deleteClient"; export * from "./deleteClient";
export * from "./listClients"; export * from "./listClients";
export * from "./updateClient";

View file

@ -1,32 +1,11 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { clients, exitNodes, olms, sites } from "@server/db/schema";
import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { findNextAvailableCidr, getNextAvailableClientSubnet } from "@server/lib/ip";
import { generateId } from "@server/auth/sessions/app"; import { generateId } from "@server/auth/sessions/app";
import config from "@server/lib/config";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const getSiteSchema = z
.object({
siteId: z.string().transform(Number).pipe(z.number())
})
.strict();
export type PickClientDefaultsResponse = { export type PickClientDefaultsResponse = {
exitNodeId: number;
siteId: number;
address: string;
publicKey: string;
name: string;
listenPort: number;
endpoint: string;
subnet: string;
olmId: string; olmId: string;
olmSecret: string; olmSecret: string;
}; };
@ -37,85 +16,11 @@ export async function pickClientDefaults(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedParams = getSiteSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { siteId } = parsedParams.data;
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId));
if (!site) {
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
}
if (site.type !== "newt") {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Site is not a newt site"
)
);
}
// TODO: more intelligent way to pick the exit node
// make sure there is an exit node by counting the exit nodes table
const nodes = await db.select().from(exitNodes);
if (nodes.length === 0) {
return next(
createHttpError(HttpCode.NOT_FOUND, "No exit nodes available")
);
}
// get the first exit node
const exitNode = nodes[0];
// make sure all the required fields are present
const sitesRequiredFields = z.object({
address: z.string(),
publicKey: z.string(),
listenPort: z.number(),
endpoint: z.string()
});
const parsedSite = sitesRequiredFields.safeParse(site);
if (!parsedSite.success) {
logger.error("Unable to pick client defaults because: " + fromError(parsedSite.error).toString());
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Site is not configured to accept client connectivity"
)
);
}
const { address, publicKey, listenPort, endpoint } = parsedSite.data;
const newSubnet = await getNextAvailableClientSubnet(site.orgId);
const olmId = generateId(15); const olmId = generateId(15);
const secret = generateId(48); const secret = generateId(48);
return response<PickClientDefaultsResponse>(res, { return response<PickClientDefaultsResponse>(res, {
data: { data: {
exitNodeId: exitNode.exitNodeId,
siteId: site.siteId,
address: address,
publicKey: publicKey,
name: site.name,
listenPort: listenPort,
endpoint: endpoint,
subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().orgs.block_size}`, // we want the block size of the whole org
olmId: olmId, olmId: olmId,
olmSecret: secret olmSecret: secret
}, },

View file

@ -0,0 +1,124 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import {
clients,
clientSites
} from "@server/db/schema";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { eq, and } from "drizzle-orm";
import { fromError } from "zod-validation-error";
const updateClientParamsSchema = z
.object({
clientId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
const updateClientSchema = z
.object({
name: z.string().min(1).max(255).optional(),
siteIds: z.array(z.string().transform(Number).pipe(z.number())).optional()
})
.strict();
export type UpdateClientBody = z.infer<typeof updateClientSchema>;
export async function updateClient(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = updateClientSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { name, siteIds } = parsedBody.data;
const parsedParams = updateClientParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { clientId } = parsedParams.data;
// Fetch the client to make sure it exists and the user has access to it
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
await db.transaction(async (trx) => {
// Update client name if provided
if (name) {
await trx
.update(clients)
.set({ name })
.where(eq(clients.clientId, clientId));
}
// Update site associations if provided
if (siteIds) {
// Delete existing site associations
await trx
.delete(clientSites)
.where(eq(clientSites.clientId, clientId));
// Create new site associations
if (siteIds.length > 0) {
await trx.insert(clientSites).values(
siteIds.map(siteId => ({
clientId,
siteId
}))
);
}
}
// Fetch the updated client
const [updatedClient] = await trx
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
return response(res, {
data: updatedClient,
success: true,
error: false,
message: "Client updated successfully",
status: HttpCode.OK
});
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -102,7 +102,7 @@ authenticated.get(
); );
authenticated.get( authenticated.get(
"/site/:siteId/pick-client-defaults", "/pick-client-defaults",
verifySiteAccess, verifySiteAccess,
verifyUserHasAction(ActionsEnum.createClient), verifyUserHasAction(ActionsEnum.createClient),
client.pickClientDefaults client.pickClientDefaults
@ -116,8 +116,8 @@ authenticated.get(
); );
authenticated.put( authenticated.put(
"/site/:siteId/client", "/org/:orgId/client",
verifySiteAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createClient), verifyUserHasAction(ActionsEnum.createClient),
client.createClient client.createClient
); );
@ -129,6 +129,13 @@ authenticated.delete(
client.deleteClient client.deleteClient
); );
authenticated.post(
"/client/:clientId",
verifyClientAccess, // this will check if the user has access to the client
verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client
client.updateClient
);
// authenticated.get( // authenticated.get(
// "/site/:siteId/roles", // "/site/:siteId/roles",
// verifySiteAccess, // verifySiteAccess,