From b6c80371319818dee0a602dc2faf2da6458fb627 Mon Sep 17 00:00:00 2001 From: Arshdeep225615024 Date: Wed, 29 Apr 2026 19:21:58 +1000 Subject: [PATCH 1/5] update incident model --- app-backend/src/models/Incident.js | 51 ++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/app-backend/src/models/Incident.js b/app-backend/src/models/Incident.js index 223dc12a7..ab3ed9a73 100644 --- a/app-backend/src/models/Incident.js +++ b/app-backend/src/models/Incident.js @@ -1,15 +1,36 @@ import mongoose from "mongoose"; +// ---------------- Attachment Schema ---------------- const attachmentSchema = new mongoose.Schema( { fileName: { type: String, required: true, }, + + originalName: { + type: String, + }, + fileUrl: { type: String, required: true, }, + + mimeType: { + type: String, + }, + + fileSize: { + type: Number, + }, + + mediaType: { + type: String, + enum: ["image", "video", "audio", "pdf", "other"], + default: "other", + }, + uploadedAt: { type: Date, default: Date.now, @@ -18,6 +39,7 @@ const attachmentSchema = new mongoose.Schema( { _id: true } ); +// ---------------- Incident Schema ---------------- const incidentSchema = new mongoose.Schema( { shiftId: { @@ -25,6 +47,7 @@ const incidentSchema = new mongoose.Schema( ref: "Shift", required: true, }, + guardId: { type: mongoose.Schema.Types.ObjectId, ref: "User", @@ -43,19 +66,43 @@ const incidentSchema = new mongoose.Schema( trim: true, }, + // NEW structured lifecycle status: { type: String, - enum: ["open", "in-progress", "resolved"], - default: "open", + enum: ["SUBMITTED", "IN_REVIEW", "RESOLVED"], + default: "SUBMITTED", + }, + + // NEW GPS location + location: { + latitude: { + type: Number, + min: -90, + max: 90, + }, + longitude: { + type: Number, + min: -180, + max: 180, + }, + }, + + // NEW standard timestamp + recordedAt: { + type: Date, + default: Date.now, }, attachments: [attachmentSchema], + // ---------------- Soft Delete ---------------- isDeleted: { type: Boolean, default: false, }, + deletedAt: Date, + deletedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", From 66343d4361494c833d3addb241e40e7a099d692e Mon Sep 17 00:00:00 2001 From: Arshdeep225615024 Date: Wed, 29 Apr 2026 22:15:08 +1000 Subject: [PATCH 2/5] Extend incident uploads to support media files --- app-backend/src/config/multer.js | 61 +++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/app-backend/src/config/multer.js b/app-backend/src/config/multer.js index dfc13656c..d7ef4f35a 100644 --- a/app-backend/src/config/multer.js +++ b/app-backend/src/config/multer.js @@ -1,34 +1,61 @@ // config/multer.js -import multer from 'multer'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import multer from "multer"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // ensure uploads dir exists -const uploadsDir = path.join(__dirname, '..', 'uploads'); +const uploadsDir = path.join(__dirname, "..", "uploads"); if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true }); // where & how to store files const storage = multer.diskStorage({ - destination: (_req, _file, cb) => cb(null, uploadsDir), - filename: (_req, file, cb) => { - const safeOriginal = file.originalname.replace(/[^\w.-]/g, '_'); - const ts = Date.now(); - cb(null, `${ts}-${safeOriginal}`); - }, + destination: (_req, _file, cb) => cb(null, uploadsDir), + filename: (_req, file, cb) => { + const safeOriginal = file.originalname.replace(/[^\w.-]/g, "_"); + const ts = Date.now(); + cb(null, `${ts}-${safeOriginal}`); + }, }); -// accept images only + 5MB limit +// supported files: images, PDFs, videos, and audio +const allowedMimeTypes = [ + // images + "image/jpeg", + "image/png", + "image/webp", + "image/heic", + + // pdf + "application/pdf", + + // videos + "video/mp4", + "video/mpeg", + "video/quicktime", + "video/webm", + + // audio + "audio/mpeg", + "audio/wav", + "audio/webm", + "audio/mp4", +]; + const fileFilter = (_req, file, cb) => { - const ok = ['image/jpeg', 'image/png', 'image/webp', 'image/heic','application/pdf' ].includes(file.mimetype); - cb(ok ? null : new Error('Only image files are allowed'), ok); + const ok = allowedMimeTypes.includes(file.mimetype); + + cb( + ok ? null : new Error("Only images, videos, audio, and PDF files are allowed"), + ok + ); }; export const upload = multer({ - storage, - fileFilter, - limits: { fileSize: 5 * 1024 * 1024 }, + storage, + fileFilter, + limits: { fileSize: 25 * 1024 * 1024 }, // 25MB }); \ No newline at end of file From 6ea1a2b7434a24799bfb01326d64174abbbf4623 Mon Sep 17 00:00:00 2001 From: Arshdeep225615024 Date: Wed, 29 Apr 2026 22:24:26 +1000 Subject: [PATCH 3/5] Enhance incident controller with GPS, lifecycle, and media metadata --- .../src/controllers/incident.controller.js | 82 ++++++++++++++----- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/app-backend/src/controllers/incident.controller.js b/app-backend/src/controllers/incident.controller.js index b82614f27..a320c5ab5 100644 --- a/app-backend/src/controllers/incident.controller.js +++ b/app-backend/src/controllers/incident.controller.js @@ -8,13 +8,31 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const validStatuses = ["SUBMITTED", "IN_REVIEW", "RESOLVED"]; + +const allowedTransitions = { + SUBMITTED: ["IN_REVIEW"], + IN_REVIEW: ["RESOLVED"], + RESOLVED: [], +}; + +const getMediaType = (mimeType) => { + if (mimeType.startsWith("image/")) return "image"; + if (mimeType.startsWith("video/")) return "video"; + if (mimeType.startsWith("audio/")) return "audio"; + if (mimeType === "application/pdf") return "pdf"; + return "other"; +}; + // CREATE INCIDENT export const createIncident = async (req, res, next) => { try { - const { shiftId, severity, description } = req.body; + const { shiftId, severity, description, latitude, longitude } = req.body; if (!shiftId || !severity || !description) { - return next(new ErrorResponse("All fields are required", 400)); + return next( + new ErrorResponse("shiftId, severity, and description are required", 400) + ); } const shift = await Shift.findById(shiftId); @@ -31,10 +49,18 @@ export const createIncident = async (req, res, next) => { guardId: req.user._id, severity, description, + status: "SUBMITTED", + recordedAt: new Date(), + location: { + latitude: latitude !== undefined ? Number(latitude) : undefined, + longitude: longitude !== undefined ? Number(longitude) : undefined, + }, }); await req.audit.log(req.user._id, ACTIONS.INCIDENT_CREATED, { incidentId: incident._id, + recordedAt: incident.recordedAt, + location: incident.location, }); res.status(201).json({ success: true, data: incident }); @@ -55,15 +81,12 @@ export const updateIncident = async (req, res, next) => { let allowedFields = []; if (req.user.role === "guard") { - // guards can only update their own incidents if (String(incident.guardId) !== String(req.user._id)) { return next(new ErrorResponse("Not authorized", 403)); } - // guards should only update limited fields allowedFields = ["description"]; } else if (req.user.role === "employer") { - // employers can only update incidents belonging to their own shifts const shift = await Shift.findById(incident.shiftId); if (!shift || String(shift.createdBy) !== String(req.user._id)) { @@ -77,6 +100,24 @@ export const updateIncident = async (req, res, next) => { return next(new ErrorResponse("Not authorized", 403)); } + if (req.body.status) { + if (!validStatuses.includes(req.body.status)) { + return next(new ErrorResponse("Invalid status value", 400)); + } + + const currentStatus = incident.status; + const nextStatus = req.body.status; + + if (!allowedTransitions[currentStatus]?.includes(nextStatus)) { + return next( + new ErrorResponse( + `Invalid status transition from ${currentStatus} to ${nextStatus}`, + 400 + ) + ); + } + } + allowedFields.forEach((field) => { if (req.body[field] !== undefined) { incident[field] = req.body[field]; @@ -109,7 +150,6 @@ export const getIncident = async (req, res, next) => { return next(new ErrorResponse("Incident not found", 404)); } - // Guard access if ( req.user.role === "guard" && String(incident.guardId._id) !== String(req.user._id) @@ -117,7 +157,6 @@ export const getIncident = async (req, res, next) => { return next(new ErrorResponse("Not authorized", 403)); } - // Employer access if (req.user.role === "employer") { const shift = await Shift.findById(incident.shiftId._id); if (String(shift.createdBy) !== String(req.user._id)) { @@ -131,7 +170,7 @@ export const getIncident = async (req, res, next) => { } }; -// LIST INCIDENTS (WITH FILTERS) +// LIST INCIDENTS export const getIncidents = async (req, res, next) => { try { const { shiftId, guardId, severity, status, startDate, endDate } = @@ -142,23 +181,26 @@ export const getIncidents = async (req, res, next) => { if (shiftId) query.shiftId = shiftId; if (guardId) query.guardId = guardId; if (severity) query.severity = severity; - if (status) query.status = status; + + if (status) { + if (!validStatuses.includes(status)) { + return next(new ErrorResponse("Invalid status filter", 400)); + } + query.status = status; + } if (startDate || endDate) { - query.createdAt = {}; - if (startDate) query.createdAt.$gte = new Date(startDate); - if (endDate) query.createdAt.$lte = new Date(endDate); + query.recordedAt = {}; + if (startDate) query.recordedAt.$gte = new Date(startDate); + if (endDate) query.recordedAt.$lte = new Date(endDate); } - // RBAC filtering if (req.user.role === "guard") { query.guardId = req.user._id; } if (req.user.role === "employer") { - const shifts = await Shift.find({ createdBy: req.user._id }).select( - "_id" - ); + const shifts = await Shift.find({ createdBy: req.user._id }).select("_id"); query.shiftId = { $in: shifts.map((s) => s._id) }; } @@ -206,14 +248,12 @@ export const uploadAttachment = async (req, res, next) => { return next(new ErrorResponse("Incident not found", 404)); } - // guard can upload only to their own incident if (req.user.role === "guard") { if (String(incident.guardId) !== String(req.user._id)) { return next(new ErrorResponse("Not authorized", 403)); } } - // employer can upload only to incidents on their own shifts if (req.user.role === "employer") { const shift = await Shift.findById(incident.shiftId); @@ -222,7 +262,6 @@ export const uploadAttachment = async (req, res, next) => { } } - // any non-admin role outside the above is not allowed if (!["guard", "employer", "admin"].includes(req.user.role)) { return next(new ErrorResponse("Not authorized", 403)); } @@ -233,7 +272,11 @@ export const uploadAttachment = async (req, res, next) => { incident.attachments.push({ fileName: req.file.filename, + originalName: req.file.originalname, fileUrl: `/uploads/${req.file.filename}`, + mimeType: req.file.mimetype, + fileSize: req.file.size, + mediaType: getMediaType(req.file.mimetype), }); await incident.save(); @@ -244,7 +287,6 @@ export const uploadAttachment = async (req, res, next) => { } }; - // GET ATTACHMENT export const getAttachment = async (req, res, next) => { try { From 171f63f1f726a04b1495856fec6fbfbaeb115715 Mon Sep 17 00:00:00 2001 From: Arshdeep225615024 Date: Wed, 29 Apr 2026 22:47:14 +1000 Subject: [PATCH 4/5] Update Swagger for incident lifecycle, GPS, and media uploads --- app-backend/src/routes/incident.routes.js | 163 ++-------------------- 1 file changed, 12 insertions(+), 151 deletions(-) diff --git a/app-backend/src/routes/incident.routes.js b/app-backend/src/routes/incident.routes.js index 3ac5d7bc2..a111bb325 100644 --- a/app-backend/src/routes/incident.routes.js +++ b/app-backend/src/routes/incident.routes.js @@ -10,7 +10,6 @@ import { } from "../controllers/incident.controller.js"; import auth from "../middleware/auth.js"; - import { authorizePermissions } from "../middleware/rbac.js"; import { upload } from "../config/multer.js"; @@ -23,7 +22,6 @@ const router = express.Router(); * description: Incident reporting and management */ - router.use(auth); /** @@ -50,38 +48,23 @@ router.use(auth); * properties: * shiftId: * type: string - * example: "65f1c6a3b5e18f9b9a3d52f77" * severity: * type: string * enum: [low, medium, high] - * example: "high" * description: * type: string - * example: "Unauthorized entry detected near the loading dock." + * latitude: + * type: number + * example: -37.8136 + * longitude: + * type: number + * example: 144.9631 * responses: * 201: * description: Incident created successfully - * 400: - * description: Missing or invalid input - * 401: - * description: Unauthorized - * 403: - * description: Forbidden - * 404: - * description: Shift not found * * get: * summary: List incidents - * description: | - * Retrieve incidents with optional filters: - * - shiftId - * - guardId - * - severity - * - status - * - startDate - * - endDate - * - * Returned data is still subject to role- and ownership-based filtering in the controller. * tags: [Incidents] * security: * - bearerAuth: [] @@ -90,52 +73,32 @@ router.use(auth); * name: shiftId * schema: * type: string - * required: false - * description: Filter by shift ID * - in: query * name: guardId * schema: * type: string - * required: false - * description: Filter by guard ID * - in: query * name: severity * schema: * type: string * enum: [low, medium, high] - * required: false - * description: Filter by severity * - in: query * name: status * schema: * type: string - * enum: [open, in-progress, resolved] - * required: false - * description: Filter by incident status + * enum: [SUBMITTED, IN_REVIEW, RESOLVED] * - in: query * name: startDate * schema: * type: string * format: date - * required: false - * description: Return incidents created on or after this date * - in: query * name: endDate * schema: * type: string * format: date - * required: false - * description: Return incidents created on or before this date - * responses: - * 200: - * description: Incidents retrieved successfully - * 401: - * description: Unauthorized - * 403: - * description: Forbidden */ -// Create router.post("/", authorizePermissions("incident:create"), createIncident); router.get("/", authorizePermissions("incident:view"), getIncidents); @@ -144,14 +107,6 @@ router.get("/", authorizePermissions("incident:view"), getIncidents); * /api/v1/incidents/{id}: * get: * summary: Retrieve a single incident - * description: | - * Return full incident details including reporter, shift, severity, - * status, timestamps, and attachments. - * - * **Access rules:** - * - **Guard**: can view only their own incident - * - **Employer**: can view only incidents linked to shifts they created - * - **Admin**: can view any incident allowed by permissions * tags: [Incidents] * security: * - bearerAuth: [] @@ -161,38 +116,15 @@ router.get("/", authorizePermissions("incident:view"), getIncidents); * required: true * schema: * type: string - * description: Incident ID * responses: * 200: * description: Incident retrieved successfully - * 401: - * description: Unauthorized - * 403: - * description: Forbidden or outside authorized ownership/scope - * 404: - * description: Incident not found * * patch: * summary: Update an incident - * description: | - * Update an existing incident with role- and ownership-based restrictions. - * - * **Update rules:** - * - **Guard**: can update only their own incident, and only the `description` field - * - **Employer**: can update only incidents linked to shifts they created, and only the `description` and `status` fields - * - **Admin**: can update `severity`, `description`, and `status` - * - * Any unauthorized field changes are ignored by the controller. * tags: [Incidents] * security: * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: Incident ID * requestBody: * required: true * content: @@ -203,50 +135,24 @@ router.get("/", authorizePermissions("incident:view"), getIncidents); * severity: * type: string * enum: [low, medium, high] - * example: medium - * description: Admin only * description: * type: string - * example: Updated after site supervisor review. - * description: Allowed for guard, employer, and admin (subject to ownership/scope) * status: * type: string - * enum: [open, in-progress, resolved] - * example: in-progress - * description: Employer and admin only + * enum: [SUBMITTED, IN_REVIEW, RESOLVED] + * example: IN_REVIEW * responses: * 200: * description: Incident updated successfully - * 401: - * description: Unauthorized - * 403: - * description: Forbidden or outside authorized ownership/scope - * 404: - * description: Incident not found * * delete: * summary: Soft-delete an incident - * description: | - * Admin-only endpoint that soft-deletes an incident while preserving audit logs. * tags: [Incidents] * security: * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: Incident ID * responses: * 200: * description: Incident deleted successfully - * 401: - * description: Unauthorized - * 403: - * description: Forbidden - * 404: - * description: Incident not found */ router.patch("/:id", authorizePermissions("incident:update"), updateIncident); @@ -259,14 +165,7 @@ router.delete("/:id", authorizePermissions("incident:delete"), deleteIncident); * post: * summary: Upload an incident attachment * description: | - * Upload a supported file (such as an image or PDF) and attach it to an existing incident. - * - * **Access rules:** - * - **Guard**: can upload only to their own incident - * - **Employer**: can upload only to incidents linked to shifts they created - * - **Admin**: can upload to any incident - * - * In addition to route permission checks, the controller validates incident ownership/scope before saving the attachment. + * Upload a supported file (images, videos, audio, or PDFs) and attach it to an existing incident. * tags: [Incidents] * security: * - bearerAuth: [] @@ -276,31 +175,17 @@ router.delete("/:id", authorizePermissions("incident:delete"), deleteIncident); * required: true * schema: * type: string - * description: Incident ID * requestBody: * required: true * content: * multipart/form-data: * schema: * type: object - * required: - * - file * properties: * file: * type: string * format: binary - * description: Supported image or PDF file - * responses: - * 200: - * description: Attachment uploaded successfully - * 400: - * description: No file uploaded - * 401: - * description: Unauthorized - * 403: - * description: Forbidden or outside authorized ownership/scope - * 404: - * description: Incident not found + * description: Supported files: images, videos, audio, and PDFs. */ router.post( @@ -315,34 +200,9 @@ router.post( * /api/v1/incidents/{id}/attachments/{attachmentId}: * get: * summary: Retrieve/download an incident attachment - * description: Download or view a file attached to an incident. * tags: [Incidents] * security: * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: Incident ID - * - in: path - * name: attachmentId - * required: true - * schema: - * type: string - * description: Attachment ID - * responses: - * 200: - * description: Attachment returned successfully - * 400: - * description: No file uploaded or invalid file type - * 401: - * description: Unauthorized - * 403: - * description: Forbidden or outside authorized ownership/scope - * 404: - * description: Incident or attachment not found */ router.get( @@ -350,4 +210,5 @@ router.get( authorizePermissions("incident:view"), getAttachment ); + export default router; \ No newline at end of file From 9f8bfbc04558c4842d1099fb548324418c7bbda7 Mon Sep 17 00:00:00 2001 From: Arshdeep225615024 Date: Tue, 5 May 2026 16:52:25 +1000 Subject: [PATCH 5/5] update swagger docs --- app-backend/src/routes/incident.routes.js | 123 +++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/app-backend/src/routes/incident.routes.js b/app-backend/src/routes/incident.routes.js index a111bb325..b69acf088 100644 --- a/app-backend/src/routes/incident.routes.js +++ b/app-backend/src/routes/incident.routes.js @@ -32,6 +32,7 @@ router.use(auth); * description: | * Create a new incident report for a shift. * Typically used by guards for shifts assigned to them. + * Supports optional GPS metadata through latitude and longitude. * tags: [Incidents] * security: * - bearerAuth: [] @@ -48,11 +49,14 @@ router.use(auth); * properties: * shiftId: * type: string + * example: "69f97b98bcb1382842655170" * severity: * type: string * enum: [low, medium, high] + * example: high * description: * type: string + * example: "Unauthorized access detected near the loading dock." * latitude: * type: number * example: -37.8136 @@ -62,9 +66,20 @@ router.use(auth); * responses: * 201: * description: Incident created successfully + * 400: + * description: Missing or invalid input + * 401: + * description: Unauthorized + * 403: + * description: Forbidden or guard not assigned to shift + * 404: + * description: Shift not found * * get: * summary: List incidents + * description: | + * Retrieve incidents with optional filters for shift, guard, severity, + * structured status lifecycle, and recorded date range. * tags: [Incidents] * security: * - bearerAuth: [] @@ -73,30 +88,45 @@ router.use(auth); * name: shiftId * schema: * type: string + * description: Filter by shift ID * - in: query * name: guardId * schema: * type: string + * description: Filter by guard ID * - in: query * name: severity * schema: * type: string * enum: [low, medium, high] + * description: Filter by severity * - in: query * name: status * schema: * type: string * enum: [SUBMITTED, IN_REVIEW, RESOLVED] + * description: Filter by incident lifecycle status * - in: query * name: startDate * schema: * type: string * format: date + * description: Return incidents recorded on or after this date * - in: query * name: endDate * schema: * type: string * format: date + * description: Return incidents recorded on or before this date + * responses: + * 200: + * description: Incidents retrieved successfully + * 400: + * description: Invalid filter value + * 401: + * description: Unauthorized + * 403: + * description: Forbidden */ router.post("/", authorizePermissions("incident:create"), createIncident); @@ -107,6 +137,9 @@ router.get("/", authorizePermissions("incident:view"), getIncidents); * /api/v1/incidents/{id}: * get: * summary: Retrieve a single incident + * description: | + * Return full incident details including shift, reporter, severity, + * status, recorded timestamp, GPS location, and attachments. * tags: [Incidents] * security: * - bearerAuth: [] @@ -116,15 +149,34 @@ router.get("/", authorizePermissions("incident:view"), getIncidents); * required: true * schema: * type: string + * description: Incident ID * responses: * 200: * description: Incident retrieved successfully + * 401: + * description: Unauthorized + * 403: + * description: Forbidden or outside authorized ownership/scope + * 404: + * description: Incident not found * * patch: * summary: Update an incident + * description: | + * Update incident details with role-based restrictions. + * + * Status follows the structured lifecycle: + * SUBMITTED → IN_REVIEW → RESOLVED. * tags: [Incidents] * security: * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Incident ID * requestBody: * required: true * content: @@ -135,24 +187,52 @@ router.get("/", authorizePermissions("incident:view"), getIncidents); * severity: * type: string * enum: [low, medium, high] + * example: medium + * description: Admin only * description: * type: string + * example: "Updated incident details after review." * status: * type: string * enum: [SUBMITTED, IN_REVIEW, RESOLVED] * example: IN_REVIEW + * description: Employer and admin only * responses: * 200: * description: Incident updated successfully + * 400: + * description: Invalid status value or invalid lifecycle transition + * 401: + * description: Unauthorized + * 403: + * description: Forbidden or outside authorized ownership/scope + * 404: + * description: Incident not found * * delete: * summary: Soft-delete an incident + * description: | + * Soft-delete an incident without permanently removing it from the database. + * This preserves audit history. * tags: [Incidents] * security: * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Incident ID * responses: * 200: * description: Incident deleted successfully + * 401: + * description: Unauthorized + * 403: + * description: Forbidden + * 404: + * description: Incident not found */ router.patch("/:id", authorizePermissions("incident:update"), updateIncident); @@ -165,7 +245,9 @@ router.delete("/:id", authorizePermissions("incident:delete"), deleteIncident); * post: * summary: Upload an incident attachment * description: | - * Upload a supported file (images, videos, audio, or PDFs) and attach it to an existing incident. + * Upload a supported file and attach it to an existing incident. + * + * Supported files: images, videos, audio, and PDFs. * tags: [Incidents] * security: * - bearerAuth: [] @@ -175,17 +257,31 @@ router.delete("/:id", authorizePermissions("incident:delete"), deleteIncident); * required: true * schema: * type: string + * description: Incident ID * requestBody: * required: true * content: * multipart/form-data: * schema: * type: object + * required: + * - file * properties: * file: * type: string * format: binary - * description: Supported files: images, videos, audio, and PDFs. + * description: Images, videos, audio, and PDF files are supported. + * responses: + * 200: + * description: Attachment uploaded successfully + * 400: + * description: No file uploaded or invalid file type + * 401: + * description: Unauthorized + * 403: + * description: Forbidden or outside authorized ownership/scope + * 404: + * description: Incident not found */ router.post( @@ -200,9 +296,32 @@ router.post( * /api/v1/incidents/{id}/attachments/{attachmentId}: * get: * summary: Retrieve/download an incident attachment + * description: Download or view a file attached to an incident. * tags: [Incidents] * security: * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Incident ID + * - in: path + * name: attachmentId + * required: true + * schema: + * type: string + * description: Attachment ID + * responses: + * 200: + * description: Attachment returned successfully + * 401: + * description: Unauthorized + * 403: + * description: Forbidden or outside authorized ownership/scope + * 404: + * description: Incident or attachment not found */ router.get(