const { Message, ConditionCheck, Rental } = require("../models"); const { Op } = require("sequelize"); /** * Service for verifying ownership/access to S3 files * Used to authorize signed URL requests for private content */ class S3OwnershipService { /** * Image size variant suffixes */ static SIZE_SUFFIXES = ["_th", "_md"]; /** * Extract the base key from a variant key (strips _th or _md suffix) * @param {string} key - S3 key like "messages/uuid_th.jpg" or "messages/uuid.jpg" * @returns {string} - Base key like "messages/uuid.jpg" */ static getBaseKey(key) { if (!key) return key; for (const suffix of this.SIZE_SUFFIXES) { // Match suffix before file extension (e.g., _th.jpg, _md.png) const regex = new RegExp(`${suffix}(\\.[^.]+)$`); if (regex.test(key)) { return key.replace(regex, "$1"); } } return key; } /** * Extract file type from S3 key * @param {string} key - S3 key like "messages/uuid.jpg" * @returns {string|null} - File type or null if unknown */ static getFileTypeFromKey(key) { if (!key) return null; const folder = key.split("/")[0]; const folderMap = { profiles: "profile", items: "item", messages: "message", forum: "forum", "condition-checks": "condition-check", }; return folderMap[folder] || null; } /** * Verify if a user can access a file * @param {string} key - S3 key * @param {string} userId - User ID making the request * @returns {Promise<{authorized: boolean, reason?: string}>} */ static async canAccessFile(key, userId) { const fileType = this.getFileTypeFromKey(key); switch (fileType) { case "profile": case "item": case "forum": // Public folders - anyone can access return { authorized: true }; case "message": return this.verifyMessageAccess(key, userId); case "condition-check": return this.verifyConditionCheckAccess(key, userId); default: return { authorized: false, reason: "Unknown file type" }; } } /** * Verify message image access - user must be sender OR receiver * @param {string} key - S3 key (may be variant like uuid_th.jpg) * @param {string} userId - User ID making the request * @returns {Promise<{authorized: boolean, reason?: string}>} */ static async verifyMessageAccess(key, userId) { // Use base key for lookup (DB stores original key, not variants) const baseKey = this.getBaseKey(key); const message = await Message.findOne({ where: { imageFilename: baseKey, [Op.or]: [{ senderId: userId }, { receiverId: userId }], }, }); return { authorized: !!message, reason: message ? null : "Not a participant in this message", }; } /** * Verify condition check image access - user must be rental owner OR renter * @param {string} key - S3 key (may be variant like uuid_th.jpg) * @param {string} userId - User ID making the request * @returns {Promise<{authorized: boolean, reason?: string}>} */ static async verifyConditionCheckAccess(key, userId) { // Use base key for lookup (DB stores original key, not variants) const baseKey = this.getBaseKey(key); const check = await ConditionCheck.findOne({ where: { imageFilenames: { [Op.contains]: [baseKey] }, }, include: [ { model: Rental, as: "rental", where: { [Op.or]: [{ ownerId: userId }, { renterId: userId }], }, }, ], }); return { authorized: !!check, reason: check ? null : "Not a participant in this rental", }; } } module.exports = S3OwnershipService;