209 lines
8.0 KiB
JavaScript
209 lines
8.0 KiB
JavaScript
const { S3Client, PutObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
|
const CoffeeService = require('../../services/subscriptions/CoffeeService');
|
|
const { logger } = require('../../middleware/logger');
|
|
|
|
function buildPictureUrlFromKey(key) {
|
|
const endpoint = process.env.EXOSCALE_ENDPOINT || '';
|
|
const bucket = process.env.EXOSCALE_BUCKET || '';
|
|
// If using S3-compatible endpoint with virtual-hosted-style, construct URL accordingly
|
|
if (endpoint.startsWith('http')) {
|
|
return `${endpoint.replace(/\/$/, '')}/${bucket}/${key}`;
|
|
}
|
|
return key; // fallback: store key only
|
|
}
|
|
|
|
exports.list = async (req, res) => {
|
|
const rows = await CoffeeService.list();
|
|
const items = (rows || []).map(r => ({
|
|
...r,
|
|
pictureUrl: r.object_storage_id ? buildPictureUrlFromKey(r.object_storage_id) : ''
|
|
}));
|
|
res.json(items);
|
|
};
|
|
|
|
exports.create = async (req, res) => {
|
|
try {
|
|
const { title, description, price } = req.body;
|
|
const currency = req.body.currency || 'EUR';
|
|
const is_featured = req.body.is_featured === 'true' || req.body.is_featured === true ? true : false;
|
|
// Fixed billing defaults
|
|
const billing_interval = 'month';
|
|
const interval_count = 1;
|
|
const state = req.body.state === 'false' || req.body.state === false ? false : true; // default available
|
|
|
|
// If file uploaded, push to Exoscale and set object_storage_id
|
|
let object_storage_id = null;
|
|
let original_filename = null;
|
|
let uploadedKey = null;
|
|
if (req.file) {
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
});
|
|
const key = `coffee/products/${Date.now()}_${req.file.originalname}`;
|
|
await s3.send(new PutObjectCommand({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
Key: key,
|
|
Body: req.file.buffer,
|
|
ContentType: req.file.mimetype,
|
|
}));
|
|
object_storage_id = key;
|
|
original_filename = req.file.originalname;
|
|
uploadedKey = key;
|
|
logger.info('[CoffeeController.create] uploaded picture', { key });
|
|
}
|
|
|
|
const created = await CoffeeService.create({
|
|
title,
|
|
description,
|
|
price: Number(price),
|
|
currency,
|
|
is_featured,
|
|
billing_interval,
|
|
interval_count,
|
|
object_storage_id,
|
|
original_filename,
|
|
state,
|
|
});
|
|
res.status(201).json({
|
|
...created,
|
|
pictureUrl: created.object_storage_id ? buildPictureUrlFromKey(created.object_storage_id) : ''
|
|
});
|
|
} catch (e) {
|
|
logger.error('[CoffeeController.create] error', { msg: e.message, stack: e.stack?.split('\n')[0] });
|
|
// best-effort cleanup of uploaded object on failure
|
|
try {
|
|
if (object_storage_id) {
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
});
|
|
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: object_storage_id }));
|
|
}
|
|
} catch (cleanupErr) {
|
|
logger.warn('[CoffeeController.create] cleanup failed', { msg: cleanupErr.message });
|
|
}
|
|
if (e.code === 'VALIDATION_ERROR') return res.status(400).json({ error: e.message, fields: e.errors });
|
|
res.status(500).json({ error: 'Failed to create product' });
|
|
}
|
|
};
|
|
|
|
exports.update = async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id, 10);
|
|
const { title, description, price } = req.body;
|
|
const currency = req.body.currency;
|
|
const is_featured = req.body.is_featured === undefined ? undefined : (req.body.is_featured === 'true' || req.body.is_featured === true);
|
|
const state = req.body.state === undefined ? undefined : (req.body.state === 'false' || req.body.state === false ? false : true);
|
|
|
|
let object_storage_id;
|
|
let original_filename;
|
|
let uploadedKey = null;
|
|
if (req.file) {
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
});
|
|
const key = `coffee/products/${Date.now()}_${req.file.originalname}`;
|
|
await s3.send(new PutObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key, Body: req.file.buffer, ContentType: req.file.mimetype }));
|
|
object_storage_id = key;
|
|
original_filename = req.file.originalname;
|
|
uploadedKey = key;
|
|
logger.info('[CoffeeController.update] uploaded new picture', { id, key });
|
|
}
|
|
|
|
const current = await CoffeeService.get(id);
|
|
if (!current) return res.status(404).json({ error: 'Not found' });
|
|
|
|
const updated = await CoffeeService.update(id, {
|
|
title: title ?? current.title,
|
|
description: description ?? current.description,
|
|
price: price !== undefined ? Number(price) : current.price,
|
|
currency: currency !== undefined ? currency : current.currency,
|
|
is_featured: is_featured !== undefined ? is_featured : !!current.is_featured,
|
|
object_storage_id: object_storage_id !== undefined ? object_storage_id : current.object_storage_id,
|
|
original_filename: original_filename !== undefined ? original_filename : current.original_filename,
|
|
state: state !== undefined ? state : !!current.state,
|
|
});
|
|
res.json({
|
|
...updated,
|
|
pictureUrl: updated?.object_storage_id ? buildPictureUrlFromKey(updated.object_storage_id) : ''
|
|
});
|
|
} catch (e) {
|
|
logger.error('[CoffeeController.update] error', { msg: e.message });
|
|
// best-effort cleanup of newly uploaded object on failure
|
|
try {
|
|
if (object_storage_id) {
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
});
|
|
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: object_storage_id }));
|
|
}
|
|
} catch (cleanupErr) {
|
|
logger.warn('[CoffeeController.update] cleanup failed', { msg: cleanupErr.message });
|
|
}
|
|
if (e.code === 'VALIDATION_ERROR') return res.status(400).json({ error: e.message, fields: e.errors });
|
|
res.status(500).json({ error: 'Failed to update product' });
|
|
}
|
|
};
|
|
|
|
exports.setState = async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id, 10);
|
|
const { state } = req.body; // boolean
|
|
const updated = await CoffeeService.setState(id, !!state);
|
|
res.json({
|
|
...updated,
|
|
pictureUrl: updated?.object_storage_id ? buildPictureUrlFromKey(updated.object_storage_id) : ''
|
|
});
|
|
} catch (e) {
|
|
res.status(400).json({ error: e.message });
|
|
}
|
|
};
|
|
|
|
exports.remove = async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id, 10);
|
|
// fetch current for object_storage_id
|
|
const current = await CoffeeService.get(id);
|
|
const ok = await CoffeeService.delete(id);
|
|
if (!ok) return res.status(404).json({ error: 'Not found' });
|
|
// best-effort delete object from storage
|
|
try {
|
|
if (current && current.object_storage_id) {
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
});
|
|
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: current.object_storage_id }));
|
|
}
|
|
} catch (cleanupErr) {
|
|
logger.warn('[CoffeeController.remove] storage delete failed', { msg: cleanupErr.message });
|
|
}
|
|
res.status(204).end();
|
|
} catch (e) {
|
|
res.status(500).json({ error: 'Failed to delete product' });
|
|
}
|
|
};
|