feat(MemberGroupPhoto): implement group photo management functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 38s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 38s
- Added MemberGroupPhoto model and established relationships with Club and User models. - Introduced new routes for managing group photos in the backend. - Enhanced frontend components to support group photo cropping and member image updates. - Updated localization files to include new terms related to group photo processing across multiple languages. - Refactored server.js to include MemberGroupPhoto in the synchronization process.
This commit is contained in:
177
backend/services/memberGroupPhotoService.js
Normal file
177
backend/services/memberGroupPhotoService.js
Normal file
@@ -0,0 +1,177 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import MemberGroupPhoto from '../models/MemberGroupPhoto.js';
|
||||
import { checkAccess, getUserByToken } from '../utils/userUtils.js';
|
||||
|
||||
class MemberGroupPhotoService {
|
||||
async list(userToken, clubId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const photos = await MemberGroupPhoto.findAll({
|
||||
where: { clubId },
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
return photos.map(photo => this.mapPhoto(photo));
|
||||
}
|
||||
|
||||
async create(userToken, clubId, file, payload = {}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
if (!file?.buffer) {
|
||||
return { status: 400, response: { success: false, error: 'No image provided' } };
|
||||
}
|
||||
if (!String(file.mimetype || '').startsWith('image/')) {
|
||||
return { status: 400, response: { success: false, error: 'File must be an image' } };
|
||||
}
|
||||
|
||||
const user = await getUserByToken(userToken);
|
||||
const title = this.sanitizeText(payload.title, 255) || this.defaultTitle();
|
||||
const description = this.sanitizeText(payload.description, 5000) || null;
|
||||
const takenAt = this.parseDate(payload.takenAt);
|
||||
|
||||
let photo = null;
|
||||
try {
|
||||
photo = await MemberGroupPhoto.create({
|
||||
clubId,
|
||||
title,
|
||||
description,
|
||||
fileName: 'pending.jpg',
|
||||
originalFileName: this.sanitizeText(file.originalname, 255) || null,
|
||||
mimeType: 'image/jpeg',
|
||||
fileSize: file.size || file.buffer.length || null,
|
||||
takenAt,
|
||||
createdByUserId: user?.id || null
|
||||
});
|
||||
|
||||
const directory = await this.ensurePhotoDirectory(clubId);
|
||||
const fileName = `${photo.id}.jpg`;
|
||||
const targetPath = path.join(directory, fileName);
|
||||
const info = await sharp(file.buffer)
|
||||
.rotate()
|
||||
.resize(2400, 2400, { fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: 88 })
|
||||
.toFile(targetPath);
|
||||
|
||||
await photo.update({
|
||||
fileName,
|
||||
fileSize: info.size || file.size || file.buffer.length || null,
|
||||
width: info.width || null,
|
||||
height: info.height || null
|
||||
});
|
||||
|
||||
return { status: 200, response: { success: true, photo: this.mapPhoto(photo) } };
|
||||
} catch (error) {
|
||||
if (photo?.id) {
|
||||
await photo.destroy().catch(() => {});
|
||||
await fs.promises.unlink(this.getPhotoPath(clubId, `${photo.id}.jpg`)).catch(() => {});
|
||||
}
|
||||
console.error('[MemberGroupPhotoService.create] error:', error);
|
||||
return { status: 500, response: { success: false, error: 'Failed to save group photo' } };
|
||||
}
|
||||
}
|
||||
|
||||
async update(userToken, clubId, photoId, payload = {}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const photo = await this.findPhoto(clubId, photoId);
|
||||
if (!photo) {
|
||||
return { status: 404, response: { success: false, error: 'Group photo not found' } };
|
||||
}
|
||||
|
||||
const updates = {};
|
||||
if (payload.title !== undefined) {
|
||||
updates.title = this.sanitizeText(payload.title, 255) || photo.title;
|
||||
}
|
||||
if (payload.description !== undefined) {
|
||||
updates.description = this.sanitizeText(payload.description, 5000) || null;
|
||||
}
|
||||
if (payload.takenAt !== undefined) {
|
||||
updates.takenAt = this.parseDate(payload.takenAt);
|
||||
}
|
||||
|
||||
await photo.update(updates);
|
||||
return { status: 200, response: { success: true, photo: this.mapPhoto(photo) } };
|
||||
}
|
||||
|
||||
async remove(userToken, clubId, photoId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const photo = await this.findPhoto(clubId, photoId);
|
||||
if (!photo) {
|
||||
return { status: 404, response: { success: false, error: 'Group photo not found' } };
|
||||
}
|
||||
|
||||
const imagePath = this.getPhotoPath(clubId, photo.fileName);
|
||||
await photo.destroy();
|
||||
await fs.promises.unlink(imagePath).catch((error) => {
|
||||
console.warn('[MemberGroupPhotoService.remove] file removal failed:', error.message || error);
|
||||
});
|
||||
|
||||
return { status: 200, response: { success: true } };
|
||||
}
|
||||
|
||||
async getImage(userToken, clubId, photoId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const photo = await this.findPhoto(clubId, photoId);
|
||||
if (!photo) {
|
||||
return { status: 404, error: 'Group photo not found' };
|
||||
}
|
||||
|
||||
const imagePath = this.getPhotoPath(clubId, photo.fileName);
|
||||
if (!fs.existsSync(imagePath)) {
|
||||
return { status: 404, error: 'Group photo file not found' };
|
||||
}
|
||||
|
||||
return { status: 200, imagePath: path.resolve(imagePath) };
|
||||
}
|
||||
|
||||
async findPhoto(clubId, photoId) {
|
||||
return MemberGroupPhoto.findOne({ where: { id: photoId, clubId } });
|
||||
}
|
||||
|
||||
mapPhoto(photo) {
|
||||
const data = typeof photo?.toJSON === 'function' ? photo.toJSON() : photo;
|
||||
return {
|
||||
id: data.id,
|
||||
clubId: data.clubId,
|
||||
title: data.title,
|
||||
description: data.description || '',
|
||||
fileName: data.fileName,
|
||||
originalFileName: data.originalFileName || '',
|
||||
mimeType: data.mimeType || 'image/jpeg',
|
||||
fileSize: data.fileSize || null,
|
||||
width: data.width || null,
|
||||
height: data.height || null,
|
||||
takenAt: data.takenAt || null,
|
||||
createdByUserId: data.createdByUserId || null,
|
||||
createdAt: data.createdAt || null,
|
||||
updatedAt: data.updatedAt || null,
|
||||
imageUrl: `/api/member-group-photos/${data.clubId}/${data.id}/image?t=${new Date(data.updatedAt || Date.now()).getTime()}`
|
||||
};
|
||||
}
|
||||
|
||||
async ensurePhotoDirectory(clubId) {
|
||||
const directory = path.join('images', 'member-group-photos', String(clubId));
|
||||
await fs.promises.mkdir(directory, { recursive: true });
|
||||
return directory;
|
||||
}
|
||||
|
||||
getPhotoPath(clubId, fileName) {
|
||||
return path.join('images', 'member-group-photos', String(clubId), fileName);
|
||||
}
|
||||
|
||||
sanitizeText(value, maxLength) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) return '';
|
||||
return text.slice(0, maxLength);
|
||||
}
|
||||
|
||||
parseDate(value) {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
defaultTitle() {
|
||||
return `Gruppenfoto ${new Date().toLocaleDateString('de-DE')}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default new MemberGroupPhotoService();
|
||||
Reference in New Issue
Block a user