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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ The backend is built with **Node.js** and **Express**, and integrates with **Fir
| Authentication | Firebase Auth |
| AI Logic | External AI Agent API (maintained in a separate repository) |

```
## Folder Structure

```
my-express-backend/
├── src/
│ ├── config/
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,18 @@
"prettier": "^3.5.3",
"serverless": "^4.17.1",
"serverless-dotenv-plugin": "^6.0.0",
"serverless-esbuild": "^1.55.1",
"serverless-offline": "^14.4.0"
},
"dependencies": {
"@eslint/compat": "^1.3.0",

"dotenv": "^16.5.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-unused-imports": "^4.1.4",
"express": "^5.1.0",
"express-winston": "^4.2.0",
"firebase-admin": "^13.4.0",
"joi": "^17.13.3",
"redis": "^5.5.6",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
Expand Down
1,779 changes: 264 additions & 1,515 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import express from 'express';
import expressWinston from 'express-winston';
import getLogger from './config/loggerConfig.js';
import helpRequestRouter from './routes/helpRequest.js';

import cacheMiddleware from './middleware/cache.js';
const logger = getLogger();
const app = express();
Expand All @@ -17,4 +19,7 @@ app.use(
);
app.use(cacheMiddleware(60)); // Cache responses for 60 seconds
app.get('/', (_, res) => res.send('API Running'));

app.use("/api/requests", helpRequestRouter);

export default app;
11 changes: 11 additions & 0 deletions src/middleware/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, { stripUnknown: true });
if (error) {
const message = error.details.map(d => d.message).join(', ');
return res.status(400).json({ error: message });
}
req.body = value;
next();
};
}
143 changes: 143 additions & 0 deletions src/routes/helpRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
createRequestSchema,
updateRequestSchema,
} from '../validation/requestSchemas.js';
import { Router } from 'express';
import getLogger from '../config/loggerConfig.js';
import FirestoreStore from '../firebase/FirestoreStore.js';
import StorageStore from '../firebase/StorageStore.js';
import { v4 as uuidv4 } from 'uuid';
import authMiddleware from '../middleware/authMiddleware.js';
import { validate } from '../middleware/validate.js';
import { firestoreDB } from '../config/firebaseConfig.js';
import admin from 'firebase-admin';

const router = Router();
const logger = getLogger();
const store = new FirestoreStore('help-requests');
const requestsCol = firestoreDB.collection('help-requests');
router.use(authMiddleware());

router.post('/', validate(createRequestSchema), async (req, res) => {
try {
const { name, contactInfo, location, description, attachments } = req.body;

const requestId = `req-${uuidv4()}`;
const chatRoomId = `room-${uuidv4()}`;

const newReq = {
requestId,
chatRoomId,
name,
contactInfo,
location,
description,
attachments: [],
status: 'pending',
createdAt: admin.firestore.FieldValue.serverTimestamp(),
history: [],
};

const result = await store.create(requestId, newReq);
if (typeof result === 'string') {
return res.status(500).json({ error: result });
}

if (attachments && attachments.length > 0) {
const storageStore = new StorageStore(`help-requests/${requestId}`);
const attachmentDetails = [];

for (const attachment of attachments) {
const fileBuffer = Buffer.from(attachment.data, 'base64');
const attachmentDetail = await storageStore.create({
fileName: attachment.name,
fileBuffer,
});
attachmentDetails.push(attachmentDetail);
}

await store.update(requestId, { attachments: attachmentDetails });
}

return res.status(201).json({ requestId, chatRoomId });
} catch (error) {
logger.error('Error creating request', {
error: error.message,
stack: error.stack,
path: req.path,
method: req.method,
});
return res.status(500).json({ error: 'Internal server error' });
}
});

router.get('/', async (_req, res) => {
try {
const snapshot = await requestsCol.get();
const summary = snapshot.docs.map((doc) => {
const data = doc.data();
return {
requestId: data.requestId,
description: data.description,
status: data.status,
attachments: data.attachments,
createdAt: data.createdAt,
};
});
res.json(summary);
} catch (error) {
logger.error('Error fetching requests', {
error: error.message,
stack: error.stack,
path: _req.path,
method: _req.method,
});
return res.status(500).json({ error: 'Internal server error' });
}
});

router.put('/:requestId', validate(updateRequestSchema), async (req, res) => {
try {
const { requestId } = req.params;
const { additionalInfo, newAttachments } = req.body;
const exists = await store.read(requestId);

if (!exists) {
return res.status(404).json({ error: 'Request not found' });
}

const updates = {};
if (additionalInfo) {
const history = Array.isArray(exists.history) ? exists.history : [];
history.push({ type: 'update', data: { additionalInfo } });
updates.history = history;
}

const attachmentsToAdd = Array.isArray(newAttachments)
? newAttachments
: [];
if (attachmentsToAdd.length) {
updates.attachments = [
...(Array.isArray(exists.attachments) ? exists.attachments : []),
...attachmentsToAdd,
];
}
updates.updatedAt = new Date().toISOString();

const result = await store.update(requestId, updates);
if (typeof result === 'string') {
return res.status(500).json({ error: result });
}
return res.json({ requestId, updatedAt: updates.updatedAt });
} catch (error) {
logger.error(`Error updating request ${req.params.requestId}`, {
error: error.message,
stack: error.stack,
path: req.path,
method: req.method,
});
return res.status(500).json({ error: 'Internal server error' });
}
});

export default router;
27 changes: 27 additions & 0 deletions src/validation/requestSchemas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Joi from 'joi';

const attachmentSchema = Joi.object({
type: Joi.string().valid('image', 'audio', 'video', 'document').required(),
url: Joi.string().uri().required(),
});

const createRequestSchema = Joi.object({
name: Joi.string().min(1).required(),
contactInfo: Joi.string().email().required(),
location: Joi.object({
lat: Joi.number().required(),
lng: Joi.number().required(),
}).required(),
description: Joi.string().min(5).required(),
attachments: Joi.array().items(attachmentSchema).default([]),
});

const updateRequestSchema = Joi.object({
additionalInfo: Joi.string().min(1).optional(),
newAttachments: Joi.array().items(attachmentSchema).default([]),
}).or('additionalInfo', 'newAttachments');

module.exports = {
createRequestSchema,
updateRequestSchema,
};
Loading