Compare commits

...

2 Commits

8 changed files with 98 additions and 46 deletions

View File

@ -17,6 +17,7 @@ model user {
id Int @id @unique @default(autoincrement()) id Int @id @unique @default(autoincrement())
name String @unique name String @unique
code String? code String?
email String?
// TODO: Prüfen ob nötig, erstmal vorbereitet. // TODO: Prüfen ob nötig, erstmal vorbereitet.
transactions transactions[] transactions transactions[]
@ -62,3 +63,5 @@ model products {
@@fulltext([name]) @@fulltext([name])
} }
// TODO: migrate all ids to uuid?

View File

@ -17,7 +17,6 @@ export function handlePrismaError(errorObj: any, res: Response, source: string)
log.db.error(source, errorObj); log.db.error(source, errorObj);
if (errorObj instanceof Prisma.PrismaClientKnownRequestError) { if (errorObj instanceof Prisma.PrismaClientKnownRequestError) {
switch (errorObj.code) { switch (errorObj.code) {
// P2002 -> "Unique constraint failed on the {constraint}" // P2002 -> "Unique constraint failed on the {constraint}"
case 'P2002': case 'P2002':
res.status(409).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'The object needs to be unique', meta: errorObj.meta }); res.status(409).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'The object needs to be unique', meta: errorObj.meta });

View File

@ -11,7 +11,7 @@ import config from './handlers/config.js';
// Express & more // Express & more
import express from 'express'; import express from 'express';
import helmet from 'helmet'; import helmet from 'helmet';
//import fileUpload from 'express-fileupload'; import fileUpload from 'express-fileupload';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import { Eta, Options } from 'eta'; import { Eta, Options } from 'eta';
@ -78,7 +78,7 @@ if (!config.global.devmode) {
); // Add headers ); // Add headers
} }
//app.use(fileUpload()); app.use(fileUpload({ useTempFiles: false, debug: config.global.devmode }));
app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json()); app.use(bodyParser.json());
@ -92,7 +92,6 @@ app.listen(config.global.http_port, config.global.http_listen_address, () => {
log.core.trace('Running from path: ' + __path); log.core.trace('Running from path: ' + __path);
config.global.devmode && log.core.error('DEVMODE ACTIVE! Do NOT use this in prod!'); config.global.devmode && log.core.error('DEVMODE ACTIVE! Do NOT use this in prod!');
// MARK: Helper Functions // MARK: Helper Functions
function buildEtaEngine() { function buildEtaEngine() {
return (path: string, opts: Options, callback: CallableFunction) => { return (path: string, opts: Options, callback: CallableFunction) => {

View File

@ -1,10 +1,12 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import db, { handlePrismaError } from '../../../../handlers/db.js'; // Database import db, { handlePrismaError } from '../../../../handlers/db.js'; // Database
import log from '../../../../handlers/log.js'; import log from '../../../../handlers/log.js';
import __path from '../../../../handlers/path.js'; import __path from '../../../../handlers/path.js';
import { schema_get, schema_post, schema_del } from './image_schema.js'; import { schema_get, schema_post, schema_del } from './image_schema.js';
import { UploadedFile } from 'express-fileupload';
// MARK: GET image // MARK: GET image
async function get(req: Request, res: Response) { async function get(req: Request, res: Response) {
@ -24,9 +26,11 @@ async function get(req: Request, res: Response) {
if (result) { if (result) {
const img_path = path.join(__path, 'images', `${value.id}.png`); const img_path = path.join(__path, 'images', `${value.id}.png`);
// Serve stored or default image // Serve stored or default image
log.api?.debug('Image exists:', fs.existsSync(img_path));
fs.existsSync(img_path) ? res.sendFile(img_path) : res.sendFile(path.join(__path, 'images', 'default.png')); fs.existsSync(img_path) ? res.sendFile(img_path) : res.sendFile(path.join(__path, 'images', 'default.png'));
} else { } else {
// Product does not exist // Product does not exist
log.api?.debug('Product does not exist, using default image ');
res.sendFile(path.join(__path, 'images', 'default.png')); res.sendFile(path.join(__path, 'images', 'default.png'));
} }
}) })
@ -36,35 +40,49 @@ async function get(req: Request, res: Response) {
} }
} }
// MARK: CREATE image (upload) // MARK: CREATE/UPDATE image (upload)
async function post(req: Request, res: Response) { async function post(req: Request, res: Response) {
const { error, value } = schema_post.validate(req.query); const { error, value } = schema_post.validate(req.query);
if (error) { if (error) {
log.api?.debug('POST image Error:', req.query, value, error.details[0].message); log.api?.debug('POST image Error:', req.query, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else { } else {
log.api?.debug('POST image Success:', req.query, value); // Check if multipart has image with file
if (!req.files || !req.files.image) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'Missing image' });
return;
}
// if (!req.files || Object.keys(req.files).length === 0) { const upload_file = req.files.image as UploadedFile;
// res.status(400).send('No files were uploaded.'); const upload_path = path.join(__path, 'images', `${value.id}.png`);
// } const allowedMimeTypes = ['image/png'];
// const upload_file = req.files?.file; // Check if mimetype is allowed
// let upload_path; if (!allowedMimeTypes.includes(upload_file.mimetype)) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'Only image/png is allowed' });
return;
}
// Check if product exists
const result = await db.products.findUnique({
where: {
id: value.id
}
});
// //uploadPath = __dirname + '/somewhere/on/your/server/' + sampleFile.name; if (!result) {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified product' });
return;
}
// const allowedMimeTypes = ['image/png', 'image/jpeg']; // Write file
// // if (!allowedMimeTypes.includes(upload_file.mimetype)) { upload_file.mv(upload_path, (err) => {
// // return res.status(400).send('Invalid file type. Only PNG and JPEG are allowed.'); if (err) {
// // } res.status(500).json({ status: 'ERROR', errorcode: 'IO_ERROR', message: 'Could not write image to disk' });
return;
// uploadedFile.mv(`/path/to/save/${uploadedFile.name}`, (err) => { }
// if (err) { log.api?.debug('File uploaded to:', upload_path);
// return res.status(500).send(err); res.status(200).json({ status: 'CREATED', message: 'Successfully uploaded image', id: result.id });
// } });
// res.send('File uploaded!');
} }
} }
@ -76,18 +94,34 @@ async function del(req: Request, res: Response) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else { } else {
log.api?.debug('DEL image Success:', req.query, value); log.api?.debug('DEL image Success:', req.query, value);
// await db.products const del_path = path.join(__path, 'images', `${value.id}.png`);
// .delete({ await db.products
// where: { .findUnique({
// id: value.id where: {
// } id: value.id
// }) }
// .then((result) => { })
// res.status(200).json({ status: 'DELETED', message: 'Successfully deleted product', id: result.id }); .then((result) => {
// }) if (result) {
// .catch((err) => { if (!fs.existsSync(del_path)) {
// handlePrismaError(err, res, 'DEL products'); res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Product exists but no image found' });
// }); return;
}
fs.unlink(del_path, (err) => {
if (err) {
res.status(500).json({ status: 'ERROR', errorcode: 'IO_ERROR', message: 'Could not delete image' });
return;
}
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted image', id: result.id });
log.api?.debug('File removed from:', del_path);
});
} else {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified product (image)' });
}
})
.catch((err) => {
handlePrismaError(err, res, 'DEL products');
});
} }
} }

View File

@ -3,22 +3,24 @@ import validator from 'joi'; // DOCS: https://joi.dev/api
// MARK: GET image // MARK: GET image
const schema_get = validator.object({ const schema_get = validator.object({
id: validator.number().positive().precision(0) id: validator.number().positive().precision(0).default(0).note('product id') // id 0 should never exist, since id autoincrement starts at 1
}); });
// MARK: CREATE image // MARK: CREATE / UPDATE image
const schema_post = validator.object({ const schema_post = validator.object({
id: validator.number().positive().precision(0) id: validator.number().positive().precision(0).required().note('product id')
//image: validator.string().required().note('product image as multipart/form-data')
}); });
// MARK: DELETE products // MARK: DELETE products
const schema_del = validator.object({ const schema_del = validator.object({
id: validator.number().positive().precision(0) id: validator.number().positive().precision(0).required().note('product id')
}); });
// Describe all schemas // Describe all schemas
const schema_get_desc = schema_get.describe(); const schema_get_desc = schema_get.describe();
const schema_post_desc = schema_post.describe(); const schema_post_desc = schema_post.describe();
const schema_patch_desc = schema_post.describe(); // Just for show (POST = PATCH)
const schema_del_desc = schema_del.describe(); const schema_del_desc = schema_del.describe();
// GET route // GET route
@ -26,6 +28,7 @@ export default async function get(req: Request, res: Response) {
res.status(200).json({ res.status(200).json({
GET: schema_get_desc, GET: schema_get_desc,
POST: schema_post_desc, POST: schema_post_desc,
PATCH: schema_patch_desc,
DELETE: schema_del_desc DELETE: schema_del_desc
}); });
} }

View File

@ -29,6 +29,16 @@ Router.use('*', function (req, res, next) {
next(); next();
}); });
// All empty strings are undefined (not null!) values (query)
Router.use('*', function (req, res, next) {
for (let key in req.query) {
if (req.query[key] === '') {
req.query[key] = undefined;
}
}
next();
});
// All api routes lowercase! Yea I know but when strict: true it matters. // All api routes lowercase! Yea I know but when strict: true it matters.
Router.route('/user').get(user_route.get).post(user_route.post).patch(user_route.patch).delete(user_route.del); Router.route('/user').get(user_route.get).post(user_route.post).patch(user_route.patch).delete(user_route.del);
Router.route('/user/describe').get(user_schema); Router.route('/user/describe').get(user_schema);
@ -39,8 +49,8 @@ Router.route('/user/codecheck/describe').get(user_codecheck_schema);
Router.route('/products').get(products_route.get).post(products_route.post).patch(products_route.patch).delete(products_route.del); Router.route('/products').get(products_route.get).post(products_route.post).patch(products_route.patch).delete(products_route.del);
Router.route('/products/describe').get(products_schema); Router.route('/products/describe').get(products_schema);
Router.route('/image').get(image_route.get).post(image_route.post).delete(image_route.del); Router.route('/image').get(image_route.get).post(image_route.post).patch(image_route.post).delete(image_route.del); // POST and PATCH are handled in 'image_route.post'
Router.route('/image/describe').get(image_schema); // TODO: Checken, ob das probleme mit dem image_provider endpoint macht. Router.route('/image/describe').get(image_schema);
Router.route('/version').get(versionRoute.get); Router.route('/version').get(versionRoute.get);
Router.route('/test').get(testRoute.get); Router.route('/test').get(testRoute.get);

View File

@ -43,7 +43,7 @@ async function get(req: Request, res: Response) {
}); });
res.status(200).json({ count, result }); res.status(200).json({ count, result });
} else { } else {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified object' }); res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified user' });
} }
}) })
.catch((err) => { .catch((err) => {
@ -75,7 +75,7 @@ async function get(req: Request, res: Response) {
}); });
res.status(200).json({ count, result }); res.status(200).json({ count, result });
} else { } else {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified object' }); res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find any users' });
} }
}) })
.catch((err) => { .catch((err) => {
@ -97,6 +97,7 @@ async function post(req: Request, res: Response) {
.create({ .create({
data: { data: {
name: value.name, name: value.name,
email: value.email,
code: value.code === '0000' ? null : value.code code: value.code === '0000' ? null : value.code
}, },
select: { select: {
@ -127,6 +128,7 @@ async function patch(req: Request, res: Response) {
}, },
data: { data: {
name: value.name, name: value.name,
email: value.email,
code: value.code === '0000' ? null : value.code code: value.code === '0000' ? null : value.code
}, },
select: { select: {

View File

@ -22,6 +22,7 @@ const schema_get = validator
// MARK: CREATE user // MARK: CREATE user
const schema_post = validator.object({ const schema_post = validator.object({
name: validator.string().min(1).max(32).required(), name: validator.string().min(1).max(32).required(),
email: validator.string().email().trim().required(),
code: validator.string().min(4).max(4).trim().regex(new RegExp(/^[0-9]+$/)) code: validator.string().min(4).max(4).trim().regex(new RegExp(/^[0-9]+$/))
}); });
@ -30,9 +31,10 @@ const schema_patch = validator
.object({ .object({
id: validator.number().positive().precision(0).required(), id: validator.number().positive().precision(0).required(),
name: validator.string().min(1).max(32), name: validator.string().min(1).max(32),
email: validator.string().email().trim(),
code: validator.string().min(4).max(4).trim().regex(new RegExp(/^[0-9]+$/)) code: validator.string().min(4).max(4).trim().regex(new RegExp(/^[0-9]+$/))
}) })
.or('name', 'code'); .or('name', 'email', 'code');
// MARK: DELETE user // MARK: DELETE user
const schema_del = validator.object({ const schema_del = validator.object({