Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2170267e8 | ||
|
|
ff1d473b81 | ||
|
|
bd384bde4a | ||
|
|
e7f052dfa0 | ||
|
|
9903c72ff5 | ||
|
|
d7d237d37b | ||
|
|
5d03f6f0f0 | ||
|
|
38e2a64255 | ||
|
|
1f56338697 | ||
|
|
172467c0a6 | ||
|
|
ff2b6dff2b | ||
|
|
9f600f87af | ||
|
|
7eeff7a396 | ||
|
|
125d70c4b1 | ||
|
|
5ebe46a81c | ||
|
|
19174ab3e5 | ||
|
|
7c953978ed | ||
|
|
db5441d0e2 | ||
|
|
1d529e3be4 | ||
|
|
23247d4319 | ||
|
|
216d9b2996 | ||
|
|
62f1e62a26 | ||
|
|
55bdae77f9 | ||
|
|
82998a7795 | ||
|
|
321ca59380 | ||
|
|
e3ecfe6ad8 | ||
|
|
f4664eec16 | ||
|
|
56f601e34b | ||
|
|
1244ff6d93 | ||
|
|
c0ea804c98 | ||
|
|
c21cf29db6 | ||
|
|
3d0da70ce6 | ||
|
|
feb061240b | ||
|
|
a924227805 | ||
|
|
2144d3f8c0 | ||
|
|
fcd6786149 | ||
|
|
ea968ebf3c | ||
|
|
8a7b852974 | ||
|
|
c8d3e3b70c | ||
|
|
7a90a6f9f4 |
No files matched your search
Generated
+3
-1
@@ -44,7 +44,7 @@
|
||||
"redux-persist": "6.0.0",
|
||||
"swr": "^2.3.6",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"zod": "^4.1.11"
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.5.2",
|
||||
@@ -7359,6 +7359,8 @@
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@
|
||||
"redux-persist": "6.0.0",
|
||||
"swr": "^2.3.6",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"zod": "^4.1.11"
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.5.2",
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { MonitorType } from "@/Types/Monitor";
|
||||
import { Typography, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const types = ["http", "ping", "port", "docker", "game", "grpc"];
|
||||
const types = ["http", "ping", "port", "docker", "game", "grpc", "websocket"];
|
||||
const typeDisplayNames: Record<string, string> = {
|
||||
http: "HTTP",
|
||||
ping: "Ping",
|
||||
@@ -15,6 +15,7 @@ const typeDisplayNames: Record<string, string> = {
|
||||
docker: "Docker",
|
||||
game: "Game",
|
||||
grpc: "gRPC",
|
||||
websocket: "WebSocket",
|
||||
};
|
||||
const statuses = ["up", "down"];
|
||||
const states = ["active", "paused"];
|
||||
|
||||
@@ -103,6 +103,14 @@ export const useMonitorForm = ({
|
||||
selectedDisks: data?.selectedDisks || [],
|
||||
};
|
||||
break;
|
||||
case "websocket":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "websocket",
|
||||
url: data?.url || "",
|
||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
defaults = {
|
||||
...base,
|
||||
|
||||
@@ -11,12 +11,17 @@ import { ConfigBox, BasePage } from "@/Components/design-elements";
|
||||
import { UserRoles } from "@/Types/User";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { UserRole, User } from "@/Types/User";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useEditUserForm } from "@/Hooks/useEditUserForm";
|
||||
import { useGet, usePatch } from "@/Hooks/UseApi";
|
||||
import { useGet, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||
import type { EditUserFormData } from "@/Validation/editUser";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DialogInput } from "@/Components/inputs/Dialog";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin";
|
||||
|
||||
interface RoleOption {
|
||||
id: UserRole;
|
||||
@@ -29,6 +34,12 @@ const EditUserPage = () => {
|
||||
const { userId } = useParams<{ userId: string }>();
|
||||
const { resolver, defaults } = useEditUserForm();
|
||||
const { patch, loading: isSaving } = usePatch();
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
const navigate = useNavigate();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const currentUser = useSelector((state: RootState) => state.auth.user);
|
||||
const isSuperAdmin = useIsSuperAdmin();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
|
||||
|
||||
@@ -60,6 +71,20 @@ const EditUserPage = () => {
|
||||
setValue("role", newRoles, { shouldValidate: true });
|
||||
};
|
||||
|
||||
const canDeleteUser =
|
||||
isAdmin &&
|
||||
userId !== currentUser?.id &&
|
||||
!user?.role?.includes("demo") &&
|
||||
(isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin"));
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
const result = await deleteFn(`/auth/users/${userId}`);
|
||||
setShowDeleteDialog(false);
|
||||
if (result) {
|
||||
navigate("/account", { state: { tab: "team" } });
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: EditUserFormData) => {
|
||||
await patch(`/auth/users/${userId}`, data);
|
||||
};
|
||||
@@ -165,17 +190,45 @@ const EditUserPage = () => {
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
sx={{ alignSelf: "flex-end", minWidth: 100 }}
|
||||
<Stack
|
||||
gap={LAYOUT.XS}
|
||||
direction="row"
|
||||
justifyContent={"flex-end"}
|
||||
width="100%"
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</Button>
|
||||
{canDeleteUser && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
sx={{ minWidth: 100 }}
|
||||
>
|
||||
{t("common.buttons.removeUser")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
sx={{ minWidth: 100 }}
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
<DialogInput
|
||||
open={showDeleteDialog}
|
||||
title={t("pages.editUser.dialog.removeUser.title")}
|
||||
content={t("pages.editUser.dialog.removeUser.content", {
|
||||
name: `${user?.firstName} ${user?.lastName}`,
|
||||
})}
|
||||
onCancel={() => setShowDeleteDialog(false)}
|
||||
onConfirm={handleDeleteUser}
|
||||
confirmText={t("common.buttons.removeUser")}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Table } from "@/Components/design-elements";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
import { useIsSuperAdmin } from "@/Hooks/useIsAdmin";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import type { User } from "@/Types/User";
|
||||
|
||||
interface TeamTableProps {
|
||||
@@ -13,7 +13,7 @@ interface TeamTableProps {
|
||||
export const TeamTable = ({ users }: TeamTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const isSuperAdmin = useIsSuperAdmin();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const headers: Header<User>[] = [
|
||||
{
|
||||
@@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
|
||||
];
|
||||
|
||||
const handleRowClick = (row: User) => {
|
||||
if (isSuperAdmin) {
|
||||
if (isAdmin) {
|
||||
navigate(`/account/team/${row.id}`);
|
||||
}
|
||||
};
|
||||
@@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
|
||||
<Table
|
||||
headers={headers}
|
||||
data={users}
|
||||
onRowClick={isSuperAdmin ? handleRowClick : undefined}
|
||||
onRowClick={isAdmin ? handleRowClick : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -146,6 +146,17 @@ const getGeneralSettingsConfig = (
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
websocket: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
|
||||
urlPlaceholder: t("pages.createMonitor.form.general.option.wsUrl.placeholder"),
|
||||
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
|
||||
showUrl: true,
|
||||
showPort: false,
|
||||
showGameSelect: false,
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: true,
|
||||
},
|
||||
};
|
||||
return configs[type] || configs.http;
|
||||
};
|
||||
@@ -331,6 +342,13 @@ const CreateMonitorPage = () => {
|
||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||
)}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="websocket"
|
||||
label={t("pages.createMonitor.form.type.optionWebSocket")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionWebSocketDescription"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
@@ -747,7 +765,9 @@ const CreateMonitorPage = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{(watchedType === "http" || watchedType === "grpc") && (
|
||||
{(watchedType === "http" ||
|
||||
watchedType === "grpc" ||
|
||||
watchedType === "websocket") && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.ignoreTls.title")}
|
||||
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
|
||||
|
||||
@@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => {
|
||||
const effectiveTypes =
|
||||
selectedTypes.length > 0
|
||||
? selectedTypes
|
||||
: ["http", "ping", "docker", "port", "game", "grpc"];
|
||||
: ["http", "ping", "docker", "port", "game", "grpc", "websocket"];
|
||||
|
||||
// Build URL for monitors with checks
|
||||
const monitorsWithChecksUrl = useMemo(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ export const MonitorTypes = [
|
||||
"port",
|
||||
"game",
|
||||
"grpc",
|
||||
"websocket",
|
||||
"unknown",
|
||||
] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
@@ -9,6 +9,7 @@ export const getMonitorPath = (type: MonitorType): string => {
|
||||
ping: "uptime",
|
||||
game: "uptime",
|
||||
grpc: "uptime",
|
||||
websocket: "uptime",
|
||||
unknown: "uptime",
|
||||
docker: "uptime",
|
||||
hardware: "infrastructure",
|
||||
|
||||
@@ -115,6 +115,13 @@ const hardwareSchema = baseSchema.extend({
|
||||
selectedDisks: z.array(z.string()),
|
||||
});
|
||||
|
||||
// WebSocket monitor schema
|
||||
const websocketSchema = baseSchema.extend({
|
||||
type: z.literal("websocket"),
|
||||
url: z.string().min(1, "WebSocket URL is required"),
|
||||
ignoreTlsErrors: z.boolean(),
|
||||
});
|
||||
|
||||
// Discriminated union of all monitor types
|
||||
export const monitorSchema = z.discriminatedUnion("type", [
|
||||
httpSchema,
|
||||
@@ -125,6 +132,7 @@ export const monitorSchema = z.discriminatedUnion("type", [
|
||||
grpcSchema,
|
||||
pagespeedSchema,
|
||||
hardwareSchema,
|
||||
websocketSchema,
|
||||
]);
|
||||
|
||||
export type MonitorFormData = z.infer<typeof monitorSchema>;
|
||||
@@ -139,4 +147,5 @@ export {
|
||||
grpcSchema,
|
||||
pagespeedSchema,
|
||||
hardwareSchema,
|
||||
websocketSchema,
|
||||
};
|
||||
@@ -46,7 +46,8 @@
|
||||
"sendTestEmail": "Send test email",
|
||||
"exportToJSON": "Export to JSON",
|
||||
"importFromJSON": "Import from JSON",
|
||||
"clearFilters": "Clear filters"
|
||||
"clearFilters": "Clear filters",
|
||||
"removeUser": "Remove user"
|
||||
},
|
||||
"charts": {
|
||||
"labels": {
|
||||
@@ -487,6 +488,10 @@
|
||||
"grpcServiceName": {
|
||||
"label": "Service name",
|
||||
"placeholder": "e.g. my.service.v1 (leave empty for overall health)"
|
||||
},
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
}
|
||||
},
|
||||
"title": "General settings",
|
||||
@@ -498,6 +503,7 @@
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
||||
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
||||
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
||||
"websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) and add a display name.",
|
||||
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
||||
}
|
||||
},
|
||||
@@ -534,6 +540,8 @@
|
||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||
"optionWebSocket": "WebSocket",
|
||||
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
||||
"optionHttp": "HTTP(S)",
|
||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||
"optionPing": "Ping",
|
||||
@@ -619,6 +627,12 @@
|
||||
"title": "Roles",
|
||||
"description": "Assign roles to the user. Multiple roles can be selected."
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
"removeUser": {
|
||||
"title": "Remove user",
|
||||
"content": "Are you sure you want to remove {{name}} from your team? This action cannot be undone."
|
||||
}
|
||||
}
|
||||
},
|
||||
"incidents": {
|
||||
|
||||
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default defineConfig(({}) => {
|
||||
let version = "3.5.0";
|
||||
let version = "3.5.1";
|
||||
|
||||
return {
|
||||
base: "/",
|
||||
|
||||
Generated
+15
-76
@@ -43,6 +43,7 @@
|
||||
"super-simple-scheduler": "1.4.5",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"winston": "^3.13.0",
|
||||
"ws": "^8.19.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -63,6 +64,7 @@
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/ping": "0.4.4",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"c8": "10.1.3",
|
||||
@@ -871,7 +873,6 @@
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
@@ -1501,7 +1502,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1546,7 +1546,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -4338,7 +4337,6 @@
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -4499,7 +4497,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -4665,6 +4662,16 @@
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@@ -4727,7 +4734,6 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -5341,7 +5347,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5920,7 +5925,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -6863,7 +6867,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
|
||||
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssnano-preset-default": "^7.0.9",
|
||||
"lilconfig": "^3.1.3"
|
||||
@@ -7631,7 +7634,6 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7993,7 +7995,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -9638,7 +9639,6 @@
|
||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
@@ -12509,7 +12509,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -14443,63 +14442,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz",
|
||||
"integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"commander": "^7.2.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^2.3.1",
|
||||
"css-what": "^6.1.0",
|
||||
"csso": "^5.0.5",
|
||||
"picocolors": "^1.0.0",
|
||||
"sax": "^1.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"svgo": "bin/svgo"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/svgo"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo/node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo/node_modules/css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo/node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"license": "CC0-1.0",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.30.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz",
|
||||
@@ -14662,7 +14604,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -14831,7 +14772,6 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -14994,7 +14934,6 @@
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -15628,9 +15567,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"super-simple-scheduler": "1.4.5",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"winston": "^3.13.0",
|
||||
"ws": "^8.19.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -78,6 +79,7 @@
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/ping": "0.4.4",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"c8": "10.1.3",
|
||||
|
||||
@@ -58,6 +58,7 @@ import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.
|
||||
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
|
||||
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
|
||||
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
|
||||
import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js";
|
||||
|
||||
// Third-party
|
||||
import axios from "axios";
|
||||
@@ -77,6 +78,7 @@ import { games, GameDig } from "gamedig";
|
||||
import jmespath from "jmespath";
|
||||
import * as grpc from "@grpc/grpc-js";
|
||||
import * as protoLoader from "@grpc/proto-loader";
|
||||
import WebSocket from "ws";
|
||||
|
||||
// Repositories
|
||||
import {
|
||||
@@ -185,6 +187,7 @@ export const initializeServices = async ({
|
||||
const portProvider = new PortProvider(net);
|
||||
const gameProvider = new GameProvider(logger, GameDig);
|
||||
const grpcProvider = new GrpcProvider(grpc, protoLoader);
|
||||
const webSocketProvider = new WebSocketProvider(WebSocket);
|
||||
|
||||
const networkService = new NetworkService(axios, logger, [
|
||||
pingProvider,
|
||||
@@ -195,6 +198,7 @@ export const initializeServices = async ({
|
||||
portProvider,
|
||||
gameProvider,
|
||||
grpcProvider,
|
||||
webSocketProvider,
|
||||
]);
|
||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||
|
||||
|
||||
@@ -180,6 +180,20 @@ class AuthController {
|
||||
}
|
||||
};
|
||||
|
||||
deleteUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
getUserByIdParamValidation.parse(req.params);
|
||||
const targetUserId = req.params.userId;
|
||||
await this.userService.deleteUserById(req.user, targetUserId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "User removed successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const allUsers = await this.userService.getAllUsers();
|
||||
|
||||
@@ -26,9 +26,10 @@ class AuthRoutes {
|
||||
|
||||
this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers);
|
||||
this.router.post("/users", verifyJWT, isAllowed(["superadmin"]), upload.single("profileImage"), this.authController.createUser);
|
||||
this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById);
|
||||
this.router.get("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getUserById);
|
||||
this.router.patch("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById);
|
||||
this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById);
|
||||
this.router.delete("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.deleteUserById);
|
||||
|
||||
this.router.patch("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
|
||||
this.router.delete("/user", verifyJWT, this.authController.deleteUser);
|
||||
|
||||
@@ -227,7 +227,8 @@ export class MonitorService implements IMonitorService {
|
||||
checksData.monitorType !== "docker" &&
|
||||
checksData.monitorType !== "port" &&
|
||||
checksData.monitorType !== "game" &&
|
||||
checksData.monitorType !== "grpc"
|
||||
checksData.monitorType !== "grpc" &&
|
||||
checksData.monitorType !== "websocket"
|
||||
) {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface IUserService {
|
||||
validateRecovery(recoveryToken: string): Promise<void>;
|
||||
resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>;
|
||||
deleteUser(user: User): Promise<void>;
|
||||
deleteUserById(actor: User, targetUserId: string): Promise<void>;
|
||||
getAllUsers(): Promise<User[]>;
|
||||
getUserById(roles: UserRole[], userId: string): Promise<User>;
|
||||
editUserById(userId: string, patch: Partial<User>): Promise<void>;
|
||||
@@ -357,13 +358,53 @@ export class UserService implements IUserService {
|
||||
await this.usersRepository.deleteById(userId);
|
||||
};
|
||||
|
||||
deleteUserById = async (actor: User, targetUserId: string) => {
|
||||
if (actor.id === targetUserId) {
|
||||
throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
|
||||
}
|
||||
|
||||
const targetUser = await this.usersRepository.findById(targetUserId);
|
||||
|
||||
if (targetUser.teamId !== actor.teamId) {
|
||||
throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 });
|
||||
}
|
||||
|
||||
if (targetUser.role.includes("demo")) {
|
||||
throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
|
||||
}
|
||||
|
||||
const actorRoles = actor.role;
|
||||
const targetRoles = targetUser.role;
|
||||
|
||||
// Check actor can manage all of target's roles
|
||||
for (const targetRole of targetRoles) {
|
||||
const canManage = actorRoles.some((actorRole) => canManageRole(actorRole, targetRole));
|
||||
if (!canManage) {
|
||||
throw new AppError({
|
||||
message: "You do not have permission to remove this user",
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteUserById",
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.usersRepository.deleteById(targetUserId);
|
||||
|
||||
this.logger.info({
|
||||
message: `User ${targetUserId} deleted by ${actor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteUserById",
|
||||
});
|
||||
};
|
||||
|
||||
getAllUsers = async () => {
|
||||
return await this.usersRepository.findAll();
|
||||
};
|
||||
|
||||
getUserById = async (roles: any, userId: any) => {
|
||||
if (!roles.includes("superadmin")) {
|
||||
throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 });
|
||||
if (!roles.includes("superadmin") && !roles.includes("admin")) {
|
||||
throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 });
|
||||
}
|
||||
const user = await this.usersRepository.findById(userId);
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
|
||||
import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js";
|
||||
import { Monitor, MonitorType } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
|
||||
import type WebSocket from "ws";
|
||||
|
||||
type WebSocketConstructor = typeof WebSocket;
|
||||
|
||||
const SERVICE_NAME = "WebSocketProvider";
|
||||
const TIMEOUT_MS = 10000;
|
||||
|
||||
export class WebSocketProvider implements IStatusProvider<WebSocketStatusPayload> {
|
||||
readonly type = "websocket";
|
||||
|
||||
constructor(private WS: WebSocketConstructor) {}
|
||||
|
||||
supports(type: MonitorType): boolean {
|
||||
return type === "websocket";
|
||||
}
|
||||
|
||||
async handle(monitor: Monitor): Promise<MonitorStatusResponse<WebSocketStatusPayload>> {
|
||||
try {
|
||||
const { url } = monitor;
|
||||
if (!url) {
|
||||
throw new Error("URL is required for WebSocket monitoring");
|
||||
}
|
||||
|
||||
const { responseTime, error } = await timeRequest(async () => {
|
||||
return new Promise<{ connected: boolean }>((resolve, reject) => {
|
||||
const options: WebSocket.ClientOptions = {};
|
||||
if (monitor.ignoreTlsErrors) {
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
const ws = new this.WS(url, options);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
ws.close();
|
||||
reject(new Error("WebSocket connection timeout"));
|
||||
}, TIMEOUT_MS);
|
||||
|
||||
ws.on("open", () => {
|
||||
clearTimeout(timeout);
|
||||
ws.close();
|
||||
resolve({ connected: true });
|
||||
});
|
||||
|
||||
ws.on("error", (err: unknown) => {
|
||||
clearTimeout(timeout);
|
||||
ws.close();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "WebSocket check failed";
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: false,
|
||||
code: NETWORK_ERROR,
|
||||
message: errorMessage,
|
||||
responseTime: responseTime,
|
||||
timings: undefined,
|
||||
payload: { connected: false },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: true,
|
||||
code: 200,
|
||||
message: "WebSocket check successful",
|
||||
responseTime: responseTime,
|
||||
timings: undefined,
|
||||
payload: { connected: true },
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||
throw new AppError({
|
||||
message: originalMessage || "Error performing WebSocket check",
|
||||
status: 500,
|
||||
service: SERVICE_NAME,
|
||||
method: "handle",
|
||||
details: { url: monitor.url },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ export type { CheckSnapshot } from "@/types/check.js";
|
||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||
export type { GeoContinent } from "@/types/geoCheck.js";
|
||||
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const;
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
|
||||
|
||||
@@ -24,7 +24,8 @@ export interface MonitorStatusResponse<
|
||||
| HardwareStatusPayload
|
||||
| DockerStatusPayload
|
||||
| GameStatusPayload
|
||||
| GrpcStatusPayload,
|
||||
| GrpcStatusPayload
|
||||
| WebSocketStatusPayload,
|
||||
> {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
@@ -111,6 +112,10 @@ export interface GrpcStatusPayload {
|
||||
servingStatus: string;
|
||||
}
|
||||
|
||||
export interface WebSocketStatusPayload {
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
export interface MonitorPayloadMap {
|
||||
ping: PingStatusPayload;
|
||||
http: HttpStatusPayload;
|
||||
@@ -120,6 +125,7 @@ export interface MonitorPayloadMap {
|
||||
port: PortStatusPayload;
|
||||
game: GameStatusPayload;
|
||||
grpc: GrpcStatusPayload;
|
||||
websocket: WebSocketStatusPayload;
|
||||
default: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export const getChecksParamValidation = z.object({
|
||||
});
|
||||
|
||||
export const getChecksQueryValidation = z.object({
|
||||
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"]).optional(),
|
||||
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]).optional(),
|
||||
sortOrder: z.enum(["asc", "desc"]).optional(),
|
||||
limit: z.coerce.number().optional(),
|
||||
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
|
||||
|
||||
@@ -21,8 +21,8 @@ export const getMonitorsByTeamIdParamValidation = z.object({});
|
||||
export const getMonitorsByTeamIdQueryValidation = z.object({
|
||||
type: z
|
||||
.union([
|
||||
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]),
|
||||
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])),
|
||||
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]),
|
||||
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
|
||||
])
|
||||
.optional(),
|
||||
filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
|
||||
@@ -37,8 +37,8 @@ export const getMonitorsWithChecksQueryValidation = z.object({
|
||||
order: z.enum(["asc", "desc"]).optional(),
|
||||
type: z
|
||||
.union([
|
||||
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]),
|
||||
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])),
|
||||
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]),
|
||||
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
|
||||
])
|
||||
.optional(),
|
||||
explain: booleanCoercion.optional(),
|
||||
|
||||
Reference in new issue
Block a user