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
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
services:
# MongoDB database
mongo:
profiles: ["web_app"]
image: mongo:6
container_name: mongo
volumes:
- mongo_data:/data/db
ports:
- "27017:27017"
networks:
- gmail-net
# React frontend
web_front:
profiles: ["web_app"]
Expand Down Expand Up @@ -26,6 +37,9 @@ services:
- ./web_server/.env
depends_on:
- run_server
- mongo
environment:
- MONGO_URI=mongodb://mongo:27017/appdb
ports:
- "${NODE_PORT:-3001}:3001"
networks:
Expand Down Expand Up @@ -91,6 +105,7 @@ services:

volumes:
app_data:
mongo_data:

networks:
gmail-net:
4 changes: 2 additions & 2 deletions web_server/.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
REACT_URL=http://localhost:3000
NODE_PORT=3001
JSON_LIMIT=10mb
JWT_SECRET=gradingMode
JWT_EXPIRATION_TIME=24h
JWT_EXPIRATION_TIME=24h
MONGO_URI=mongodb://localhost:27017/appdb
4 changes: 4 additions & 0 deletions web_server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
NODE_PORT=3001
JWT_SECRET=replace_me
JWT_EXPIRATION_TIME=24h
MONGO_URI=mongodb://mongo:27017/appdb
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
24 changes: 23 additions & 1 deletion web_server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,32 @@ const inbox = require('./routes/mails');
const users = require('./routes/users');
const labels = require('./routes/labels');
const blacklist = require('./routes/blacklist');
const { connectDB } = require('./config/db');
const health = require('./routes/health');
// Dev routes are disabled by default. Uncomment if needed for local debugging.
// const devUsers = require('./routes/devUsers');
// const devLabels = require('./routes/devLabels');
// const devMails = require('./routes/devMails');

app.use('/api/mails', inbox);
app.use('/api', users);
app.use('/api/labels', labels);
app.use('/api/blacklist', blacklist);
app.use('/health', health);
// app.use('/dev/users', devUsers);
// app.use('/dev/labels', devLabels);
// app.use('/dev/mails', devMails);

app.listen(PORT);
const start = async () => {
try {
await connectDB();
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
} catch (err) {
console.error('Failed to start server:', err && err.message ? err.message : err);
process.exit(1);
}
};

start();
51 changes: 51 additions & 0 deletions web_server/config/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const mongoose = require('mongoose');

mongoose.set('strictQuery', true);

const MAX_RETRIES = 3;
const RETRY_DELAYS_MS = [1000, 2000, 4000];

function addConnectionListeners() {
const connection = mongoose.connection;
connection.on('connected', () => {
console.log('Mongo connected');
});
connection.on('error', (err) => {
console.error('Mongo connection error:', err && err.message ? err.message : err);
});
connection.on('disconnected', () => {
console.warn('Mongo disconnected');
});
}

async function connectDB() {
addConnectionListeners();

const mongoUri = process.env.MONGO_URI;

if (!mongoUri) {
throw new Error('MONGO_URI is not defined in environment variables');
}

for (let attemptIndex = 0; attemptIndex < MAX_RETRIES; attemptIndex += 1) {
try {
await mongoose.connect(mongoUri);
return mongoose.connection;
} catch (error) {
const isLastAttempt = attemptIndex === MAX_RETRIES - 1;
if (isLastAttempt) {
throw error;
}
const delay = RETRY_DELAYS_MS[attemptIndex] || 1000;
console.warn(`Mongo connection attempt ${attemptIndex + 1} failed. Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}

// Should never reach here
throw new Error('Unknown Mongo connection error');
}

module.exports = { connectDB };


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
Loading