Compare commits
92
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a54fdd89ab | ||
|
|
6aa9360b98 | ||
|
|
398e5d7314 | ||
|
|
53ee45172d | ||
|
|
1ea6dafb1a | ||
|
|
9785dc146e | ||
|
|
1e2a4b4a84 | ||
|
|
5e0d8f0550 | ||
|
|
bffc4ee813 | ||
|
|
ad9ff7f5df | ||
|
|
8277c4ed13 | ||
|
|
f65238da6c | ||
|
|
64b8aab4d6 | ||
|
|
7244b35091 | ||
|
|
699883e0b1 | ||
|
|
f7211711ce | ||
|
|
cb0dd878d6 | ||
|
|
8c5be9ef00 | ||
|
|
d1b6d08c73 | ||
|
|
2d68e96782 | ||
|
|
306f2a60a2 | ||
|
|
abf6dc2908 | ||
|
|
7ce6068757 | ||
|
|
a7d9bb822e | ||
|
|
142ecaaf8b | ||
|
|
90bbc764fc | ||
|
|
ae4c433246 | ||
|
|
d2461ef118 | ||
|
|
47162f2064 | ||
|
|
8b9c41464a | ||
|
|
51b6ec1751 | ||
|
|
eff4f8ad69 | ||
|
|
bca902471d | ||
|
|
1b3f2f0365 | ||
|
|
be12991997 | ||
|
|
54596fe776 | ||
|
|
8d637dbcac | ||
|
|
771fd5b50f | ||
|
|
8dedf2a742 | ||
|
|
c882daf000 | ||
|
|
1aa7ae8f11 | ||
|
|
517139cbf4 | ||
|
|
ef049b6521 | ||
|
|
33f8641f3a | ||
|
|
2f22a52116 | ||
|
|
8af13b04ec | ||
|
|
ac77216f07 | ||
|
|
a43363ebfc | ||
|
|
0ca738b080 | ||
|
|
091c36cbf3 | ||
|
|
120708ed47 | ||
|
|
6a1ab622b3 | ||
|
|
0aef8b652f | ||
|
|
20863fbce7 | ||
|
|
fa7eaa4788 | ||
|
|
ee9c08b1bb | ||
|
|
7ee30e12f5 | ||
|
|
c5f57eda62 | ||
|
|
2bf3c08f3c | ||
|
|
187c8eaba1 | ||
|
|
a37bc63f31 | ||
|
|
c8c4bac2c7 | ||
|
|
9fecfa562f | ||
|
|
7991f3f6a1 | ||
|
|
692d4d89d6 | ||
|
|
f1b104b78d | ||
|
|
3c2b571313 | ||
|
|
abec4e6c70 | ||
|
|
d022f7d849 | ||
|
|
0fb210e9e1 | ||
|
|
ce4cb63035 | ||
|
|
f637b01631 | ||
|
|
e390bd764f | ||
|
|
30e8cfc9ec | ||
|
|
80d6868a1d | ||
|
|
942d75afcd | ||
|
|
c34fcc5cb3 | ||
|
|
409f9f6dc6 | ||
|
|
525206c1c1 | ||
|
|
9a85389b8c | ||
|
|
eeac784922 | ||
|
|
19b873ca87 | ||
|
|
0f99c7004d | ||
|
|
6a566c3693 | ||
|
|
dfcf0309fe | ||
|
|
395b7fbe2a | ||
|
|
d2c13fac5a | ||
|
|
dd16ac060a | ||
|
|
5628c202f7 | ||
|
|
372fc9869b | ||
|
|
87a6640c1d | ||
|
|
4e31499f78 |
No files matched your search
@@ -68,19 +68,23 @@ VITE_APP_LOG_LEVEL="debug"
|
||||
### Backend Layers
|
||||
```
|
||||
server/src/
|
||||
├── controllers/ # Route handlers (authController, monitorController, etc.)
|
||||
├── service/ # Business logic
|
||||
│ ├── business/ # Core monitoring logic
|
||||
│ ├── infrastructure/ # Server/system utilities
|
||||
│ └── system/ # App-level settings
|
||||
├── api/ # HTTP layer
|
||||
│ ├── controllers/ # Route handlers (authController, monitorController, etc.)
|
||||
│ ├── middleware/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
||||
│ ├── routes/ # API route definitions
|
||||
│ └── validation/ # Zod request-payload schemas
|
||||
├── domain/ # Business logic + data access, one folder per entity
|
||||
│ ├── monitors/ # e.g. monitor.service.ts, monitor.repository.ts, monitor.type.ts
|
||||
│ ├── checks/
|
||||
│ ├── incidents/
|
||||
│ └── … # users, teams, status-pages, notifications, etc.
|
||||
├── service/ # Cross-cutting services (e.g. infrastructure/)
|
||||
├── config/ # App config wiring (controllers, routes, services, envValidation)
|
||||
├── db/
|
||||
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
|
||||
│ ├── migration/ # Database migrations (run on startup)
|
||||
│ └── modules/ # Database-specific modules
|
||||
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
||||
├── routes/v1/ # API route definitions
|
||||
├── validation/ # Joi input validation schemas
|
||||
└── repositories/ # Data access layer
|
||||
│ └── migration/ # Database migrations (run on startup)
|
||||
├── templates/ # Email/notification templates
|
||||
├── types/ # Shared const tuples + derived types
|
||||
└── utils/ # Shared utilities
|
||||
```
|
||||
|
||||
### Frontend Structure
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
"format-check": "prettier --check ."
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*": "prettier --write"
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
|
||||
@@ -31,6 +31,7 @@ export const useStatusPageForm = ({
|
||||
const defaults: StatusPageFormData = {
|
||||
companyName: data?.companyName || "",
|
||||
url: data?.url || generateDefaultUrl(),
|
||||
customDomain: data?.customDomain ?? null,
|
||||
timezone: resolveTimezone(data?.timezone),
|
||||
type: data?.type || ["uptime"],
|
||||
color: data?.color || "#13715B",
|
||||
|
||||
@@ -33,6 +33,26 @@ export const MongoStats = ({ diagnostics }: MongoStatsProps) => {
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.readyState")}
|
||||
subtitle={mongoStats.readyState.toString()}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.readsPerSecond")}
|
||||
subtitle={mongoStats.readsPerSecond.toFixed(0)}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.insertsPerSecond")}
|
||||
subtitle={mongoStats.insertsPerSecond.toFixed(0)}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.updatesPerSecond")}
|
||||
subtitle={mongoStats.updatesPerSecond.toFixed(0)}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.deletesPerSecond")}
|
||||
subtitle={mongoStats.deletesPerSecond.toFixed(0)}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.writesPerSecond")}
|
||||
subtitle={mongoStats.writesPerSecond.toFixed(0)}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.host")}
|
||||
subtitle={mongoStats.host}
|
||||
|
||||
@@ -151,6 +151,11 @@ const CreateStatusPage = () => {
|
||||
fd.append("isPublished", String(data.isPublished));
|
||||
if (data.companyName) fd.append("companyName", data.companyName);
|
||||
if (data.url) fd.append("url", data.url);
|
||||
if (data.customDomain !== undefined && data.customDomain !== null) {
|
||||
fd.append("customDomain", data.customDomain);
|
||||
} else {
|
||||
fd.append("customDomain", "");
|
||||
}
|
||||
if (data.timezone) fd.append("timezone", data.timezone);
|
||||
if (data.color) fd.append("color", data.color);
|
||||
fd.append("showCharts", String(data.showCharts));
|
||||
@@ -284,6 +289,27 @@ const CreateStatusPage = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="customDomain"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
fieldLabel={t(
|
||||
"pages.statusPages.form.basicInfo.option.customDomain.label"
|
||||
)}
|
||||
placeholder={t(
|
||||
"pages.statusPages.form.basicInfo.option.customDomain.placeholder"
|
||||
)}
|
||||
helperText={
|
||||
fieldState.error?.message ||
|
||||
t("pages.statusPages.form.basicInfo.option.customDomain.helper")
|
||||
}
|
||||
error={!!fieldState.error}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Settings, ExternalLink } from "lucide-react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
|
||||
import { type StatusPage } from "@/Types/StatusPage";
|
||||
import { getStatusPagePublicUrl } from "@/Utils/statusPageUrl";
|
||||
|
||||
interface HeaderStatusPageControlsProps {
|
||||
isAdmin: boolean;
|
||||
@@ -50,7 +51,7 @@ export const HeaderStatusPageControls = ({
|
||||
<Typography
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`,
|
||||
getStatusPagePublicUrl(statusPage),
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
|
||||
@@ -14,6 +14,11 @@ import {
|
||||
type StatusPageResponse,
|
||||
type StatusPageTheme,
|
||||
} from "@/Types/StatusPage";
|
||||
import {
|
||||
buildStatusPageApiPath,
|
||||
getStatusPagePublicUrl,
|
||||
isCustomDomainHost,
|
||||
} from "@/Utils/statusPageUrl";
|
||||
import { HeaderStatusPageControls } from "@/Pages/StatusPage/Status/Components/HeaderStatusPageControls";
|
||||
import { StatusPageThemeProvider } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
|
||||
import {
|
||||
@@ -67,9 +72,14 @@ const StatusPageView = () => {
|
||||
const isAdmin = useIsAdmin();
|
||||
const location = useLocation();
|
||||
|
||||
const isPublic = location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
|
||||
const onCustomDomainHost = isCustomDomainHost();
|
||||
const isPublic =
|
||||
onCustomDomainHost || location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
|
||||
|
||||
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
|
||||
const apiUrl = buildStatusPageApiPath({
|
||||
url,
|
||||
useCustomDomain: onCustomDomainHost,
|
||||
});
|
||||
|
||||
const { data, isLoading, error } = useGet<StatusPageResponse>(
|
||||
apiUrl,
|
||||
@@ -134,7 +144,7 @@ const StatusPageView = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const publicUrl = `${window.location.origin}${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
|
||||
const publicUrl = getStatusPagePublicUrl(statusPage);
|
||||
return (
|
||||
<BasePage
|
||||
loading={isLoading}
|
||||
|
||||
@@ -99,9 +99,9 @@ export const boldStyles = (
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
mb: "36px",
|
||||
},
|
||||
brand: {
|
||||
@@ -130,7 +130,7 @@ export const boldStyles = (
|
||||
hero: {
|
||||
position: "relative",
|
||||
borderRadius: "22px",
|
||||
padding: "36px 36px 32px",
|
||||
padding: { xs: "24px 20px", md: "36px 36px 32px" },
|
||||
mb: "28px",
|
||||
background: isDark ? heroBgDark : heroBgLight,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
@@ -149,13 +149,14 @@ export const boldStyles = (
|
||||
},
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
fontSize: 34,
|
||||
fontSize: { xs: 24, md: 34 },
|
||||
fontWeight: 800,
|
||||
letterSpacing: "-0.02em",
|
||||
lineHeight: 1.1,
|
||||
color: tokens.text,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
flexWrap: "wrap",
|
||||
gap: "10px",
|
||||
},
|
||||
heroCheck: (tone) => ({
|
||||
@@ -171,9 +172,15 @@ export const boldStyles = (
|
||||
}),
|
||||
heroSub: { mt: "10px", mb: 0, color: tokens.textMuted, fontSize: 14 },
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "16px" },
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: { xs: "center", md: "flex-end" },
|
||||
mb: "16px",
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
width: { xs: "100%", md: "auto" },
|
||||
maxWidth: "100%",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "999px",
|
||||
background: tokens.surface,
|
||||
@@ -181,16 +188,20 @@ export const boldStyles = (
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "8px 18px",
|
||||
padding: { xs: "8px 6px", md: "8px 18px" },
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.12em",
|
||||
letterSpacing: { xs: "0.04em", md: "0.12em" },
|
||||
transition: "background 0.2s, color 0.2s",
|
||||
borderRadius: "999px",
|
||||
"&:hover": { color: active ? tokens.up : tokens.text },
|
||||
@@ -217,13 +228,21 @@ export const boldStyles = (
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "24px",
|
||||
alignItems: { xs: "start", md: "center" },
|
||||
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
|
||||
gridTemplateAreas: {
|
||||
xs: `"name" "status" "meta"`,
|
||||
md: `"name name" "meta status"`,
|
||||
},
|
||||
columnGap: { md: "24px" },
|
||||
rowGap: { xs: "10px", md: "6px" },
|
||||
p: "22px 28px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
|
||||
monitorName: {
|
||||
gridArea: "name",
|
||||
minWidth: 0,
|
||||
width: { xs: "100%", md: "auto" },
|
||||
fontWeight: 700,
|
||||
fontSize: 17,
|
||||
letterSpacing: "-0.01em",
|
||||
@@ -233,10 +252,9 @@ export const boldStyles = (
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gridArea: "meta",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "6px",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
@@ -273,6 +291,8 @@ export const boldStyles = (
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
gridArea: "status",
|
||||
justifySelf: "start",
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
padding: "6px 14px",
|
||||
@@ -296,10 +316,10 @@ export const boldStyles = (
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 28px 22px",
|
||||
padding: { xs: "0 12px 18px", md: "0 28px 22px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
height: 48,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
@@ -312,10 +332,10 @@ export const boldStyles = (
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 28px",
|
||||
padding: { xs: "0 12px", md: "0 28px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
alignItems: "flex-end",
|
||||
height: 48,
|
||||
},
|
||||
|
||||
@@ -90,9 +90,9 @@ export const editorialStyles = (
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
alignItems: { xs: "flex-start", md: "flex-end" },
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
mb: "48px",
|
||||
borderBottom: `2px solid ${tokens.text}`,
|
||||
pb: "18px",
|
||||
@@ -119,7 +119,7 @@ export const editorialStyles = (
|
||||
},
|
||||
|
||||
statusLine: {
|
||||
fontSize: 22,
|
||||
fontSize: { xs: 18, md: 22 },
|
||||
fontWeight: 400,
|
||||
m: 0,
|
||||
mb: "8px",
|
||||
@@ -144,26 +144,32 @@ export const editorialStyles = (
|
||||
letterSpacing: "0.12em",
|
||||
color: tokens.textMuted,
|
||||
m: 0,
|
||||
mb: "56px",
|
||||
mb: { xs: "32px", md: "56px" },
|
||||
},
|
||||
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
justifyContent: { xs: "center", md: "flex-end" },
|
||||
mb: "20px",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
width: { xs: "100%", md: "auto" },
|
||||
maxWidth: "100%",
|
||||
border: `1px solid ${tokens.text}`,
|
||||
"& > button + button": { borderLeft: `1px solid ${tokens.text}` },
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.2em",
|
||||
padding: "6px 14px",
|
||||
letterSpacing: { xs: "0.05em", md: "0.2em" },
|
||||
padding: { xs: "6px 6px", md: "6px 14px" },
|
||||
border: 0,
|
||||
background: active ? tokens.text : "transparent",
|
||||
color: active ? tokens.bg : tokens.text,
|
||||
@@ -188,12 +194,20 @@ export const editorialStyles = (
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "baseline",
|
||||
gap: "20px",
|
||||
alignItems: { xs: "start", md: "baseline" },
|
||||
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
|
||||
gridTemplateAreas: {
|
||||
xs: `"name" "status" "meta"`,
|
||||
md: `"name name" "meta status"`,
|
||||
},
|
||||
columnGap: { md: "20px" },
|
||||
rowGap: { xs: "10px", md: "6px" },
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
|
||||
monitorName: {
|
||||
gridArea: "name",
|
||||
minWidth: 0,
|
||||
width: { xs: "100%", md: "auto" },
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.015em",
|
||||
@@ -203,14 +217,13 @@ export const editorialStyles = (
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
gridArea: "meta",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
fontSize: 12,
|
||||
color: tokens.textMuted,
|
||||
mt: "6px",
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
},
|
||||
pill: {
|
||||
textTransform: "uppercase",
|
||||
@@ -234,6 +247,8 @@ export const editorialStyles = (
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
gridArea: "status",
|
||||
justifySelf: "start",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
@@ -250,7 +265,7 @@ export const editorialStyles = (
|
||||
mt: "16px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "2px",
|
||||
gap: { xs: "1px", md: "2px" },
|
||||
height: 40,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
@@ -262,7 +277,7 @@ export const editorialStyles = (
|
||||
mt: "16px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "2px",
|
||||
gap: { xs: "1px", md: "2px" },
|
||||
alignItems: "flex-end",
|
||||
height: 40,
|
||||
},
|
||||
|
||||
@@ -11,24 +11,24 @@ export const ModernHero = ({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box sx={styles.hero}>
|
||||
<Box sx={styles.pulse(overall.tone)} />
|
||||
<Box sx={styles.statusCopy}>
|
||||
<Box sx={styles.heroRow}>
|
||||
<Box sx={styles.pulse(overall.tone)} />
|
||||
<Box
|
||||
component="h1"
|
||||
sx={styles.heroTitle}
|
||||
>
|
||||
{overall.message}
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export interface ModernStyles {
|
||||
logoImg: SxProps<Theme>;
|
||||
hero: SxProps<Theme>;
|
||||
pulse: (tone: OverallTone) => SxProps<Theme>;
|
||||
statusCopy: SxProps<Theme>;
|
||||
heroRow: SxProps<Theme>;
|
||||
heroTitle: SxProps<Theme>;
|
||||
heroSub: SxProps<Theme>;
|
||||
heroIcon: (tone: OverallTone) => SxProps<Theme>;
|
||||
@@ -117,9 +117,9 @@ export const modernStyles = (
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
mb: "32px",
|
||||
},
|
||||
brand: {
|
||||
@@ -148,11 +148,11 @@ export const modernStyles = (
|
||||
hero: {
|
||||
position: "relative",
|
||||
borderRadius: "20px",
|
||||
padding: "28px 32px",
|
||||
padding: { xs: "20px", md: "28px 32px" },
|
||||
mb: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
boxShadow: cardShadow,
|
||||
@@ -182,25 +182,37 @@ export const modernStyles = (
|
||||
},
|
||||
};
|
||||
},
|
||||
statusCopy: { flex: 1, minWidth: 0 },
|
||||
heroRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: { xs: "14px", md: "20px" },
|
||||
},
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
mb: "4px",
|
||||
fontSize: 22,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: { xs: 18, md: 22 },
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.02em",
|
||||
color: tokens.text,
|
||||
},
|
||||
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
|
||||
heroIcon: (tone) => ({
|
||||
display: { xs: "none", md: "flex" },
|
||||
color: toneColor(tone, tokens),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
}),
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "14px" },
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: { xs: "center", md: "flex-end" },
|
||||
mb: "14px",
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
width: { xs: "100%", md: "auto" },
|
||||
maxWidth: "100%",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "999px",
|
||||
background: tokens.surface,
|
||||
@@ -208,16 +220,20 @@ export const modernStyles = (
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "6px 16px",
|
||||
padding: { xs: "6px 6px", md: "6px 16px" },
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
letterSpacing: { xs: "0.04em", md: "0.08em" },
|
||||
transition: "background 0.2s, color 0.2s",
|
||||
borderRadius: "999px",
|
||||
"&:hover": { color: active ? tokens.up : tokens.text },
|
||||
@@ -256,13 +272,21 @@ export const modernStyles = (
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
alignItems: { xs: "start", md: "center" },
|
||||
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
|
||||
gridTemplateAreas: {
|
||||
xs: `"name" "status" "meta"`,
|
||||
md: `"name name" "meta status"`,
|
||||
},
|
||||
columnGap: { md: "20px" },
|
||||
rowGap: { xs: "10px", md: "6px" },
|
||||
p: "18px 24px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
|
||||
monitorName: {
|
||||
gridArea: "name",
|
||||
minWidth: 0,
|
||||
width: { xs: "100%", md: "auto" },
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
letterSpacing: "-0.005em",
|
||||
@@ -272,10 +296,9 @@ export const modernStyles = (
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gridArea: "meta",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "6px",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
@@ -299,6 +322,8 @@ export const modernStyles = (
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
gridArea: "status",
|
||||
justifySelf: "start",
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
padding: "5px 12px",
|
||||
@@ -319,10 +344,10 @@ export const modernStyles = (
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 24px 20px",
|
||||
padding: { xs: "0 12px 16px", md: "0 24px 20px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
height: 46,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
@@ -334,10 +359,10 @@ export const modernStyles = (
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 24px",
|
||||
padding: { xs: "0 12px", md: "0 24px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
alignItems: "flex-end",
|
||||
height: 46,
|
||||
},
|
||||
|
||||
@@ -11,24 +11,24 @@ export const RefinedHero = ({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box sx={styles.hero}>
|
||||
<Box sx={styles.statusDot(overall.tone)} />
|
||||
<Box sx={styles.statusCopy}>
|
||||
<Box sx={styles.heroRow}>
|
||||
<Box sx={styles.statusDot(overall.tone)} />
|
||||
<Box
|
||||
component="h1"
|
||||
sx={styles.heroTitle}
|
||||
>
|
||||
{overall.message}
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export interface RefinedStyles {
|
||||
company: SxProps<Theme>;
|
||||
hero: SxProps<Theme>;
|
||||
statusDot: (tone: OverallTone) => SxProps<Theme>;
|
||||
statusCopy: SxProps<Theme>;
|
||||
heroRow: SxProps<Theme>;
|
||||
heroTitle: SxProps<Theme>;
|
||||
heroSub: SxProps<Theme>;
|
||||
heroIcon: (tone: OverallTone) => SxProps<Theme>;
|
||||
@@ -102,9 +102,9 @@ export const refinedStyles = (
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
mb: "28px",
|
||||
},
|
||||
brand: {
|
||||
@@ -133,10 +133,10 @@ export const refinedStyles = (
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: tokens.radius,
|
||||
padding: "22px 24px",
|
||||
padding: { xs: "18px 20px", md: "22px 24px" },
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
boxShadow: cardShadow,
|
||||
mb: "20px",
|
||||
},
|
||||
@@ -148,10 +148,15 @@ export const refinedStyles = (
|
||||
boxShadow: `0 0 0 4px ${toneSoft(tone, tokens)}`,
|
||||
flexShrink: 0,
|
||||
}),
|
||||
statusCopy: { flex: 1, minWidth: 0 },
|
||||
heroRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: { xs: "12px", md: "16px" },
|
||||
},
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
mb: "2px",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: 17,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "-0.01em",
|
||||
@@ -159,14 +164,21 @@ export const refinedStyles = (
|
||||
},
|
||||
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
|
||||
heroIcon: (tone) => ({
|
||||
display: { xs: "none", md: "flex" },
|
||||
color: toneColor(tone, tokens),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
}),
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "12px" },
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: { xs: "center", md: "flex-end" },
|
||||
mb: "12px",
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
width: { xs: "100%", md: "auto" },
|
||||
maxWidth: "100%",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "8px",
|
||||
background: tokens.surface,
|
||||
@@ -174,11 +186,15 @@ export const refinedStyles = (
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "5px 14px",
|
||||
padding: { xs: "5px 6px", md: "5px 14px" },
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
borderRadius: "5px",
|
||||
@@ -207,13 +223,21 @@ export const refinedStyles = (
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
alignItems: { xs: "start", md: "center" },
|
||||
gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "1fr auto" },
|
||||
gridTemplateAreas: {
|
||||
xs: `"name" "status" "meta"`,
|
||||
md: `"name name" "meta status"`,
|
||||
},
|
||||
columnGap: { md: "16px" },
|
||||
rowGap: { xs: "10px", md: "4px" },
|
||||
p: "16px 20px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
cardLeft: { minWidth: 0, flex: 1, width: { xs: "100%", md: "auto" } },
|
||||
monitorName: {
|
||||
gridArea: "name",
|
||||
minWidth: 0,
|
||||
width: { xs: "100%", md: "auto" },
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
letterSpacing: "-0.005em",
|
||||
@@ -223,10 +247,9 @@ export const refinedStyles = (
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gridArea: "meta",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "4px",
|
||||
alignItems: { xs: "flex-start", md: "center" },
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
@@ -251,6 +274,8 @@ export const refinedStyles = (
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
gridArea: "status",
|
||||
justifySelf: "start",
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
padding: "4px 10px",
|
||||
@@ -261,10 +286,10 @@ export const refinedStyles = (
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 20px 16px",
|
||||
padding: { xs: "0 10px 14px", md: "0 20px 16px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
height: 42,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
@@ -276,10 +301,10 @@ export const refinedStyles = (
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 20px",
|
||||
padding: { xs: "0 10px", md: "0 20px" },
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
gap: { xs: "1px", md: "3px" },
|
||||
alignItems: "flex-end",
|
||||
height: 42,
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
statusBadgeKey,
|
||||
} from "@/Pages/StatusPage/Status/themes/shared/overallStatus";
|
||||
import { useStatusPageTheme } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
|
||||
import { formatPercentage } from "@/Utils/FormatUtils";
|
||||
|
||||
type StatusPageMonitor = Monitor & { checks?: Monitor["recentChecks"] };
|
||||
|
||||
@@ -101,8 +102,9 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
|
||||
return (
|
||||
<Box sx={styles.page}>
|
||||
<Box
|
||||
<Stack
|
||||
component="header"
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
sx={styles.top}
|
||||
>
|
||||
<HeaderSlot
|
||||
@@ -112,7 +114,7 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
monitorCount={monitors.length}
|
||||
styles={styles}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<HeroSlot
|
||||
statusPage={statusPage}
|
||||
@@ -161,6 +163,7 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
const showInfra = isHardware && statusPage.showInfrastructure !== false;
|
||||
const showChart = !isHardware && statusPage.showCharts !== false;
|
||||
const badgeTone = monitorBadgeTone(monitor.status);
|
||||
const uptimePercentage = formatPercentage(monitor.uptimePercentage ?? 0);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -169,32 +172,39 @@ export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
sx={styles.card}
|
||||
>
|
||||
<Box sx={styles.cardRow}>
|
||||
<Box sx={styles.cardLeft}>
|
||||
<Box sx={styles.monitorName}>{monitor.name}</Box>
|
||||
<Box sx={styles.monitorMeta}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={isHardware ? styles.pillHardware : styles.pill}
|
||||
>
|
||||
{getMonitorTypeLabel(monitor.type, t)}
|
||||
</Box>
|
||||
{monitor.url && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.monitorUrl}
|
||||
title={monitor.url}
|
||||
>
|
||||
{monitor.url}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={styles.monitorName}>{monitor.name}</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.badge(badgeTone)}
|
||||
>
|
||||
{t(statusBadgeKey[monitor.status])}
|
||||
</Box>
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
sx={styles.monitorMeta}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={isHardware ? styles.pillHardware : styles.pill}
|
||||
>
|
||||
{getMonitorTypeLabel(monitor.type, t)}
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ ...styles.pill }}
|
||||
>
|
||||
{uptimePercentage}
|
||||
</Box>
|
||||
{monitor.url && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.monitorUrl}
|
||||
title={monitor.url}
|
||||
>
|
||||
{monitor.url}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{showInfra && (
|
||||
|
||||
@@ -11,7 +11,9 @@ import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
|
||||
import { getStatusPagePublicPath, getStatusPagePublicUrl } from "@/Utils/statusPageUrl";
|
||||
|
||||
import { type StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
interface StatusPagesTableProps {
|
||||
data: StatusPage[];
|
||||
@@ -53,8 +55,7 @@ export const StatusPagesTable = ({
|
||||
const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => {
|
||||
if (row.isPublished) {
|
||||
e.stopPropagation();
|
||||
const url = `${PUBLIC_STATUS_PAGE_PREFIX}/${row.url}`;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
window.open(getStatusPagePublicUrl(row), "_blank", "noopener,noreferrer");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,7 +71,9 @@ export const StatusPagesTable = ({
|
||||
content: t("pages.statusPages.table.headers.url"),
|
||||
render: (row) => {
|
||||
const content = row.isPublished
|
||||
? `/${row.url}`
|
||||
? row.customDomain
|
||||
? getStatusPagePublicPath(row)
|
||||
: `/${row.url}`
|
||||
: t("pages.statusPages.table.unpublished");
|
||||
return (
|
||||
<Stack
|
||||
|
||||
@@ -25,6 +25,8 @@ import type { RootState } from "@/Types/state";
|
||||
import { Checkbox } from "@/Components/inputs";
|
||||
import type { Tag } from "@/Types/Tag";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
import { getUptimePercentageColor } from "@/Utils/MonitorUtils";
|
||||
import { formatPercentage } from "@/Utils/FormatUtils";
|
||||
|
||||
interface MonitorTableProps {
|
||||
monitors: Monitor[];
|
||||
@@ -259,6 +261,24 @@ export const MonitorTable = ({
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "uptime",
|
||||
content: t("pages.uptime.table.headers.uptime"),
|
||||
render: (row) => {
|
||||
const uptimePercentage = row.uptimePercentage ?? 0;
|
||||
const formattedPercentage = formatPercentage(uptimePercentage);
|
||||
const paletteKey = getUptimePercentageColor(uptimePercentage);
|
||||
const color = theme.palette[paletteKey].main;
|
||||
return (
|
||||
<Stack alignItems={"center"}>
|
||||
<ColoredLabel
|
||||
text={formattedPercentage}
|
||||
color={color}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tags",
|
||||
content: t("common.table.headers.tags"),
|
||||
|
||||
@@ -54,8 +54,24 @@ import Logs from "@/Pages/Logs";
|
||||
import { ProtectedRoute, RoleProtectedRoute } from "@/Components/routing/RouteProtected";
|
||||
|
||||
import CreateMonitor from "@/Pages/CreateMonitor";
|
||||
import { isCustomDomainHost } from "@/Utils/statusPageUrl";
|
||||
|
||||
const Routes = () => {
|
||||
if (isCustomDomainHost()) {
|
||||
return (
|
||||
<LibRoutes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Status />}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={<Status />}
|
||||
/>
|
||||
</LibRoutes>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LibRoutes>
|
||||
<Route
|
||||
|
||||
@@ -23,6 +23,11 @@ export interface V8HeapStats {
|
||||
|
||||
export interface MongoStats {
|
||||
readyState: number;
|
||||
readsPerSecond: number;
|
||||
insertsPerSecond: number;
|
||||
updatesPerSecond: number;
|
||||
deletesPerSecond: number;
|
||||
writesPerSecond: number;
|
||||
host: string;
|
||||
port: number;
|
||||
dbName: string;
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface StatusPage {
|
||||
type: MonitorDisplayType[];
|
||||
companyName: string;
|
||||
url: string;
|
||||
customDomain?: string | null;
|
||||
timezone?: string;
|
||||
color: string;
|
||||
monitors: string[];
|
||||
|
||||
@@ -63,6 +63,16 @@ export const getResponseTimeColor = (responseTime: number): PaletteKey => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getUptimePercentageColor = (uptimePercentage: number): PaletteKey => {
|
||||
if (uptimePercentage >= 0.75) {
|
||||
return "success";
|
||||
} else if (uptimePercentage >= 0.5) {
|
||||
return "warning";
|
||||
} else {
|
||||
return "error";
|
||||
}
|
||||
};
|
||||
|
||||
export const getInfraGaugeColor = (val: number, theme: any) => {
|
||||
if (val < 50) {
|
||||
return theme.palette.success.main;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST as string | undefined;
|
||||
|
||||
const getClientOriginProtocol = (): string => {
|
||||
const source =
|
||||
CLIENT_HOST || (typeof window !== "undefined" ? window.location.origin : undefined);
|
||||
if (!source) {
|
||||
return "https:";
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(source).protocol;
|
||||
} catch {
|
||||
return "https:";
|
||||
}
|
||||
};
|
||||
|
||||
const buildCustomDomainOrigin = (customDomain: string): string =>
|
||||
`${getClientOriginProtocol()}//${customDomain}`;
|
||||
|
||||
export const getAppHostname = (): string | null => {
|
||||
const source =
|
||||
CLIENT_HOST || (typeof window !== "undefined" ? window.location.origin : undefined);
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(source).hostname.toLowerCase();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const isCustomDomainHost = (): boolean => {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const appHostname = getAppHostname();
|
||||
if (!appHostname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.location.hostname.toLowerCase() !== appHostname;
|
||||
};
|
||||
|
||||
export const getStatusPagePublicPath = (
|
||||
statusPage: Pick<StatusPage, "url" | "customDomain">
|
||||
): string => {
|
||||
if (statusPage.customDomain) {
|
||||
return buildCustomDomainOrigin(statusPage.customDomain);
|
||||
}
|
||||
|
||||
return `${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
|
||||
};
|
||||
|
||||
export const getStatusPagePublicUrl = (
|
||||
statusPage: Pick<StatusPage, "url" | "customDomain">
|
||||
): string => {
|
||||
if (statusPage.customDomain) {
|
||||
return buildCustomDomainOrigin(statusPage.customDomain);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
return `${window.location.origin}${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
|
||||
}
|
||||
|
||||
return `${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
|
||||
};
|
||||
|
||||
export const buildStatusPageApiPath = (options: {
|
||||
url?: string;
|
||||
useCustomDomain?: boolean;
|
||||
}): string | null => {
|
||||
const query = "type=uptime&type=infrastructure";
|
||||
|
||||
if (options.useCustomDomain && typeof window !== "undefined") {
|
||||
const domain = encodeURIComponent(window.location.hostname);
|
||||
return `/status-page/resolve?domain=${domain}&${query}`;
|
||||
}
|
||||
|
||||
if (options.url) {
|
||||
return `/status-page/${options.url}?${query}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -9,6 +9,23 @@ import { STATUS_PAGE_THEMES, STATUS_PAGE_THEME_MODES } from "@/Types/StatusPage"
|
||||
// field definitions.
|
||||
export const statusPageStepRegistry = z.registry<{ step: number }>();
|
||||
|
||||
const statusPageHostnameRegex =
|
||||
/^(?=.{1,253}$)([a-zA-Z0-9_](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
|
||||
|
||||
const normalizeCustomDomainInput = (raw: string | null): string | null => {
|
||||
if (raw === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = raw.trim().toLowerCase();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const withoutScheme = trimmed.replace(/^https?:\/\//, "");
|
||||
return withoutScheme.split("/")[0]?.split(":")[0] ?? null;
|
||||
};
|
||||
|
||||
export const statusPageSchema = z.object({
|
||||
companyName: z
|
||||
.string()
|
||||
@@ -22,6 +39,12 @@ export const statusPageSchema = z.object({
|
||||
/^[a-z0-9-]+$/,
|
||||
"URL can only contain lowercase letters, numbers, and hyphens"
|
||||
),
|
||||
customDomain: z
|
||||
.union([z.string(), z.null()])
|
||||
.transform(normalizeCustomDomainInput)
|
||||
.refine((domain) => domain === null || statusPageHostnameRegex.test(domain), {
|
||||
message: "Enter a valid domain name (e.g. status.example.com)",
|
||||
}),
|
||||
timezone: z.string().optional(),
|
||||
type: z
|
||||
.array(z.enum(["uptime", "infrastructure"]))
|
||||
|
||||
@@ -975,6 +975,11 @@
|
||||
},
|
||||
"mongoDBStats": {
|
||||
"readyState": "Ready state",
|
||||
"readsPerSecond": "Reads/s",
|
||||
"insertsPerSecond": "Inserts/s",
|
||||
"updatesPerSecond": "Updates/s",
|
||||
"deletesPerSecond": "Deletes/s",
|
||||
"writesPerSecond": "Total writes/s",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"dbName": "Database name",
|
||||
@@ -1509,6 +1514,11 @@
|
||||
"url": {
|
||||
"label": "Your status page address",
|
||||
"placeholder": "my-status-page"
|
||||
},
|
||||
"customDomain": {
|
||||
"label": "Custom domain",
|
||||
"placeholder": "status.example.com",
|
||||
"helper": "Optional. Point a DNS record to this Checkmate instance to serve the status page at your own domain."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1607,7 +1617,8 @@
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"responseTime": "Response time"
|
||||
"responseTime": "Response time",
|
||||
"uptime": "Uptime %"
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ LOG_LEVEL=debug
|
||||
# Database
|
||||
# --------
|
||||
DB_CONNECTION_STRING=mongodb://localhost:27017/uptime_db
|
||||
QUEUE_TYPE=superSimpleQueue
|
||||
QUEUE_MODE="primary"
|
||||
QUEUE_PRIMARY_PROCESSES="true"
|
||||
|
||||
# JWT Authentication
|
||||
# ------------------
|
||||
|
||||
@@ -39,7 +39,7 @@ export default [
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
|
||||
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
|
||||
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ const config: Config = {
|
||||
"^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@/validation/(.*)\\.js$": "<rootDir>/src/validation/$1.js",
|
||||
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.ts",
|
||||
"^@/(.*)\\.ts$": "<rootDir>/src/$1.ts",
|
||||
"^@/(.*)\\.js$": "<rootDir>/src/$1.ts",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"ignore": ["node_modules/*"],
|
||||
"watch": ["src/**/*.ts", "src/**/*.js", "*.json"],
|
||||
"watch": ["src/**/*.ts", "src/**/*.js", "*.json", ".env"],
|
||||
"ext": "ts,js,json"
|
||||
}
|
||||
+145
-1
@@ -3,7 +3,7 @@
|
||||
"info": {
|
||||
"title": "Checkmate API",
|
||||
"version": "1.0.0",
|
||||
"description": "Generated from Zod validation schemas. Source of truth: server/src/validation/*.ts"
|
||||
"description": "Generated from Zod validation schemas. Source of truth: server/src/api/validation/*.ts"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@@ -398,6 +398,12 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customUpCodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7016,6 +7022,13 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customUpCodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7685,6 +7698,13 @@
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"customUpCodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8209,6 +8229,12 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customUpCodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10422,6 +10448,17 @@
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+$"
|
||||
},
|
||||
"customDomain": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^(?=.{1,253}$)([a-zA-Z0-9_](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,63}$"
|
||||
},
|
||||
{
|
||||
"nullable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10625,6 +10662,17 @@
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+$"
|
||||
},
|
||||
"customDomain": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^(?=.{1,253}$)([a-zA-Z0-9_](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,63}$"
|
||||
},
|
||||
{
|
||||
"nullable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10894,6 +10942,102 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/status-page/resolve": {
|
||||
"get": {
|
||||
"tags": ["status-page"],
|
||||
"summary": "Resolve a published status page by custom domain",
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["uptime", "infrastructure"]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["uptime", "infrastructure"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true,
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"required": false,
|
||||
"name": "timeFrame",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": false,
|
||||
"name": "domain",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"enum": [true]
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": ["success", "msg"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Status page not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"enum": [false]
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": ["success", "msg"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/status-page/{url}": {
|
||||
"get": {
|
||||
"tags": ["status-page"],
|
||||
|
||||
@@ -28,7 +28,7 @@ export function getOpenApiSpec(): JsonObject {
|
||||
info: {
|
||||
title: "Checkmate API",
|
||||
version: "1.0.0",
|
||||
description: "Generated from Zod validation schemas. Source of truth: server/src/validation/*.ts",
|
||||
description: "Generated from Zod validation schemas. Source of truth: server/src/api/validation/*.ts",
|
||||
},
|
||||
servers: [{ url: "/api/v1", description: "Current server" }],
|
||||
tags: [
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
recoveryValidation,
|
||||
recoveryTokenBodyValidation,
|
||||
newPasswordValidation,
|
||||
} from "@/validation/authValidation.js";
|
||||
} from "@/api/validation/authValidation.js";
|
||||
import {
|
||||
getUserByIdParamValidation,
|
||||
editUserByIdParamValidation,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
createUserBodyValidation,
|
||||
editUserBodyValidation,
|
||||
userResponseSchema,
|
||||
} from "@/validation/userValidation.js";
|
||||
} from "@/api/validation/userValidation.js";
|
||||
|
||||
const tags = ["auth"];
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getTeamChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
getChecksSummaryByTeamIdQueryValidation,
|
||||
} from "@/validation/checkValidation.js";
|
||||
} from "@/api/validation/checkValidation.js";
|
||||
|
||||
const tags = ["checks"];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, okUnknown, standardErrors } from "../helpers.js";
|
||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/api/validation/checkValidation.js";
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, okJson, okJsonNoData, standardErrors } from "../helpers.js";
|
||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/api/validation/incidentValidation.js";
|
||||
|
||||
const tags = ["incidents"];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, json, okJson, okJsonNoData, standardErrors } from "../helpers.js";
|
||||
import { z } from "zod";
|
||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
|
||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/api/validation/authValidation.js";
|
||||
|
||||
const tags = ["invite"];
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
deleteMaintenanceWindowByIdParamValidation,
|
||||
editMaintenanceWindowByIdParamValidation,
|
||||
editMaintenanceByIdWindowBodyValidation,
|
||||
} from "@/validation/maintenanceWindowValidation.js";
|
||||
} from "@/api/validation/maintenanceWindowValidation.js";
|
||||
|
||||
const tags = ["maintenance-window"];
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
monitorResponseSchema,
|
||||
} from "@/validation/monitorValidation.js";
|
||||
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
|
||||
} from "@/api/validation/monitorValidation.js";
|
||||
import { updateNotificationsValidation } from "@/api/validation/notificationValidation.js";
|
||||
|
||||
const tags = ["monitors"];
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getNotificationByIdParamValidation,
|
||||
editNotificationParamValidation,
|
||||
testAllNotificationsBodyValidation,
|
||||
} from "@/validation/notificationValidation.js";
|
||||
} from "@/api/validation/notificationValidation.js";
|
||||
|
||||
const tags = ["notifications"];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, json, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
||||
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
||||
import { updateAppSettingsBodyValidation } from "@/api/validation/settingsValidation.js";
|
||||
import { sendTestEmailBodyValidation } from "@/api/validation/notificationValidation.js";
|
||||
|
||||
const tags = ["settings"];
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { registry } from "../registry.js";
|
||||
import { bearer, multipart, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
|
||||
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation } from "@/validation/statusPageValidation.js";
|
||||
import {
|
||||
createStatusPageBodyValidation,
|
||||
getStatusPageParamValidation,
|
||||
getStatusPageQueryValidation,
|
||||
resolveStatusPageQueryValidation,
|
||||
} from "@/api/validation/statusPageValidation.js";
|
||||
|
||||
const tags = ["status-page"];
|
||||
|
||||
@@ -33,6 +38,15 @@ registry.registerPath({
|
||||
responses: { "200": okUnknown, ...standardErrors },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/status-page/resolve",
|
||||
tags,
|
||||
summary: "Resolve a published status page by custom domain",
|
||||
request: { query: resolveStatusPageQueryValidation },
|
||||
responses: { "200": okUnknown, "404": { description: "Status page not found" }, "500": standardErrors["500"] },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/status-page/{url}",
|
||||
|
||||
Generated
+1181
-988
File diff suppressed because it is too large.
Load diff
+2
-5
@@ -22,7 +22,7 @@
|
||||
"format-check": "prettier --check ."
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*": "prettier --write"
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -33,7 +33,6 @@
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@pulsecron/pulse": "1.6.8",
|
||||
"axios": "^1.15.2",
|
||||
"bcryptjs": "3.0.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
@@ -48,12 +47,10 @@
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
"ioredis": "^5.4.2",
|
||||
"isomorphic-dompurify": "^2.26.0",
|
||||
"jmespath": "^0.16.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"less-simple-scheduler": "2.6.0",
|
||||
"mailersend": "^2.2.0",
|
||||
"mjml": "^5.0.0-alpha.4",
|
||||
"mongoose": "^8.3.3",
|
||||
@@ -62,7 +59,6 @@
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.5",
|
||||
"ssl-checker": "2.0.10",
|
||||
"super-simple-scheduler": "1.9.2",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"winston": "^3.13.0",
|
||||
"ws": "^8.19.0",
|
||||
@@ -96,6 +92,7 @@
|
||||
"globals": "^15.14.0",
|
||||
"jest": "^30.2.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"mongodb-memory-server": "11.2.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-ini": "1.3.0",
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import mongoose from "mongoose";
|
||||
import { MonitorModel } from "../dist/domain/monitors/monitor.model.js";
|
||||
import { UserModel } from "../dist/domain/users/user.model.js";
|
||||
|
||||
// Seeds a large number of HTTP monitors against real, resolving domains so the
|
||||
// scheduler can be exercised under load. Run after `npm run build` (imports the
|
||||
// compiled models from dist/, like scripts/generate-checks.js).
|
||||
//
|
||||
// node scripts/seed-monitors.js # 10k monitors, owner auto-detected
|
||||
// COUNT=2000 INTERVAL=30000 node scripts/seed-monitors.js
|
||||
// CLEAR=true node scripts/seed-monitors.js # remove prior seed monitors first
|
||||
// TEAM_ID=<id> USER_ID=<id> node scripts/seed-monitors.js
|
||||
//
|
||||
// NOTE: the running queue only schedules monitors it finds at init(), so restart
|
||||
// the server after seeding for the new monitors to actually start checking.
|
||||
|
||||
const DEFAULT_COUNT = 500;
|
||||
const DEFAULT_BATCH = 1_000;
|
||||
const DEFAULT_INTERVAL = 60_000;
|
||||
const NAME_PREFIX = "Seed";
|
||||
// Stored in `description` so a re-run with CLEAR=true can find and remove them.
|
||||
const SEED_MARKER = "checkmate-scaling-seed";
|
||||
|
||||
// A spread of well-known, reliably-resolving domains across regions, TLDs and
|
||||
// CDNs. Monitors round-robin through this pool, so the load is split across many
|
||||
// hosts rather than hammering one. Every entry returns a real HTTP response
|
||||
// (some may 403/429 under load — still a valid response for scaling purposes).
|
||||
const DOMAINS = [
|
||||
"google.com",
|
||||
"youtube.com",
|
||||
"facebook.com",
|
||||
"wikipedia.org",
|
||||
"amazon.com",
|
||||
"reddit.com",
|
||||
"x.com",
|
||||
"instagram.com",
|
||||
"linkedin.com",
|
||||
"microsoft.com",
|
||||
"apple.com",
|
||||
"cloudflare.com",
|
||||
"github.com",
|
||||
"gitlab.com",
|
||||
"stackoverflow.com",
|
||||
"netflix.com",
|
||||
"bing.com",
|
||||
"yahoo.com",
|
||||
"duckduckgo.com",
|
||||
"mozilla.org",
|
||||
"wordpress.org",
|
||||
"wordpress.com",
|
||||
"adobe.com",
|
||||
"salesforce.com",
|
||||
"oracle.com",
|
||||
"ibm.com",
|
||||
"intel.com",
|
||||
"nvidia.com",
|
||||
"amd.com",
|
||||
"dell.com",
|
||||
"hp.com",
|
||||
"cisco.com",
|
||||
"vmware.com",
|
||||
"redhat.com",
|
||||
"ubuntu.com",
|
||||
"debian.org",
|
||||
"kernel.org",
|
||||
"python.org",
|
||||
"nodejs.org",
|
||||
"npmjs.com",
|
||||
"docker.com",
|
||||
"kubernetes.io",
|
||||
"golang.org",
|
||||
"rust-lang.org",
|
||||
"php.net",
|
||||
"ruby-lang.org",
|
||||
"java.com",
|
||||
"spring.io",
|
||||
"djangoproject.com",
|
||||
"flask.palletsprojects.com",
|
||||
"vuejs.org",
|
||||
"reactjs.org",
|
||||
"angular.io",
|
||||
"svelte.dev",
|
||||
"jquery.com",
|
||||
"npm.im",
|
||||
"pypi.org",
|
||||
"rubygems.org",
|
||||
"packagist.org",
|
||||
"maven.apache.org",
|
||||
"apache.org",
|
||||
"nginx.org",
|
||||
"postgresql.org",
|
||||
"mysql.com",
|
||||
"mongodb.com",
|
||||
"redis.io",
|
||||
"elastic.co",
|
||||
"grafana.com",
|
||||
"prometheus.io",
|
||||
"hashicorp.com",
|
||||
"digitalocean.com",
|
||||
"linode.com",
|
||||
"vultr.com",
|
||||
"heroku.com",
|
||||
"netlify.com",
|
||||
"vercel.com",
|
||||
"render.com",
|
||||
"fly.io",
|
||||
"railway.app",
|
||||
"cloudinary.com",
|
||||
"twilio.com",
|
||||
"stripe.com",
|
||||
"paypal.com",
|
||||
"shopify.com",
|
||||
"squarespace.com",
|
||||
"wix.com",
|
||||
"godaddy.com",
|
||||
"namecheap.com",
|
||||
"name.com",
|
||||
"gandi.net",
|
||||
"bbc.co.uk",
|
||||
"theguardian.com",
|
||||
"nytimes.com",
|
||||
"washingtonpost.com",
|
||||
"cnn.com",
|
||||
"reuters.com",
|
||||
"bloomberg.com",
|
||||
"forbes.com",
|
||||
"wsj.com",
|
||||
"economist.com",
|
||||
"nature.com",
|
||||
"sciencedirect.com",
|
||||
"arxiv.org",
|
||||
"mit.edu",
|
||||
"stanford.edu",
|
||||
"harvard.edu",
|
||||
"berkeley.edu",
|
||||
"ox.ac.uk",
|
||||
"cam.ac.uk",
|
||||
"ethz.ch",
|
||||
"spotify.com",
|
||||
"soundcloud.com",
|
||||
"twitch.tv",
|
||||
"vimeo.com",
|
||||
"dailymotion.com",
|
||||
"imdb.com",
|
||||
"wikimedia.org",
|
||||
"archive.org",
|
||||
"ietf.org",
|
||||
"w3.org",
|
||||
"who.int",
|
||||
"un.org",
|
||||
"europa.eu",
|
||||
"gov.uk",
|
||||
"canada.ca",
|
||||
"alibaba.com",
|
||||
"aliexpress.com",
|
||||
"baidu.com",
|
||||
"tencent.com",
|
||||
"naver.com",
|
||||
"rakuten.co.jp",
|
||||
"yandex.com",
|
||||
"ozon.ru",
|
||||
"mercadolibre.com",
|
||||
"flipkart.com",
|
||||
"booking.com",
|
||||
"airbnb.com",
|
||||
"expedia.com",
|
||||
"tripadvisor.com",
|
||||
"uber.com",
|
||||
"dropbox.com",
|
||||
"box.com",
|
||||
"atlassian.com",
|
||||
"trello.com",
|
||||
"notion.so",
|
||||
"slack.com",
|
||||
"zoom.us",
|
||||
"asana.com",
|
||||
"figma.com",
|
||||
"canva.com",
|
||||
];
|
||||
|
||||
const parseObjectId = (value) => {
|
||||
if (!value) return null;
|
||||
try {
|
||||
return new mongoose.Types.ObjectId(value);
|
||||
} catch {
|
||||
console.warn(`Ignoring invalid ObjectId '${value}'`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Monitors need a real teamId/userId to show up for an account and to be picked
|
||||
// up by the queue. Prefer explicit env ids, otherwise derive from the oldest
|
||||
// existing user (the typical single-tenant dev setup).
|
||||
async function resolveOwner() {
|
||||
const teamId = parseObjectId(process.env.TEAM_ID);
|
||||
const userId = parseObjectId(process.env.USER_ID);
|
||||
if (teamId && userId) return { teamId, userId };
|
||||
|
||||
const user = await UserModel.findOne(userId ? { _id: userId } : {}).sort({ createdAt: 1 });
|
||||
if (!user?.teamId) {
|
||||
throw new Error(
|
||||
"Could not determine an owner: no existing user with a teamId. " + "Create an account first, or pass TEAM_ID and USER_ID explicitly."
|
||||
);
|
||||
}
|
||||
return { teamId: teamId ?? user.teamId, userId: user._id };
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const mongoUri = process.env.MONGO_URI ?? "mongodb://localhost:27017/uptime_db";
|
||||
const total = Number(process.env.COUNT ?? DEFAULT_COUNT);
|
||||
const batchSize = Number(process.env.BATCH_SIZE ?? DEFAULT_BATCH);
|
||||
const interval = Number(process.env.INTERVAL ?? DEFAULT_INTERVAL);
|
||||
|
||||
console.log(`Connecting to MongoDB at ${mongoUri}`);
|
||||
await mongoose.connect(mongoUri);
|
||||
|
||||
const { teamId, userId } = await resolveOwner();
|
||||
|
||||
if (process.env.CLEAR === "true") {
|
||||
const { deletedCount } = await MonitorModel.deleteMany({ description: SEED_MARKER });
|
||||
console.log(`Removed ${deletedCount} previously seeded monitors`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Seeding ${total} http monitors for team ${teamId} (user ${userId}) across ${DOMAINS.length} domains, ` +
|
||||
`interval ${interval}ms, in batches of ${batchSize}`
|
||||
);
|
||||
|
||||
const docs = [];
|
||||
const startTime = Date.now();
|
||||
let inserted = 0;
|
||||
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
const domain = DOMAINS[i % DOMAINS.length];
|
||||
docs.push({
|
||||
userId,
|
||||
teamId,
|
||||
name: `${NAME_PREFIX} ${String(i + 1).padStart(5, "0")} ${domain}`,
|
||||
description: SEED_MARKER,
|
||||
type: "http",
|
||||
url: `https://${domain}`,
|
||||
isActive: true,
|
||||
interval,
|
||||
lastEvaluatedAt: 0, // evaluator watermark; without it findUnevaluatedByMonitorId gets new Date(undefined)
|
||||
});
|
||||
|
||||
if (docs.length === batchSize) {
|
||||
await MonitorModel.insertMany(docs, { ordered: false });
|
||||
inserted += docs.length;
|
||||
console.log(`Inserted ${inserted} / ${total}`);
|
||||
docs.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (docs.length > 0) {
|
||||
await MonitorModel.insertMany(docs, { ordered: false });
|
||||
inserted += docs.length;
|
||||
}
|
||||
|
||||
await mongoose.disconnect();
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(`Finished inserting ${inserted} monitors in ${duration}s`);
|
||||
console.log("Restart the server so the queue schedules the new monitors.");
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Failed to seed monitors", error);
|
||||
process.exit(1);
|
||||
});
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||
import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/api/controllers/controllerUtils.js";
|
||||
|
||||
import {
|
||||
registrationBodyValidation,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
recoveryValidation,
|
||||
recoveryTokenBodyValidation,
|
||||
newPasswordValidation,
|
||||
} from "@/validation/authValidation.js";
|
||||
} from "@/api/validation/authValidation.js";
|
||||
|
||||
import {
|
||||
editUserBodyValidation,
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
editUserByIdBodyValidation,
|
||||
editSuperadminUserByIdBodyValidation,
|
||||
editUserPasswordByIdBodyValidation,
|
||||
} from "@/validation/userValidation.js";
|
||||
import { IUserService } from "@/service/index.js";
|
||||
} from "@/api/validation/userValidation.js";
|
||||
import { IUserService } from "@/domain/users/user.service.js";
|
||||
|
||||
const SERVICE_NAME = "authController";
|
||||
|
||||
+3
-3
@@ -6,9 +6,9 @@ import {
|
||||
getTeamChecksQueryValidation,
|
||||
getChecksSummaryByTeamIdQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
} from "@/validation/checkValidation.js";
|
||||
import { ICheckService } from "@/service/index.js";
|
||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||
} from "@/api/validation/checkValidation.js";
|
||||
import { ICheckService } from "@/domain/checks/check.service.js";
|
||||
import { requireTeamId } from "@/api/controllers/controllerUtils.js";
|
||||
|
||||
const SERVICE_NAME = "checkController";
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js";
|
||||
import { Monitor, MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.types.js";
|
||||
import { UserRole } from "@/domain/users/user.type.js";
|
||||
import sslChecker, { SSLDetails } from "ssl-checker";
|
||||
type SSLCheckerType = typeof sslChecker;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { IDiagnosticService } from "@/service/index.js";
|
||||
import { IDiagnosticService } from "@/domain/diagnostics/diagnostic.service.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
const SERVICE_NAME = "diagnosticController";
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
||||
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/api/validation/checkValidation.js";
|
||||
import type { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js";
|
||||
import { requireTeamId } from "./controllerUtils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
const SERVICE_NAME = "geoCheckController";
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { requireTeamId, requireUserId, requireUserEmail, extractString } from "./controllerUtils.js";
|
||||
import { IIncidentService } from "@/service/index.js";
|
||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
||||
import { IIncidentService } from "@/domain/incidents/incident.service.js";
|
||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/api/validation/incidentValidation.js";
|
||||
|
||||
const SERVICE_NAME = "IncidentController";
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
|
||||
import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||
import { IInviteService } from "@/service/index.js";
|
||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/api/validation/authValidation.js";
|
||||
import { requireFirstName, requireTeamId, requireUserRoles } from "@/api/controllers/controllerUtils.js";
|
||||
import { IInviteService } from "@/domain/invites/invite.service.js";
|
||||
const SERVICE_NAME = "inviteController";
|
||||
|
||||
export interface IInviteController {
|
||||
File renamed without changes.
+3
-3
@@ -7,9 +7,9 @@ import {
|
||||
getMaintenanceWindowsByMonitorIdParamValidation,
|
||||
getMaintenanceWindowsByTeamIdQueryValidation,
|
||||
deleteMaintenanceWindowByIdParamValidation,
|
||||
} from "@/validation/maintenanceWindowValidation.js";
|
||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||
import { IMaintenanceWindowService } from "@/service/index.js";
|
||||
} from "@/api/validation/maintenanceWindowValidation.js";
|
||||
import { requireTeamId } from "@/api/controllers/controllerUtils.js";
|
||||
import { IMaintenanceWindowService } from "@/domain/maintenance-windows/maintenance-window.service.js";
|
||||
|
||||
export interface IMaintenanceWindowController {
|
||||
createMaintenanceWindows: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
|
||||
import { updateNotificationsValidation } from "@/api/validation/notificationValidation.js";
|
||||
import {
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
getUptimeDetailsByIdQueryValidation,
|
||||
importMonitorsBodyValidation,
|
||||
bulkPauseMonitorBodyValidation,
|
||||
} from "@/validation/monitorValidation.js";
|
||||
} from "@/api/validation/monitorValidation.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
||||
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/api/controllers/controllerUtils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService, INotificationsService } from "@/service/index.js";
|
||||
import { IMonitorService } from "@/domain/monitors/monitor.service.js";
|
||||
import { INotificationsService } from "@/domain/notifications/notification.service.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
|
||||
+3
-3
@@ -7,11 +7,11 @@ import {
|
||||
testNotificationBodyValidation,
|
||||
editNotificationParamValidation,
|
||||
testAllNotificationsBodyValidation,
|
||||
} from "@/validation/notificationValidation.js";
|
||||
} from "@/api/validation/notificationValidation.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { INotificationsService } from "@/service/index.js";
|
||||
import { INotificationsService } from "@/domain/notifications/notification.service.js";
|
||||
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
|
||||
|
||||
const SERVICE_NAME = "NotificationController";
|
||||
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
import { IJobQueue } from "@/service/index.js";
|
||||
import { getQueueJobsQueryValidation } from "@/validation/queueValidation.js";
|
||||
import { IWorker } from "@/worker/worker.interface.js";
|
||||
import { getQueueJobsQueryValidation } from "@/api/validation/queueValidation.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
const SERVICE_NAME = "JobQueueController";
|
||||
@@ -13,9 +13,9 @@ export interface IJobQueueController {
|
||||
|
||||
class JobQueueController implements IJobQueueController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private jobQueue: IJobQueue;
|
||||
constructor(jobQueue: IJobQueue) {
|
||||
this.jobQueue = jobQueue;
|
||||
private worker: IWorker;
|
||||
constructor(worker: IWorker) {
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -24,7 +24,7 @@ class JobQueueController implements IJobQueueController {
|
||||
|
||||
getMetrics = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const metrics = await this.jobQueue.getMetrics();
|
||||
const metrics = await this.worker.getMetrics();
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
msg: "Queue metrics fetched successfully",
|
||||
@@ -38,7 +38,7 @@ class JobQueueController implements IJobQueueController {
|
||||
getJobs = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const pagination = getQueueJobsQueryValidation.parse(req.query);
|
||||
const { jobs, count } = await this.jobQueue.getJobs(pagination);
|
||||
const { jobs, count } = await this.worker.getJobs(pagination);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Queue jobs fetched successfully",
|
||||
@@ -52,8 +52,8 @@ class JobQueueController implements IJobQueueController {
|
||||
getAllMetrics = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const pagination = getQueueJobsQueryValidation.parse(req.query);
|
||||
const { jobs, count } = await this.jobQueue.getJobs(pagination);
|
||||
const metrics = await this.jobQueue.getMetrics();
|
||||
const { jobs, count } = await this.worker.getJobs(pagination);
|
||||
const metrics = await this.worker.getMetrics();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Queue metrics fetched successfully",
|
||||
@@ -66,7 +66,7 @@ class JobQueueController implements IJobQueueController {
|
||||
|
||||
flushQueue = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const result = await this.jobQueue.flushQueues();
|
||||
const result = await this.worker.flushQueues();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Queue flushed successfully",
|
||||
+5
-4
@@ -1,9 +1,10 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
||||
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
||||
import { updateAppSettingsBodyValidation } from "@/api/validation/settingsValidation.js";
|
||||
import { sendTestEmailBodyValidation } from "@/api/validation/notificationValidation.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IEmailService, ISettingsService } from "@/service/index.js";
|
||||
import { Settings } from "@/types/settings.js";
|
||||
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
|
||||
import { IEmailService } from "@/service/emailService.js";
|
||||
import { Settings } from "@/domain/app-settings/app-settings.type.js";
|
||||
|
||||
const SERVICE_NAME = "SettingsController";
|
||||
|
||||
+73
-37
@@ -5,13 +5,16 @@ import {
|
||||
getStatusPageParamValidation,
|
||||
getStatusPageQueryValidation,
|
||||
imageValidation,
|
||||
} from "@/validation/statusPageValidation.js";
|
||||
resolveStatusPageQueryValidation,
|
||||
} from "@/api/validation/statusPageValidation.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
||||
import { IStatusPageService } from "@/service/business/statusPageService.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import { requireTeamId, requireUserId } from "@/api/controllers/controllerUtils.js";
|
||||
import { IStatusPageService } from "@/domain/status-pages/status-page.service.js";
|
||||
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
|
||||
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
|
||||
import { NormalizeData } from "@/utils/dataUtils.js";
|
||||
import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js";
|
||||
import type { StatusPage } from "@/domain/status-pages/status-page.type.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageController";
|
||||
|
||||
@@ -20,6 +23,7 @@ export interface IStatusPageController {
|
||||
createStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||
updateStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||
getStatusPageByUrl(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||
resolveStatusPageByDomain(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||
getStatusPagesByTeamId(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||
deleteStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||
}
|
||||
@@ -39,6 +43,41 @@ class StatusPageController implements IStatusPageController {
|
||||
return StatusPageController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
private buildStatusPagePayload = async (statusPage: StatusPage, req: Request) => {
|
||||
if (!statusPage.isPublished) {
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
if (statusPage.teamId !== teamId) {
|
||||
throw new AppError({ message: "Forbidden", status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await this.settingsService.getDBSettings();
|
||||
const showURL = settings.showURL;
|
||||
|
||||
const monitors = await this.monitorsRepository.findByIds(statusPage.monitors);
|
||||
const monitorOrder = new Map(statusPage.monitors.map((id, index) => [id, index]));
|
||||
const sortedMonitors = [...monitors].sort((a, b) => {
|
||||
const orderA = monitorOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
||||
const orderB = monitorOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
const normalizedMonitors = sortedMonitors.map((monitor) => {
|
||||
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
|
||||
if (!showURL) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { url, port, secret, notifications, ...rest } = monitor;
|
||||
return { ...rest, checks: normalizedChecks };
|
||||
}
|
||||
return { ...monitor, checks: normalizedChecks };
|
||||
});
|
||||
|
||||
return {
|
||||
statusPage,
|
||||
monitors: normalizedMonitors,
|
||||
};
|
||||
};
|
||||
|
||||
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
createStatusPageBodyValidation.parse(req.body);
|
||||
@@ -96,47 +135,44 @@ class StatusPageController implements IStatusPageController {
|
||||
}
|
||||
|
||||
const statusPage = await this.statusPageService.getStatusPageByUrl(req.params.url as string);
|
||||
const data = await this.buildStatusPagePayload(statusPage, req);
|
||||
|
||||
if (!statusPage.isPublished) {
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
if (statusPage.teamId !== teamId) {
|
||||
throw new AppError({ message: "Forbidden", status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await this.settingsService.getDBSettings();
|
||||
const showURL = settings.showURL;
|
||||
|
||||
const monitors = await this.monitorsRepository.findByIds(statusPage.monitors);
|
||||
// Sort monitors according to the order in statusPage.monitors
|
||||
const monitorOrder = new Map(statusPage.monitors.map((id, index) => [id, index]));
|
||||
const sortedMonitors = [...monitors].sort((a, b) => {
|
||||
const orderA = monitorOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
||||
const orderB = monitorOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
const normalizedMonitors = sortedMonitors.map((monitor) => {
|
||||
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
|
||||
if (!showURL) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { url, port, secret, notifications, ...rest } = monitor;
|
||||
return { ...rest, checks: normalizedChecks };
|
||||
}
|
||||
return { ...monitor, checks: normalizedChecks };
|
||||
});
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page retrieved successfully",
|
||||
data: {
|
||||
statusPage,
|
||||
monitors: normalizedMonitors,
|
||||
},
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
resolveStatusPageByDomain = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
resolveStatusPageQueryValidation.parse(req.query);
|
||||
|
||||
const domain = resolveStatusPageDomainFromRequest(req.hostname, req.query.domain as string | undefined);
|
||||
if (!domain) {
|
||||
throw new AppError({ message: "Domain is required", status: 400 });
|
||||
}
|
||||
|
||||
const statusPage = await this.statusPageService.getStatusPageByCustomDomain(domain);
|
||||
if (!statusPage.isPublished) {
|
||||
throw new AppError({ message: "Status page not found", status: 404 });
|
||||
}
|
||||
|
||||
const data = await this.buildStatusPagePayload(statusPage, req);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page retrieved successfully",
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, type StatusPage } from "@/types/statusPage.js";
|
||||
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, type StatusPage } from "@/domain/status-pages/status-page.type.js";
|
||||
|
||||
/**
|
||||
* When STATUS_PAGE_THEMES_ENABLED is false, the controller drops incoming
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { ITagsService } from "@/service/index.js";
|
||||
import { ITagsService } from "@/domain/tags/tag.service.js";
|
||||
import { requireTeamId } from "./controllerUtils.js";
|
||||
import {
|
||||
createTagBodyValidation,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getTagByIdParamValidation,
|
||||
editTagParamValidation,
|
||||
deleteTagParamValidation,
|
||||
} from "@/validation/index.js";
|
||||
} from "@/api/validation/index.js";
|
||||
|
||||
export interface ITagsController {
|
||||
createTag: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { MulterError } from "multer";
|
||||
import { logger } from "@/utils/logger.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
// Multer throws MulterError, send 413 Payload Too Large / 400
|
||||
const multerErrorStatus = (error: MulterError): number => (error.code === "LIMIT_FILE_SIZE" ? 413 : 400);
|
||||
|
||||
const handleErrors = (error: unknown, req: Request, res: Response, _next: NextFunction) => {
|
||||
const status = error instanceof AppError ? error.status || 500 : 500;
|
||||
const status = error instanceof MulterError ? multerErrorStatus(error) : error instanceof AppError ? error.status || 500 : 500;
|
||||
const message = error instanceof AppError ? error.message : error instanceof Error ? error.message : "Server error";
|
||||
const service = error instanceof AppError ? error.service : "unknownService";
|
||||
const service = error instanceof AppError ? error.service : error instanceof MulterError ? "uploadMiddleware" : "unknownService";
|
||||
const method = error instanceof AppError ? error.method : "unknownMethod";
|
||||
logger.error({
|
||||
message: message,
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
const SERVICE_NAME = "allowedRoles";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import type { UserRole } from "@/types/index.js";
|
||||
import type { UserRole } from "@/domain/users/user.type.js";
|
||||
|
||||
const isAllowed = (allowedRoles: UserRole[]) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,68 @@
|
||||
import type { CorsOptions } from "cors";
|
||||
import type { IStatusPagesRepository } from "@/domain/status-pages/status-page-repository.interface.js";
|
||||
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
|
||||
|
||||
const CUSTOM_DOMAIN_CORS_POSITIVE_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const CUSTOM_DOMAIN_CORS_NEGATIVE_CACHE_TTL_MS = 60 * 1000;
|
||||
|
||||
type CustomDomainCorsCacheEntry = {
|
||||
allowed: boolean;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
const getCustomDomainCorsCacheTtlMs = (allowed: boolean): number =>
|
||||
allowed ? CUSTOM_DOMAIN_CORS_POSITIVE_CACHE_TTL_MS : CUSTOM_DOMAIN_CORS_NEGATIVE_CACHE_TTL_MS;
|
||||
|
||||
export const createStatusPageCorsOrigin = (clientHost: string, statusPagesRepository: IStatusPagesRepository): CorsOptions["origin"] => {
|
||||
const cache = new Map<string, CustomDomainCorsCacheEntry>();
|
||||
|
||||
const cacheOriginResult = (customDomain: string, allowed: boolean) => {
|
||||
cache.set(customDomain, {
|
||||
allowed,
|
||||
expiresAt: Date.now() + getCustomDomainCorsCacheTtlMs(allowed),
|
||||
});
|
||||
};
|
||||
|
||||
return (origin, callback) => {
|
||||
if (!origin) {
|
||||
callback(null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (origin === clientHost) {
|
||||
callback(null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let customDomain: string | null = null;
|
||||
try {
|
||||
customDomain = normalizeStatusPageDomain(new URL(origin).hostname);
|
||||
} catch {
|
||||
callback(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customDomain) {
|
||||
callback(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = cache.get(customDomain);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
callback(null, cached.allowed ? origin : false);
|
||||
return;
|
||||
}
|
||||
|
||||
statusPagesRepository
|
||||
.findByCustomDomain(customDomain)
|
||||
.then((statusPage) => {
|
||||
const allowed = statusPage.isPublished;
|
||||
cacheOriginResult(customDomain, allowed);
|
||||
callback(null, allowed ? origin : false);
|
||||
})
|
||||
.catch(() => {
|
||||
cacheOriginResult(customDomain, false);
|
||||
callback(null, false);
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const PUBLIC_STATUS_PAGE_API_PREFIX = "/api/v1/status-page";
|
||||
|
||||
export const isPublicStatusPageApiPath = (method: string, path: string): boolean => {
|
||||
if (!path.startsWith(PUBLIC_STATUS_PAGE_API_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (method !== "GET" && method !== "OPTIONS") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subPath = path.slice(PUBLIC_STATUS_PAGE_API_PREFIX.length).split("?")[0] ?? "";
|
||||
const slug = subPath.replace(/^\//, "");
|
||||
|
||||
if (slug === "resolve") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!slug || slug === "team" || slug.includes("/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import multer from "multer";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ImageMimeTypes, MAX_IMAGE_SIZE_BYTES } from "@/types/upload.js";
|
||||
|
||||
// Reusable multer instance for handling image uploads
|
||||
const imageUpload = multer({
|
||||
limits: {
|
||||
fileSize: MAX_IMAGE_SIZE_BYTES,
|
||||
files: 1,
|
||||
},
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (!(ImageMimeTypes as readonly string[]).includes(file.mimetype)) {
|
||||
cb(
|
||||
new AppError({
|
||||
status: 415,
|
||||
message: "File must be a valid image (jpeg, jpg, or png)",
|
||||
service: "uploadMiddleware",
|
||||
method: "fileFilter",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
cb(null, true);
|
||||
},
|
||||
});
|
||||
|
||||
export { imageUpload };
|
||||
@@ -2,8 +2,8 @@ import { NextFunction, Request, Response } from "express";
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import type { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import type { User } from "@/types/user.js";
|
||||
import type { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
|
||||
import type { User } from "@/domain/users/user.type.js";
|
||||
|
||||
const SERVICE_NAME = "verifyJWT";
|
||||
const TOKEN_PREFIX = "Bearer ";
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { NextFunction, Request, RequestHandler, Response } from "express";
|
||||
import { IStatusPagesRepository } from "@/repositories/index.js";
|
||||
import { IStatusPagesRepository } from "@/domain/status-pages/status-page-repository.interface.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
export const createVerifyStatusPageAccess = (statusPagesRepository: IStatusPagesRepository, verifyJWT: RequestHandler) => {
|
||||
Executable → Regular
+12
-8
@@ -1,9 +1,7 @@
|
||||
import { Router, RequestHandler } from "express";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import multer from "multer";
|
||||
import { IAuthController } from "@/controllers/authController.js";
|
||||
|
||||
const upload = multer();
|
||||
import { isAllowed } from "@/api/middleware/isAllowed.js";
|
||||
import { imageUpload } from "@/api/middleware/upload.js";
|
||||
import { IAuthController } from "@/api/controllers/authController.js";
|
||||
|
||||
class AuthRoutes {
|
||||
private router: Router;
|
||||
@@ -16,7 +14,7 @@ class AuthRoutes {
|
||||
}
|
||||
|
||||
initRoutes(verifyJWT: RequestHandler) {
|
||||
this.router.post("/register", upload.single("profileImage"), this.authController.registerUser);
|
||||
this.router.post("/register", imageUpload.single("profileImage"), this.authController.registerUser);
|
||||
this.router.post("/login", this.authController.loginUser);
|
||||
|
||||
this.router.post("/recovery/request", this.authController.requestRecovery);
|
||||
@@ -26,13 +24,19 @@ class AuthRoutes {
|
||||
this.router.get("/users/superadmin", this.authController.checkSuperadminExists);
|
||||
|
||||
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.post("/users", verifyJWT, isAllowed(["superadmin"]), imageUpload.single("profileImage"), this.authController.createUser);
|
||||
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"), isAllowed(["admin", "superadmin", "user"]), this.authController.editUser);
|
||||
this.router.patch(
|
||||
"/user",
|
||||
verifyJWT,
|
||||
imageUpload.single("profileImage"),
|
||||
isAllowed(["admin", "superadmin", "user"]),
|
||||
this.authController.editUser
|
||||
);
|
||||
this.router.delete("/user", verifyJWT, isAllowed(["admin", "superadmin", "user"]), this.authController.deleteUser);
|
||||
}
|
||||
|
||||
Executable → Regular
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import { ICheckController } from "@/controllers/checkController.js";
|
||||
import { ICheckController } from "@/api/controllers/checkController.js";
|
||||
|
||||
class CheckRoutes {
|
||||
private router: Router;
|
||||
Executable → Regular
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { RequestHandler, Router } from "express";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import { IDiagnosticController } from "@/controllers/diagnosticController.js";
|
||||
import { IDiagnosticController } from "@/api/controllers/diagnosticController.js";
|
||||
|
||||
class DiagnosticRoutes {
|
||||
private router: Router;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IGeoCheckController } from "@/controllers/geoCheckController.js";
|
||||
import { IGeoCheckController } from "@/api/controllers/geoCheckController.js";
|
||||
import { Router } from "express";
|
||||
|
||||
class GeoCheckRoutes {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import { IIncidentController } from "@/controllers/incidentController.js";
|
||||
import { IIncidentController } from "@/api/controllers/incidentController.js";
|
||||
|
||||
class IncidentRoutes {
|
||||
private router: Router;
|
||||
Executable → Regular
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { RequestHandler, Router } from "express";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import { IInviteController } from "@/controllers/inviteController.js";
|
||||
import { IInviteController } from "@/api/controllers/inviteController.js";
|
||||
|
||||
class InviteRoutes {
|
||||
private router: Router;
|
||||
Executable → Regular
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import { isAllowed } from "@/middleware/isAllowed.js";
|
||||
import { ILogController } from "@/controllers/logController.js";
|
||||
import { isAllowed } from "@/api/middleware/isAllowed.js";
|
||||
import { ILogController } from "@/api/controllers/logController.js";
|
||||
class LogRoutes {
|
||||
private router: Router;
|
||||
private logController: ILogController;
|
||||
Executable → Regular
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { IMaintenanceWindowController } from "@/controllers/maintenanceWindowController.js";
|
||||
import { IMaintenanceWindowController } from "@/api/controllers/maintenanceWindowController.js";
|
||||
import { Router } from "express";
|
||||
|
||||
class MaintenanceWindowRoutes {
|
||||
Executable → Regular
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import { isAllowed } from "@/middleware/isAllowed.js";
|
||||
import { IMonitorController } from "@/controllers/monitorController.js";
|
||||
import { isAllowed } from "@/api/middleware/isAllowed.js";
|
||||
import { IMonitorController } from "@/api/controllers/monitorController.js";
|
||||
|
||||
class MonitorRoutes {
|
||||
private router: Router;
|
||||
Executable → Regular
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { INotificationController } from "@/controllers/notificationController.js";
|
||||
import { INotificationController } from "@/api/controllers/notificationController.js";
|
||||
import { Router } from "express";
|
||||
class NotificationRoutes {
|
||||
private router: Router;
|
||||
Executable → Regular
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import { isAllowed } from "@/middleware/isAllowed.js";
|
||||
import { IJobQueueController } from "@/controllers/queueController.js";
|
||||
import { isAllowed } from "@/api/middleware/isAllowed.js";
|
||||
import { IJobQueueController } from "@/api/controllers/queueController.js";
|
||||
class QueueRoutes {
|
||||
private router: Router;
|
||||
private queueController: IJobQueueController;
|
||||
Executable → Regular
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import { ISettingsController } from "@/controllers/settingsController.js";
|
||||
import { ISettingsController } from "@/api/controllers/settingsController.js";
|
||||
|
||||
class SettingsRoutes {
|
||||
private router: Router;
|
||||
Executable → Regular
+5
-5
@@ -1,7 +1,6 @@
|
||||
import { IStatusPageController } from "@/controllers/statusPageController.js";
|
||||
import { IStatusPageController } from "@/api/controllers/statusPageController.js";
|
||||
import { RequestHandler, Router } from "express";
|
||||
import multer from "multer";
|
||||
const upload = multer();
|
||||
import { imageUpload } from "@/api/middleware/upload.js";
|
||||
|
||||
class StatusPageRoutes {
|
||||
private router: Router;
|
||||
@@ -16,9 +15,10 @@ class StatusPageRoutes {
|
||||
initRoutes(verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) {
|
||||
this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId);
|
||||
|
||||
this.router.post("/", upload.single("logo"), verifyJWT, this.statusPageController.createStatusPage);
|
||||
this.router.put("/:id", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage);
|
||||
this.router.post("/", imageUpload.single("logo"), verifyJWT, this.statusPageController.createStatusPage);
|
||||
this.router.put("/:id", imageUpload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage);
|
||||
|
||||
this.router.get("/resolve", this.statusPageController.resolveStatusPageByDomain);
|
||||
this.router.get("/:url", verifyStatusPageAccess, this.statusPageController.getStatusPageByUrl);
|
||||
this.router.delete("/:id", verifyJWT, this.statusPageController.deleteStatusPage);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ITagsController } from "@/controllers/tagController.js";
|
||||
import { ITagsController } from "@/api/controllers/tagController.js";
|
||||
import { Router } from "express";
|
||||
class TagRoutes {
|
||||
private router: Router;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { passwordPattern, nameValidation, lowercaseEmailValidation } from "./shared.js";
|
||||
import { UserRoles } from "@/types/user.js";
|
||||
import { UserRoles } from "@/domain/users/user.type.js";
|
||||
|
||||
//****************************************
|
||||
// Auth Validations
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
import { MonitorTypes } from "@/types/monitor.js";
|
||||
import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js";
|
||||
import { MonitorTypes } from "@/domain/monitors/monitor.types.js";
|
||||
|
||||
//****************************************
|
||||
// Check Validations
|
||||
File renamed without changes.
@@ -9,7 +9,7 @@
|
||||
export * from "./shared.js";
|
||||
|
||||
// Environment validation
|
||||
export * from "./envValidation.js";
|
||||
export * from "@/config/envValidation.js";
|
||||
|
||||
// Domain-specific validations
|
||||
export * from "./authValidation.js";
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
import { DurationUnits } from "@/types/maintenanceWindow.js";
|
||||
import { DurationUnits } from "@/domain/maintenance-windows/maintenance-window.type.js";
|
||||
|
||||
const dateToString = z.coerce.date().transform((d) => d.toISOString());
|
||||
|
||||
+16
-2
@@ -1,7 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion, dnsHostnameRegex, dnsServerValidation } from "./shared.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
import { DnsRecordTypes, MonitorMatchMethods, MonitorStatuses, MonitorTypes, PageSpeedStrategies } from "@/types/monitor.js";
|
||||
import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js";
|
||||
import {
|
||||
DnsRecordTypes,
|
||||
HttpStatusCodeSet,
|
||||
MonitorMatchMethods,
|
||||
MonitorStatuses,
|
||||
MonitorTypes,
|
||||
PageSpeedStrategies,
|
||||
} from "@/domain/monitors/monitor.types.js";
|
||||
|
||||
const httpStatusCode = z.number().refine((code) => HttpStatusCodeSet.has(code), { message: "Must be a valid HTTP status code" });
|
||||
|
||||
export const getMonitorByIdParamValidation = z.object({
|
||||
monitorId: z.string().min(1, "Monitor ID is required"),
|
||||
@@ -81,6 +90,7 @@ export const createMonitorBodyValidation = z
|
||||
tempAlertThreshold: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
customUpCodes: z.array(httpStatusCode).default([]),
|
||||
secret: z.string().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
@@ -110,6 +120,7 @@ export const editMonitorBodyValidation = z
|
||||
interval: z.number().optional(),
|
||||
notifications: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
customUpCodes: z.array(httpStatusCode).optional(),
|
||||
secret: z.string().optional(),
|
||||
ignoreTlsErrors: z.boolean().optional(),
|
||||
useAdvancedMatching: z.boolean().optional(),
|
||||
@@ -180,6 +191,7 @@ const importedMonitorSchema = z
|
||||
uptimePercentage: z.number().optional(),
|
||||
notifications: z.array(z.string()).default([]),
|
||||
tags: z.array(z.string()).default([]),
|
||||
customUpCodes: z.array(httpStatusCode).default([]),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number().default(100),
|
||||
cpuAlertCounter: z.number().default(5),
|
||||
@@ -241,6 +253,7 @@ export const monitorResponseSchema = z
|
||||
matchMethod: z.enum(MonitorMatchMethods).optional(),
|
||||
notifications: z.array(z.string()),
|
||||
tags: z.array(z.string()),
|
||||
customUpCodes: z.array(httpStatusCode).optional(),
|
||||
secret: z.string().optional(),
|
||||
cpuAlertThreshold: z.number(),
|
||||
memoryAlertThreshold: z.number(),
|
||||
@@ -259,5 +272,6 @@ export const monitorResponseSchema = z
|
||||
userId: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
lastEvaluatedAt: z.number(),
|
||||
})
|
||||
.passthrough();
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CHECK_TTL_SENTINEL } from "@/types/check.js";
|
||||
import { CHECK_TTL_SENTINEL } from "@/domain/checks/check.type.js";
|
||||
import { z } from "zod";
|
||||
|
||||
//****************************************
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { type UserRole } from "@/types/user.js";
|
||||
import { type UserRole } from "@/domain/users/user.type.js";
|
||||
|
||||
export const passwordPattern =
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/]+$/;
|
||||
+18
-2
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes } from "@/types/statusPage.js";
|
||||
import { booleanCoercion, dnsHostnameRegex } from "./shared.js";
|
||||
import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes } from "@/domain/status-pages/status-page.type.js";
|
||||
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
|
||||
|
||||
//****************************************
|
||||
// Status Page Validations
|
||||
@@ -15,6 +16,20 @@ export const getStatusPageQueryValidation = z.object({
|
||||
timeFrame: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
export const resolveStatusPageQueryValidation = getStatusPageQueryValidation.extend({
|
||||
domain: z.string().optional(),
|
||||
});
|
||||
|
||||
const customDomainValidation = z.preprocess(
|
||||
(val) => {
|
||||
if (val === undefined || val === null) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeStatusPageDomain(String(val));
|
||||
},
|
||||
z.union([z.string().regex(dnsHostnameRegex, "Enter a valid domain name (e.g. status.example.com)"), z.null()]).optional()
|
||||
);
|
||||
|
||||
export const createStatusPageBodyValidation = z
|
||||
.object({
|
||||
type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])),
|
||||
@@ -22,6 +37,7 @@ export const createStatusPageBodyValidation = z
|
||||
url: z.string().regex(/^[a-zA-Z0-9_-]+$/, {
|
||||
message: "URL can only contain letters, numbers, underscores, and hyphens",
|
||||
}),
|
||||
customDomain: customDomainValidation,
|
||||
timezone: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
monitors: z.array(z.string().regex(/^[0-9a-fA-F]{24}$/, "Must be a valid monitor ID")).min(1, "At least one monitor is required"),
|
||||
File renamed without changes.
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { UserRoles } from "@/types/user.js";
|
||||
import { UserRoles } from "@/domain/users/user.type.js";
|
||||
import { nameValidation, lowercaseEmailValidation, passwordValidation } from "./shared.js";
|
||||
|
||||
//****************************************
|
||||
+21
-13
@@ -1,17 +1,19 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import cors from "cors";
|
||||
import cors, { type CorsOptions } from "cors";
|
||||
import helmet from "helmet";
|
||||
import compression from "compression";
|
||||
import cookieParser from "cookie-parser";
|
||||
import swaggerUi, { type JsonObject } from "swagger-ui-express";
|
||||
import { handleErrors } from "@/middleware/handleErrors.js";
|
||||
import { generalApiLimiter } from "@/middleware/rateLimiter.js";
|
||||
import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js";
|
||||
import { handleErrors } from "@/api/middleware/handleErrors.js";
|
||||
import { generalApiLimiter } from "@/api/middleware/rateLimiter.js";
|
||||
import { sanitizeBody, sanitizeQuery } from "@/api/middleware/sanitization.js";
|
||||
import { setupRoutes } from "@/config/routes.js";
|
||||
import { InitializedServices } from "@/config/services.js";
|
||||
import { InitializedControllers } from "@/config/controllers.js";
|
||||
import { EnvConfig } from "@/service/system/settingsService.js";
|
||||
import { EnvConfig } from "@/domain/app-settings/app-settings.service.js";
|
||||
import { createStatusPageCorsOrigin } from "@/api/middleware/statusPageCorsOrigin.js";
|
||||
import { isPublicStatusPageApiPath } from "@/api/middleware/statusPagePublicApiPath.js";
|
||||
|
||||
export const createApp = ({
|
||||
services,
|
||||
@@ -28,17 +30,23 @@ export const createApp = ({
|
||||
}) => {
|
||||
const allowedOrigin = envSettings.clientHost;
|
||||
const app = express();
|
||||
const defaultCorsOptions: CorsOptions = {
|
||||
origin: allowedOrigin,
|
||||
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
|
||||
allowedHeaders: ["Content-Type", "Authorization", "Accept-Language"],
|
||||
credentials: true,
|
||||
};
|
||||
const publicStatusPageCorsOrigin = createStatusPageCorsOrigin(allowedOrigin, services.statusPagesRepository);
|
||||
|
||||
app.use(generalApiLimiter);
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: allowedOrigin,
|
||||
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
|
||||
allowedHeaders: ["Content-Type", "Authorization", "Accept-Language"],
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
app.use((req, res, next) => {
|
||||
const corsOptions = isPublicStatusPageApiPath(req.method, req.path)
|
||||
? { ...defaultCorsOptions, origin: publicStatusPageCorsOrigin }
|
||||
: defaultCorsOptions;
|
||||
|
||||
return cors(corsOptions)(req, res, next);
|
||||
});
|
||||
|
||||
app.use(express.static(frontendPath));
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import MonitorController from "../controllers/monitorController.js";
|
||||
import AuthController from "../controllers/authController.js";
|
||||
import SettingsController from "../controllers/settingsController.js";
|
||||
import CheckController from "../controllers/checkController.js";
|
||||
import GeoCheckController from "../controllers/geoCheckController.js";
|
||||
import InviteController from "../controllers/inviteController.js";
|
||||
import MaintenanceWindowController from "../controllers/maintenanceWindowController.js";
|
||||
import QueueController from "../controllers/queueController.js";
|
||||
import LogController from "../controllers/logController.js";
|
||||
import StatusPageController from "../controllers/statusPageController.js";
|
||||
import NotificationController from "../controllers/notificationController.js";
|
||||
import TagsController from "../controllers/tagController.js";
|
||||
import DiagnosticController from "../controllers/diagnosticController.js";
|
||||
import IncidentController from "../controllers/incidentController.js";
|
||||
import MonitorController from "../api/controllers/monitorController.js";
|
||||
import AuthController from "../api/controllers/authController.js";
|
||||
import SettingsController from "../api/controllers/settingsController.js";
|
||||
import CheckController from "../api/controllers/checkController.js";
|
||||
import GeoCheckController from "../api/controllers/geoCheckController.js";
|
||||
import InviteController from "../api/controllers/inviteController.js";
|
||||
import MaintenanceWindowController from "../api/controllers/maintenanceWindowController.js";
|
||||
import QueueController from "../api/controllers/queueController.js";
|
||||
import LogController from "../api/controllers/logController.js";
|
||||
import StatusPageController from "../api/controllers/statusPageController.js";
|
||||
import NotificationController from "../api/controllers/notificationController.js";
|
||||
import TagsController from "../api/controllers/tagController.js";
|
||||
import DiagnosticController from "../api/controllers/diagnosticController.js";
|
||||
import IncidentController from "../api/controllers/incidentController.js";
|
||||
import type { InitializedServices } from "@/config/services.js";
|
||||
|
||||
export interface InitializedControllers {
|
||||
@@ -39,7 +39,7 @@ export const initializeControllers = (services: InitializedServices): Initialize
|
||||
geoCheckController: new GeoCheckController(services.geoChecksService),
|
||||
inviteController: new InviteController(services.inviteService),
|
||||
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
|
||||
queueController: new QueueController(services.jobQueue),
|
||||
queueController: new QueueController(services.worker),
|
||||
logController: new LogController(services.logger),
|
||||
statusPageController: new StatusPageController(services.statusPageService, services.monitorsRepository, services.settingsService),
|
||||
notificationController: new NotificationController(services.notificationsService, services.monitorsRepository),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { DbTypes, QueueTypes, QueueModes } from "@/types/settings.js";
|
||||
import { booleanCoercion } from "./shared.js";
|
||||
import { DbTypes, QueueModes } from "@/domain/app-settings/app-settings.type.js";
|
||||
import { booleanCoercion } from "@/api/validation/shared.js";
|
||||
|
||||
const envSchema = z.object({
|
||||
// Server Configuration
|
||||
@@ -12,8 +12,8 @@ const envSchema = z.object({
|
||||
DB_CONNECTION_STRING: z.string().min(1, "Database connection string is required"),
|
||||
DB_TYPE: z.enum(DbTypes).default("mongodb"),
|
||||
|
||||
QUEUE_TYPE: z.enum(QueueTypes).default("superSimpleQueue"),
|
||||
QUEUE_MODE: z.enum(QueueModes).default("primary"),
|
||||
QUEUE_PRIMARY_PROCESSES: booleanCoercion.default(true),
|
||||
|
||||
// JWT Authentication
|
||||
JWT_SECRET: z.string().min(1, "JWT_SECRET is required"),
|
||||
+17
-17
@@ -1,25 +1,25 @@
|
||||
import type { Application } from "express";
|
||||
import { createVerifyJWT } from "../middleware/verifyJWT.js";
|
||||
import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js";
|
||||
import { authApiLimiter } from "../middleware/rateLimiter.js";
|
||||
import { createVerifyJWT } from "../api/middleware/verifyJWT.js";
|
||||
import { createVerifyStatusPageAccess } from "../api/middleware/verifyStatusPageAccess.js";
|
||||
import { authApiLimiter } from "../api/middleware/rateLimiter.js";
|
||||
import type { InitializedControllers } from "./controllers.js";
|
||||
import type { InitializedServices } from "./services.js";
|
||||
|
||||
import AuthRoutes from "../routes/authRoute.js";
|
||||
import InviteRoutes from "../routes/inviteRoute.js";
|
||||
import MonitorRoutes from "../routes/monitorRoute.js";
|
||||
import CheckRoutes from "../routes/checkRoute.js";
|
||||
import GeoCheckRoutes from "../routes/geoCheckRoutes.js";
|
||||
import SettingsRoutes from "../routes/settingsRoute.js";
|
||||
import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js";
|
||||
import StatusPageRoutes from "../routes/statusPageRoute.js";
|
||||
import QueueRoutes from "../routes/queueRoute.js";
|
||||
import LogRoutes from "../routes/logRoutes.js";
|
||||
import DiagnosticRoutes from "../routes/diagnosticRoute.js";
|
||||
import NotificationRoutes from "../routes/notificationRoute.js";
|
||||
import TagRoutes from "../routes/tagRoutes.js";
|
||||
import AuthRoutes from "../api/routes/authRoute.js";
|
||||
import InviteRoutes from "../api/routes/inviteRoute.js";
|
||||
import MonitorRoutes from "../api/routes/monitorRoute.js";
|
||||
import CheckRoutes from "../api/routes/checkRoute.js";
|
||||
import GeoCheckRoutes from "../api/routes/geoCheckRoutes.js";
|
||||
import SettingsRoutes from "../api/routes/settingsRoute.js";
|
||||
import MaintenanceWindowRoutes from "../api/routes/maintenanceWindowRoute.js";
|
||||
import StatusPageRoutes from "../api/routes/statusPageRoute.js";
|
||||
import QueueRoutes from "../api/routes/queueRoute.js";
|
||||
import LogRoutes from "../api/routes/logRoutes.js";
|
||||
import DiagnosticRoutes from "../api/routes/diagnosticRoute.js";
|
||||
import NotificationRoutes from "../api/routes/notificationRoute.js";
|
||||
import TagRoutes from "../api/routes/tagRoutes.js";
|
||||
|
||||
import IncidentRoutes from "../routes/incidentRoute.js";
|
||||
import IncidentRoutes from "../api/routes/incidentRoute.js";
|
||||
|
||||
export const setupRoutes = (app: Application, controllers: InitializedControllers, services: InitializedServices) => {
|
||||
const verifyJWT = createVerifyJWT(services.settingsService);
|
||||
|
||||
+130
-187
@@ -1,75 +1,53 @@
|
||||
import { Mongoose } from "mongoose";
|
||||
import MongoDB from "../db/MongoDB.js";
|
||||
import { IDb } from "@/db/IDb.js";
|
||||
import {
|
||||
// Service classes
|
||||
NetworkService,
|
||||
EmailService,
|
||||
BufferService,
|
||||
GlobalPingService,
|
||||
LessSimpleQueue,
|
||||
SuperSimpleQueue,
|
||||
QueueHelper,
|
||||
NotificationsService,
|
||||
TagsService,
|
||||
StatusService,
|
||||
NotificationMessageBuilder,
|
||||
MonitorService,
|
||||
StatusPageService,
|
||||
UserService,
|
||||
CheckService,
|
||||
GeoChecksService,
|
||||
DiagnosticService,
|
||||
InviteService,
|
||||
MaintenanceWindowService,
|
||||
IncidentService,
|
||||
// Notification providers
|
||||
WebhookProvider,
|
||||
SlackProvider,
|
||||
EmailProvider,
|
||||
DiscordProvider,
|
||||
PagerDutyProvider,
|
||||
MatrixProvider,
|
||||
TeamsProvider,
|
||||
TelegramProvider,
|
||||
PushoverProvider,
|
||||
TwilioProvider,
|
||||
NtfyProvider,
|
||||
// Interfaces
|
||||
INetworkService,
|
||||
IEmailService,
|
||||
IBufferService,
|
||||
IJobQueue,
|
||||
INotificationsService,
|
||||
ITagsService,
|
||||
IStatusService,
|
||||
IMonitorService,
|
||||
IUserService,
|
||||
ICheckService,
|
||||
IGeoChecksService,
|
||||
IDiagnosticService,
|
||||
IInviteService,
|
||||
IMaintenanceWindowService,
|
||||
IStatusPageService,
|
||||
IIncidentService,
|
||||
INotificationMessageBuilder,
|
||||
ISettingsService,
|
||||
SettingsService,
|
||||
EnvConfig,
|
||||
} from "@/service/index.js";
|
||||
import { hostname } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import MongoDB from "../db/db.mongo.js";
|
||||
import { IDb } from "@/db/db.interface.js";
|
||||
import { EnvConfig, ISettingsService, SettingsService } from "@/domain/app-settings/app-settings.service.js";
|
||||
import { CheckService, ICheckService } from "@/domain/checks/check.service.js";
|
||||
import { DiagnosticService, IDiagnosticService } from "@/domain/diagnostics/diagnostic.service.js";
|
||||
import { GeoChecksService, IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js";
|
||||
import { IIncidentService, IncidentService } from "@/domain/incidents/incident.service.js";
|
||||
import { IInviteService, InviteService } from "@/domain/invites/invite.service.js";
|
||||
import { IMaintenanceWindowService, MaintenanceWindowService } from "@/domain/maintenance-windows/maintenance-window.service.js";
|
||||
import { IMonitorService, MonitorService } from "@/domain/monitors/monitor.service.js";
|
||||
import { INotificationMessageBuilder, NotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js";
|
||||
import { INotificationsService, NotificationsService } from "@/domain/notifications/notification.service.js";
|
||||
import { DiscordProvider } from "@/domain/notifications/providers/discord.js";
|
||||
import { EmailProvider } from "@/domain/notifications/providers/email.js";
|
||||
import { MatrixProvider } from "@/domain/notifications/providers/matrix.js";
|
||||
import { NtfyProvider } from "@/domain/notifications/providers/ntfy.js";
|
||||
import { PagerDutyProvider } from "@/domain/notifications/providers/pagerduty.js";
|
||||
import { PushoverProvider } from "@/domain/notifications/providers/pushover.js";
|
||||
import { SlackProvider } from "@/domain/notifications/providers/slack.js";
|
||||
import { TeamsProvider } from "@/domain/notifications/providers/teams.js";
|
||||
import { TelegramProvider } from "@/domain/notifications/providers/telegram.js";
|
||||
import { TwilioProvider } from "@/domain/notifications/providers/twilio.js";
|
||||
import { WebhookProvider } from "@/domain/notifications/providers/webhook.js";
|
||||
import { IStatusPageService, StatusPageService } from "@/domain/status-pages/status-page.service.js";
|
||||
import { ITagsService, TagsService } from "@/domain/tags/tag.service.js";
|
||||
import { IUserService, UserService } from "@/domain/users/user.service.js";
|
||||
import { BufferService, IBufferService } from "@/service/bufferService.js";
|
||||
import { EmailService, IEmailService } from "@/service/emailService.js";
|
||||
import { GlobalPingService } from "@/service/globalPingService.js";
|
||||
import { WorkerHelper } from "@/worker/worker.helper.js";
|
||||
import { IWorker } from "@/worker/worker.interface.js";
|
||||
import { INetworkService, NetworkService } from "@/service/networkService.js";
|
||||
import { IStatusService, StatusService } from "@/service/statusService.js";
|
||||
import { MonitorStatusPolicy } from "@/worker/worker.monitor-status-policy.js";
|
||||
|
||||
// Network providers
|
||||
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
|
||||
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
|
||||
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
|
||||
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
|
||||
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
|
||||
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
|
||||
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";
|
||||
import { DNSProvider } from "@/service/infrastructure/network/DNSProvider.js";
|
||||
import { PingProvider } from "@/service/network/PingProvider.js";
|
||||
import { HttpProvider } from "@/service/network/HttpProvider.js";
|
||||
import { AdvancedMatcher } from "@/service/network/AdvancedMatcher.js";
|
||||
import { PageSpeedProvider } from "@/service/network/PageSpeedProvider.js";
|
||||
import { HardwareProvider } from "@/service/network/HardwareProvider.js";
|
||||
import { DockerProvider } from "@/service/network/DockerProvider.js";
|
||||
import { PortProvider } from "@/service/network/PortProvider.js";
|
||||
import { GameProvider } from "@/service/network/GameProvider.js";
|
||||
import { GrpcProvider } from "@/service/network/GrpcProvider.js";
|
||||
import { WebSocketProvider } from "@/service/network/WebSocketProvider.js";
|
||||
import { DNSProvider } from "@/service/network/DNSProvider.js";
|
||||
|
||||
// Third-party
|
||||
import { Resolver } from "dns/promises";
|
||||
@@ -93,40 +71,44 @@ import * as protoLoader from "@grpc/proto-loader";
|
||||
import WebSocket from "ws";
|
||||
|
||||
// Repositories
|
||||
import {
|
||||
MongoMonitorsRepository,
|
||||
MongoChecksRepository,
|
||||
MongoGeoChecksRepository,
|
||||
MongoMonitorStatsRepository,
|
||||
MongoStatusPagesRepository,
|
||||
MongoUsersRepository,
|
||||
MongoInvitesRepository,
|
||||
MongoRecoveryTokensRepository,
|
||||
MongoNotificationsRepository,
|
||||
MongoTagsRepository,
|
||||
MongoIncidentsRepository,
|
||||
MongoTeamsRepository,
|
||||
MongoMaintenanceWindowsRepository,
|
||||
MongoSettingsRepository,
|
||||
MongoQueueWorkersRepository,
|
||||
IMonitorsRepository,
|
||||
IChecksRepository,
|
||||
IGeoChecksRepository,
|
||||
IMonitorStatsRepository,
|
||||
IStatusPagesRepository,
|
||||
IUsersRepository,
|
||||
IInvitesRepository,
|
||||
IRecoveryTokensRepository,
|
||||
ISettingsRepository,
|
||||
INotificationsRepository,
|
||||
ITagsRepository,
|
||||
IIncidentsRepository,
|
||||
ITeamsRepository,
|
||||
IMaintenanceWindowsRepository,
|
||||
} from "@/repositories/index.js";
|
||||
import { ISettingsRepository } from "@/domain/app-settings/app-settings-repository.interface.js";
|
||||
import MongoSettingsRepository from "@/domain/app-settings/app-settings.repository.mongo.js";
|
||||
import { IChecksRepository } from "@/domain/checks/check.repository.interface.js";
|
||||
import MongoChecksRepository from "@/domain/checks/check.repository.mongo.js";
|
||||
import { IGeoChecksRepository } from "@/domain/geo-checks/geo-check.repository.interface.js";
|
||||
import MongoGeoChecksRepository from "@/domain/geo-checks/geo-check.repository.mongo.js";
|
||||
import { IIncidentsRepository } from "@/domain/incidents/incident.repository.interface.js";
|
||||
import MongoIncidentsRepository from "@/domain/incidents/incident.repository.mongo.js";
|
||||
import { IInvitesRepository } from "@/domain/invites/invite.repository.interface.js";
|
||||
import MongoInvitesRepository from "@/domain/invites/invite.repository.mongo.js";
|
||||
import { IMaintenanceWindowsRepository } from "@/domain/maintenance-windows/maintenance-window.repository.interface.js";
|
||||
import MongoMaintenanceWindowsRepository from "@/domain/maintenance-windows/maintenance-window.repository.mongo.js";
|
||||
import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.repository.interface.js";
|
||||
import MongoMonitorStatsRepository from "@/domain/monitor-stats/monitor-stats.repository.mongo.js";
|
||||
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
|
||||
import MongoMonitorsRepository from "@/domain/monitors/monitor.repository.mongo.js";
|
||||
import { INotificationsRepository } from "@/domain/notifications/notification.repository.interface.js";
|
||||
import MongoNotificationsRepository from "@/domain/notifications/notification.repository.mongo.js";
|
||||
import { IRecoveryTokensRepository } from "@/domain/recovery-tokens/recovery-token.repository.interface.js";
|
||||
import MongoRecoveryTokensRepository from "@/domain/recovery-tokens/recovery-token.repository.mongo.js";
|
||||
import { IStatusPagesRepository } from "@/domain/status-pages/status-page-repository.interface.js";
|
||||
import MongoStatusPagesRepository from "@/domain/status-pages/status-page-repository.mongo.js";
|
||||
import { ITagsRepository } from "@/domain/tags/tag.repository.interface.js";
|
||||
import MongoTagsRepository from "@/domain/tags/tag.repository.mongo.js";
|
||||
import { ITeamsRepository } from "@/domain/teams/team.repository.interface.js";
|
||||
import MongoTeamsRepository from "@/domain/teams/team.repository.model.js";
|
||||
import { IUsersRepository } from "@/domain/users/user.repository.interface.js";
|
||||
import MongoUsersRepository from "@/domain/users/user.repository.mongo.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { type QueueMode } from "@/types/settings.js";
|
||||
import { NotificationReactor } from "@/worker/reactors/reactor.notification.js";
|
||||
import { ReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js";
|
||||
import { IncidentReactor } from "@/worker/reactors/reactor.incident.js";
|
||||
import { CheckPipeline, GeoChecksPipeline } from "@/worker/worker.check-pipeline.js";
|
||||
import { CheckProducer } from "@/worker/worker.check-producer.js";
|
||||
import { CheckEvaluator } from "@/worker/worker.check-evaluator.js";
|
||||
import { DBQueueWorker } from "@/worker/worker.db-queue.js";
|
||||
import MongoJobsRepository from "@/domain/jobs/job.repository.mongo.js";
|
||||
import MongoQueueWorkersRepository from "@/domain/queue-workers/queue-worker.repository.mongo.js";
|
||||
|
||||
export type InitializedServices = {
|
||||
settingsService: ISettingsService;
|
||||
@@ -135,7 +117,7 @@ export type InitializedServices = {
|
||||
emailService: IEmailService;
|
||||
bufferService: IBufferService;
|
||||
statusService: IStatusService;
|
||||
jobQueue: IJobQueue;
|
||||
worker: IWorker;
|
||||
userService: IUserService;
|
||||
checkService: ICheckService;
|
||||
geoChecksService: IGeoChecksService;
|
||||
@@ -171,12 +153,10 @@ export const initializeServices = async ({
|
||||
logger,
|
||||
envSettings,
|
||||
settingsService,
|
||||
queueMode = "primary",
|
||||
}: {
|
||||
logger: ILogger;
|
||||
envSettings: EnvConfig;
|
||||
settingsService: ISettingsService;
|
||||
queueMode?: QueueMode;
|
||||
}): Promise<InitializedServices> => {
|
||||
// Create DB
|
||||
|
||||
@@ -184,58 +164,6 @@ export const initializeServices = async ({
|
||||
db = new MongoDB(logger, envSettings);
|
||||
await db.connect();
|
||||
|
||||
// ** NOTE **
|
||||
// const dbType = envSettings.dbType;
|
||||
// DB type has been fixed to MongoDB for now
|
||||
|
||||
// if (dbType === "mongodb") {
|
||||
// db = new MongoDB(logger, envSettings);
|
||||
// }
|
||||
|
||||
// if (!db) {
|
||||
// throw new AppError({ message: "Unsupported database type", status: 500 });
|
||||
// }
|
||||
|
||||
// await db.connect();
|
||||
|
||||
// Repositories
|
||||
|
||||
// ** NOTE **
|
||||
// DB Type fixed to MongoDB for now
|
||||
// let monitorsRepository: IMonitorsRepository;
|
||||
// let checksRepository: IChecksRepository;
|
||||
// let geoChecksRepository: IGeoChecksRepository;
|
||||
// let monitorStatsRepository: IMonitorStatsRepository;
|
||||
// let statusPagesRepository: IStatusPagesRepository;
|
||||
// let usersRepository: IUsersRepository;
|
||||
// let invitesRepository: IInvitesRepository;
|
||||
// let recoveryTokensRepository: IRecoveryTokensRepository;
|
||||
// let settingsRepository: ISettingsRepository;
|
||||
// let notificationsRepository: INotificationsRepository;
|
||||
// let tagsRepository: ITagsRepository;
|
||||
// let incidentsRepository: IIncidentsRepository;
|
||||
// let teamsRepository: ITeamsRepository;
|
||||
// let maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
|
||||
// if (dbType === "mongodb") {
|
||||
// monitorsRepository = new MongoMonitorsRepository();
|
||||
// checksRepository = new MongoChecksRepository(logger);
|
||||
// geoChecksRepository = new MongoGeoChecksRepository(logger);
|
||||
// monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
// statusPagesRepository = new MongoStatusPagesRepository();
|
||||
// usersRepository = new MongoUsersRepository();
|
||||
// invitesRepository = new MongoInvitesRepository();
|
||||
// recoveryTokensRepository = new MongoRecoveryTokensRepository();
|
||||
// settingsRepository = new MongoSettingsRepository();
|
||||
// notificationsRepository = new MongoNotificationsRepository();
|
||||
// tagsRepository = new MongoTagsRepository();
|
||||
// incidentsRepository = new MongoIncidentsRepository();
|
||||
// teamsRepository = new MongoTeamsRepository();
|
||||
// maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||
// } else {
|
||||
// throw new AppError({ message: "Unsupported database type", status: 500 });
|
||||
// }
|
||||
|
||||
const monitorsRepository = new MongoMonitorsRepository();
|
||||
const checksRepository = new MongoChecksRepository(logger);
|
||||
const geoChecksRepository = new MongoGeoChecksRepository(logger);
|
||||
@@ -250,6 +178,9 @@ export const initializeServices = async ({
|
||||
const incidentsRepository = new MongoIncidentsRepository();
|
||||
const teamsRepository = new MongoTeamsRepository();
|
||||
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||
// One identity per process
|
||||
const workerId = `${hostname()}:${process.pid}:${randomUUID()}`;
|
||||
const jobsRepository = new MongoJobsRepository(workerId);
|
||||
const queueWorkersRepository = new MongoQueueWorkersRepository();
|
||||
|
||||
// Inject settings repository into settings service (now that DB is connected)
|
||||
@@ -296,7 +227,7 @@ export const initializeServices = async ({
|
||||
monitorsRepository,
|
||||
});
|
||||
|
||||
const bufferService = new BufferService(logger, checkService, geoChecksService, settingsService);
|
||||
const bufferService = new BufferService(logger, checkService, geoChecksService, settingsService, jobsRepository);
|
||||
|
||||
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
||||
|
||||
@@ -334,42 +265,53 @@ export const initializeServices = async ({
|
||||
|
||||
const tagsService = new TagsService(tagsRepository, monitorsRepository);
|
||||
|
||||
const queueHelper = new QueueHelper(
|
||||
const monitorStatusPolicy = new MonitorStatusPolicy();
|
||||
// reactors and dispatcher
|
||||
const notificationReactor = new NotificationReactor(notificationsService);
|
||||
const incidentReactor = new IncidentReactor(incidentService);
|
||||
const reactorDispatcher = new ReactorDispatcher(logger, [notificationReactor, incidentReactor]);
|
||||
|
||||
// Check producer/evaluator
|
||||
const checkProducer = new CheckProducer(monitorsRepository, maintenanceWindowsRepository, checkService, networkService, bufferService, logger);
|
||||
const checkEvaluator = new CheckEvaluator(statusService, monitorStatusPolicy);
|
||||
|
||||
// pipelines
|
||||
const checkPipeline = new CheckPipeline(checkProducer, checkEvaluator);
|
||||
|
||||
const geoCheckPipeline = new GeoChecksPipeline(maintenanceWindowsRepository, geoChecksService, bufferService, logger);
|
||||
|
||||
const workerHelper = new WorkerHelper(
|
||||
logger,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationsService,
|
||||
checkService,
|
||||
settingsService,
|
||||
bufferService,
|
||||
incidentService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
jobsRepository,
|
||||
teamsRepository,
|
||||
monitorStatsRepository,
|
||||
checksRepository,
|
||||
incidentsRepository,
|
||||
geoChecksService,
|
||||
geoChecksRepository
|
||||
geoChecksRepository,
|
||||
reactorDispatcher,
|
||||
checkPipeline,
|
||||
geoCheckPipeline
|
||||
);
|
||||
|
||||
if (queueMode === "worker" && envSettings.queueType !== "lessSimpleQueue") {
|
||||
throw new AppError({
|
||||
message: `Worker mode requires QUEUE_TYPE="lessSimpleQueue" got ${envSettings.queueType}`,
|
||||
status: 500,
|
||||
service: "config services",
|
||||
method: "initializeServices",
|
||||
});
|
||||
}
|
||||
|
||||
let jobQueue: IJobQueue;
|
||||
switch (envSettings.queueType) {
|
||||
case "lessSimpleQueue":
|
||||
jobQueue = await LessSimpleQueue.create(logger, queueHelper, monitorsRepository, queueWorkersRepository, envSettings, queueMode);
|
||||
break;
|
||||
default:
|
||||
jobQueue = await SuperSimpleQueue.create(logger, queueHelper, monitorsRepository);
|
||||
}
|
||||
const worker = await DBQueueWorker.create(
|
||||
logger,
|
||||
jobsRepository,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
checkService,
|
||||
checkProducer,
|
||||
checkEvaluator,
|
||||
geoCheckPipeline,
|
||||
reactorDispatcher,
|
||||
workerHelper,
|
||||
queueWorkersRepository,
|
||||
envSettings.queueMode,
|
||||
envSettings.queuePrimaryProcesses,
|
||||
workerId
|
||||
);
|
||||
|
||||
// Business services
|
||||
const userService = new UserService({
|
||||
@@ -378,7 +320,7 @@ export const initializeServices = async ({
|
||||
settingsService,
|
||||
logger,
|
||||
jwt,
|
||||
jobQueue,
|
||||
worker,
|
||||
monitorsRepository,
|
||||
usersRepository,
|
||||
invitesRepository,
|
||||
@@ -396,9 +338,10 @@ export const initializeServices = async ({
|
||||
const maintenanceWindowService = new MaintenanceWindowService({
|
||||
monitorsRepository,
|
||||
maintenanceWindowsRepository,
|
||||
jobsRepository,
|
||||
});
|
||||
const monitorService = new MonitorService({
|
||||
jobQueue,
|
||||
worker,
|
||||
logger,
|
||||
games,
|
||||
monitorsRepository,
|
||||
@@ -418,7 +361,7 @@ export const initializeServices = async ({
|
||||
emailService,
|
||||
bufferService,
|
||||
statusService,
|
||||
jobQueue,
|
||||
worker,
|
||||
userService,
|
||||
checkService,
|
||||
geoChecksService,
|
||||
|
||||
File renamed without changes.
Loaded 100 of 326 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user