Merge branch 'master' into feat/ping

This commit is contained in:
Alexander Holliday
2024-06-15 20:05:14 -07:00
committed by GitHub
12 changed files with 216 additions and 64 deletions

View File

@@ -17,6 +17,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Design Guidelines][#design-guidelines]
- [Styleguides](#Developer-guide-for-issue-lifecycle-process)
@@ -54,7 +55,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Version of the interpreter, compiler, SDK, runtime environment and package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
@@ -80,15 +81,11 @@ Once it's filed:
This section guides you through submitting an enhancement suggestion for CONTRIBUTING.md, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](/issues).
@@ -99,6 +96,15 @@ Enhancement suggestions are tracked as [GitHub issues](/issues).
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to.
- **Explain why this enhancement would be useful** to most CONTRIBUTING.md users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
### Design guidelines
We have a Figma file that includes:
- All the dashboard elements and components
- The design guideline for the app
You can see it [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). Since it is read-only, we encourage you to copy to your own Figma page, then work on it.
### Developer Guide for Issue Lifecycle Process
[This document](https://docs.google.com/document/d/1Gy3LiimGUNoSiWAMbwyK3SeMADcCMjCLu6cQYoawtSE/edit#heading=h.1lj2lgut6m7h) outlines the process every developer should follow for managing the issues lifecycle.

View File

@@ -10,7 +10,18 @@
# BlueWave Uptime
BlueWave uptime monitoring application
BlueWave Uptime is an open source server monitoring application. It is a tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performing optimally, providing real-time alerts and reports on the availability, downtime, and response time of the monitored services.
## Contributing
You are welcome to provide contributions to the project. It uses React on the FE, and Nodejs and MongoDB on the BE, hence if you are comfortable with working with those technologies, you are encouraged to send your PRs. Please read [Contributor's guideline](https://github.com/bluewave-labs/bluewave-uptime/blob/master/CONTRIBUTING.md)
Note that We have a Figma file that includes:
- All the dashboard elements and components
- The design guideline for the app
You can see the designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). Since it is read-only, we encourage you to copy to your own Figma page, then work on it.
## Getting Started

View File

@@ -10,7 +10,7 @@ const {
editAlertBodyValidation,
deleteAlertParamValidation,
} = require("../validation/joi");
const { successMessages } = require("../utils/messages");
const SERVICE_NAME = "alerts";
/**
@@ -38,7 +38,7 @@ const createAlert = async (req, res, next) => {
const alert = await req.db.createAlert(alertData);
return res
.status(200)
.json({ success: true, msg: "Alert created", data: alert });
.json({ success: true, msg: successMessages.ALERT_CREATE, data: alert });
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -66,9 +66,11 @@ const getAlertsByUserId = async (req, res, next) => {
try {
const alerts = await req.db.getAlertsByUserId(req.params.userId);
return res
.status(200)
.json({ success: true, msg: "Got alerts", data: alerts });
return res.status(200).json({
success: true,
msg: successMessages.ALERT_GET_BY_USER,
data: alerts,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -90,7 +92,7 @@ const getAlertsByMonitorId = async (req, res, next) => {
const alerts = await req.db.getAlertsByMonitorId(req.params.monitorId);
return res.status(200).json({
success: true,
msg: "Got alerts by Monitor",
msg: successMessages.ALERT_GET_BY_MONITOR,
data: alerts,
});
} catch (error) {
@@ -114,7 +116,7 @@ const getAlertById = async (req, res, next) => {
const alert = await req.db.getAlertById(req.params.alertId);
return res.status(200).json({
success: true,
msg: "Got Alert By alertID",
msg: successMessages.ALERT_GET_BY_ID,
data: alert,
});
} catch (error) {
@@ -141,7 +143,7 @@ const editAlert = async (req, res, next) => {
const alert = await req.db.editAlert(req.params.alertId, alertData);
return res.status(200).json({
success: true,
msg: "Edited alert",
msg: successMessages.ALERT_EDIT,
data: alert,
});
} catch (error) {
@@ -164,7 +166,7 @@ const deleteAlert = async (req, res, next) => {
const deleted = await req.db.deleteAlert(req.params.alertId);
return res.status(200).json({
success: true,
msg: "Deleted alert",
msg: successMessages.ALERT_DELETE,
data: deleted,
});
} catch (error) {

View File

@@ -10,6 +10,7 @@ const {
} = require("../validation/joi");
const logger = require("../utils/logger");
require("dotenv").config();
const { errorMessages, successMessages } = require("../utils/messages");
var jwt = require("jsonwebtoken");
const SERVICE_NAME = "auth";
const { sendEmail } = require("../utils/sendEmail");
@@ -49,7 +50,7 @@ const registerController = async (req, res, next) => {
// Create a new user
try {
const newUser = await req.db.insertUser(req, res);
logger.info("New user created!", {
logger.info(successMessages.AUTH_CREATE_USER, {
service: SERVICE_NAME,
userId: newUser._id,
});
@@ -64,9 +65,11 @@ const registerController = async (req, res, next) => {
"Registered."
);
return res
.status(200)
.json({ success: true, msg: "User created", data: token });
return res.status(200).json({
success: true,
msg: successMessages.AUTH_CREATE_USER,
data: token,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -92,7 +95,7 @@ const loginController = async (req, res, next) => {
const match = await user.comparePassword(req.body.password);
if (match !== true) {
throw new Error("Incorrect password");
throw new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
}
// Remove password from user object. Should this be abstracted to DB layer?
@@ -101,9 +104,11 @@ const loginController = async (req, res, next) => {
// Happy path, return token
const token = issueToken(userWithoutPassword);
return res
.status(200)
.json({ success: true, msg: "Found user", data: token });
return res.status(200).json({
success: true,
msg: successMessages.AUTH_LOGIN_USER,
data: token,
});
} catch (error) {
error.status = 500;
// Anything else should be an error
@@ -124,7 +129,7 @@ const userEditController = async (req, res, next) => {
}
if (req.params.userId !== req.user._id.toString()) {
const error = new Error("Unauthorized access");
const error = new Error(errorMessages.AUTH_UNAUTHORIZED);
error.status = 401;
error.service = SERVICE_NAME;
next(error);
@@ -133,9 +138,11 @@ const userEditController = async (req, res, next) => {
try {
const updatedUser = await req.db.updateUser(req, res);
return res
.status(200)
.json({ success: true, msg: "User updated", data: updatedUser });
return res.status(200).json({
success: true,
msg: successMessages.AUTH_UPDATE_USER,
data: updatedUser,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -166,7 +173,7 @@ const recoveryRequestController = async (req, res, next) => {
);
return res.status(200).json({
success: true,
msg: "Created recovery token",
msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN,
});
}
// TODO Email token to user
@@ -193,7 +200,7 @@ const validateRecoveryTokenController = async (req, res, next) => {
// TODO Redirect user to reset password after validating token
return res.status(200).json({
success: true,
msg: "Token is valid",
msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN,
});
} catch (error) {
error.service = SERVICE_NAME;
@@ -216,7 +223,13 @@ const resetPasswordController = async (req, res, next) => {
try {
await newPasswordValidation.validateAsync(req.body);
user = await req.db.resetPassword(req, res);
res.status(200).json({ success: true, msg: "Password reset", data: user });
res
.status(200)
.json({
success: true,
msg: successMessages.AUTH_RESET_PASSWORD,
data: user,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);

View File

@@ -4,6 +4,7 @@ const {
getChecksParamValidation,
deleteChecksParamValidation,
} = require("../validation/joi");
const { successMessages } = require("../utils/messages");
const SERVICE_NAME = "check";
const createCheck = async (req, res, next) => {
@@ -23,7 +24,7 @@ const createCheck = async (req, res, next) => {
const check = await req.db.createCheck(checkData);
return res
.status(200)
.json({ success: true, msg: "Check created", data: check });
.json({ success: true, msg: successMessages.CHECK_CREATE, data: check });
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -45,7 +46,7 @@ const getChecks = async (req, res, next) => {
const checks = await req.db.getChecks(req.params.monitorId);
return res
.status(200)
.json({ success: true, msg: "Checks retrieved", data: checks });
.json({ success: true, msg: successMessages.CHECK_GET, data: checks });
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -67,7 +68,11 @@ const deleteChecks = async (req, res, next) => {
const deletedCount = await req.db.deleteChecks(req.params.monitorId);
return res
.status(200)
.json({ success: true, msg: "Checks deleted", data: { deletedCount } });
.json({
success: true,
msg: successMessages.CHECK_DELETE,
data: { deletedCount },
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);

View File

@@ -5,6 +5,8 @@ const {
} = require("../validation/joi");
const SERVICE_NAME = "monitorController";
const { errorMessages, successMessages } = require("../utils/messages");
const { error } = require("winston");
/**
* Returns all monitors
@@ -17,7 +19,11 @@ const SERVICE_NAME = "monitorController";
const getAllMonitors = async (req, res, next) => {
try {
const monitors = await req.db.getAllMonitors();
return res.json({ success: true, msg: "Monitors found", data: monitors });
return res.json({
success: true,
msg: successMessages.MONITOR_GET_ALL,
data: monitors,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -45,12 +51,16 @@ const getMonitorById = async (req, res, next) => {
try {
const monitor = await req.db.getMonitorById(req, res);
if (!monitor) {
const error = new Error("Monitor not found");
const error = new Error(errorMessages.MONITOR_GET_BY_ID);
error.status = 404;
throw error;
}
return res.json({ success: true, msg: "Monitor found", data: monitor });
return res.json({
success: true,
msg: successMessages.MONTIOR_GET_BY_ID,
data: monitor,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -78,14 +88,14 @@ const getMonitorsByUserId = async (req, res, next) => {
const monitors = await req.db.getMonitorsByUserId(req, res);
if (monitors && monitors.length === 0) {
const err = new Error("No monitors found");
const err = new Error(errorMessages.MONITOR_GET_BY_USER_ID);
err.status = 404;
throw err;
}
return res.json({
success: true,
msg: `Monitors for user ${userId} found`,
msg: successMessages.MONITOR_GET_BY_USER_ID(userId),
data: monitors,
});
} catch (error) {
@@ -118,9 +128,11 @@ const createMonitor = async (req, res, next) => {
const monitor = await req.db.createMonitor(req, res);
// Add monitor to job queue
req.jobQueue.addJob(monitor._id, monitor);
return res
.status(201)
.json({ success: true, msg: "Monitor created", data: monitor });
return res.status(201).json({
success: true,
msg: successMessages.MONITOR_CREATE,
data: monitor,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -157,7 +169,9 @@ const deleteMonitor = async (req, res, next) => {
* when it is deleted so there is no orphaned data
* We also need to make sure to stop all running services for this monitor
*/
return res.status(200).json({ success: true, msg: "Monitor deleted" });
return res
.status(200)
.json({ success: true, msg: successMessages.MONITOR_DELETE });
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -188,7 +202,11 @@ const editMonitor = async (req, res, next) => {
const editedMonitor = await req.db.editMonitor(req, res);
return res
.status(200)
.json({ success: true, msg: "Monitor edited", data: editedMonitor });
.json({
success: true,
msg: successMessages.MONITOR_EDIT,
data: editedMonitor,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);

View File

@@ -6,7 +6,8 @@ const Alert = require("../models/Alert");
const RecoveryToken = require("../models/RecoveryToken");
const crypto = require("crypto");
const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key
const { errorMessages, successMessages } = require("../utils/messages");
const { error } = require("console");
const connect = async () => {
try {
await mongoose.connect(process.env.DB_CONNECTION_STRING);
@@ -36,7 +37,7 @@ const insertUser = async (req, res) => {
return await UserModel.findOne({ _id: newUser._id }).select("-password"); // .select() doesn't work with create, need to save then find
} catch (error) {
if (error.code === DUPLICATE_KEY_CODE) {
throw new Error("Email already exists");
throw new Error(errorMessages.DB_USER_EXISTS);
}
throw error;
}
@@ -61,7 +62,7 @@ const getUserByEmail = async (req, res) => {
if (user) {
return user;
} else {
throw new Error("User not found");
throw new Error(errorMessages.DB_USER_NOT_FOUND);
}
} catch (error) {
throw error;
@@ -125,7 +126,7 @@ const validateRecoveryToken = async (req, res) => {
if (recoveryToken !== null) {
return recoveryToken;
} else {
throw new Error("Token not found");
throw new Error(errorMessages.DB_TOKEN_NOT_FOUND);
}
} catch (error) {
throw error;
@@ -142,7 +143,7 @@ const resetPassword = async (req, res) => {
const match = await user.comparePassword(newPassword);
if (match === true) {
throw new Error("New password must be different from old password");
throw new Error(errorMessages.DB_RESET_PASSWORD_BAD_MATCH);
}
if (user !== null) {
@@ -155,7 +156,7 @@ const resetPassword = async (req, res) => {
}).select("-password");
return userWithoutPassword;
} else {
throw new Error("User not found");
throw new Error(errorMessages.DB_USER_NOT_FOUND);
}
} catch (error) {
throw error;
@@ -249,7 +250,7 @@ const deleteMonitor = async (req, res) => {
try {
const monitor = await Monitor.findByIdAndDelete(monitorId);
if (!monitor) {
throw new Error(`Monitor with id ${monitorId} not found`);
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
}
return monitor;
} catch (error) {
@@ -338,7 +339,7 @@ const deleteChecks = async (monitorId) => {
if (result.deletedCount > 0) {
return result.deletedCount;
} else {
throw new Error(`No checks found for monitor with id ${monitorId}`);
throw new Error(errorMessages.DB_DELETE_CHECKS(monitorId));
}
} catch (error) {
throw error;
@@ -458,7 +459,9 @@ const deleteAlert = async (alertId) => {
const result = await Alert.findByIdAndDelete(alertId);
if (result) {
return result;
} else throw new Error(`Alert with id ${alertId} not found`);
} else {
throw new Error(errorMessages.DB_DELETE_ALERT(alertId));
}
} catch (error) {
throw error;
}

View File

@@ -1,9 +1,10 @@
const logger = require("../utils/logger");
const { errorMessages } = require("../utils/messages");
const handleErrors = (error, req, res, next) => {
const status = error.status || 500;
const message = error.message || "Something went wrong";
const service = error.service || "Unknown service";
const message = error.message || errorMessages.FRIENDLY_ERROR;
const service = error.service || errorMessages.UNKNOWN_SERVICE;
logger.error(error.message, { service: service });
res.status(status).json({ success: false, msg: message });
};

View File

@@ -2,7 +2,7 @@ const jwt = require("jsonwebtoken");
const logger = require("../utils/logger");
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
const { errorMessages } = require("../utils/messages");
/**
* Verifies the JWT token
* @function
@@ -15,7 +15,7 @@ const verifyJWT = (req, res, next) => {
const token = req.headers["authorization"];
// Make sure a token is provided
if (!token) {
const error = new Error("No token provided");
const error = new Error(error.errorMessages.NO_AUTH_TOKEN);
error.status = 401;
error.service = SERVICE_NAME;
next(error);
@@ -27,8 +27,12 @@ const verifyJWT = (req, res, next) => {
// Verify the token's authenticity
jwt.verify(parsedToken, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
logger.error("Invalid token", { service: SERVICE_NAME });
return res.status(401).json({ success: false, msg: "Invalid token" });
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
service: SERVICE_NAME,
});
return res
.status(401)
.json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN });
}
//Add the user to the request object for use in the route
req.user = decoded;

View File

@@ -1,5 +1,6 @@
const logger = require("../utils/logger");
const SERVICE_NAME = "verifyOwnership";
const { errorMessages } = require("../utils/messages");
const verifyOwnership = (Model, paramName) => {
return async (req, res, next) => {
@@ -9,10 +10,10 @@ const verifyOwnership = (Model, paramName) => {
const doc = await Model.findById(documentId);
//If the document is not found, return a 404 error
if (!doc) {
logger.error("Document not found", {
logger.error(errorMessages.VERIFY_OWNER_NOT_FOUND, {
service: SERVICE_NAME,
});
const error = new Error("Document not found");
const error = new Error(errorMessages.VERIFY_OWNER_NOT_FOUND);
error.status = 404;
throw error;
}
@@ -20,7 +21,7 @@ const verifyOwnership = (Model, paramName) => {
// If the userID does not match the document's userID, return a 403 error
if (userId.toString() !== doc.userId.toString()) {
console.log("boom");
const error = new Error("Unauthorized access");
const error = new Error(errorMessages.VERIFY_OWNER_UNAUTHORIZED);
error.status = 403;
throw error;
}

View File

@@ -6,6 +6,7 @@ const connection = {
};
const JOBS_PER_WORKER = 5;
const logger = require("../utils/logger");
const { errorMessages, successMessages } = require("../utils/messages");
const SERVICE_NAME = "JobQueue";
const axios = require("axios");
const ping = require("ping");
@@ -155,7 +156,9 @@ class JobQueue {
await worker.close();
} catch (error) {
// Catch the error instead of throwing it
console.error("Error closing worker", error);
logger.error(errorMessages.JOB_QUEUE_WORKER_CLOSE, {
service: SERVICE_NAME,
});
}
}
return true;
@@ -214,12 +217,12 @@ class JobQueue {
every: monitor.interval,
});
if (deleted) {
logger.info("Job removed from queue", {
logger.info(successMessages.JOB_QUEUE_DELETE_JOB, {
service: SERVICE_NAME,
jobId: monitor.id,
});
} else {
logger.error("Job not found in queue", {
logger.error(errorMessages.JOB_QUEUE_DELETE_JOB, {
service: SERVICE_NAME,
jobId: monitor.id,
});
@@ -241,8 +244,12 @@ class JobQueue {
}
console.log(jobs);
await this.queue.obliterate();
logger.info(successMessages.JOB_QUEUE_OBLITERATE, {
service: SERVICE_NAME,
});
return true;
} catch (error) {
logger.error(errorMessages.JOB_QUEUE_OBLITERATE);
throw error;
}
}

81
Server/utils/messages.js Normal file
View File

@@ -0,0 +1,81 @@
const errorMessages = {
// General Errors:
FRIENDLY_ERROR: "Something went wrong...",
UNKNOWN_ERROR: "An unknown error occurred",
// Auth Controller
UNAUTHORIZED: "Unauthorized access",
//Error handling middleware
UNKNOWN_SERVICE: "Unknown service",
NO_AUTH_TOKEN: "No auth token provided",
INVALID_AUTH_TOKEN: "Invalid auth token",
//Onwership Middleware
VERIFY_OWNER_NOT_FOUND: "Document not found",
VERIFY_OWNER_UNAUTHORIZED: "Unauthorized access",
//DB Errors
DB_USER_EXISTS: "User already exists",
DB_USER_NOT_FOUND: "User not found",
DB_TOKEN_NOT_FOUND: "Token not found",
DB_RESET_PASSWORD_BAD_MATCH:
"New password must be different from old password",
DB_FIND_MONTIOR_BY_ID: (monitorId) =>
`Monitor with id ${monitorId} not found`,
DB_DELETE_CHECKS: (monitorId) =>
`No checks found for monitor with id ${monitorId}`,
DB_DELETE_ALERT: (alertId) => `Alert with id ${alertId} not found`,
//Auth errors
AUTH_INCORRECT_PASSWORD: "Incorrect password",
AUTH_UNAUTHORIZED: "Unauthorized access",
// Monitor Errros
MONITOR_GET_BY_ID: "Monitor not found",
MONITOR_GET_BY_USER_ID: "No monitors found for user",
// Job Queue Errors
JOB_QUEUE_WORKER_CLOSE: "Error closing worker",
JOB_QUEUE_DELETE_JOB: "Job not found in queue",
JOB_QUEUE_OBLITERATE: "Error obliterating queue",
};
const successMessages = {
//Alert Controller
ALERT_CREATE: "Alert created successfully",
ALERT_GET_BY_USER: "Got alerts successfully",
ALERT_GET_BY_MONITOR: "Got alerts by Monitor successfully",
ALERT_GET_BY_ID: "Got alert by Id successfully",
ALERT_EDIT: "Alert edited successfully",
ALERT_DELETE: "Alert deleted successfully",
// Auth Controller
AUTH_CREATE_USER: "User created successfully",
AUTH_LOGIN_USER: "User logged in successfully",
AUTH_UPDATE_USER: "User updated successfully",
AUTH_CREATE_RECOVERY_TOKEN: "Recovery token created successfully",
AUTH_VERIFY_RECOVERY_TOKEN: "Recovery token verified successfully",
AUTH_RESET_PASSWORD: "Password reset successfully",
// Check Controller
CHECK_CREATE: "Check created successfully",
CHECK_GET: "Got checks successfully",
CHECK_DELETE: "Checks deleted successfully",
//Monitor Controller
MONITOR_GET_ALL: "Got all monitors successfully",
MONTIOR_GET_BY_ID: "Got monitor by Id successfully",
MONITOR_GET_BY_USER_ID: (userId) => `Got monitor for ${userId} successfully"`,
MONITOR_CREATE: "Monitor created successfully",
MONITOR_DELETE: "Monitor deleted successfully",
MONITOR_EDIT: "Monitor edited successfully",
//Job Queue
JOB_QUEUE_DELETE_JOB: "Job removed successfully",
JOB_QUEUE_OBLITERATE: "Queue OBLITERATED!!!",
};
module.exports = {
errorMessages,
successMessages,
};