Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions web_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ And build it
**Make sure you already cloned the project and built it using docker compose** </br>
In case you want to change any configuration value related to the bloom filter (port, bloom filter integers etc.) change the relevant dockerfile or docker compose file.

- To run the web server and MongoDB via compose (profile web_app)
```bash
docker compose --profile web_app up -d --build web_server mongo
```

- Environment
- Ensure `.env` exists in `web_server/` or use `.env.example` as a template.
- `MONGO_URI` should be `mongodb://mongo:27017/appdb` when using compose.

- Health check
```bash
curl http://localhost:3001/health
```

- Dev routes
- Disabled by default in `app.js` (commented `app.use('/dev/*', ...)`).
- Uncomment locally only if you need seed/list endpoints for debugging.

- To run the web server and bloom filter server
```bash
docker-compose up web_server
Expand Down
30 changes: 17 additions & 13 deletions web_server/controllers/labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ const Labels = require('../models/labels');
* Return all labels (flat list), belonging only to the authenticated user.
* Responds: [{ id, name, parent }]
*/
exports.getAllLabels = (req, res) => {
exports.getAllLabels = async (req, res) => {
const userId = +req.user.id;
const all = Labels.getAllLabels(userId);
const all = await Labels.getAllLabels(userId);
// Only send fields required by frontend; "parent" is id of parent label (or null for root)
res.status(200).json(all.map(l => ({
id: l.id,
Expand All @@ -22,12 +22,12 @@ exports.getAllLabels = (req, res) => {
* Request body: { name }
* Responds: created label object, 201 status.
*/
exports.createNewLabel = (req, res) => {
exports.createNewLabel = async (req, res) => {
// User ID from authentication middleware
const userId = +req.user.id;
const { name } = req.body;
if (!name) return res.status(400).json({ error: 'Name required' });
const lab = Labels.createNewLabel(userId, name);
const lab = await Labels.createNewLabel(userId, name);
if (!lab) return res.status(409).json({ error: 'Label name already exists' });
res.status(201).location(`/api/labels/${lab.id}`).json(lab);
};
Expand All @@ -38,13 +38,13 @@ exports.createNewLabel = (req, res) => {
* Request body: { name }
* Responds: created sublabel object, or 404 if parent not found.
*/
exports.createSublabel = (req, res) => {
exports.createSublabel = async (req, res) => {
const userId = +req.user.id;
const parentId = +req.params.id;
const { name } = req.body;
if (!name) return res.status(400).json({ error: 'Name required' });
// Model should handle parent existence/ownership
const lab = Labels.createSublabel(userId, parentId, name);
const lab = await Labels.createSublabel(userId, parentId, name);
if (!lab) return res.status(404).json({ error: 'Parent not found' });
else if (!lab) return res.status(409).json({ error: 'Label name already exists' });
res.status(201).location(`/api/labels/${lab.id}`).json(lab);
Expand All @@ -56,30 +56,34 @@ exports.createSublabel = (req, res) => {
* Request body: { name }
* Responds: 204 on success, 404 if not found.
*/
exports.editLabel = (req, res) => {
exports.editLabel = async (req, res) => {
const id = +req.params.id;
const userId = +req.user.id;
const { name } = req.body;
if (!name) return res.status(400).json({ error: 'Name required' });
const ok = Labels.editLabelById(id,userId, name);
if (!ok) return res.status(404).json({ error: 'Not found' });

res.status(204).end();
try {
const ok = await Labels.editLabelById(id, userId, name);
if (!ok) return res.status(404).json({ error: 'Not found' });
res.status(204).end();
} catch (e) {
if (String(e && e.message) === '409') return res.status(409).json({ error: 'Label name already exists' });
return res.status(500).json({ error: 'Internal error' });
}
};

/**
* DELETE /api/labels/:id
* Delete label (and, usually, its sublabels) by ID.
* Responds: 204 on success, 404 if not found.
*/
exports.deleteLabel = (req, res) => {
exports.deleteLabel = async (req, res) => {
const labelId = +req.params.id;
const userId = +req.user.id;

if (!(labelId))
return res.status(400).json({ error: 'Invalid label ID' });

const deleted = Labels.deleteLabelById(labelId, userId);
const deleted = await Labels.deleteLabelById(labelId, userId);
if (!deleted)
return res.status(404).json({ error: 'Label not found or not owned by user' });

Expand Down
66 changes: 33 additions & 33 deletions web_server/controllers/mails.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const {convertLabelsToIds, labelsToFullElement} = require("../utils/labels");
* - code 200 and list of ordered by the time sent mails objects
* - code 400 if user isn't authenticated
*/
const getLastMailsOrdered = (req, res) => {
const getLastMailsOrdered = async (req, res) => {
// Make sure the user is authenticated, if not - a bad request (400)
const userId = Number(req.user.id);
if (!userId)
Expand All @@ -23,7 +23,7 @@ const getLastMailsOrdered = (req, res) => {
const limit = Number(req.query.limit) || 50;

const labelIdFilter = Number(req.query.label);
let {paged, total} = Mails.getUserMails(userId, limit, inboxType, page);
let {paged, total} = await Mails.getUserMails(userId, limit, inboxType, page);

// Filter by label id if provided
if (!isNaN(labelIdFilter)) {
Expand All @@ -32,14 +32,14 @@ const getLastMailsOrdered = (req, res) => {
}

// replace in the mails the user ids and labels ids with user and label elements so we can show names and emails
const fullMails = paged.map(m => {
const fullMails = await Promise.all(paged.map(async (m) => {
return {
...m,
from: usersToFullElement([m.from])[0],
sentTo: usersToFullElement(m.sentTo || []),
labels: labelsToFullElement(userId, m.labels || [])
from: (await usersToFullElement([m.from]))[0],
sentTo: await usersToFullElement(m.sentTo || []),
labels: await labelsToFullElement(userId, m.labels || [])
}
})
}))
// we weren't instructed to return 404 if mails is empty, just do a 200 code one
return res.status(200).json(
{
Expand All @@ -58,7 +58,7 @@ const getLastMailsOrdered = (req, res) => {
* - code 400 if there's no id in input
* - code 404 if there's no mail with the id
*/
const getMailById = (req, res) => {
const getMailById = async (req, res) => {
// Make sure we got an id in the request
const id = parseInt(req.params.id);
if (isNaN(id))
Expand All @@ -68,7 +68,7 @@ const getMailById = (req, res) => {
if (!userId)
return res.status(400).json({error: 'No valid user ID was given'});
// Find the mail with the given id
const mail = Mails.getMail(id);
const mail = await Mails.getMail(id);
// Return 404 if not found, or a 200 with the mail as a json object
if (!mail)
return res.status(404).json({error: `No mail found with ID: ${id}`});
Expand All @@ -77,9 +77,9 @@ const getMailById = (req, res) => {
return res.status(403).json({error: 'mail do not belong to user'});
return res.status(200).json({
...mail,
from: usersToFullElement([mail.from])[0],
sentTo: usersToFullElement(mail.sentTo || []),
labels: labelsToFullElement(userId, mail.labels || [])
from: (await usersToFullElement([mail.from]))[0],
sentTo: await usersToFullElement(mail.sentTo || []),
labels: await labelsToFullElement(userId, mail.labels || [])
});
}

Expand All @@ -103,10 +103,10 @@ const createNewMail = async (req, res) => {
try {
// Fetch the mail object's attributes from the request, for later use
const {subject, body, sentTo = [], saveAsDraft = false, files = []} = req.body;
const sentToIds = convertMailsToIds(sentTo);
const sentToIds = await convertMailsToIds(sentTo);
// handle this as a draft
if (saveAsDraft) {
const draftMail = Mails.saveDraft(userId, subject, body, sentToIds, files)
const draftMail = await Mails.saveDraft(userId, subject, body, sentToIds, files)
return res.status(201).location(`/mails/${draftMail.id}`).json(draftMail);
}
// handle this as sending a mail
Expand All @@ -116,7 +116,7 @@ const createNewMail = async (req, res) => {
if (blacklisted)
return res.status(403).json({error: 'Mail contains blacklisted URLs - failed creating a new mail'});
// No blacklisted URLs found, send the new mail
const newMail = Mails.sendNewMail(userId, subject, body, sentToIds, files);
const newMail = await Mails.sendNewMail(userId, subject, body, sentToIds, files);
if (newMail === false)
return res.status(500).json({error: 'Failed to create new mail'});
return res.status(201).location(`/mails/${newMail.id}`).json(newMail);
Expand All @@ -136,7 +136,7 @@ const createNewMail = async (req, res) => {
* - code 400 is user isn't authenticated
* - code 500 if any other error occurred
*/
const updateMail = (req, res) => {
const updateMail = async (req, res) => {
// Make sure the user is authenticated, if not - a bad request (400)
const userId = Number(req.user.id);
if (!userId)
Expand All @@ -145,8 +145,8 @@ const updateMail = (req, res) => {
const mailId = Number(req.params.id);
if (isNaN(mailId))
return res.status(400).json({error: 'error no valid mail id was given'});
const mail = Mails.getMail(mailId);
if (!mailId || mail.owner != userId)
const mail = await Mails.getMail(mailId);
if (!mail || mail.owner != userId)
return res.status(404).json({error: 'error mail not found'});
// edit it as a draft
if (mail.isDraft)
Expand All @@ -155,7 +155,7 @@ const updateMail = (req, res) => {
// otherwise it’s a mail already sent - only allow flags & labels
const {isRead, isStarred, isTrashed, isSpam, labels} = req.body || {};
const labelsIds = convertLabelsToIds(userId, labels || []);
const updated = Mails.editSentMail(mailId, isRead, isStarred, isTrashed, isSpam, labelsIds);
const updated = await Mails.editSentMail(mailId, isRead, isStarred, isTrashed, isSpam, labelsIds);
if (updated)
return res.status(200).json(updated);
if (updated === 404)
Expand All @@ -181,21 +181,21 @@ const editDraft = async (req, res, userId, mail) => {
return res.status(400).json({error: 'error only drafts can be updated'});
// Get the input params and edit the mail
const {subject, body, sentTo = [], saveAsDraft = true, files = []} = req.body;
const sentToIds = convertMailsToIds(sentTo);
const sentToIds = await convertMailsToIds(sentTo);

// just update the fields in this draft
if (saveAsDraft) {
const updated = Mails.updateDraft(mail.id, subject, body, sentToIds, files);
const updated = await Mails.updateDraft(mail.id, subject, body, sentToIds, files);
return res.status(200).json(updated);
}
// turn the draft to a new mail
const urls = extractUrls(body);
const blacklisted = await Blacklist.isInBlacklist(urls);
if (blacklisted)
return res.status(403).json({error: 'error mail contains blacklisted URLs'});
// delete the draft and send a new mail
Mails.deleteMail(userId, mail.id);
const ownerMail = Mails.sendNewMail(userId, subject, body, sentToIds, files);
// send a new mail and then delete the draft
const ownerMail = await Mails.sendNewMail(userId, subject, body, sentToIds, files);
await Mails.deleteMail(userId, mail.id);
return res.status(201).location(`/mails/${ownerMail.id}`).json(ownerMail);
}

Expand All @@ -209,7 +209,7 @@ const editDraft = async (req, res, userId, mail) => {
* - 404 if mail not found
* - 400 if missing input or user isn't authenticated or has no access to the mail
*/
const deleteMailById = (req, res) => {
const deleteMailById = async (req, res) => {
// Make sure we got an id in the request
const mailId = Number(req.params.id);
if (isNaN(mailId))
Expand All @@ -219,7 +219,7 @@ const deleteMailById = (req, res) => {
if (isNaN(userId))
return res.status(400).json({error: 'User not authenticated'});
// Delete the desired mail and return matching result
const mail = Mails.deleteMail(userId, mailId);
const mail = await Mails.deleteMail(userId, mailId);
if (mail === 404)
return res.status(404).json({error: 'mail was not found'});
if (mail === 400)
Expand All @@ -235,7 +235,7 @@ const deleteMailById = (req, res) => {
* @returns {*} array of mails objects that contains the query, if encountered a problem returns code 400
* with the description.
*/
const getMailsByQuery = (req, res) => {
const getMailsByQuery = async (req, res) => {
// Make sure the user is authenticated, if not - a bad request (400)
const userId = Number(req.user.id);
if (!userId)
Expand All @@ -245,15 +245,15 @@ const getMailsByQuery = (req, res) => {
if (!query)
return res.status(400).json({error: 'Empty query'});
// Find the mails and return them, if there are no mails returns an empty array
const rawMails = Mails.searchInInbox(query, userId);
const fullMails = rawMails.map(m => {
const rawMails = await Mails.searchInInbox(query, userId);
const fullMails = await Promise.all(rawMails.map(async (m) => {
return {
...m,
from: usersToFullElement([m.from])[0],
sentTo: usersToFullElement(m.sentTo || []),
labels: labelsToFullElement(userId, m.labels || [])
from: (await usersToFullElement([m.from]))[0],
sentTo: await usersToFullElement(m.sentTo || []),
labels: await labelsToFullElement(userId, m.labels || [])
}
})
}))
return res.status(200).json(fullMails);
}

Expand Down
15 changes: 8 additions & 7 deletions web_server/controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const signupUser = async (req, res) => {
}

// Don't create if the mail is already taken
if (Users.userExist(mail)) {
if (await Users.userExist(mail)) {
return res.status(400).json({ error: 'mail already exists' });
}

Expand Down Expand Up @@ -50,13 +50,13 @@ const signupUser = async (req, res) => {
* - 400 if invalid id
* - 404 if no user found
*/
const getUser = (req, res) => {
const getUser = async (req, res) => {
const id = Number(req.params.id);
if (!id || isNaN(id)) {
return res.status(400).json({ error: 'Invalid user ID' });
}

const user = Users.getUserById(id);
const user = await Users.getUserById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
Expand All @@ -81,13 +81,13 @@ const loginUser = async (req, res) => {
return res.status(400).json({ error: 'mail and password required' });
}

const user = Users.isAuthorizeUser(mail, password);
const user = await Users.isAuthorizeUser(mail, password);
if (!user) {
return res.status(401).json({ error: 'wrong mail or password' });
}

const token = signToken(user);
return res.status(200).json({ token });
return res.status(200).json({ token, user });
};

/**
Expand Down Expand Up @@ -143,13 +143,14 @@ const isTokenValid = (req, res) => {
* - 200 and an array of `{ id, name, mail }` (max 10)
* - 400 if missing query parameter `q`
*/
const searchUsers = (req, res) => {
const searchUsers = async (req, res) => {
const query = req.query.q?.toLowerCase();
if (!query) {
return res.status(400).json({ error: 'Missing query parameter' });
}

const matched = Users.getAllUsers()
const all = await Users.getAllUsers();
const matched = all
.filter(user =>
user.fullName.toLowerCase().includes(query) ||
user.mail.toLowerCase().includes(query)
Expand Down
Loading