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 "./verifySiteResourceAccess";
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,
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);

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));
// 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, {

View File

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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,

View File

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

View File

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

View File

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

View File

@@ -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

View File

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