From 91c2f7f0d5106bdfd4a0ff2c14b7e44acc3baee6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 8 May 2025 13:11:51 -0700 Subject: [PATCH 01/38] Remove DB settings from settings service --- server/controllers/settingsController.js | 1 + server/db/models/AppSettings.js | 45 ++--------------------- server/db/mongo/MongoDB.js | 6 +-- server/db/mongo/modules/settingsModule.js | 3 +- server/index.js | 16 ++++---- server/service/settingsService.js | 39 +++----------------- 6 files changed, 24 insertions(+), 86 deletions(-) diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index 54643dfbb..561ea8266 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -32,6 +32,7 @@ class SettingsController { } try { + console.log(req.body); await this.db.updateAppSettings(req.body); const updatedSettings = { ...(await this.settingsService.reloadSettings()) }; delete updatedSettings.jwtSecret; diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js index 6415b229d..3942305e8 100755 --- a/server/db/models/AppSettings.js +++ b/server/db/models/AppSettings.js @@ -2,44 +2,9 @@ import mongoose from "mongoose"; const AppSettingsSchema = mongoose.Schema( { - apiBaseUrl: { + language: { type: String, - required: true, - default: "http://localhost:5000/api/v1", - }, - logLevel: { - type: String, - default: "debug", - enum: ["debug", "none", "error", "warn"], - }, - clientHost: { - type: String, - required: true, - default: "http://localhost:5173", - }, - jwtSecret: { - type: String, - required: true, - default: "my_secret", - }, - dbType: { - type: String, - required: true, - default: "MongoDB", - }, - dbConnectionString: { - type: String, - required: true, - default: "mongodb://localhost:27017/uptime_db", - }, - redisUrl: { - type: String, - default: "redis://127.0.0.1:6379", - }, - jwtTTL: { - type: String, - required: true, - default: "2h", + default: "gb", }, pagespeedApiKey: { type: String, @@ -47,19 +12,15 @@ const AppSettingsSchema = mongoose.Schema( }, systemEmailHost: { type: String, - default: "smtp.gmail.com", }, systemEmailPort: { type: Number, - default: 465, }, systemEmailAddress: { type: String, - default: "", }, systemEmailPassword: { type: String, - default: "", }, singleton: { type: Boolean, @@ -73,4 +34,4 @@ const AppSettingsSchema = mongoose.Schema( } ); -export default mongoose.model("AppSettings", AppSettingsSchema); \ No newline at end of file +export default mongoose.model("AppSettings", AppSettingsSchema); diff --git a/server/db/mongo/MongoDB.js b/server/db/mongo/MongoDB.js index dd9826de5..b657caec4 100755 --- a/server/db/mongo/MongoDB.js +++ b/server/db/mongo/MongoDB.js @@ -76,7 +76,8 @@ import * as diagnosticModule from "./modules/diagnosticModule.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; - constructor() { + constructor({ appSettings }) { + this.appSettings = appSettings; Object.assign(this, userModule); Object.assign(this, inviteModule); Object.assign(this, recoveryModule); @@ -95,8 +96,7 @@ class MongoDB { connect = async () => { try { const connectionString = - process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db"; - console.log("Connecting to MongoDB with connection string:", connectionString); + this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; await mongoose.connect(connectionString); // If there are no AppSettings, create one await AppSettings.findOneAndUpdate( diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js index 3b5f68e19..b817d2a32 100755 --- a/server/db/mongo/modules/settingsModule.js +++ b/server/db/mongo/modules/settingsModule.js @@ -14,10 +14,11 @@ const getAppSettings = async () => { const updateAppSettings = async (newSettings) => { try { + console.log(newSettings); const settings = await AppSettings.findOneAndUpdate( {}, { $set: newSettings }, - { new: true } + { new: true, upsert: true } ); return settings; } catch (error) { diff --git a/server/index.js b/server/index.js index 5fa10bcab..f7cab5dbe 100755 --- a/server/index.js +++ b/server/index.js @@ -139,19 +139,21 @@ const shutdown = async () => { // Need to wrap server setup in a function to handle async nature of JobQueue const startApp = async () => { const app = express(); - const allowedOrigin = process.env.CLIENT_HOST; // Create and Register Primary services const translationService = new TranslationService(logger); const stringService = new StringService(translationService); ServiceRegistry.register(StringService.SERVICE_NAME, stringService); - // Create DB - const db = new MongoDB(); - await db.connect(); - // Create services const settingsService = new SettingsService(AppSettings); - await settingsService.loadSettings(); + const appSettings = settingsService.loadSettings(); + + // Create DB + const db = new MongoDB({ appSettings }); + await db.connect(); + + // Set allowed origin + const allowedOrigin = appSettings.clientHost; const networkService = new NetworkService( axios, @@ -237,7 +239,7 @@ const startApp = async () => { ServiceRegistry.get(SettingsService.SERVICE_NAME), ServiceRegistry.get(JobQueue.SERVICE_NAME), ServiceRegistry.get(StringService.SERVICE_NAME), - ServiceRegistry.get(EmailService.SERVICE_NAME), + ServiceRegistry.get(EmailService.SERVICE_NAME) ); const settingsController = new SettingsController( diff --git a/server/service/settingsService.js b/server/service/settingsService.js index 6c7beb456..7c522de96 100755 --- a/server/service/settingsService.js +++ b/server/service/settingsService.js @@ -1,6 +1,4 @@ const SERVICE_NAME = "SettingsService"; -import dotenv from "dotenv"; -dotenv.config(); const envConfig = { logLevel: process.env.LOG_LEVEL, clientHost: process.env.CLIENT_HOST, @@ -20,8 +18,6 @@ const envConfig = { * SettingsService * * This service is responsible for loading and managing the application settings. - * It gives priority to environment variables and will only load settings - * from the database if they are not set in the environment. */ class SettingsService { static SERVICE_NAME = SERVICE_NAME; @@ -34,40 +30,17 @@ class SettingsService { this.settings = { ...envConfig }; } /** - * Load settings from the database and merge with environment settings. - * If there are any settings that weren't set by environment variables, use user settings from the database. - * @returns {Promise} The merged settings. - * @throws Will throw an error if settings are not found in the database or if settings have not been loaded. - */ async loadSettings() { - try { - const dbSettings = await this.appSettings.findOne(); - if (!this.settings) { - throw new Error("Settings not found"); - } - - // If there are any settings that weren't set by environment variables, use user settings from DB - for (const key in envConfig) { - if ( - typeof envConfig?.[key] === "undefined" && - typeof dbSettings?.[key] !== "undefined" - ) { - this.settings[key] = dbSettings[key]; - } - } - - await this.appSettings.updateOne({}, { $set: this.settings }, { upsert: true }); - return this.settings; - } catch (error) { - error.service === undefined ? (error.service = SERVICE_NAME) : null; - error.method === undefined ? (error.method = "loadSettings") : null; - throw error; - } + * Load settings from env settings + * @returns {Object>} The settings. + */ + loadSettings() { + return this.settings; } /** * Reload settings by calling loadSettings. * @returns {Promise} The reloaded settings. */ - async reloadSettings() { + reloadSettings() { return this.loadSettings(); } /** From 7a855ef47adf2265121c236097059c7c6555fd7c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 8 May 2025 13:24:24 -0700 Subject: [PATCH 02/38] return db settings only to client, sanitize keys --- server/controllers/settingsController.js | 18 ++++++++---------- server/service/settingsService.js | 5 +++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index 561ea8266..4d561216e 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -11,16 +11,15 @@ class SettingsController { } getAppSettings = async (req, res, next) => { - try { - const settings = { ...(await this.settingsService.getSettings()) }; - delete settings.jwtSecret; - return res.success({ - msg: this.stringService.getAppSettings, - data: settings, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAppSettings")); + const dbSettings = await this.settingsService.getDBSettings(); + const sanitizedSettings = { ...dbSettings }; + if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") { + sanitizedSettings.pagespeedApiKey = "********"; } + return res.success({ + msg: this.stringService.getAppSettings, + data: sanitizedSettings, + }); }; updateAppSettings = async (req, res, next) => { @@ -32,7 +31,6 @@ class SettingsController { } try { - console.log(req.body); await this.db.updateAppSettings(req.body); const updatedSettings = { ...(await this.settingsService.reloadSettings()) }; delete updatedSettings.jwtSecret; diff --git a/server/service/settingsService.js b/server/service/settingsService.js index 7c522de96..6a673daa4 100755 --- a/server/service/settingsService.js +++ b/server/service/settingsService.js @@ -54,6 +54,11 @@ class SettingsService { } return this.settings; } + + async getDBSettings() { + const settings = await this.appSettings.findOne({ singleton: true }).lean(); + return settings; + } } export default SettingsService; From 57d87114dec5243f92107e70b70f1467ac34a833 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 8 May 2025 13:44:59 -0700 Subject: [PATCH 03/38] add a temp api key field to separate control state form visual state --- client/src/Pages/Settings/index.jsx | 78 +++++++++++++---------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 554ee6857..79e5ea657 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -1,5 +1,5 @@ // Components -import { Box, Stack, Typography, Button, Switch } from "@mui/material"; +import { Box, Stack, Typography, Button } from "@mui/material"; import TextInput from "../../Components/Inputs/TextInput"; import Link from "../../Components/Link"; import Select from "../../Components/Inputs/Select"; @@ -25,12 +25,7 @@ import { } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { update } from "../../Features/Auth/authSlice"; import PropTypes from "prop-types"; -import { - setTimezone, - setMode, - setDistributedUptimeEnabled, - setLanguage, -} from "../../Features/UI/uiSlice"; +import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice"; import timezones from "../../Utils/timezones.json"; import { useState, useEffect } from "react"; import { networkService } from "../../main"; @@ -52,15 +47,18 @@ const Settings = () => { const { isLoading: authIsLoading } = useSelector((state) => state.auth); const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui); const { mode } = useSelector((state) => state.ui); + const { pagespeedApiKey } = useSelector((state) => state.settings); const [checksIsLoading, setChecksIsLoading] = useState(false); const [form, setForm] = useState({ enableDistributedUptime: distributedUptimeEnabled, ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, - pagespeedApiKey: "", + pagespeedApiKey: pagespeedApiKey, }); + const [version, setVersion] = useState("unknown"); const [apiKeyFieldType, setApiKeyFieldType] = useState("password"); - const [isApiKeySet, setIsApiKeySet] = useState(false); + const [isApiKeySet, setIsApiKeySet] = useState(pagespeedApiKey ? true : false); + const [tempPSKey, setTempPSKey] = useState(""); const [errors, setErrors] = useState({}); const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false }; const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState); @@ -87,25 +85,7 @@ const Settings = () => { useEffect(() => { dispatch(getAppSettings()); - }, []); - - const { pagespeedApiKey } = useSelector((state) => state.settings); - - useEffect(() => { - if (pagespeedApiKey) { - setIsApiKeySet(true); - setForm((prev) => ({ - ...prev, - pagespeedApiKey: t("maskedPageSpeedKeyPlaceholder"), - })); - } else { - setIsApiKeySet(false); - setForm((prev) => ({ - ...prev, - pagespeedApiKey: "", - })); - } - }, [pagespeedApiKey]); + }, [dispatch]); const handleChange = (event) => { const { type, checked, value, id } = event.target; @@ -118,16 +98,18 @@ const Settings = () => { return; } + if (id === "pagespeedApiKey") { + setTempPSKey(value); + return; + } + let inputValue = value; if (id === "ttl") { inputValue = value.replace(/[^0-9]/g, ""); } const updatedForm = { ...form, [id]: inputValue }; - const { error } = settingsValidation.validate( - updatedForm, - { abortEarly: false } - ); + const { error } = settingsValidation.validate(updatedForm, { abortEarly: false }); if (!error || error.details.length === 0) { setErrors({}); @@ -139,7 +121,7 @@ const Settings = () => { setErrors(newErrors); logger.error("Validation errors:", error.details); } - + setForm(updatedForm); }; @@ -153,7 +135,14 @@ const Settings = () => { const updatedUser = { ...user, checkTTL: form.ttl }; const [userAction, settingsAction] = await Promise.all([ dispatch(update({ localData: updatedUser })), - dispatch(updateAppSettings({ settings: { language: language, pagespeedApiKey: form.pagespeedApiKey } })), + dispatch( + updateAppSettings({ + settings: { + language: language, + pagespeedApiKey: tempPSKey ? tempPSKey : form.pagespeedApiKey, + }, + }) + ), ]); if (userAction.payload.success && settingsAction.payload.success) { @@ -221,7 +210,7 @@ const Settings = () => { ...prev, pagespeedApiKey: "", })); - }; + }; const languages = Object.keys(i18n.options.resources || {}); @@ -324,7 +313,7 @@ const Settings = () => { { } /> {isApiKeySet && ( - - {t("pageSpeedApiKeyFieldResetLabel")} - - + + {t("pageSpeedApiKeyFieldResetLabel")} + + )} From 27ea9b2aedc011d51c00647dbb3214969f77b99c Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Fri, 9 May 2025 22:07:20 +1000 Subject: [PATCH 04/38] fix: button font size is now 13px --- client/src/Utils/Theme/globalTheme.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index 6a6724150..13442ace1 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -147,6 +147,7 @@ const baseTheme = (palette) => ({ ], height: 34, fontWeight: 400, + fontSize: "var(--env-var-font-size-medium)", borderRadius: 4, boxShadow: "none", textTransform: "none", From 2ee381f0b7680c9f0e5d07e0c42adcd90e24abb6 Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Fri, 9 May 2025 22:12:50 +1000 Subject: [PATCH 05/38] fix: changed from space indentation to tabs --- client/src/Utils/Theme/globalTheme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index 13442ace1..ca32a3ad6 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -147,7 +147,7 @@ const baseTheme = (palette) => ({ ], height: 34, fontWeight: 400, - fontSize: "var(--env-var-font-size-medium)", + fontSize: "var(--env-var-font-size-medium)", borderRadius: 4, boxShadow: "none", textTransform: "none", From 6304212645d232ae8f421fc2d9a03e51c81dc9fd Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Sat, 10 May 2025 02:31:16 +1000 Subject: [PATCH 06/38] fix: removed css reference, theme fontSize changed --- client/src/Utils/Theme/globalTheme.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index ca32a3ad6..f7feed7fd 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -9,7 +9,7 @@ const shadow = const baseTheme = (palette) => ({ typography: { fontFamily: fontFamilyPrimary, - fontSize: 14, + fontSize: typographyLevels.base, h1: { fontSize: typographyLevels.xl, color: palette.primary.contrastText, @@ -147,7 +147,6 @@ const baseTheme = (palette) => ({ ], height: 34, fontWeight: 400, - fontSize: "var(--env-var-font-size-medium)", borderRadius: 4, boxShadow: "none", textTransform: "none", From 5d0877d91db090dd8de9de06ad58e5547a00e2cc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 09:33:35 -0700 Subject: [PATCH 07/38] use port 52345 for server by default --- docker/dev/docker-compose.yaml | 4 ++-- docker/dev/nginx/conf.d/default.conf | 18 +++++++++--------- docker/dev/server.Dockerfile | 2 +- docker/dist/server.Dockerfile | 2 +- docker/prod/docker-compose.yaml | 2 +- docker/prod/nginx/conf.d/default.conf | 8 ++++---- docker/prod/server.Dockerfile | 2 +- docker/staging/docker-compose.yaml | 2 +- docker/staging/nginx/conf.d/default.conf | 8 ++++---- docker/staging/server.Dockerfile | 2 +- server/index.js | 7 +++---- server/service/settingsService.js | 17 ++++++++++------- 12 files changed, 38 insertions(+), 36 deletions(-) diff --git a/docker/dev/docker-compose.yaml b/docker/dev/docker-compose.yaml index 73904da37..50b573d38 100755 --- a/docker/dev/docker-compose.yaml +++ b/docker/dev/docker-compose.yaml @@ -5,7 +5,7 @@ services: ports: - "80:80" environment: - UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1" + UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1" UPTIME_APP_CLIENT_HOST: "http://localhost" volumes: - ./nginx/conf.d:/etc/nginx/conf.d/ @@ -15,7 +15,7 @@ services: image: uptime_server:latest restart: always ports: - - "5000:5000" + - "52345:52345" env_file: - server.env depends_on: diff --git a/docker/dev/nginx/conf.d/default.conf b/docker/dev/nginx/conf.d/default.conf index 9a7690aa5..e2c2a7268 100755 --- a/docker/dev/nginx/conf.d/default.conf +++ b/docker/dev/nginx/conf.d/default.conf @@ -12,7 +12,7 @@ server { } # location /api/ { - # proxy_pass http://server:5000/api/; + # proxy_pass http://server:52345/api/; # proxy_http_version 1.1; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; @@ -24,12 +24,12 @@ server { # proxy_cache off; # } - location /api-docs/ { - proxy_pass http://server:5000/api-docs/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } + # location /api-docs/ { + # proxy_pass http://server:52345/api-docs/; + # proxy_http_version 1.1; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } } \ No newline at end of file diff --git a/docker/dev/server.Dockerfile b/docker/dev/server.Dockerfile index 202422dcc..d9d99a09d 100755 --- a/docker/dev/server.Dockerfile +++ b/docker/dev/server.Dockerfile @@ -8,6 +8,6 @@ RUN npm install COPY ./server/ ./ -EXPOSE 5000 +EXPOSE 52345 CMD ["node", "index.js"] \ No newline at end of file diff --git a/docker/dist/server.Dockerfile b/docker/dist/server.Dockerfile index 202422dcc..d9d99a09d 100755 --- a/docker/dist/server.Dockerfile +++ b/docker/dist/server.Dockerfile @@ -8,6 +8,6 @@ RUN npm install COPY ./server/ ./ -EXPOSE 5000 +EXPOSE 52345 CMD ["node", "index.js"] \ No newline at end of file diff --git a/docker/prod/docker-compose.yaml b/docker/prod/docker-compose.yaml index bc720862f..db142396d 100755 --- a/docker/prod/docker-compose.yaml +++ b/docker/prod/docker-compose.yaml @@ -25,7 +25,7 @@ services: image: ghcr.io/bluewave-labs/checkmate:backend-demo restart: always ports: - - "5000:5000" + - "52345:52345" env_file: - server.env depends_on: diff --git a/docker/prod/nginx/conf.d/default.conf b/docker/prod/nginx/conf.d/default.conf index 98e1bd97d..8afeb93b7 100755 --- a/docker/prod/nginx/conf.d/default.conf +++ b/docker/prod/nginx/conf.d/default.conf @@ -16,7 +16,7 @@ server { } location /api/ { - proxy_pass http://server:5000/api/; + proxy_pass http://server:52345/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -25,7 +25,7 @@ server { } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://server:52345/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -50,7 +50,7 @@ server { } location /api/ { - proxy_pass http://server:5000/api/; + proxy_pass http://server:52345/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -59,7 +59,7 @@ server { } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://server:52345/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile index 998bbe4fe..c80f468f8 100755 --- a/docker/prod/server.Dockerfile +++ b/docker/prod/server.Dockerfile @@ -10,6 +10,6 @@ RUN npm install COPY ./server ./ -EXPOSE 5000 +EXPOSE 52345 CMD ["node", "index.js"] \ No newline at end of file diff --git a/docker/staging/docker-compose.yaml b/docker/staging/docker-compose.yaml index d491a729e..b9ca52b6e 100755 --- a/docker/staging/docker-compose.yaml +++ b/docker/staging/docker-compose.yaml @@ -27,7 +27,7 @@ services: image: ghcr.io/bluewave-labs/checkmate:backend-staging restart: always ports: - - "5000:5000" + - "52345:52345" env_file: - server.env depends_on: diff --git a/docker/staging/nginx/conf.d/default.conf b/docker/staging/nginx/conf.d/default.conf index 4ea324b11..c755ddef1 100755 --- a/docker/staging/nginx/conf.d/default.conf +++ b/docker/staging/nginx/conf.d/default.conf @@ -16,7 +16,7 @@ server { } location /api/ { - proxy_pass http://server:5000/api/; + proxy_pass http://server:52345/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -29,7 +29,7 @@ server { } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://server:52345/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -54,7 +54,7 @@ server { } location /api/ { - proxy_pass http://server:5000/api/; + proxy_pass http://server:52345/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -67,7 +67,7 @@ server { } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://server:52345/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile index 998bbe4fe..c80f468f8 100755 --- a/docker/staging/server.Dockerfile +++ b/docker/staging/server.Dockerfile @@ -10,6 +10,6 @@ RUN npm install COPY ./server ./ -EXPOSE 5000 +EXPOSE 52345 CMD ["node", "index.js"] \ No newline at end of file diff --git a/server/index.js b/server/index.js index f7cab5dbe..f299b32e9 100755 --- a/server/index.js +++ b/server/index.js @@ -100,8 +100,6 @@ const openApiSpec = JSON.parse( let server; -const PORT = 5000; - const shutdown = async () => { if (isShuttingDown) { return; @@ -217,8 +215,9 @@ const startApp = async () => { await translationService.initialize(); - server = app.listen(PORT, () => { - logger.info({ message: `server started on port:${PORT}` }); + const port = appSettings.port || 52345; + server = app.listen(port, () => { + logger.info({ message: `Server started on port:${port}` }); }); process.on("SIGUSR2", shutdown); diff --git a/server/service/settingsService.js b/server/service/settingsService.js index 6a673daa4..548d76eee 100755 --- a/server/service/settingsService.js +++ b/server/service/settingsService.js @@ -1,18 +1,21 @@ const SERVICE_NAME = "SettingsService"; const envConfig = { + nodeEnv: process.env.NODE_ENV, logLevel: process.env.LOG_LEVEL, - clientHost: process.env.CLIENT_HOST, - jwtSecret: process.env.JWT_SECRET, - dbType: process.env.DB_TYPE, - dbConnectionString: process.env.DB_CONNECTION_STRING, - redisUrl: process.env.REDIS_URL, - jwtTTL: process.env.TOKEN_TTL, - pagespeedApiKey: process.env.PAGESPEED_API_KEY, systemEmailHost: process.env.SYSTEM_EMAIL_HOST, systemEmailPort: process.env.SYSTEM_EMAIL_PORT, systemEmailUser: process.env.SYSTEM_EMAIL_USER, systemEmailAddress: process.env.SYSTEM_EMAIL_ADDRESS, systemEmailPassword: process.env.SYSTEM_EMAIL_PASSWORD, + jwtSecret: process.env.JWT_SECRET, + jwtTTL: process.env.TOKEN_TTL, + clientHost: process.env.CLIENT_HOST, + dbConnectionString: process.env.DB_CONNECTION_STRING, + redisUrl: process.env.REDIS_URL, + callbackUrl: process.env.CALLBACK_URL, + port: process.env.PORT, + pagespeedApiKey: process.env.PAGESPEED_API_KEY, + uprockApiKey: process.env.UPROCK_API_KEY, }; /** * SettingsService From 3a574b2f4ed0f34c0c6ab126529faddf03f20e6c Mon Sep 17 00:00:00 2001 From: mohadeseh safari Date: Fri, 9 May 2025 14:40:07 -0400 Subject: [PATCH 08/38] translate menu items --- client/src/Components/Sidebar/index.jsx | 44 ++++++++++++---------- client/src/Pages/Uptime/Monitors/index.jsx | 7 +++- client/src/locales/gb.json | 25 +++++++++++- client/src/locales/ru.json | 25 +++++++++++- client/src/locales/tr.json | 23 +++++++++++ 5 files changed, 101 insertions(+), 23 deletions(-) diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx index 81edcd3cc..dd548476a 100644 --- a/client/src/Components/Sidebar/index.jsx +++ b/client/src/Components/Sidebar/index.jsx @@ -47,46 +47,47 @@ import "./index.css"; import { useLocation, useNavigate } from "react-router"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; import { clearAuthState } from "../../Features/Auth/authSlice"; import { toggleSidebar } from "../../Features/UI/uiSlice"; import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -const menu = [ - { name: "Uptime", path: "uptime", icon: }, - { name: "Pagespeed", path: "pagespeed", icon: }, - { name: "Infrastructure", path: "infrastructure", icon: }, +const getMenu = (t) => [ + { name: t("menu.uptime"), path: "uptime", icon: }, + { name: t("menu.pagespeed"), path: "pagespeed", icon: }, + { name: t("menu.infrastructure"), path: "infrastructure", icon: }, { - name: "Distributed uptime", + name: t("menu.distributedUptime"), path: "distributed-uptime", icon: , }, - { name: "Incidents", path: "incidents", icon: }, + { name: t("menu.incidents"), path: "incidents", icon: }, - { name: "Status pages", path: "status", icon: }, - { name: "Maintenance", path: "maintenance", icon: }, - // { name: "Integrations", path: "integrations", icon: }, + { name: t("menu.statusPages"), path: "status", icon: }, + { name: t("menu.maintenance"), path: "maintenance", icon: }, + // { name: t("menu.integrations"), path: "integrations", icon: }, { - name: "Settings", + name: t("menu.settings"), icon: , path: "settings", }, ]; -const otherMenuItems = [ - { name: "Support", path: "support", icon: }, +const getOtherMenuItems = (t) => [ + { name: t("menu.support"), path: "support", icon: }, { - name: "Discussions", + name: t("menu.discussions"), path: "discussions", icon: , }, - { name: "Docs", path: "docs", icon: }, - { name: "Changelog", path: "changelog", icon: }, + { name: t("menu.docs"), path: "docs", icon: }, + { name: t("menu.changelog"), path: "changelog", icon: }, ]; -const accountMenuItems = [ - { name: "Profile", path: "account/profile", icon: }, - { name: "Password", path: "account/password", icon: }, - { name: "Team", path: "account/team", icon: }, +const getAccountMenuItems = (t) => [ + { name: t("menu.profile"), path: "account/profile", icon: }, + { name: t("menu.password"), path: "account/password", icon: }, + { name: t("menu.team"), path: "account/team", icon: }, ]; /* TODO this could be a key in nested Path would be the link */ @@ -118,7 +119,12 @@ function Sidebar() { const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); + const { t } = useTranslation(); const authState = useSelector((state) => state.auth); + + const menu = getMenu(t); + const otherMenuItems = getOtherMenuItems(t); + const accountMenuItems = getAccountMenuItems(t); const collapsed = useSelector((state) => state.ui.sidebar.collapsed); const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false }); const [anchorEl, setAnchorEl] = useState(null); diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 60a308141..ce9123816 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -31,11 +31,11 @@ import PropTypes from "prop-types"; import useFetchMonitorsWithSummary from "../../../Hooks/useFetchMonitorsWithSummary"; import useFetchMonitorsWithChecks from "../../../Hooks/useFetchMonitorsWithChecks"; import { useTranslation } from "react-i18next"; -const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; const TYPES = ["http", "ping", "docker", "port"]; const CreateMonitorButton = ({ shouldRender }) => { // Utils const navigate = useNavigate(); + const { t } = useTranslation(); if (shouldRender === false) { return; } @@ -49,7 +49,7 @@ const CreateMonitorButton = ({ shouldRender }) => { navigate("/uptime/create"); }} > - Create new + {t("createNew")} ); @@ -78,10 +78,13 @@ const UptimeMonitors = () => { // Utils const theme = useTheme(); + const navigate = useNavigate(); const isAdmin = useIsAdmin(); const dispatch = useDispatch(); const { t } = useTranslation(); + const BREADCRUMBS = [{ name: t("menu.uptime"), path: "/uptime" }]; + // Handlers const handleChangePage = (event, newPage) => { setPage(newPage); diff --git a/client/src/locales/gb.json b/client/src/locales/gb.json index 49993eb89..dacf84eb8 100644 --- a/client/src/locales/gb.json +++ b/client/src/locales/gb.json @@ -439,6 +439,29 @@ "pageSpeedApiKeyFieldLabel": "PageSpeed API key", "pageSpeedApiKeyFieldDescription": "Enter your Google PageSpeed API key to enable pagespeed monitoring. Click Reset to update the key.", "pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.", - "reset": "Reset" + "reset": "Reset", + "createNew": "Create new", + "monitorState": { + "paused": "paused", + "resumed": "resumed" + }, + "menu": { + "uptime": "Uptime", + "pagespeed": "Pagespeed", + "infrastructure": "Infrastructure", + "distributedUptime": "Distributed uptime", + "incidents": "Incidents", + "statusPages": "Status pages", + "maintenance": "Maintenance", + "integrations": "Integrations", + "settings": "Settings", + "support": "Support", + "discussions": "Discussions", + "docs": "Docs", + "changelog": "Changelog", + "profile": "Profile", + "password": "Password", + "team": "Team" + } } diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index 5536b5e45..a37923557 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -393,5 +393,28 @@ "maintenanceTableActionMenuDialogTitle": "", "pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.", "pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать", - "pageSpeedAddApiKey": "как добавить ваш ключ API." + "pageSpeedAddApiKey": "как добавить ваш ключ API.", + "createNew": "Создать новый", + "monitorState": { + "paused": "приостановлен", + "resumed": "возобновлен" + }, + "menu": { + "uptime": "Аптайм", + "pagespeed": "Скорость страницы", + "infrastructure": "Инфраструктура", + "distributedUptime": "Распределенный аптайм", + "incidents": "Инциденты", + "statusPages": "Страницы статуса", + "maintenance": "Обслуживание", + "integrations": "Интеграции", + "settings": "Настройки", + "support": "Поддержка", + "discussions": "Обсуждения", + "docs": "Документация", + "changelog": "История изменений", + "profile": "Профиль", + "password": "Пароль", + "team": "Команда" + } } \ No newline at end of file diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 5df644fdd..e7e95a339 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -427,5 +427,28 @@ "validationFailed": "", "noFileSelected": "", "fallbackPage": "" + }, + "createNew": "Yeni oluştur", + "monitorState": { + "paused": "duraklatıldı", + "resumed": "devam ettirildi" + }, + "menu": { + "uptime": "Çalışma Süresi", + "pagespeed": "Sayfa Hızı", + "infrastructure": "Altyapı", + "distributedUptime": "Dağıtılmış Çalışma Süresi", + "incidents": "Olaylar", + "statusPages": "Durum Sayfaları", + "maintenance": "Bakım", + "integrations": "Entegrasyonlar", + "settings": "Ayarlar", + "support": "Destek", + "discussions": "Tartışmalar", + "docs": "Belgeler", + "changelog": "Değişiklik Günlüğü", + "profile": "Profil", + "password": "Şifre", + "team": "Takım" } } From 13c4390f967512f7fccd39ee54d621d808136a58 Mon Sep 17 00:00:00 2001 From: mohadeseh safari Date: Fri, 9 May 2025 14:58:13 -0400 Subject: [PATCH 09/38] fix: translate UI elements in menu and uptime page - Added translations for sidebar menu items in all languages - Fixed "Create new" button to use translation instead of hardcoded text - Added missing translations for "Bulk Import" in Russian and Turkish - Translated status box labels (UP, DOWN, PAUSED) in the uptime monitors page - Made greeting component fully translatable with proper context - Fixed lint warnings in components --- .../Components/MonitorCreateHeader/index.jsx | 6 ++-- .../Monitors/Components/StatusBoxes/index.jsx | 9 ++++-- client/src/Utils/greeting.jsx | 8 ++++-- client/src/locales/gb.json | 10 +++++++ client/src/locales/ru.json | 23 ++++++++++++++- client/src/locales/tr.json | 28 +++++++++++++------ 6 files changed, 66 insertions(+), 18 deletions(-) diff --git a/client/src/Components/MonitorCreateHeader/index.jsx b/client/src/Components/MonitorCreateHeader/index.jsx index 05a9b7f9b..15d6d5c0f 100644 --- a/client/src/Components/MonitorCreateHeader/index.jsx +++ b/client/src/Components/MonitorCreateHeader/index.jsx @@ -6,7 +6,7 @@ import { useTheme } from "@emotion/react"; const CreateMonitorHeader = ({ isAdmin, - label = "Create new", + label, isLoading = true, path, bulkPath, @@ -14,6 +14,8 @@ const CreateMonitorHeader = ({ const navigate = useNavigate(); const { t } = useTranslation(); const theme = useTheme(); + + // Use the provided label or fall back to the translated default if (!isAdmin) return null; @@ -30,7 +32,7 @@ const CreateMonitorHeader = ({ color="accent" onClick={() => navigate(path)} > - {label} + {label || t("createNew")} {bulkPath && ( + + + + + {t("settingsSystemReset")} + + {t("settingsSystemResetDescription")} + + + + {t("settingsRemoveAllMonitors")} + + + setIsOpen(false)} + confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")} + onConfirm={() => { + const syntheticEvent = { + target: { + name: "deleteMonitors", + }, + }; + handleChange(syntheticEvent); + setIsOpen(false); + }} + isLoading={isLoading || authIsLoading || checksIsLoading} + /> + + + ); +}; + +SettingsDemoMonitors.propTypes = { + isLoading: PropTypes.bool, + authIsLoading: PropTypes.bool, + checksIsLoading: PropTypes.bool, + handleChange: PropTypes.func, +}; + +export default SettingsDemoMonitors; diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/Settings/SettingsPagespeed.jsx new file mode 100644 index 000000000..87f558f32 --- /dev/null +++ b/client/src/Pages/Settings/SettingsPagespeed.jsx @@ -0,0 +1,81 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +const SettingsPagespeed = ({ HEADING_SX, settings, setSettings }) => { + const { t } = useTranslation(); + const theme = useTheme(); + + // Local state + const [apiKey, setApiKey] = useState(""); + const [isApiKeySet, setIsApiKeySet] = useState(false); + const [hasBeenReset, setHasBeenReset] = useState(false); + + // Handler + const handleChange = (e) => { + setApiKey(e.target.value); + setSettings({ ...settings, pagespeedApiKey: e.target.value }); + }; + + useEffect(() => { + console.log(settings); + setIsApiKeySet(Boolean(settings.pagespeedApiKey) && apiKey === ""); + }, [settings, apiKey]); + + return ( + + + {t("pageSpeedApiKeyFieldTitle")} + {t("pageSpeedApiKeyFieldDescription")} + + + {(isApiKeySet === false || hasBeenReset === true) && ( + } + /> + )} + + {isApiKeySet === true && hasBeenReset === false && ( + + {t("pageSpeedApiKeyFieldResetLabel")} + + + )} + + + ); +}; + +SettingsPagespeed.propTypes = { + HEADING_SX: PropTypes.object, + settings: PropTypes.object, + setSettings: PropTypes.func, +}; + +export default SettingsPagespeed; diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/Settings/SettingsStats.jsx new file mode 100644 index 000000000..c9178b1da --- /dev/null +++ b/client/src/Pages/Settings/SettingsStats.jsx @@ -0,0 +1,84 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import Dialog from "../../Components/Dialog"; + +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; + +const SettingsStats = ({ HEADING_SX, handleChange, settings, errors }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + return ( + + + + {t("settingsHistoryAndMonitoring")} + + + {t("settingsHistoryAndMonitoringDescription")} + + + + + + {t("settingsClearAllStats")} + + + + setIsOpen(false)} + confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")} + onConfirm={() => { + const syntheticEvent = { + target: { + name: "deleteStats", + }, + }; + handleChange(syntheticEvent); + setIsOpen(false); + }} + isLoading={false} + /> + + ); +}; + +SettingsStats.propTypes = { + HEADING_SX: PropTypes.object, + handleChange: PropTypes.func, + settings: PropTypes.object, + errors: PropTypes.object, +}; + +export default SettingsStats; diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx new file mode 100644 index 000000000..e6ba1a77b --- /dev/null +++ b/client/src/Pages/Settings/SettingsTimeZone.jsx @@ -0,0 +1,43 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import ConfigBox from "../../Components/ConfigBox"; +import Select from "../../Components/Inputs/Select"; +import timezones from "../../Utils/timezones.json"; + +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useTranslation } from "react-i18next"; +const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + {t("settingsGeneralSettings")} + + {t("settingsDisplayTimezone")}-{" "} + {t("settingsDisplayTimezoneDescription")} + + + + + + + + ); +}; + +SettingsUI.propTypes = { + HEADING_SX: PropTypes.object, + handleChange: PropTypes.func, + mode: PropTypes.string, + language: PropTypes.string, +}; + +export default SettingsUI; diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 79e5ea657..e1e8c3859 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -1,116 +1,67 @@ -// Components -import { Box, Stack, Typography, Button } from "@mui/material"; -import TextInput from "../../Components/Inputs/TextInput"; -import Link from "../../Components/Link"; -import Select from "../../Components/Inputs/Select"; -import { useIsAdmin } from "../../Hooks/useIsAdmin"; -import Dialog from "../../Components/Dialog"; -import ConfigBox from "../../Components/ConfigBox"; -import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; -import { getAppSettings } from "../../Features/Settings/settingsSlice"; -// import { -// WalletMultiButton, -// WalletDisconnectButton, -// } from "@solana/wallet-adapter-react-ui"; - -//Utils -import { useTheme } from "@emotion/react"; -import { logger } from "../../Utils/Logger"; -import { useDispatch, useSelector } from "react-redux"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Breadcrumbs from "../../Components/Breadcrumbs"; +import SettingsTimeZone from "./SettingsTimeZone"; +import SettingsUI from "./SettingsUI"; +import SettingsPagespeed from "./SettingsPagespeed"; +import SettingsDemoMonitors from "./SettingsDemoMonitors"; +import SettingsAbout from "./SettingsAbout"; +import Button from "@mui/material/Button"; +// Utils +import { settingsValidation } from "../../Validation/validation"; import { createToast } from "../../Utils/toastUtils"; +import { useState } from "react"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useSelector, useDispatch } from "react-redux"; +import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice"; +import { getAppSettings } from "../../Features/Settings/settingsSlice"; +import SettingsStats from "./SettingsStats"; import { deleteMonitorChecksByTeamId, addDemoMonitors, deleteAllMonitors, } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { update } from "../../Features/Auth/authSlice"; -import PropTypes from "prop-types"; -import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice"; -import timezones from "../../Utils/timezones.json"; -import { useState, useEffect } from "react"; -import { networkService } from "../../main"; -import { settingsValidation } from "../../Validation/validation"; -import { updateAppSettings } from "../../Features/Settings/settingsSlice"; -import { useTranslation } from "react-i18next"; +import { useFetchSettings, useSaveSettings } from "../../Hooks/useFetchSettings"; +import { UseDeleteMonitorStats } from "../../Hooks/useDeleteMonitorStats"; // Constants -const SECONDS_PER_DAY = 86400; +const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; const Settings = () => { - const theme = useTheme(); - const { t, i18n } = useTranslation(); - const isAdmin = useIsAdmin(); + // Redux state + const { mode, language, timezone } = useSelector((state) => state.ui); const { user } = useSelector((state) => state.auth); - const { language } = useSelector((state) => state.ui); - const { checkTTL } = user; - const { isLoading } = useSelector((state) => state.uptimeMonitors); - const { isLoading: authIsLoading } = useSelector((state) => state.auth); - const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui); - const { mode } = useSelector((state) => state.ui); - const { pagespeedApiKey } = useSelector((state) => state.settings); - const [checksIsLoading, setChecksIsLoading] = useState(false); - const [form, setForm] = useState({ - enableDistributedUptime: distributedUptimeEnabled, - ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, - pagespeedApiKey: pagespeedApiKey, + + // Local state + const [settings, setSettings] = useState({}); + const [errors, setErrors] = useState({}); + + // Network + const [isSettingsLoading, settingsError] = useFetchSettings({ + settings, + setSettings, }); - const [version, setVersion] = useState("unknown"); - const [apiKeyFieldType, setApiKeyFieldType] = useState("password"); - const [isApiKeySet, setIsApiKeySet] = useState(pagespeedApiKey ? true : false); - const [tempPSKey, setTempPSKey] = useState(""); - const [errors, setErrors] = useState({}); - const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false }; - const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState); + const [isSaving, saveError, saveSettings] = useSaveSettings(); + + const [deleteMonitorStats, isDeletingMonitorStats] = UseDeleteMonitorStats(); + + // Setup + const theme = useTheme(); + const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) }; + const { t, i18n } = useTranslation(); const dispatch = useDispatch(); - //Fetching latest release version from github - useEffect(() => { - const fetchLatestVersion = async () => { - let version = "unknown"; - try { - const response = await networkService.fetchGithubLatestRelease(); - if (!response.status === 200) { - throw new Error("Failed to fetch latest version"); - } - version = response.data.tag_name; - } catch (error) { - createToast({ body: error.message || "Error fetching latest version" }); // Set error message - } finally { - setVersion(version); - } - }; - fetchLatestVersion(); - }, []); + // Handlers + const handleChange = async (e) => { + const { name, value } = e.target; - useEffect(() => { - dispatch(getAppSettings()); - }, [dispatch]); - - const handleChange = (event) => { - const { type, checked, value, id } = event.target; - - if (type === "checkbox") { - setForm((prev) => ({ - ...prev, - [id]: checked, - })); - return; - } - - if (id === "pagespeedApiKey") { - setTempPSKey(value); - return; - } - - let inputValue = value; - if (id === "ttl") { - inputValue = value.replace(/[^0-9]/g, ""); - } - - const updatedForm = { ...form, [id]: inputValue }; - const { error } = settingsValidation.validate(updatedForm, { abortEarly: false }); + // Build next state early + const newSettings = { ...settings, [name]: value }; + // Validate + const { error } = settingsValidation.validate(newSettings, { abortEarly: false }); if (!error || error.details.length === 0) { setErrors({}); } else { @@ -119,397 +70,122 @@ const Settings = () => { newErrors[err.path[0]] = err.message; }); setErrors(newErrors); - logger.error("Validation errors:", error.details); } - setForm(updatedForm); - }; - - // TODO Handle saving - const handleSave = async () => { - try { - setChecksIsLoading(true); - await networkService.updateChecksTTL({ - ttl: form.ttl, - }); - const updatedUser = { ...user, checkTTL: form.ttl }; - const [userAction, settingsAction] = await Promise.all([ - dispatch(update({ localData: updatedUser })), - dispatch( - updateAppSettings({ - settings: { - language: language, - pagespeedApiKey: tempPSKey ? tempPSKey : form.pagespeedApiKey, - }, - }) - ), - ]); - - if (userAction.payload.success && settingsAction.payload.success) { - createToast({ body: t("settingsSuccessSaved") }); - } else { - throw new Error("Failed to save settings"); - } - } catch (error) { - createToast({ body: t("settingsFailedToSave") }); - } finally { - setChecksIsLoading(false); + if (name === "timezone") { + dispatch(setTimezone({ timezone: value })); } - }; - const handleClearStats = async () => { - try { - const action = await dispatch(deleteMonitorChecksByTeamId({ teamId: user.teamId })); - - if (deleteMonitorChecksByTeamId.fulfilled.match(action)) { - createToast({ body: t("settingsStatsCleared") }); - } else { - createToast({ body: t("settingsFailedToClearStats") }); - } - } catch (error) { - logger.error(error); - createToast({ body: t("settingsFailedToClearStats") }); - } finally { - setIsOpen(deleteStatsMonitorsInitState); + if (name === "mode") { + dispatch(setMode(value)); } - }; - const handleInsertDemoMonitors = async () => { - try { - const action = await dispatch(addDemoMonitors()); - if (addDemoMonitors.fulfilled.match(action)) { - createToast({ body: t("settingsDemoMonitorsAdded") }); - } else { + if (name === "language") { + dispatch(setLanguage(value)); + i18n.changeLanguage(value); + } + + if (name === "deleteStats") { + await deleteMonitorStats({ teamId: user.teamId }); + return; + } + + if (name === "demo") { + try { + const action = await dispatch(addDemoMonitors()); + if (addDemoMonitors.fulfilled.match(action)) { + createToast({ body: t("settingsDemoMonitorsAdded") }); + } else { + createToast({ body: t("settingsFailedToAddDemoMonitors") }); + } + } catch (error) { createToast({ body: t("settingsFailedToAddDemoMonitors") }); } - } catch (error) { - logger.error(error); - createToast({ Body: t("settingsFailedToAddDemoMonitors") }); + return; } - }; - const handleDeleteAllMonitors = async () => { - try { - const action = await dispatch(deleteAllMonitors()); - if (deleteAllMonitors.fulfilled.match(action)) { - createToast({ body: t("settingsMonitorsDeleted") }); - } else { + if (name === "deleteMonitors") { + try { + const action = await dispatch(deleteAllMonitors()); + if (deleteAllMonitors.fulfilled.match(action)) { + createToast({ body: t("settingsMonitorsDeleted") }); + } else { + createToast({ body: t("settingsFailedToDeleteMonitors") }); + } + } catch (error) { createToast({ body: t("settingsFailedToDeleteMonitors") }); } - } catch (error) { - logger.error(error); - createToast({ Body: t("settingsFailedToDeleteMonitors") }); - } finally { - setIsOpen(deleteStatsMonitorsInitState); + return; } + + setSettings(newSettings); }; - const handleResetApiKey = () => { - setIsApiKeySet(false); - setForm((prev) => ({ - ...prev, - pagespeedApiKey: "", - })); + const handleSave = () => { + const { error } = settingsValidation.validate(settings, { abortEarly: false }); + if (!error || error.details.length === 0) { + setErrors({}); + } else { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + } + saveSettings(settings); }; - const languages = Object.keys(i18n.options.resources || {}); - return ( - + + + Settings + + + + + + - - - {t("settingsGeneralSettings")} - - {t("settingsDisplayTimezone")}-{" "} - {t("settingsDisplayTimezoneDescription")} - - - - { - dispatch(setMode(e.target.value)); - }} - items={[ - { _id: "light", name: "Light" }, - { _id: "dark", name: "Dark" }, - ]} - > - - - - {/* {isAdmin && ( - - - {t("settingsDistributedUptime")} - - {t("settingsDistributedUptimeDescription")} - - - - { - dispatch(setDistributedUptimeEnabled(e.target.checked)); - }} - /> - {distributedUptimeEnabled === true - ? t("settingsEnabled") - : t("settingsDisabled")} - - - )} */} - - - {t("pageSpeedApiKeyFieldTitle")} - - {t("pageSpeedApiKeyFieldDescription")} - - - - - ) - } - /> - {isApiKeySet && ( - - {t("pageSpeedApiKeyFieldResetLabel")} - - - )} - - - {/* {isAdmin && ( - - - {t("settingsWallet")} - - {t("settingsWalletDescription")} - - - - - - - - - - )} */} - {isAdmin && ( - - - {t("settingsHistoryAndMonitoring")} - - {t("settingsHistoryAndMonitoringDescription")} - - - - - - {t("settingsClearAllStats")} - - - - setIsOpen(deleteStatsMonitorsInitState)} - confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")} - onConfirm={handleClearStats} - isLoading={isLoading || authIsLoading || checksIsLoading} - /> - - )} - {isAdmin && ( - <> - {/* Demo Monitors Section */} - - - {t("settingsDemoMonitors")} - - {t("settingsDemoMonitorsDescription")} - - - - {t("settingsAddDemoMonitors")} - - - - - {/* System Reset Section */} - - - {t("settingsSystemReset")} - - {t("settingsSystemResetDescription")} - - - - {t("settingsRemoveAllMonitors")} - - - setIsOpen(deleteStatsMonitorsInitState)} - confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")} - onConfirm={handleDeleteAllMonitors} - isLoading={isLoading || authIsLoading || checksIsLoading} - /> - - - )} - - - - {t("settingsAbout")} - - - Checkmate {version} - - {t("settingsDevelopedBy")} - - - - - 0} + variant="contained" + color="accent" + sx={{ px: theme.spacing(12), mt: theme.spacing(20) }} + onClick={handleSave} > - - + {t("settingsSave")} + - + ); }; -Settings.propTypes = { - isAdmin: PropTypes.bool, -}; export default Settings; From 10d575a7ce1f2a5605e32387aceedcf0a49b9510 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:29:05 -0700 Subject: [PATCH 19/38] add hooks --- client/src/Hooks/useDeleteMonitorStats.js | 24 ++++++++++ client/src/Hooks/useFetchSettings.js | 55 +++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 client/src/Hooks/useDeleteMonitorStats.js create mode 100644 client/src/Hooks/useFetchSettings.js diff --git a/client/src/Hooks/useDeleteMonitorStats.js b/client/src/Hooks/useDeleteMonitorStats.js new file mode 100644 index 000000000..64138929f --- /dev/null +++ b/client/src/Hooks/useDeleteMonitorStats.js @@ -0,0 +1,24 @@ +import { useState } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const UseDeleteMonitorStats = () => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const deleteMonitorStats = async ({ teamId }) => { + setIsLoading(true); + try { + const res = await networkService.deleteChecksByTeamId({ teamId }); + createToast({ body: t("settingsStatsCleared") }); + } catch (error) { + createToast({ body: t("settingsFailedToClearStats") }); + } finally { + setIsLoading(false); + } + }; + + return [deleteMonitorStats, isLoading]; +}; + +export { UseDeleteMonitorStats }; diff --git a/client/src/Hooks/useFetchSettings.js b/client/src/Hooks/useFetchSettings.js new file mode 100644 index 000000000..15859795f --- /dev/null +++ b/client/src/Hooks/useFetchSettings.js @@ -0,0 +1,55 @@ +import { useState, useEffect } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const useFetchSettings = ({ setSettings }) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + useEffect(() => { + const fetchSettings = async () => { + setIsLoading(true); + try { + const response = await networkService.getAppSettings(); + setSettings(response?.data?.data); + } catch (error) { + createToast({ body: "Failed to fetch settings" }); + setError(error); + } finally { + setIsLoading(false); + } + }; + fetchSettings(); + }, []); + + return [isLoading, error]; +}; + +const useSaveSettings = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const { t } = useTranslation(); + + const saveSettings = async (settings) => { + setIsLoading(true); + try { + await networkService.updateAppSettings({ settings }); + if (settings.checkTTL) { + await networkService.updateChecksTTL({ + ttl: settings.checkTTL, + }); + } + createToast({ body: t("settingsSuccessSaved") }); + } catch (error) { + createToast({ body: t("settingsFailedToSave") }); + + setError(error); + } finally { + setIsLoading(false); + } + }; + + return [isLoading, error, saveSettings]; +}; + +export { useFetchSettings, useSaveSettings }; From f7d780a8be597a5408cc6c76ead859337efad3f9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:09 -0700 Subject: [PATCH 20/38] add email settings component --- client/src/Pages/Settings/SettingsEmail.jsx | 126 ++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 client/src/Pages/Settings/SettingsEmail.jsx diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx new file mode 100644 index 000000000..ecc2a51f2 --- /dev/null +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -0,0 +1,126 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; +const SettingsEmail = ({ + HEADER_SX, + handleChange, + settingsData, + setSettingsData, + isPasswordSet, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const [password, setPassword] = useState(""); + const [hasBeenReset, setHasBeenReset] = useState(false); + + const handlePasswordChange = (e) => { + setPassword(e.target.value); + setSettingsData({ + ...settingsData, + settings: { ...settingsData.settings, systemEmailPassword: e.target.value }, + }); + }; + + return ( + + + {t("settingsEmail")} + {t("settingsEmailDescription")} + + + + + {t("settingsEmailHost")} + + + + {t("settingsEmailPort")} + + + + {t("settingsEmailUser")} + + + + {t("settingsEmailAddress")} + + + {(isPasswordSet === false || hasBeenReset === true) && ( + + {t("settingsEmailPassword")} + } + /> + + )} + {isPasswordSet === true && hasBeenReset === false && ( + + {t("settingsEmailFieldResetLabel")} + + + )} + + + + ); +}; + +SettingsEmail.propTypes = { + settingsData: PropTypes.object, + setSettingsData: PropTypes.func, + handleChange: PropTypes.func, + HEADER_SX: PropTypes.object, + isPasswordSet: PropTypes.bool, +}; + +export default SettingsEmail; From d0ec67236d304b53cfb10e5d5b1dc802a5f0febd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:24 -0700 Subject: [PATCH 21/38] allow systememail user --- server/validation/joi.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/validation/joi.js b/server/validation/joi.js index b28ed3850..997d35d65 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -431,6 +431,7 @@ const updateAppSettingsBodyValidation = joi.object({ systemEmailPort: joi.number().allow(""), systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), + systemEmailUser: joi.string().allow(""), }); //**************************************** From 0a723572a2de8c57925263287f85e5c1d243880c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:44 -0700 Subject: [PATCH 22/38] move transporter creation --- server/service/emailService.js | 49 +++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/server/service/emailService.js b/server/service/emailService.js index c9e34eeda..57f8c78e6 100755 --- a/server/service/emailService.js +++ b/server/service/emailService.js @@ -29,7 +29,10 @@ class EmailService { this.mjml2html = mjml2html; this.nodemailer = nodemailer; this.logger = logger; + this.init(); + } + init = async () => { /** * Loads an email template from the filesystem. * @@ -67,34 +70,14 @@ class EmailService { serverIsUpTemplate: this.loadTemplate("serverIsUp"), passwordResetTemplate: this.loadTemplate("passwordReset"), hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"), - testEmailTemplate: this.loadTemplate("testEmailTemplate") + testEmailTemplate: this.loadTemplate("testEmailTemplate"), }; /** * The email transporter used to send emails. * @type {Object} */ - - const { - systemEmailHost, - systemEmailPort, - systemEmailUser, - systemEmailAddress, - systemEmailPassword, - } = this.settingsService.getSettings(); - - const emailConfig = { - host: systemEmailHost, - port: systemEmailPort, - secure: true, - auth: { - user: systemEmailUser || systemEmailAddress, - pass: systemEmailPassword, - }, - }; - - this.transporter = this.nodemailer.createTransport(emailConfig); - } + }; /** * Asynchronously builds and sends an email using a specified template and context. @@ -106,6 +89,28 @@ class EmailService { * @returns {Promise} A promise that resolves to the messageId of the sent email. */ buildAndSendEmail = async (template, context, to, subject) => { + // TODO - Consider an update transporter method so this only needs to be recreated when smtp settings change + const { + systemEmailHost, + systemEmailPort, + systemEmailUser, + systemEmailAddress, + systemEmailPassword, + } = await this.settingsService.getDBSettings(); + + const emailConfig = { + host: systemEmailHost, + port: systemEmailPort, + secure: true, + auth: { + user: systemEmailUser || systemEmailAddress, + pass: systemEmailPassword, + }, + connectionTimeout: 5000, + }; + + this.transporter = this.nodemailer.createTransport(emailConfig); + const buildHtml = async (template, context) => { try { const mjml = this.templateLookup[template](context); From 0f80524127b80bb757126145cc7e0aea4ee206ce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:53:03 -0700 Subject: [PATCH 23/38] unset email password --- server/db/mongo/modules/settingsModule.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js index 2c1b402c3..d3568ad32 100755 --- a/server/db/mongo/modules/settingsModule.js +++ b/server/db/mongo/modules/settingsModule.js @@ -21,6 +21,11 @@ const updateAppSettings = async (newSettings) => { delete update.$set.pagespeedApiKey; } + if (newSettings.systemEmailPassword === "") { + update.$unset = { systemEmailPassword: "" }; + delete update.$set.systemEmailPassword; + } + const settings = await AppSettings.findOneAndUpdate({}, update, { new: true, upsert: true, From fcd6af259eb873b12fe1c1e3b75d746f6da695a8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:53:20 -0700 Subject: [PATCH 24/38] add optional email user --- server/db/models/AppSettings.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js index 1113bbe46..06ba8c1df 100755 --- a/server/db/models/AppSettings.js +++ b/server/db/models/AppSettings.js @@ -25,6 +25,9 @@ const AppSettingsSchema = mongoose.Schema( systemEmailPassword: { type: String, }, + systemEmailUser: { + type: String, + }, singleton: { type: Boolean, required: true, From 3ccfc462c11e6fecddb721049a92e3e6ff450d69 Mon Sep 17 00:00:00 2001 From: mohadeseh safari Date: Fri, 9 May 2025 17:53:21 -0400 Subject: [PATCH 25/38] Transalated all strings in TeamPanel --- .../TabPanels/Account/TeamPanel.jsx | 40 +++++++++---------- client/src/locales/gb.json | 23 +++++++++++ client/src/locales/ru.json | 23 +++++++++++ client/src/locales/tr.json | 23 +++++++++++ 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/client/src/Components/TabPanels/Account/TeamPanel.jsx b/client/src/Components/TabPanels/Account/TeamPanel.jsx index 9448c632b..fe4b201dd 100644 --- a/client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -40,7 +40,7 @@ const TeamPanel = () => { const headers = [ { id: "name", - content: "Name", + content: t("teamPanel.table.name"), render: (row) => { return ( @@ -48,16 +48,16 @@ const TeamPanel = () => { {row.firstName + " " + row.lastName} - Created {new Date(row.createdAt).toLocaleDateString()} + {t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()} ); }, }, - { id: "email", content: "Email", render: (row) => row.email }, + { id: "email", content: t("teamPanel.table.email"), render: (row) => row.email }, { id: "role", - content: "Role", + content: t("teamPanel.table.role"), render: (row) => row.role, }, ]; @@ -198,7 +198,7 @@ const TeamPanel = () => { spellCheck="false" gap={SPACING_GAP} > - Team members + {t("teamPanel.teamMembers")} { filled={(filter === "all").toString()} onClick={() => setFilter("all")} > - All + {t("teamPanel.filter.all")} @@ -238,22 +238,20 @@ const TeamPanel = () => { color="accent" onClick={() => setIsOpen(true)} > - Invite a team member + {t("teamPanel.inviteTeamMember")} { marginBottom={SPACING_GAP} type="email" id="input-team-member" - placeholder="Email" + placeholder={t("teamPanel.email")} value={toInvite.email} onChange={handleChange} error={errors.email ? true : false} @@ -270,7 +268,7 @@ const TeamPanel = () => { />