mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
update olm and client routes
This commit is contained in:
@@ -28,3 +28,4 @@ export * from "./verifyClientsEnabled";
|
||||
export * from "./verifyUserIsOrgOwner";
|
||||
export * from "./verifySiteResourceAccess";
|
||||
export * from "./logActionAudit";
|
||||
export * from "./verifyOlmAccess";
|
||||
|
||||
45
server/middlewares/verifyOlmAccess.ts
Normal file
45
server/middlewares/verifyOlmAccess.ts
Normal 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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<typeof createClientSchema>;
|
||||
export type CreateClientBody = z.infer<typeof bodySchema>;
|
||||
|
||||
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<any> {
|
||||
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,20 +181,14 @@ 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 (!validSecret) {
|
||||
if (existingOlm) {
|
||||
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) => {
|
||||
// TODO: more intelligent way to pick the exit node
|
||||
@@ -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);
|
||||
|
||||
240
server/routers/client/createUserClient.ts
Normal file
240
server/routers/client/createUserClient.ts
Normal 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
// this is a machine client
|
||||
if (!client.userId && client.olmId) {
|
||||
await trx
|
||||
.delete(clients)
|
||||
.where(eq(clients.clientId, clientId));
|
||||
.where(eq(clients.olmId, client.olmId));
|
||||
}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./deleteClient";
|
||||
export * from "./listClients";
|
||||
export * from "./updateClient";
|
||||
export * from "./getClient";
|
||||
export * from "./createUserClient";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof createOlmBodySchema>;
|
||||
|
||||
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<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,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
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<CreateOlmResponse>(res, {
|
||||
data: {
|
||||
olmId,
|
||||
secret
|
||||
// token,
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
@@ -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<any> {
|
||||
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(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
export * from "./deleteUserOlm";
|
||||
export * from "./listUserOlms";
|
||||
export * from "./deleteUserOlm";
|
||||
|
||||
@@ -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<any> {
|
||||
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<ListOlmsResponse>(res, {
|
||||
return response<ListUserOlmsResponse>(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(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,10 +46,14 @@ 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<Device[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -58,7 +63,9 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
|
||||
const fetchDevices = async () => {
|
||||
setLoading(true);
|
||||
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) {
|
||||
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"}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("viewDevicesDescription") || "Manage your connected devices"}
|
||||
{t("viewDevicesDescription") ||
|
||||
"Manage your connected devices"}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -141,29 +156,45 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="pl-3">{t("name") || "Name"}</TableHead>
|
||||
<TableHead>{t("dateCreated") || "Date Created"}</TableHead>
|
||||
<TableHead >{t("actions") || "Actions"}</TableHead>
|
||||
<TableHead className="pl-3">
|
||||
{t("name") || "Name"}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("dateCreated") ||
|
||||
"Date Created"}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("actions") || "Actions"}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{devices.map((device) => (
|
||||
<TableRow key={device.olmId}>
|
||||
<TableCell className="font-medium">
|
||||
{device.name || t("unnamedDevice") || "Unnamed Device"}
|
||||
{device.name ||
|
||||
t("unnamedDevice") ||
|
||||
"Unnamed Device"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{moment(device.dateCreated).format("lll")}
|
||||
{moment(
|
||||
device.dateCreated
|
||||
).format("lll")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedDevice(device);
|
||||
setIsDeleteModalOpen(true);
|
||||
setSelectedDevice(
|
||||
device
|
||||
);
|
||||
setIsDeleteModalOpen(
|
||||
true
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("delete") || "Delete"}
|
||||
{t("delete") ||
|
||||
"Delete"}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -175,7 +206,9 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("close") || "Close"}</Button>
|
||||
<Button variant="outline">
|
||||
{t("close") || "Close"}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
@@ -192,8 +225,14 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>{t("deviceQuestionRemove") || "Are you sure you want to delete this device?"}</p>
|
||||
<p>{t("deviceMessageRemove") || "This action cannot be undone."}</p>
|
||||
<p>
|
||||
{t("deviceQuestionRemove") ||
|
||||
"Are you sure you want to delete this device?"}
|
||||
</p>
|
||||
<p>
|
||||
{t("deviceMessageRemove") ||
|
||||
"This action cannot be undone."}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("deviceDeleteConfirm") || "Delete Device"}
|
||||
@@ -205,4 +244,3 @@ export default function ViewDevicesDialog({ open, setOpen }: ViewDevicesDialogPr
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user