chore: relocate backend into apps/api ahead of monorepo merge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:20:40 +08:00
co-authored by Claude Sonnet 5
parent af44598df6
commit eaa6d2276a
388 changed files with 0 additions and 13998 deletions
+73
View File
@@ -0,0 +1,73 @@
/***********************************************************************************************************************************************************************
* 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 };