-
Notifications
You must be signed in to change notification settings - Fork 209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat/be/status page implementation #1138
Changes from 8 commits
cc0ecf2
505de31
285fbf2
f3a29c3
8164ca2
b6781a4
6b04f9a
a27cadc
54fca68
75ca6f3
5817698
a4a01b0
1476e9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { handleError, handleValidationError } from "./controllerUtils.js"; | ||
import { | ||
createStatusPageBodyValidation, | ||
getStatusPageParamValidation, | ||
} from "../validation/joi.js"; | ||
import { successMessages } from "../utils/messages.js"; | ||
|
||
const SERVICE_NAME = "statusPageController"; | ||
|
||
const createStatusPage = async (req, res, next) => { | ||
try { | ||
await createStatusPageBodyValidation.validateAsync(req.body); | ||
} catch (error) { | ||
next(handleValidationError(error, SERVICE_NAME)); | ||
return; | ||
} | ||
|
||
try { | ||
const statusPage = await req.db.createStatusPage(req.body); | ||
return res.status(200).json({ | ||
success: true, | ||
msg: successMessages.STATUS_PAGE_CREATE, | ||
data: statusPage, | ||
}); | ||
} catch (error) { | ||
next(handleError(error, SERVICE_NAME, "createStatusPage")); | ||
} | ||
}; | ||
|
||
const getStatusPageByUrl = async (req, res, next) => { | ||
try { | ||
await getStatusPageParamValidation.validateAsync(req.params); | ||
} catch (error) { | ||
next(handleValidationError(error, SERVICE_NAME)); | ||
return; | ||
} | ||
|
||
try { | ||
const { url } = req.params; | ||
const statusPage = await req.db.getStatusPageByUrl(url); | ||
return res.status(200).json({ | ||
success: true, | ||
msg: successMessages.STATUS_PAGE_BY_URL, | ||
data: statusPage, | ||
}); | ||
} catch (error) { | ||
next(handleError(error, SERVICE_NAME, "getStatusPage")); | ||
} | ||
}; | ||
|
||
export { createStatusPage, getStatusPageByUrl }; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||||||
import mongoose from "mongoose"; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const StatusPageSchema = mongoose.Schema( | ||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||
companyName: { | ||||||||||||||||||||||||||||||||||
type: String, | ||||||||||||||||||||||||||||||||||
required: true, | ||||||||||||||||||||||||||||||||||
default: "", | ||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
url: { | ||||||||||||||||||||||||||||||||||
type: String, | ||||||||||||||||||||||||||||||||||
required: true, | ||||||||||||||||||||||||||||||||||
default: "", | ||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
timezone: { | ||||||||||||||||||||||||||||||||||
type: String, | ||||||||||||||||||||||||||||||||||
required: true, | ||||||||||||||||||||||||||||||||||
default: "America/Toronto", | ||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
color: { | ||||||||||||||||||||||||||||||||||
type: String, | ||||||||||||||||||||||||||||||||||
required: true, | ||||||||||||||||||||||||||||||||||
default: "#4169E1", | ||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
Comment on lines
+21
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Color field's got no chill! 🍝 The color field needs validation to ensure it contains valid hex color codes. Let's make this validation cleaner than your sweater: color: {
type: String,
required: true,
default: "#4169E1",
+ validate: {
+ validator: function(v) {
+ return /^#[0-9A-F]{6}$/i.test(v);
+ },
+ message: props => `${props.value} is not a valid hex color code!`
+ }
}, 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
theme: { | ||||||||||||||||||||||||||||||||||
type: String, | ||||||||||||||||||||||||||||||||||
required: true, | ||||||||||||||||||||||||||||||||||
default: "light", | ||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
Comment on lines
+26
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Theme field's not ready for the spotlight! 🍝 The theme field should be restricted to specific valid options rather than accepting any string. Here's an improvement that'll make your knees weak: theme: {
type: String,
required: true,
default: "light",
+ enum: {
+ values: ["light", "dark"],
+ message: '{VALUE} is not a supported theme'
+ }
}, 📝 Committable suggestion
Suggested change
Comment on lines
+26
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to store the theme as well? I thought that was dealt with locally There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @marcelluscaio yes I'm not quite sure of the usage of this one either, I just followed the spec and there was a If it turns out we don't need this we can remove it when we're clear on its usage 👍 Thanks for being vigilant! |
||||||||||||||||||||||||||||||||||
monitors: [ | ||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||
type: mongoose.Schema.Types.ObjectId, | ||||||||||||||||||||||||||||||||||
ref: "Monitor", | ||||||||||||||||||||||||||||||||||
required: true, | ||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
], | ||||||||||||||||||||||||||||||||||
Comment on lines
+31
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion These monitor refs need some indexes to perform! 🍝 The Add this index like you're dropping bars: monitors: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
required: true,
},
],
+}, {
+ timestamps: true,
+ indexes: [{ monitors: 1 }]
|
||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
{ timestamps: true } | ||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
export default mongoose.model("StatusPage", StatusPageSchema); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import StatusPage from "../../models/StatusPage.js"; | ||
import { errorMessages } from "../../../utils/messages.js"; | ||
|
||
const SERVICE_NAME = "statusPageModule"; | ||
|
||
const createStatusPage = async (statusPageData) => { | ||
try { | ||
const isUnique = await urlIsUnique(statusPageData.url); | ||
if (!isUnique) { | ||
const error = new Error(errorMessages.STATUS_PAGE_URL_NOT_UNIQUE); | ||
error.status = 400; | ||
throw error; | ||
} | ||
const statusPage = new StatusPage({ ...statusPageData }); | ||
await statusPage.save(); | ||
return statusPage; | ||
} catch (error) { | ||
error.service = SERVICE_NAME; | ||
error.method = "createStatusPage"; | ||
throw error; | ||
} | ||
}; | ||
|
||
const getStatusPageByUrl = async (url) => { | ||
try { | ||
const statusPage = await StatusPage.findOne({ url }); | ||
if (statusPage === null || statusPage === undefined) { | ||
const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND); | ||
error.status = 404; | ||
|
||
throw error; | ||
} | ||
return statusPage; | ||
} catch (error) { | ||
error.service = SERVICE_NAME; | ||
error.method = "getStatusPageByUrl"; | ||
throw error; | ||
} | ||
}; | ||
|
||
const urlIsUnique = async (url) => { | ||
const statusPage = await StatusPage.find({ url }); | ||
if (statusPage.length > 0) return false; | ||
return true; | ||
}; | ||
|
||
export { createStatusPage, getStatusPageByUrl }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import express from "express"; | ||
import { verifyJWT } from "../middleware/verifyJWT.js"; | ||
import { | ||
createStatusPage, | ||
getStatusPageByUrl, | ||
} from "../controllers/statusPageController.js"; | ||
const router = express.Router(); | ||
|
||
router.get("/:url", getStatusPageByUrl); | ||
router.post("/:url", verifyJWT, createStatusPage); | ||
|
||
export default router; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -51,6 +51,10 @@ const errorMessages = { | |||||||||||||||||||||
|
||||||||||||||||||||||
// PING Operations | ||||||||||||||||||||||
PING_CANNOT_RESOLVE: "No response", | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Status Page Errors | ||||||||||||||||||||||
STATUS_PAGE_NOT_FOUND: "Status page not found", | ||||||||||||||||||||||
STATUS_PAGE_URL_NOT_UNIQUE: "Status page url must be unique", | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
const successMessages = { | ||||||||||||||||||||||
|
@@ -115,6 +119,10 @@ const successMessages = { | |||||||||||||||||||||
// App Settings | ||||||||||||||||||||||
GET_APP_SETTINGS: "Got app settings successfully", | ||||||||||||||||||||||
UPDATE_APP_SETTINGS: "Updated app settings successfully", | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Status Page | ||||||||||||||||||||||
STATUS_PAGE_BY_URL: "Got status page by url successfully", | ||||||||||||||||||||||
STATUS_PAGE_CREATE: "Status page created successfully", | ||||||||||||||||||||||
Comment on lines
+122
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Yo, we're missing some success messages for common operations! While the current success messages are on point, we should add messages for other standard CRUD operations to maintain consistency. Add these success messages to complete the set: // Status Page
STATUS_PAGE_BY_URL: "Got status page by url successfully",
STATUS_PAGE_CREATE: "Status page created successfully",
+STATUS_PAGE_UPDATE: "Status page updated successfully",
+STATUS_PAGE_DELETE: "Status page deleted successfully",
+STATUS_PAGE_GET_ALL: "Got all status pages successfully", 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export { errorMessages, successMessages }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -410,6 +410,32 @@ const updateAppSettingsBodyValidation = joi.object({ | |
systemEmailPassword: joi.string().allow(""), | ||
}); | ||
|
||
//**************************************** | ||
// Status Page Validation | ||
//**************************************** | ||
|
||
const getStatusPageParamValidation = joi.object({ | ||
url: joi.string().required(), | ||
}); | ||
Comment on lines
+417
to
+419
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add URL format validation to prevent invalid URLs. The URL validation could be strengthened by adding format checks. const getStatusPageParamValidation = joi.object({
- url: joi.string().required(),
+ url: joi.string().uri().lowercase().required()
+ .pattern(/^[a-z0-9-]+$/)
+ .messages({
+ 'string.uri': 'URL must be a valid URI',
+ 'string.pattern.base': 'URL can only contain lowercase letters, numbers, and hyphens'
+ }),
});
|
||
|
||
const createStatusPageBodyValidation = joi.object({ | ||
companyName: joi.string().required(), | ||
url: joi.string().required(), | ||
timezone: joi.string().required(), | ||
color: joi.string().required(), | ||
theme: joi.string().required(), | ||
monitors: joi | ||
.array() | ||
.items(joi.string().pattern(/^[0-9a-fA-F]{24}$/)) | ||
.required() | ||
.messages({ | ||
"string.pattern.base": "Must be a valid monitor ID", | ||
"array.base": "Monitors must be an array", | ||
"array.empty": "At least one monitor is required", | ||
"any.required": "Monitors are required", | ||
}), | ||
}); | ||
|
||
export { | ||
roleValidatior, | ||
loginValidation, | ||
|
@@ -465,4 +491,6 @@ export { | |
editMaintenanceWindowByIdParamValidation, | ||
editMaintenanceByIdWindowBodyValidation, | ||
updateAppSettingsBodyValidation, | ||
createStatusPageBodyValidation, | ||
getStatusPageParamValidation, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Timezone validation's weak, arms are heavy! 🍝
The timezone field accepts any string, which could lead to invalid timezone values being stored. We should validate against a list of valid IANA timezone identifiers.
Drop this validation like it's hot:
📝 Committable suggestion