/*********************************************************************************************************************************************************************** * File Name: avatar.service.js * Type of Program: Service * Description: Shared avatar processing/orchestration for admin + client self-profile. * Server-side authoritative resize — every avatar is normalized to a fixed * 200x200 JPEG regardless of what the client sends, so browser-side cropping * (see AvatarUploadDialog.jsx) is a UX convenience, not the enforcement point. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Aug. 10, 2026 ***********************************************************************************************************************************************************************/ 'use strict'; const sharp = require('sharp'); const { uploadFile, deleteFile } = require('./s3.service'); const AVATAR_SIZE = 200; const JPEG_QUALITY = 90; // ─── resizeAvatarBuffer ───────────────────────────────────────────────────────── // Normalizes any accepted input (JPEG/PNG/WebP/GIF) into a fixed 200x200 JPEG. // GIF animation and PNG transparency are intentionally dropped — avatars render // in an opaque round mask, so a single static frame is all that's ever shown. async function resizeAvatarBuffer(buffer) { try { return await sharp(buffer) .rotate() // respect EXIF orientation before crop .resize(AVATAR_SIZE, AVATAR_SIZE, { fit: 'cover', position: 'centre' }) .jpeg({ quality: JPEG_QUALITY }) .toBuffer(); } catch (err) { throw Object.assign(new Error('Could not process the uploaded image.'), { status: 400, cause: err }); } } // ─── replaceUserAvatar ────────────────────────────────────────────────────────── // Deletes the old S3 object (if any), resizes the new upload, stores it, and // returns the avatar metadata object. Does not persist to the user row — // callers own that so they can merge it into personal_info their own way. async function replaceUserAvatar(user, file) { const oldKey = user.personal_info?.avatar?.uuid; if (oldKey) await deleteFile(oldKey).catch(() => {}); const resized = await resizeAvatarBuffer(file.buffer); const { url, uuid } = await uploadFile({ buffer: resized, originalname: 'avatar.jpg', mimetype: 'image/jpeg', ownerType: 'avatar', }); return { url, uuid, name: file.originalname, mime_type: 'image/jpeg', size: resized.length, }; } // ─── removeUserAvatar ─────────────────────────────────────────────────────────── async function removeUserAvatar(user) { const key = user.personal_info?.avatar?.uuid; if (!key) throw Object.assign(new Error('No avatar to remove.'), { status: 404 }); await deleteFile(key).catch(() => {}); } module.exports = { resizeAvatarBuffer, replaceUserAvatar, removeUserAvatar };