Files
Checkmate/client/src/Validation/validation.js

456 lines
13 KiB
JavaScript

import joi from "joi";
import dayjs from "dayjs";
const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number.";
const nameSchema = joi
.string()
.max(50)
.trim()
.pattern(/^[\p{L}\p{M}''()\-\. ]+$/u)
.messages({
"string.empty": "auth.common.inputs.firstName.errors.empty",
"string.max": "auth.common.inputs.firstName.errors.length",
"string.pattern.base": "auth.common.inputs.firstName.errors.pattern",
});
const lastnameSchema = joi
.string()
.max(50)
.trim()
.pattern(/^[\p{L}\p{M}''()\-\. ]+$/u)
.messages({
"string.empty": "auth.common.inputs.lastName.errors.empty",
"string.max": "auth.common.inputs.lastName.errors.length",
"string.pattern.base": "auth.common.inputs.lastName.errors.pattern",
});
const newPasswordSchema = joi
.string()
.trim()
.min(8)
.custom((value, helpers) => {
if (!/[A-Z]/.test(value)) {
return helpers.error("uppercase");
}
return value;
})
.custom((value, helpers) => {
if (!/[a-z]/.test(value)) {
return helpers.error("lowercase");
}
return value;
})
.custom((value, helpers) => {
if (!/\d/.test(value)) {
return helpers.error("number");
}
return value;
})
.custom((value, helpers) => {
if (!/[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/.test(value)) {
return helpers.error("special");
}
return value;
})
.messages({
"string.empty": "auth.common.inputs.password.errors.empty",
"string.min": "auth.common.inputs.password.errors.length",
uppercase: "auth.common.inputs.password.errors.uppercase",
lowercase: "auth.common.inputs.password.errors.lowercase",
number: "auth.common.inputs.password.errors.number",
special: "auth.common.inputs.password.errors.special",
});
const newOrChangedCredentials = joi.object({
firstName: nameSchema,
lastName: lastnameSchema,
email: joi
.string()
.trim()
.email({ tlds: { allow: false } })
.lowercase()
.messages({
"string.empty": "auth.common.inputs.email.errors.empty",
"string.email": "auth.common.inputs.email.errors.invalid",
}),
password: newPasswordSchema,
newPassword: newPasswordSchema,
confirm: joi
.string()
.trim()
.custom((value, helpers) => {
const { password } = helpers.prefs.context;
if (value !== password) {
return helpers.error("different");
}
return value;
})
.messages({
"string.empty": "auth.common.inputs.passwordConfirm.errors.empty",
different: "auth.common.inputs.passwordConfirm.errors.different",
}),
role: joi.array(),
teamId: joi.string().allow("").optional(),
inviteToken: joi.string().allow(""),
});
const loginCredentials = joi.object({
email: joi
.string()
.trim()
.email({ tlds: { allow: false } })
.lowercase()
.messages({
"string.empty": "auth.common.inputs.email.errors.empty",
"string.email": "auth.common.inputs.email.errors.invalid",
}),
password: joi.string().messages({
"string.empty": "auth.common.inputs.password.errors.empty",
}),
});
const monitorValidation = joi.object({
_id: joi.string(),
userId: joi.string(),
teamId: joi.string(),
url: joi.when("type", {
is: "docker",
then: joi
.string()
.trim()
.regex(/^[a-z0-9]{64}$/)
.messages({
"string.empty": "This field is required.",
"string.pattern.base": "Please enter a valid 64-character Docker container ID.",
}),
otherwise: joi
.string()
.trim()
.custom((value, helpers) => {
// Regex from https://gist.github.com/dperini/729294
var urlRegex = new RegExp(
"^" +
// protocol identifier (optional)
// short syntax // still required
"(?:(?:https?|ftp):\\/\\/)?" +
// user:pass BasicAuth (optional)
"(?:" +
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
// excludes network & broadcast addresses
// (first & last IP address of each class)
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
"|" +
// host & domain names, may end with dot
// can be replaced by a shortest alternative
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
"(?:" +
"(?:" +
"[a-z0-9\\u00a1-\\uffff]" +
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
")?" +
"[a-z0-9\\u00a1-\\uffff]\\." +
")+" +
// TLD identifier name, may end with dot
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
")" +
// port number (optional)
"(?::\\d{2,5})?" +
// resource path (optional)
"(?:[/?#]\\S*)?" +
"$",
"i"
);
if (!urlRegex.test(value)) {
return helpers.error("string.invalidUrl");
}
return value;
})
.messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
"string.invalidUrl": "Please enter a valid URL with optional port",
}),
}),
port: joi
.number()
.integer()
.min(1)
.max(65535)
.when("type", {
is: "port",
then: joi.number().messages({
"number.base": "Port must be a number.",
"number.min": "Port must be at least 1.",
"number.max": "Port must be at most 65535.",
"any.required": "Port is required for port monitors.",
}),
otherwise: joi.optional(),
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
type: joi.string().trim().messages({ "string.empty": "This field is required." }),
ignoreTlsErrors: joi.boolean(),
interval: joi.number().messages({
"number.base": "Frequency must be a number.",
"any.required": "Frequency is required.",
}),
expectedValue: joi.string().allow(null, ""),
jsonPath: joi.string().allow(null, ""),
matchMethod: joi.string().allow(null, ""),
});
const imageValidation = joi.object({
type: joi.string().valid("image/jpeg", "image/png").messages({
"any.only": "Invalid file format.",
"string.empty": "File type required.",
}),
size: joi
.number()
.max(3 * 1024 * 1024)
.messages({
"number.base": "File size must be a number.",
"number.max": "File size must be less than 3 MB.",
"number.empty": "File size required.",
}),
});
const logoImageValidation = joi
.object({
src: joi.string(),
name: joi.string(),
type: joi
.string()
.valid("image/jpeg", "image/png")
.allow(null) // Allow null and empty string
.messages({
"any.only": "Invalid file format.",
"string.empty": "File type required.",
})
.optional(),
size: joi
.number()
.max(3000000)
.allow(null) // Allow null and empty string
.messages({
"number.base": "File size must be a number.",
"number.max": "File size must be less than 3MB.",
"number.empty": "File size required.",
})
.optional(),
})
.allow(null)
.optional(); // Make entire object optional
const statusPageValidation = joi.object({
type: joi.string().valid("uptime").required(),
isPublished: joi.bool(),
companyName: joi
.string()
.trim()
.messages({ "string.empty": "Company name is required." }),
url: joi
.string()
.pattern(/^[a-zA-Z0-9_-]+$/) // Only allow alphanumeric, underscore, and hyphen
.required()
.messages({
"string.pattern.base":
"URL can only contain letters, numbers, underscores, and hyphens",
}),
timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }),
color: joi.string().trim().messages({ "string.empty": "Color is required." }),
theme: joi.string(),
monitors: joi.array().min(1).required().messages({
"string.pattern.base": "Must be a valid monitor ID",
"array.base": "Monitors must be an array",
"array.min": "At least one monitor is required",
"array.empty": "At least one monitor is required",
"any.required": "At least one monitor is required",
}),
subMonitors: joi.array().optional(),
logo: logoImageValidation,
showUptimePercentage: joi.boolean(),
showCharts: joi.boolean(),
showAdminLoginLink: joi.boolean(),
});
const settingsValidation = joi.object({
checkTTL: joi.number().required().messages({
"string.empty": "Please enter a value",
"number.base": "Please enter a valid number",
"any.required": "Please enter a value",
}),
pagespeedApiKey: joi.string().allow("").optional(),
language: joi.string().required(),
timezone: joi.string().allow("").optional(),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(null, ""),
systemEmailSecure: joi.boolean().optional(),
systemEmailPool: joi.boolean().optional(),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
systemEmailConnectionHost: joi.string().allow("").optional(),
systemEmailTLSServername: joi.string().allow(""),
systemEmailIgnoreTLS: joi.boolean(),
systemEmailRequireTLS: joi.boolean(),
systemEmailRejectUnauthorized: joi.boolean(),
});
const dayjsValidator = (value, helpers) => {
if (!dayjs(value).isValid()) {
return helpers.error("any.invalid");
}
return value;
};
const maintenanceWindowValidation = joi.object({
repeat: joi.string(),
startDate: joi.custom(dayjsValidator, "Day.js date validation"),
startTime: joi.custom(dayjsValidator, "Day.js date validation"),
duration: joi.number().integer().min(0),
durationUnit: joi.string(),
name: joi.string(),
monitors: joi.array().min(1),
});
const advancedSettingsValidation = joi.object({
apiBaseUrl: joi.string().uri({ allowRelative: true }).trim().messages({
"string.empty": "API base url is required.",
"string.uri": "The URL you provided is not valid.",
}),
logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(null, ""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailConnectionHost: joi.string().allow(""),
jwtTTLNum: joi.number().messages({
"number.base": "JWT TTL is required.",
}),
jwtTTLUnits: joi
.string()
.trim()
.custom((value, helpers) => {
if (!["days", "hours"].includes(value)) {
return helpers.message("JWT TTL unit is required.");
}
return value;
}),
dbType: joi.string().trim().messages({
"string.empty": "DB type is required.",
}),
redisHost: joi.string().trim().messages({
"string.empty": "Redis host is required.",
}),
redisPort: joi.number().allow(null, ""),
pagespeedApiKey: joi.string().allow(""),
});
const infrastructureMonitorValidation = joi.object({
url: joi
.string()
.trim()
.custom((value, helpers) => {
const urlRegex =
/^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;
if (!urlRegex.test(value)) {
return helpers.error("string.invalidUrl");
}
return value;
})
.messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
"string.invalidUrl": "Please enter a valid URL with optional port",
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
secret: joi.string().trim().messages({ "string.empty": "This field is required." }),
usage_cpu: joi.number().messages({
"number.base": THRESHOLD_COMMON_BASE_MSG,
}),
cpu: joi.boolean(),
memory: joi.boolean(),
disk: joi.boolean(),
temperature: joi.boolean(),
usage_memory: joi.number().messages({
"number.base": THRESHOLD_COMMON_BASE_MSG,
}),
usage_disk: joi.number().messages({
"number.base": THRESHOLD_COMMON_BASE_MSG,
}),
usage_temperature: joi.number().messages({
"number.base": "Temperature must be a number.",
}),
// usage_system: joi.number().messages({
// "number.base": "System load must be a number.",
// }),
// usage_swap: joi.number().messages({
// "number.base": "Swap used must be a number.",
// }),
interval: joi.number().messages({
"number.base": "Frequency must be a number.",
"any.required": "Frequency is required.",
}),
notifications: joi.array().items(joi.string()),
});
const notificationValidation = joi.object({
notificationName: joi.string().required().messages({
"string.empty": "Notification name is required",
"any.required": "Notification name is required",
}),
type: joi
.string()
.valid("email", "webhook", "slack", "discord", "pager_duty")
.required()
.messages({
"string.empty": "Notification type is required",
"any.required": "Notification type is required",
"any.only": "Notification type must be email, webhook, or pager_duty",
}),
address: joi.when("type", {
is: "email",
then: joi
.string()
.email({ tlds: { allow: false } })
.required()
.messages({
"string.empty": "E-mail address cannot be empty",
"any.required": "E-mail address is required",
"string.email": "Please enter a valid e-mail address",
}),
otherwise: joi.string().uri().required().messages({
"string.empty": "Webhook URL cannot be empty",
"any.required": "Webhook URL is required",
"string.uri": "Please enter a valid Webhook URL",
}),
}),
});
export {
newOrChangedCredentials,
loginCredentials,
imageValidation,
monitorValidation,
settingsValidation,
maintenanceWindowValidation,
advancedSettingsValidation,
infrastructureMonitorValidation,
statusPageValidation,
logoImageValidation,
notificationValidation,
};