From 2274a3525ba4875955b76dfdef49ae4d5f7408bc Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 6 Nov 2025 20:12:54 -0800 Subject: [PATCH] update olm and client routes --- server/middlewares/index.ts | 3 +- server/middlewares/verifyOlmAccess.ts | 45 ++++ server/routers/client/createClient.ts | 59 ++--- server/routers/client/createUserClient.ts | 240 ++++++++++++++++++ server/routers/client/deleteClient.ts | 11 +- server/routers/client/index.ts | 3 +- server/routers/external.ts | 23 +- server/routers/integration.ts | 18 +- .../olm/{createOlm.ts => createUserOlm.ts} | 76 +++--- .../olm/{deleteOlm.ts => deleteUserOlm.ts} | 57 ++--- server/routers/olm/index.ts | 7 +- .../olm/{listOlms.ts => listUserOlms.ts} | 52 ++-- .../[orgId]/settings/clients/create/page.tsx | 3 +- src/components/ViewDevicesDialog.tsx | 84 ++++-- 14 files changed, 495 insertions(+), 186 deletions(-) create mode 100644 server/middlewares/verifyOlmAccess.ts create mode 100644 server/routers/client/createUserClient.ts rename server/routers/olm/{createOlm.ts => createUserOlm.ts} (61%) rename server/routers/olm/{deleteOlm.ts => deleteUserOlm.ts} (65%) rename server/routers/olm/{listOlms.ts => listUserOlms.ts} (73%) diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 66a92809..4f8755d7 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -27,4 +27,5 @@ export * from "./verifyDomainAccess"; export * from "./verifyClientsEnabled"; export * from "./verifyUserIsOrgOwner"; export * from "./verifySiteResourceAccess"; -export * from "./logActionAudit"; \ No newline at end of file +export * from "./logActionAudit"; +export * from "./verifyOlmAccess"; diff --git a/server/middlewares/verifyOlmAccess.ts b/server/middlewares/verifyOlmAccess.ts new file mode 100644 index 00000000..7b31ae7d --- /dev/null +++ b/server/middlewares/verifyOlmAccess.ts @@ -0,0 +1,45 @@ +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import { db, olms } from "@server/db"; +import { and, eq } from "drizzle-orm"; + +export async function verifyOlmAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const userId = req.user!.userId; + const olmId = req.params.olmId || req.body.olmId || req.query.olmId; + + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + const [existingOlm] = await db + .select() + .from(olms) + .where(and(eq(olms.olmId, olmId), eq(olms.userId, userId))); + + if (!existingOlm) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this olm" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error checking if user has access to this user" + ) + ); + } +} diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index 5e73476a..651b1119 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -8,8 +8,6 @@ import { roleClients, userClients, olms, - clientSites, - exitNodes, orgs, sites } from "@server/db"; @@ -20,45 +18,44 @@ import logger from "@server/logger"; import { eq, and } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import moment from "moment"; -import { hashPassword, verifyPassword } from "@server/auth/password"; -import { isValidCIDR, isValidIP } from "@server/lib/validators"; +import { hashPassword } from "@server/auth/password"; +import { isValidIP } from "@server/lib/validators"; import { isIpInCidr } from "@server/lib/ip"; -import { OpenAPITags, registry } from "@server/openApi"; import { listExitNodes } from "#dynamic/lib/exitNodes"; import { generateId } from "@server/auth/sessions/app"; +import { OpenAPITags, registry } from "@server/openApi"; -const createClientParamsSchema = z +const paramsSchema = z .object({ orgId: z.string() }) .strict(); -const createClientSchema = z +const bodySchema = z .object({ name: z.string().min(1).max(255), - siteIds: z.array(z.number().int().positive()), olmId: z.string(), - secret: z.string().optional(), + secret: z.string(), subnet: z.string(), type: z.enum(["olm"]) }) .strict(); -export type CreateClientBody = z.infer; +export type CreateClientBody = z.infer; export type CreateClientResponse = Client; registry.registerPath({ method: "put", path: "/org/{orgId}/client", - description: "Create a new client.", + description: "Create a new client for an organization.", tags: [OpenAPITags.Client, OpenAPITags.Org], request: { - params: createClientParamsSchema, + params: paramsSchema, body: { content: { "application/json": { - schema: createClientSchema + schema: bodySchema } } } @@ -72,7 +69,7 @@ export async function createClient( next: NextFunction ): Promise { try { - const parsedBody = createClientSchema.safeParse(req.body); + const parsedBody = bodySchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( @@ -82,9 +79,9 @@ export async function createClient( ); } - const { name, type, siteIds, olmId, secret, subnet } = parsedBody.data; + const { name, type, olmId, secret, subnet } = parsedBody.data; - const parsedParams = createClientParamsSchema.safeParse(req.params); + const parsedParams = paramsSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( @@ -184,19 +181,13 @@ export async function createClient( .where(eq(olms.olmId, olmId)) .limit(1); - // TODO: HOW DO WE WANT TO AUTH THAT YOU CAN ADOPT AN EXISTING OLM CROSS ORG OTHER THAN MAKING SURE THE SECRET IS CORRECT - if (existingOlm && secret) { - // verify the secret - const validSecret = await verifyPassword( - secret, - existingOlm.secretHash + if (existingOlm) { + return next( + createHttpError( + HttpCode.CONFLICT, + `OLM with ID ${olmId} already exists` + ) ); - - if (!validSecret) { - return next( - createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect on existing olm") - ); - } } await db.transaction(async (trx) => { @@ -237,21 +228,11 @@ export async function createClient( if (req.user && req.userOrgRoleId != adminRole.roleId) { // make sure the user can access the client trx.insert(userClients).values({ - userId: req.user?.userId!, + userId: req.user.userId, clientId: newClient.clientId }); } - // Create site to client associations - if (siteIds && siteIds.length > 0) { - await trx.insert(clientSites).values( - siteIds.map((siteId) => ({ - clientId: newClient.clientId, - siteId - })) - ); - } - let secretToUse = secret; if (!secretToUse) { secretToUse = generateId(48); diff --git a/server/routers/client/createUserClient.ts b/server/routers/client/createUserClient.ts new file mode 100644 index 00000000..c6f535eb --- /dev/null +++ b/server/routers/client/createUserClient.ts @@ -0,0 +1,240 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { + roles, + Client, + clients, + roleClients, + userClients, + olms, + orgs, + sites +} 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 { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import { isValidIP } from "@server/lib/validators"; +import { isIpInCidr } from "@server/lib/ip"; +import { listExitNodes } from "#dynamic/lib/exitNodes"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + orgId: z.string(), + userId: z.string() + }) + .strict(); + +const bodySchema = z + .object({ + name: z.string().min(1).max(255), + olmId: z.string(), + subnet: z.string(), + type: z.enum(["olm"]) + }) + .strict(); + +export type CreateClientAndOlmBody = z.infer; + +export type CreateClientAndOlmResponse = Client; + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/user/{userId}/client", + description: + "Create a new client for a user and associate it with an existing olm.", + tags: [OpenAPITags.Client, OpenAPITags.Org, OpenAPITags.User], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function createUserClient( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { name, type, olmId, subnet } = parsedBody.data; + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, userId } = parsedParams.data; + + if (!isValidIP(subnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid subnet format. Please provide a valid CIDR notation." + ) + ); + } + + const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId)); + + if (!org) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Organization with ID ${orgId} not found` + ) + ); + } + + if (!org.subnet) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Organization with ID ${orgId} has no subnet defined` + ) + ); + } + + if (!isIpInCidr(subnet, org.subnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IP is not in the CIDR range of the subnet." + ) + ); + } + + const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org + + // make sure the subnet is unique + const subnetExistsClients = await db + .select() + .from(clients) + .where( + and(eq(clients.subnet, updatedSubnet), eq(clients.orgId, orgId)) + ) + .limit(1); + + if (subnetExistsClients.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + `Subnet ${updatedSubnet} already exists in clients` + ) + ); + } + + const subnetExistsSites = await db + .select() + .from(sites) + .where( + and(eq(sites.address, updatedSubnet), eq(sites.orgId, orgId)) + ) + .limit(1); + + if (subnetExistsSites.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + `Subnet ${updatedSubnet} already exists in sites` + ) + ); + } + + // check if the olmId already exists + const [existingOlm] = await db + .select() + .from(olms) + .where(eq(olms.olmId, olmId)) + .limit(1); + + if (!existingOlm) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `OLM with ID ${olmId} does not exist` + ) + ); + } + + await db.transaction(async (trx) => { + // TODO: more intelligent way to pick the exit node + const exitNodesList = await listExitNodes(orgId); + const randomExitNode = + exitNodesList[Math.floor(Math.random() * exitNodesList.length)]; + + const [adminRole] = await trx + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); + + if (!adminRole) { + return next( + createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) + ); + } + + const [newClient] = await trx + .insert(clients) + .values({ + exitNodeId: randomExitNode.exitNodeId, + orgId, + name, + subnet: updatedSubnet, + type, + olmId, // this is to lock it to a specific olm even if the olm moves across clients + userId + }) + .returning(); + + await trx.insert(roleClients).values({ + roleId: adminRole.roleId, + clientId: newClient.clientId + }); + + trx.insert(userClients).values({ + userId, + clientId: newClient.clientId + }); + + return response(res, { + data: newClient, + success: true, + error: false, + message: "Site created successfully", + status: HttpCode.CREATED + }); + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts index a7512574..e4752175 100644 --- a/server/routers/client/deleteClient.ts +++ b/server/routers/client/deleteClient.ts @@ -67,9 +67,14 @@ export async function deleteClient( .where(eq(clientSites.clientId, clientId)); // Then delete the client itself - await trx - .delete(clients) - .where(eq(clients.clientId, clientId)); + await trx.delete(clients).where(eq(clients.clientId, clientId)); + + // this is a machine client + if (!client.userId && client.olmId) { + await trx + .delete(clients) + .where(eq(clients.olmId, client.olmId)); + } }); return response(res, { diff --git a/server/routers/client/index.ts b/server/routers/client/index.ts index 385c7bed..8e88c11e 100644 --- a/server/routers/client/index.ts +++ b/server/routers/client/index.ts @@ -3,4 +3,5 @@ export * from "./createClient"; export * from "./deleteClient"; export * from "./listClients"; export * from "./updateClient"; -export * from "./getClient"; \ No newline at end of file +export * from "./getClient"; +export * from "./createUserClient"; diff --git a/server/routers/external.ts b/server/routers/external.ts index f71f9250..6b84c3ab 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -39,7 +39,8 @@ import { verifyClientsEnabled, verifyUserHasAction, verifyUserIsOrgOwner, - verifySiteResourceAccess + verifySiteResourceAccess, + verifyOlmAccess } from "@server/middlewares"; import { ActionsEnum } from "@server/auth/actions"; import rateLimit, { ipKeyGenerator } from "express-rate-limit"; @@ -160,6 +161,7 @@ authenticated.put( client.createClient, ); +// TODO: Separate into a deleteUserClient (for user clients) and deleteClient (for machine clients) authenticated.delete( "/client/:clientId", verifyClientsEnabled, @@ -758,22 +760,23 @@ authenticated.delete( // createNewt // ); -// only for logged in user authenticated.put( - "/olm", - olm.createOlm + "/user/:userId/olm", + verifyIsLoggedInUser, + olm.createUserOlm ); -// only for logged in user authenticated.get( - "/olms", - olm.listOlms + "/user/:userId/olms", + verifyIsLoggedInUser, + olm.listUserOlms ); -// only for logged in user authenticated.delete( - "/olm/:olmId", - olm.deleteOlm + "/user/:userId/olm/:olmId", + verifyIsLoggedInUser, + verifyOlmAccess, + olm.deleteUserOlm ); authenticated.put( diff --git a/server/routers/integration.ts b/server/routers/integration.ts index dab8af69..a70b4e32 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -11,7 +11,6 @@ import * as accessToken from "./accessToken"; import * as apiKeys from "./apiKeys"; import * as idp from "./idp"; import * as siteResource from "./siteResource"; -import * as olm from "./olm"; import { verifyApiKey, verifyApiKeyOrgAccess, @@ -589,13 +588,6 @@ authenticated.delete( // newt.createNewt // ); -authenticated.put( - "/user/:userId/olm", - verifyApiKeyUserAccess, - verifyApiKeyHasAction(ActionsEnum.createOlm), - olm.createOlm -); - authenticated.get( `/org/:orgId/api-keys`, verifyApiKeyIsRoot, @@ -728,6 +720,16 @@ authenticated.put( client.createClient ); +authenticated.put( + "/org/:orgId/user/:userId/client", + verifyClientsEnabled, + verifyApiKeyOrgAccess, + verifyApiKeyUserAccess, + verifyApiKeyHasAction(ActionsEnum.createClient), + logActionAudit(ActionsEnum.createClient), + client.createUserClient +); + authenticated.delete( "/client/:clientId", verifyClientsEnabled, diff --git a/server/routers/olm/createOlm.ts b/server/routers/olm/createUserOlm.ts similarity index 61% rename from server/routers/olm/createOlm.ts rename to server/routers/olm/createUserOlm.ts index eff67a88..9b64c1a9 100644 --- a/server/routers/olm/createOlm.ts +++ b/server/routers/olm/createUserOlm.ts @@ -1,46 +1,58 @@ import { NextFunction, Request, Response } from "express"; import { db } from "@server/db"; -import { hash } from "@node-rs/argon2"; import HttpCode from "@server/types/HttpCode"; import { z } from "zod"; import { olms } from "@server/db"; import createHttpError from "http-errors"; import response from "@server/lib/response"; -import { SqliteError } from "better-sqlite3"; import moment from "moment"; -import { generateId, generateSessionToken } from "@server/auth/sessions/app"; -import { createOlmSession } from "@server/auth/sessions/olm"; +import { generateId } from "@server/auth/sessions/app"; import { fromError } from "zod-validation-error"; import { hashPassword } from "@server/auth/password"; +import { OpenAPITags, registry } from "@server/openApi"; -export const createOlmBodySchema = z.object({}); - -export type CreateOlmBody = z.infer; - -export type CreateOlmResponse = { - // token: string; - olmId: string; - secret: string; -}; - -const createOlmSchema = z +const bodySchema = z .object({ name: z.string().min(1).max(255) }) .strict(); -const createOlmParamsSchema = z - .object({ - userId: z.string().optional() - }); +const paramsSchema = z.object({ + userId: z.string() +}); -export async function createOlm( +export type CreateOlmBody = z.infer; + +export type CreateOlmResponse = { + olmId: string; + secret: string; +}; + +registry.registerPath({ + method: "put", + path: "/user/{userId}/olm", + description: "Create a new olm for a user.", + tags: [OpenAPITags.User, OpenAPITags.Client], + request: { + body: { + content: { + "application/json": { + schema: bodySchema + } + } + }, + params: paramsSchema + }, + responses: {} +}); + +export async function createUserOlm( req: Request, res: Response, next: NextFunction ): Promise { try { - const parsedBody = createOlmSchema.safeParse(req.body); + const parsedBody = bodySchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( @@ -52,7 +64,7 @@ export async function createOlm( const { name } = parsedBody.data; - const parsedParams = createOlmParamsSchema.safeParse(req.params); + const parsedParams = paramsSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( @@ -63,20 +75,6 @@ export async function createOlm( } const { userId } = parsedParams.data; - let userIdFinal = userId; - - if (req.user) { // overwrite the user with the one calling because we want to assign the olm to the user creating it - userIdFinal = req.user.userId; - } - - if (!userIdFinal) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Either userId must be provided or request must be authenticated" - ) - ); - } const olmId = generateId(15); const secret = generateId(48); @@ -85,20 +83,16 @@ export async function createOlm( await db.insert(olms).values({ olmId: olmId, - userId: userIdFinal, + userId, name, secretHash, dateCreated: moment().toISOString() }); - // const token = generateSessionToken(); - // await createOlmSession(token, olmId); - return response(res, { data: { olmId, secret - // token, }, success: true, error: false, diff --git a/server/routers/olm/deleteOlm.ts b/server/routers/olm/deleteUserOlm.ts similarity index 65% rename from server/routers/olm/deleteOlm.ts rename to server/routers/olm/deleteUserOlm.ts index 7ba41dc5..d0c1e246 100644 --- a/server/routers/olm/deleteOlm.ts +++ b/server/routers/olm/deleteUserOlm.ts @@ -8,28 +8,33 @@ import response from "@server/lib/response"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; -const deleteOlmParamsSchema = z +const paramsSchema = z .object({ + userId: z.string(), olmId: z.string() }) .strict(); -export async function deleteOlm( +registry.registerPath({ + method: "delete", + path: "/user/{userId}/olm/{olmId}", + description: "Delete an olm for a user.", + tags: [OpenAPITags.User, OpenAPITags.Client], + request: { + params: paramsSchema + }, + responses: {} +}); + +export async function deleteUserOlm( req: Request, res: Response, next: NextFunction ): Promise { try { - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - const parsedParams = deleteOlmParamsSchema.safeParse(req.params); + const parsedParams = paramsSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( @@ -41,31 +46,6 @@ export async function deleteOlm( const { olmId } = parsedParams.data; - // Verify the OLM belongs to the current user - const [existingOlm] = await db - .select() - .from(olms) - .where(eq(olms.olmId, olmId)) - .limit(1); - - if (!existingOlm) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Olm with ID ${olmId} not found` - ) - ); - } - - if (existingOlm.userId !== userId) { - return next( - createHttpError( - HttpCode.FORBIDDEN, - "You do not have permission to delete this device" - ) - ); - } - // Delete associated clients and the OLM in a transaction await db.transaction(async (trx) => { // Find all clients associated with this OLM @@ -83,9 +63,7 @@ export async function deleteOlm( // Delete all associated clients if (associatedClients.length > 0) { - await trx - .delete(clients) - .where(eq(clients.olmId, olmId)); + await trx.delete(clients).where(eq(clients.olmId, olmId)); } // Finally, delete the OLM itself @@ -109,4 +87,3 @@ export async function deleteOlm( ); } } - diff --git a/server/routers/olm/index.ts b/server/routers/olm/index.ts index 5e28b96a..6882d019 100644 --- a/server/routers/olm/index.ts +++ b/server/routers/olm/index.ts @@ -1,7 +1,8 @@ export * from "./handleOlmRegisterMessage"; export * from "./getOlmToken"; -export * from "./createOlm"; +export * from "./createUserOlm"; export * from "./handleOlmRelayMessage"; export * from "./handleOlmPingMessage"; -export * from "./listOlms"; -export * from "./deleteOlm"; \ No newline at end of file +export * from "./deleteUserOlm"; +export * from "./listUserOlms"; +export * from "./deleteUserOlm"; diff --git a/server/routers/olm/listOlms.ts b/server/routers/olm/listUserOlms.ts similarity index 73% rename from server/routers/olm/listOlms.ts rename to server/routers/olm/listUserOlms.ts index c61e1d8d..1cc0a802 100644 --- a/server/routers/olm/listOlms.ts +++ b/server/routers/olm/listUserOlms.ts @@ -8,8 +8,9 @@ import response from "@server/lib/response"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; -const listOlmsSchema = z.object({ +const querySchema = z.object({ limit: z .string() .optional() @@ -24,7 +25,25 @@ const listOlmsSchema = z.object({ .pipe(z.number().int().nonnegative()) }); -export type ListOlmsResponse = { +const paramsSchema = z + .object({ + userId: z.string() + }) + .strict(); + +registry.registerPath({ + method: "delete", + path: "/user/{userId}/olms", + description: "List all olms for a user.", + tags: [OpenAPITags.User, OpenAPITags.Client], + request: { + query: querySchema, + params: paramsSchema + }, + responses: {} +}); + +export type ListUserOlmsResponse = { olms: Array<{ olmId: string; dateCreated: string; @@ -40,21 +59,13 @@ export type ListOlmsResponse = { }; }; -export async function listOlms( +export async function listUserOlms( req: Request, res: Response, next: NextFunction ): Promise { try { - const userId = req.user?.userId; - - if (!userId) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - const parsedQuery = listOlmsSchema.safeParse(req.query); + const parsedQuery = querySchema.safeParse(req.query); if (!parsedQuery.success) { return next( createHttpError( @@ -66,6 +77,18 @@ export async function listOlms( const { limit, offset } = parsedQuery.data; + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { userId } = parsedParams.data; + // Get total count const [totalCountResult] = await db .select({ count: count() }) @@ -90,7 +113,7 @@ export async function listOlms( .limit(limit) .offset(offset); - return response(res, { + return response(res, { data: { olms: userOlms, pagination: { @@ -101,7 +124,7 @@ export async function listOlms( }, success: true, error: false, - message: "OLMs retrieved successfully", + message: "Olms retrieved successfully", status: HttpCode.OK }); } catch (error) { @@ -114,4 +137,3 @@ export async function listOlms( ); } } - diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 196caca4..47657063 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -52,7 +52,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { CreateClientBody, CreateClientResponse, - PickClientDefaultsResponse + PickClientDefaultsResponse, } from "@server/routers/client"; import { ListSitesResponse } from "@server/routers/site"; import { toast } from "@app/hooks/useToast"; @@ -258,7 +258,6 @@ export default function Page() { const payload: CreateClientBody = { name: data.name, type: data.method as "olm", - siteIds: data.siteIds.map((site) => parseInt(site.id)), olmId: clientDefaults.olmId, secret: clientDefaults.olmSecret, subnet: data.subnet diff --git a/src/components/ViewDevicesDialog.tsx b/src/components/ViewDevicesDialog.tsx index 07f7a340..428e850a 100644 --- a/src/components/ViewDevicesDialog.tsx +++ b/src/components/ViewDevicesDialog.tsx @@ -17,7 +17,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; -import { ListOlmsResponse } from "@server/routers/olm"; +import { ListUserOlmsResponse } from "@server/routers/olm"; import { ResponseT } from "@server/types/Response"; import { Table, @@ -30,6 +30,7 @@ import { import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { RefreshCw } from "lucide-react"; import moment from "moment"; +import { useUserContext } from "@app/hooks/useUserContext"; type ViewDevicesDialogProps = { open: boolean; @@ -45,11 +46,15 @@ type Device = { userId: string | null; }; -export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogProps) { +export default function ViewDevicesDialog({ + open, + setOpen +}: ViewDevicesDialogProps) { const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); - + const { user } = useUserContext(); + const [devices, setDevices] = useState([]); const [loading, setLoading] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -58,7 +63,9 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr const fetchDevices = async () => { setLoading(true); try { - const res = await api.get>("/olms"); + const res = await api.get>( + `/user/${user?.userId}/olms` + ); if (res.data.success && res.data.data) { setDevices(res.data.data.olms); } @@ -67,7 +74,10 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr toast({ variant: "destructive", title: t("errorLoadingDevices") || "Error loading devices", - description: formatAxiosError(error, t("failedToLoadDevices") || "Failed to load devices") + description: formatAxiosError( + error, + t("failedToLoadDevices") || "Failed to load devices" + ) }); } finally { setLoading(false); @@ -78,17 +88,18 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr if (open) { fetchDevices(); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); const deleteDevice = async (olmId: string) => { try { - await api.delete(`/olm/${olmId}`); + await api.delete(`/user/${user?.userId}/olm/${olmId}`); toast({ title: t("deviceDeleted") || "Device deleted", - description: t("deviceDeletedDescription") || "The device has been successfully deleted." + description: + t("deviceDeletedDescription") || + "The device has been successfully deleted." }); - setDevices(devices.filter(d => d.olmId !== olmId)); + setDevices(devices.filter((d) => d.olmId !== olmId)); setIsDeleteModalOpen(false); setSelectedDevice(null); } catch (error: any) { @@ -96,7 +107,10 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr toast({ variant: "destructive", title: t("errorDeletingDevice") || "Error deleting device", - description: formatAxiosError(error, t("failedToDeleteDevice") || "Failed to delete device") + description: formatAxiosError( + error, + t("failedToDeleteDevice") || "Failed to delete device" + ) }); } }; @@ -124,7 +138,8 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr {t("viewDevices") || "View Devices"} - {t("viewDevicesDescription") || "Manage your connected devices"} + {t("viewDevicesDescription") || + "Manage your connected devices"} @@ -141,29 +156,45 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr - {t("name") || "Name"} - {t("dateCreated") || "Date Created"} - {t("actions") || "Actions"} + + {t("name") || "Name"} + + + {t("dateCreated") || + "Date Created"} + + + {t("actions") || "Actions"} + {devices.map((device) => ( - {device.name || t("unnamedDevice") || "Unnamed Device"} + {device.name || + t("unnamedDevice") || + "Unnamed Device"} - {moment(device.dateCreated).format("lll")} + {moment( + device.dateCreated + ).format("lll")} @@ -175,7 +206,9 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr - + @@ -192,8 +225,14 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr }} dialog={
-

{t("deviceQuestionRemove") || "Are you sure you want to delete this device?"}

-

{t("deviceMessageRemove") || "This action cannot be undone."}

+

+ {t("deviceQuestionRemove") || + "Are you sure you want to delete this device?"} +

+

+ {t("deviceMessageRemove") || + "This action cannot be undone."} +

} buttonText={t("deviceDeleteConfirm") || "Delete Device"} @@ -205,4 +244,3 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr ); } -