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 "./verifyUserIsOrgOwner";
|
||||||
export * from "./verifySiteResourceAccess";
|
export * from "./verifySiteResourceAccess";
|
||||||
export * from "./logActionAudit";
|
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,
|
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);
|
||||||
|
|||||||
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));
|
.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, {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user