update olm and client routes

This commit is contained in:
miloschwartz
2025-11-06 20:12:54 -08:00
parent 999fb2fff1
commit 2274a3525b
14 changed files with 495 additions and 186 deletions

View File

@@ -28,3 +28,4 @@ export * from "./verifyClientsEnabled";
export * from "./verifyUserIsOrgOwner"; export * from "./verifyUserIsOrgOwner";
export * from "./verifySiteResourceAccess"; export * from "./verifySiteResourceAccess";
export * from "./logActionAudit"; export * from "./logActionAudit";
export * from "./verifyOlmAccess";

View File

@@ -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"
)
);
}
}

View File

@@ -8,8 +8,6 @@ import {
roleClients, roleClients,
userClients, userClients,
olms, olms,
clientSites,
exitNodes,
orgs, orgs,
sites sites
} from "@server/db"; } from "@server/db";
@@ -20,45 +18,44 @@ import logger from "@server/logger";
import { eq, and } from "drizzle-orm"; 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, verifyPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP } from "@server/lib/validators"; import { isValidIP } from "@server/lib/validators";
import { isIpInCidr } from "@server/lib/ip"; import { isIpInCidr } from "@server/lib/ip";
import { OpenAPITags, registry } from "@server/openApi";
import { listExitNodes } from "#dynamic/lib/exitNodes"; import { listExitNodes } from "#dynamic/lib/exitNodes";
import { generateId } from "@server/auth/sessions/app"; import { generateId } from "@server/auth/sessions/app";
import { OpenAPITags, registry } from "@server/openApi";
const createClientParamsSchema = z const paramsSchema = z
.object({ .object({
orgId: z.string() orgId: z.string()
}) })
.strict(); .strict();
const createClientSchema = z const bodySchema = z
.object({ .object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
siteIds: z.array(z.number().int().positive()),
olmId: z.string(), olmId: z.string(),
secret: z.string().optional(), secret: z.string(),
subnet: z.string(), subnet: z.string(),
type: z.enum(["olm"]) type: z.enum(["olm"])
}) })
.strict(); .strict();
export type CreateClientBody = z.infer<typeof createClientSchema>; export type CreateClientBody = z.infer<typeof bodySchema>;
export type CreateClientResponse = Client; export type CreateClientResponse = Client;
registry.registerPath({ registry.registerPath({
method: "put", method: "put",
path: "/org/{orgId}/client", path: "/org/{orgId}/client",
description: "Create a new client.", description: "Create a new client for an organization.",
tags: [OpenAPITags.Client, OpenAPITags.Org], tags: [OpenAPITags.Client, OpenAPITags.Org],
request: { request: {
params: createClientParamsSchema, params: paramsSchema,
body: { body: {
content: { content: {
"application/json": { "application/json": {
schema: createClientSchema schema: bodySchema
} }
} }
} }
@@ -72,7 +69,7 @@ export async function createClient(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedBody = createClientSchema.safeParse(req.body); const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( 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) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
@@ -184,20 +181,14 @@ export async function createClient(
.where(eq(olms.olmId, olmId)) .where(eq(olms.olmId, olmId))
.limit(1); .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) {
if (existingOlm && secret) {
// verify the secret
const validSecret = await verifyPassword(
secret,
existingOlm.secretHash
);
if (!validSecret) {
return next( return next(
createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect on existing olm") createHttpError(
HttpCode.CONFLICT,
`OLM with ID ${olmId} already exists`
)
); );
} }
}
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// TODO: more intelligent way to pick the exit node // TODO: more intelligent way to pick the exit node
@@ -237,21 +228,11 @@ export async function createClient(
if (req.user && req.userOrgRoleId != adminRole.roleId) { if (req.user && req.userOrgRoleId != adminRole.roleId) {
// make sure the user can access the client // make sure the user can access the client
trx.insert(userClients).values({ trx.insert(userClients).values({
userId: req.user?.userId!, userId: req.user.userId,
clientId: newClient.clientId 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; let secretToUse = secret;
if (!secretToUse) { if (!secretToUse) {
secretToUse = generateId(48); secretToUse = generateId(48);

View File

@@ -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<typeof bodySchema>;
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<any> {
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<CreateClientAndOlmResponse>(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")
);
}
}

View File

@@ -67,9 +67,14 @@ export async function deleteClient(
.where(eq(clientSites.clientId, clientId)); .where(eq(clientSites.clientId, clientId));
// Then delete the client itself // Then delete the client itself
await trx.delete(clients).where(eq(clients.clientId, clientId));
// this is a machine client
if (!client.userId && client.olmId) {
await trx await trx
.delete(clients) .delete(clients)
.where(eq(clients.clientId, clientId)); .where(eq(clients.olmId, client.olmId));
}
}); });
return response(res, { return response(res, {

View File

@@ -4,3 +4,4 @@ export * from "./deleteClient";
export * from "./listClients"; export * from "./listClients";
export * from "./updateClient"; export * from "./updateClient";
export * from "./getClient"; export * from "./getClient";
export * from "./createUserClient";

View File

@@ -39,7 +39,8 @@ import {
verifyClientsEnabled, verifyClientsEnabled,
verifyUserHasAction, verifyUserHasAction,
verifyUserIsOrgOwner, verifyUserIsOrgOwner,
verifySiteResourceAccess verifySiteResourceAccess,
verifyOlmAccess
} from "@server/middlewares"; } from "@server/middlewares";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import rateLimit, { ipKeyGenerator } from "express-rate-limit";
@@ -160,6 +161,7 @@ authenticated.put(
client.createClient, client.createClient,
); );
// TODO: Separate into a deleteUserClient (for user clients) and deleteClient (for machine clients)
authenticated.delete( authenticated.delete(
"/client/:clientId", "/client/:clientId",
verifyClientsEnabled, verifyClientsEnabled,
@@ -758,22 +760,23 @@ authenticated.delete(
// createNewt // createNewt
// ); // );
// only for logged in user
authenticated.put( authenticated.put(
"/olm", "/user/:userId/olm",
olm.createOlm verifyIsLoggedInUser,
olm.createUserOlm
); );
// only for logged in user
authenticated.get( authenticated.get(
"/olms", "/user/:userId/olms",
olm.listOlms verifyIsLoggedInUser,
olm.listUserOlms
); );
// only for logged in user
authenticated.delete( authenticated.delete(
"/olm/:olmId", "/user/:userId/olm/:olmId",
olm.deleteOlm verifyIsLoggedInUser,
verifyOlmAccess,
olm.deleteUserOlm
); );
authenticated.put( authenticated.put(

View File

@@ -11,7 +11,6 @@ import * as accessToken from "./accessToken";
import * as apiKeys from "./apiKeys"; import * as apiKeys from "./apiKeys";
import * as idp from "./idp"; import * as idp from "./idp";
import * as siteResource from "./siteResource"; import * as siteResource from "./siteResource";
import * as olm from "./olm";
import { import {
verifyApiKey, verifyApiKey,
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
@@ -589,13 +588,6 @@ authenticated.delete(
// newt.createNewt // newt.createNewt
// ); // );
authenticated.put(
"/user/:userId/olm",
verifyApiKeyUserAccess,
verifyApiKeyHasAction(ActionsEnum.createOlm),
olm.createOlm
);
authenticated.get( authenticated.get(
`/org/:orgId/api-keys`, `/org/:orgId/api-keys`,
verifyApiKeyIsRoot, verifyApiKeyIsRoot,
@@ -728,6 +720,16 @@ authenticated.put(
client.createClient client.createClient
); );
authenticated.put(
"/org/:orgId/user/:userId/client",
verifyClientsEnabled,
verifyApiKeyOrgAccess,
verifyApiKeyUserAccess,
verifyApiKeyHasAction(ActionsEnum.createClient),
logActionAudit(ActionsEnum.createClient),
client.createUserClient
);
authenticated.delete( authenticated.delete(
"/client/:clientId", "/client/:clientId",
verifyClientsEnabled, verifyClientsEnabled,

View File

@@ -1,46 +1,58 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { hash } from "@node-rs/argon2";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { z } from "zod"; import { z } from "zod";
import { olms } from "@server/db"; import { olms } from "@server/db";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { SqliteError } from "better-sqlite3";
import moment from "moment"; import moment from "moment";
import { generateId, generateSessionToken } from "@server/auth/sessions/app"; import { generateId } from "@server/auth/sessions/app";
import { createOlmSession } from "@server/auth/sessions/olm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { OpenAPITags, registry } from "@server/openApi";
export const createOlmBodySchema = z.object({}); const bodySchema = z
export type CreateOlmBody = z.infer<typeof createOlmBodySchema>;
export type CreateOlmResponse = {
// token: string;
olmId: string;
secret: string;
};
const createOlmSchema = z
.object({ .object({
name: z.string().min(1).max(255) name: z.string().min(1).max(255)
}) })
.strict(); .strict();
const createOlmParamsSchema = z const paramsSchema = z.object({
.object({ userId: z.string()
userId: z.string().optional()
}); });
export async function createOlm( export type CreateOlmBody = z.infer<typeof bodySchema>;
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, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedBody = createOlmSchema.safeParse(req.body); const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
@@ -52,7 +64,7 @@ export async function createOlm(
const { name } = parsedBody.data; const { name } = parsedBody.data;
const parsedParams = createOlmParamsSchema.safeParse(req.params); const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
@@ -63,20 +75,6 @@ export async function createOlm(
} }
const { userId } = parsedParams.data; 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 olmId = generateId(15);
const secret = generateId(48); const secret = generateId(48);
@@ -85,20 +83,16 @@ export async function createOlm(
await db.insert(olms).values({ await db.insert(olms).values({
olmId: olmId, olmId: olmId,
userId: userIdFinal, userId,
name, name,
secretHash, secretHash,
dateCreated: moment().toISOString() dateCreated: moment().toISOString()
}); });
// const token = generateSessionToken();
// await createOlmSession(token, olmId);
return response<CreateOlmResponse>(res, { return response<CreateOlmResponse>(res, {
data: { data: {
olmId, olmId,
secret secret
// token,
}, },
success: true, success: true,
error: false, error: false,

View File

@@ -8,28 +8,33 @@ import response from "@server/lib/response";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
const deleteOlmParamsSchema = z const paramsSchema = z
.object({ .object({
userId: z.string(),
olmId: z.string() olmId: z.string()
}) })
.strict(); .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, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const userId = req.user?.userId; const parsedParams = paramsSchema.safeParse(req.params);
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
const parsedParams = deleteOlmParamsSchema.safeParse(req.params);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
@@ -41,31 +46,6 @@ export async function deleteOlm(
const { olmId } = parsedParams.data; 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 // Delete associated clients and the OLM in a transaction
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// Find all clients associated with this OLM // Find all clients associated with this OLM
@@ -83,9 +63,7 @@ export async function deleteOlm(
// Delete all associated clients // Delete all associated clients
if (associatedClients.length > 0) { if (associatedClients.length > 0) {
await trx await trx.delete(clients).where(eq(clients.olmId, olmId));
.delete(clients)
.where(eq(clients.olmId, olmId));
} }
// Finally, delete the OLM itself // Finally, delete the OLM itself
@@ -109,4 +87,3 @@ export async function deleteOlm(
); );
} }
} }

View File

@@ -1,7 +1,8 @@
export * from "./handleOlmRegisterMessage"; export * from "./handleOlmRegisterMessage";
export * from "./getOlmToken"; export * from "./getOlmToken";
export * from "./createOlm"; export * from "./createUserOlm";
export * from "./handleOlmRelayMessage"; export * from "./handleOlmRelayMessage";
export * from "./handleOlmPingMessage"; export * from "./handleOlmPingMessage";
export * from "./listOlms"; export * from "./deleteUserOlm";
export * from "./deleteOlm"; export * from "./listUserOlms";
export * from "./deleteUserOlm";

View File

@@ -8,8 +8,9 @@ import response from "@server/lib/response";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
const listOlmsSchema = z.object({ const querySchema = z.object({
limit: z limit: z
.string() .string()
.optional() .optional()
@@ -24,7 +25,25 @@ const listOlmsSchema = z.object({
.pipe(z.number().int().nonnegative()) .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<{ olms: Array<{
olmId: string; olmId: string;
dateCreated: string; dateCreated: string;
@@ -40,21 +59,13 @@ export type ListOlmsResponse = {
}; };
}; };
export async function listOlms( export async function listUserOlms(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const userId = req.user?.userId; const parsedQuery = querySchema.safeParse(req.query);
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
const parsedQuery = listOlmsSchema.safeParse(req.query);
if (!parsedQuery.success) { if (!parsedQuery.success) {
return next( return next(
createHttpError( createHttpError(
@@ -66,6 +77,18 @@ export async function listOlms(
const { limit, offset } = parsedQuery.data; 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 // Get total count
const [totalCountResult] = await db const [totalCountResult] = await db
.select({ count: count() }) .select({ count: count() })
@@ -90,7 +113,7 @@ export async function listOlms(
.limit(limit) .limit(limit)
.offset(offset); .offset(offset);
return response<ListOlmsResponse>(res, { return response<ListUserOlmsResponse>(res, {
data: { data: {
olms: userOlms, olms: userOlms,
pagination: { pagination: {
@@ -101,7 +124,7 @@ export async function listOlms(
}, },
success: true, success: true,
error: false, error: false,
message: "OLMs retrieved successfully", message: "Olms retrieved successfully",
status: HttpCode.OK status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
@@ -114,4 +137,3 @@ export async function listOlms(
); );
} }
} }

View File

@@ -52,7 +52,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { import {
CreateClientBody, CreateClientBody,
CreateClientResponse, CreateClientResponse,
PickClientDefaultsResponse PickClientDefaultsResponse,
} from "@server/routers/client"; } from "@server/routers/client";
import { ListSitesResponse } from "@server/routers/site"; import { ListSitesResponse } from "@server/routers/site";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
@@ -258,7 +258,6 @@ export default function Page() {
const payload: CreateClientBody = { const payload: CreateClientBody = {
name: data.name, name: data.name,
type: data.method as "olm", type: data.method as "olm",
siteIds: data.siteIds.map((site) => parseInt(site.id)),
olmId: clientDefaults.olmId, olmId: clientDefaults.olmId,
secret: clientDefaults.olmSecret, secret: clientDefaults.olmSecret,
subnet: data.subnet subnet: data.subnet

View File

@@ -17,7 +17,7 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/api"; 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 { ResponseT } from "@server/types/Response";
import { import {
Table, Table,
@@ -30,6 +30,7 @@ import {
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { RefreshCw } from "lucide-react"; import { RefreshCw } from "lucide-react";
import moment from "moment"; import moment from "moment";
import { useUserContext } from "@app/hooks/useUserContext";
type ViewDevicesDialogProps = { type ViewDevicesDialogProps = {
open: boolean; open: boolean;
@@ -45,10 +46,14 @@ type Device = {
userId: string | null; userId: string | null;
}; };
export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogProps) { export default function ViewDevicesDialog({
open,
setOpen
}: ViewDevicesDialogProps) {
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext(); const { env } = useEnvContext();
const api = createApiClient({ env }); const api = createApiClient({ env });
const { user } = useUserContext();
const [devices, setDevices] = useState<Device[]>([]); const [devices, setDevices] = useState<Device[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -58,7 +63,9 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
const fetchDevices = async () => { const fetchDevices = async () => {
setLoading(true); setLoading(true);
try { try {
const res = await api.get<ResponseT<ListOlmsResponse>>("/olms"); const res = await api.get<ResponseT<ListUserOlmsResponse>>(
`/user/${user?.userId}/olms`
);
if (res.data.success && res.data.data) { if (res.data.success && res.data.data) {
setDevices(res.data.data.olms); setDevices(res.data.data.olms);
} }
@@ -67,7 +74,10 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
toast({ toast({
variant: "destructive", variant: "destructive",
title: t("errorLoadingDevices") || "Error loading devices", 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 { } finally {
setLoading(false); setLoading(false);
@@ -78,17 +88,18 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
if (open) { if (open) {
fetchDevices(); fetchDevices();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]); }, [open]);
const deleteDevice = async (olmId: string) => { const deleteDevice = async (olmId: string) => {
try { try {
await api.delete(`/olm/${olmId}`); await api.delete(`/user/${user?.userId}/olm/${olmId}`);
toast({ toast({
title: t("deviceDeleted") || "Device deleted", 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); setIsDeleteModalOpen(false);
setSelectedDevice(null); setSelectedDevice(null);
} catch (error: any) { } catch (error: any) {
@@ -96,7 +107,10 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
toast({ toast({
variant: "destructive", variant: "destructive",
title: t("errorDeletingDevice") || "Error deleting device", 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("viewDevices") || "View Devices"}
</CredenzaTitle> </CredenzaTitle>
<CredenzaDescription> <CredenzaDescription>
{t("viewDevicesDescription") || "Manage your connected devices"} {t("viewDevicesDescription") ||
"Manage your connected devices"}
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
@@ -141,29 +156,45 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="pl-3">{t("name") || "Name"}</TableHead> <TableHead className="pl-3">
<TableHead>{t("dateCreated") || "Date Created"}</TableHead> {t("name") || "Name"}
<TableHead >{t("actions") || "Actions"}</TableHead> </TableHead>
<TableHead>
{t("dateCreated") ||
"Date Created"}
</TableHead>
<TableHead>
{t("actions") || "Actions"}
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{devices.map((device) => ( {devices.map((device) => (
<TableRow key={device.olmId}> <TableRow key={device.olmId}>
<TableCell className="font-medium"> <TableCell className="font-medium">
{device.name || t("unnamedDevice") || "Unnamed Device"} {device.name ||
t("unnamedDevice") ||
"Unnamed Device"}
</TableCell> </TableCell>
<TableCell> <TableCell>
{moment(device.dateCreated).format("lll")} {moment(
device.dateCreated
).format("lll")}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setSelectedDevice(device); setSelectedDevice(
setIsDeleteModalOpen(true); device
);
setIsDeleteModalOpen(
true
);
}} }}
> >
{t("delete") || "Delete"} {t("delete") ||
"Delete"}
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -175,7 +206,9 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline">{t("close") || "Close"}</Button> <Button variant="outline">
{t("close") || "Close"}
</Button>
</CredenzaClose> </CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
@@ -192,8 +225,14 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
}} }}
dialog={ dialog={
<div> <div>
<p>{t("deviceQuestionRemove") || "Are you sure you want to delete this device?"}</p> <p>
<p>{t("deviceMessageRemove") || "This action cannot be undone."}</p> {t("deviceQuestionRemove") ||
"Are you sure you want to delete this device?"}
</p>
<p>
{t("deviceMessageRemove") ||
"This action cannot be undone."}
</p>
</div> </div>
} }
buttonText={t("deviceDeleteConfirm") || "Delete Device"} buttonText={t("deviceDeleteConfirm") || "Delete Device"}
@@ -205,4 +244,3 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
</> </>
); );
} }