Centralized errors and added their translations. Didn't test
This commit is contained in:
parent
47e90764e7
commit
7a2ab7dd5b
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"authentication not found": "Authentication token not found!",
|
||||||
|
"authentication invalid": "Authentication token is invalid",
|
||||||
|
"user duplicate": "Such user already exists!",
|
||||||
|
"user invalid syntax": "Invalid syntax in one of user's parameters!",
|
||||||
|
"user already in group": "User is already in group!",
|
||||||
|
"username taken": "Username is taken!",
|
||||||
|
"username not found": "Such username not found!",
|
||||||
|
"username required": "Username is required!",
|
||||||
|
"password required": "Password is required!",
|
||||||
|
"password invalid": "Wrong password!",
|
||||||
|
"group name taken": "Group name is taken!",
|
||||||
|
"group name not found": "Group with such name not found!",
|
||||||
|
"group id not found": "Group with such ID not found!",
|
||||||
|
"group not an owner": "You are not an owner of this group!",
|
||||||
|
"group not a member": "You are not a member of this group!",
|
||||||
|
"abstract product not found": "Such abstract product is not found!",
|
||||||
|
"hash without file": "Please, supply a hash of an image!",
|
||||||
|
"file without hash": "Please, supply an image!",
|
||||||
|
"abstract product invalid syntax": "Invalid syntax in one of user's parameters!",
|
||||||
|
"barcode not found": "Such barcode not found!",
|
||||||
|
"barcode duplicate": "Such barcode already exists!",
|
||||||
|
"barcode too long": "This barcode is too long!",
|
||||||
|
"barcode invalid syntax": "Invalid syntax in barcode!",
|
||||||
|
"abstract product name not found": "Abstract product with such name not found!",
|
||||||
|
"abstract product name duplicate": "Abstract product with such name already exists!",
|
||||||
|
"abstract product name too long": "Abstract product name is too long!",
|
||||||
|
"abstract product name invalid syntax": "Invalid syntax in abstract product name!",
|
||||||
|
"net weight too long": "Specified net weight is too long!",
|
||||||
|
"net weight invalid syntax": "Invalid syntax in specified net weight!",
|
||||||
|
"image hash too long": "Specified image hash is too long!",
|
||||||
|
"image hash invalid syntax": "Invalid syntax in specified image hash!",
|
||||||
|
"category too long": "Specified category is too long!",
|
||||||
|
"category invalid syntax": "Invalid syntax in specified category!",
|
||||||
|
"category not found": "Such category not found!",
|
||||||
|
"unit too long": "Specified unit is too long!",
|
||||||
|
"unit invalid syntax": "Invalid syntax in specified unit!",
|
||||||
|
"product not found": "Such product not found!",
|
||||||
|
"product invalid syntax": "Invalid syntax in one of product's parameters!",
|
||||||
|
"abstract product id not found": "Specified abstract product id not found!",
|
||||||
|
"abstract product id invalid syntax": "Invalid syntax in abstract product id!",
|
||||||
|
"expiry date too long": "Specified expiry date is too long!",
|
||||||
|
"expiry date invalid syntax": "Invalid syntax in specified expiry date!",
|
||||||
|
"categories not found": "Such category not found!",
|
||||||
|
"categories duplicate": "Such category already exists!",
|
||||||
|
"categories invalid syntax": "Invalid syntax in one of category parameters!",
|
||||||
|
"ok": "Successfull.",
|
||||||
|
"invalid syntax": "Invalid syntax!",
|
||||||
|
"png only": "Only .png files are allowed!",
|
||||||
|
"too long": "Specified value is too long!",
|
||||||
|
"unknown": "Unknown server error. Please, contact the develoeper!"
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"authentication not found": "Токен аутентификации не найден!",
|
||||||
|
"authentication invalid": "Токен аутентификации неверен!",
|
||||||
|
"user duplicate": "Такой пользователь уже существует!",
|
||||||
|
"user invalid syntax": "Неправильный синткасис в одном из параметров пользователя!",
|
||||||
|
"user already in group": "Пользователь уже в группе!",
|
||||||
|
"username taken": "Имя пользователя занято!",
|
||||||
|
"username not found": "Такое имя пользователя не найдено!",
|
||||||
|
"username required": "Требуется имя пользователя!",
|
||||||
|
"password required": "Требуется пароль!",
|
||||||
|
"password invalid": "Неверный пароль!",
|
||||||
|
"group name taken": "Такое имя группы уже существует!",
|
||||||
|
"group name not found": "Группы с таким именем не существует!",
|
||||||
|
"group id not found": "Группы с таким ID не существует!",
|
||||||
|
"group not an owner": "Вы не владелец этой группы!",
|
||||||
|
"group not a member": "Вы не участник этой группы!",
|
||||||
|
"abstract product not found": "Такой абстрактный продукт не найден!",
|
||||||
|
"hash without file": "Пожалуйста, укажите хеш изображения!",
|
||||||
|
"file without hash": "Пожалуйста, укажите изображение!",
|
||||||
|
"abstract product invalid syntax": "Неправильный синткасис в одном из параметров абстрактного продукта!",
|
||||||
|
"barcode not found": "Такой штрихкод не найден!",
|
||||||
|
"barcode duplicate": "Такой штрихкод уже существует!",
|
||||||
|
"barcode too long": "Этот штрихкод слишком большой!",
|
||||||
|
"barcode invalid syntax": "Этот штрихкод неверен!",
|
||||||
|
"abstract product name not found": "абстрактного продукта с таким именем не найдено!",
|
||||||
|
"abstract product name duplicate": "абстрактный продукт с таким именем уже существует!",
|
||||||
|
"abstract product name too long": "Имя абстрактного продукта слишком длинное!",
|
||||||
|
"abstract product name invalid syntax": "Неверный синткасис в имени абстрактного продукта!",
|
||||||
|
"net weight too long": "Указанный вес нетто слишком длинный!",
|
||||||
|
"net weight invalid syntax": "Неверный указанный вес нетто!",
|
||||||
|
"image hash too long": "Указанный хеш изображения слишком длинный!",
|
||||||
|
"image hash invalid syntax": "Неверный указанный хеш изображения!",
|
||||||
|
"category too long": "Указанная категория слишком длинная!",
|
||||||
|
"category invalid syntax": "Неверный синткасис в категории!",
|
||||||
|
"category not found": "Такая категория не найдена!!",
|
||||||
|
"unit too long": "Указанная единица измерения слишком длинная!",
|
||||||
|
"unit invalid syntax": "Неправильный синткасис в указанной единице измерения!",
|
||||||
|
"product not found": "Такой продукт не найден!",
|
||||||
|
"product invalid syntax": "Неверный синткасис в одном из параметров продукта!",
|
||||||
|
"abstract product id not found": "Абстрактный продукт с таким ID не найден!",
|
||||||
|
"abstract product id invalid syntax": "Неверный синткасис ID абстрактного продукта!",
|
||||||
|
"expiry date too long": "Указанная дата годен до слишком длинная!",
|
||||||
|
"expiry date invalid syntax": "Неверный синткасис в указанной дате годен до!",
|
||||||
|
"categories not found": "Накая категория не найдена!",
|
||||||
|
"categories duplicate": "Такая категория уже существует!",
|
||||||
|
"categories invalid syntax": "Неверный синткасис одного из параметров категории!",
|
||||||
|
"ok": "Успешно.",
|
||||||
|
"invalid syntax": "Неправильный синткасис!",
|
||||||
|
"png only": "Разрешены только файлы формата .png!",
|
||||||
|
"too long": "Указанное значение слишком длинное!",
|
||||||
|
"unknown": "Неизвестная ошибка сервера! Пожалуйста, сообщите разработчику!"
|
||||||
|
}
|
|
@ -1,41 +1,32 @@
|
||||||
import AbstractProductService from '../services/abstractproduct.js';
|
import AbstractProductService from '../services/abstractproduct.js';
|
||||||
import statuses from '../utils/status.js';
|
|
||||||
import log from '../utils/log.js';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import customError from '../response/customError.js';
|
||||||
|
import responseCodes from '../response/responseCodes.js';
|
||||||
|
import translate from '../utils/translate.js';
|
||||||
|
|
||||||
const TAG = "/controllers/abstractproduct.js";
|
const TAG = "/controllers/abstractproduct.js";
|
||||||
|
|
||||||
class AbstractProductController {
|
class AbstractProductController {
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
try {
|
|
||||||
const { groupId, localId, barcode, name, net_weight, image_filename, category, unit } = req.body;
|
const { groupId, localId, barcode, name, net_weight, image_filename, category, unit } = req.body;
|
||||||
|
|
||||||
const tempPath = req.file.path;
|
const tempPath = req.file.path;
|
||||||
const targetPath = path.join(path.resolve(path.dirname('')), `/uploads/${image_filename}.png`);
|
const targetPath = path.join(path.resolve(path.dirname('')), `/uploads/${image_filename}.png`);
|
||||||
|
|
||||||
if (path.extname(req.file.originalname).toLowerCase() !== ".png") {
|
if (path.extname(req.file.originalname).toLowerCase() !== ".png") {
|
||||||
return res.status(400).send("Only .png files are allowed")
|
fs.rmSync(tempPath);
|
||||||
|
throw new customError(`create abstract product only png allowed`, responseCodes.responses.general.png_only);
|
||||||
}
|
}
|
||||||
|
|
||||||
//fs.renameSync(tempPath, targetPath);
|
|
||||||
fs.copyFileSync(tempPath, targetPath);
|
fs.copyFileSync(tempPath, targetPath);
|
||||||
fs.rmSync(tempPath);
|
fs.rmSync(tempPath);
|
||||||
|
|
||||||
await AbstractProductService.create(groupId, localId, barcode, name, net_weight, image_filename, category, unit);
|
await AbstractProductService.create(groupId, localId, barcode, name, net_weight, image_filename, category, unit);
|
||||||
return res.status(200).send("Successfull");
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
} catch (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case statuses.duplicate:
|
|
||||||
return res.status(400).send(e.message);
|
|
||||||
default:
|
|
||||||
log.error(e.original);
|
|
||||||
return res.status(500).send(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
try {
|
|
||||||
let { groupId, localId, barcode, name, net_weight, image_filename, category, unit } = req.body;
|
let { groupId, localId, barcode, name, net_weight, image_filename, category, unit } = req.body;
|
||||||
|
|
||||||
const tempPath = req.file.path;
|
const tempPath = req.file.path;
|
||||||
|
@ -49,31 +40,21 @@ class AbstractProductController {
|
||||||
if (net_weight) await AbstractProductService.updateNetWeight(groupId, localId, net_weight);
|
if (net_weight) await AbstractProductService.updateNetWeight(groupId, localId, net_weight);
|
||||||
|
|
||||||
if (image_filename && tempPath) {
|
if (image_filename && tempPath) {
|
||||||
//fs.renameSync(tempPath, targetPath);
|
|
||||||
fs.copyFileSync(tempPath, targetPath);
|
fs.copyFileSync(tempPath, targetPath);
|
||||||
fs.rmSync(tempPath);
|
fs.rmSync(tempPath);
|
||||||
await AbstractProductService.updateImageFilename(groupId, localId, image_filename);
|
await AbstractProductService.updateImageFilename(groupId, localId, image_filename);
|
||||||
} else if (image_filename && !tempPath) {
|
} else if (image_filename && !tempPath) {
|
||||||
return res.status(400).send("You must supply image file along with its hash");
|
throw new customError(`Abstract product update image hash without file`, responseCodes.responses.abstractproducts.hash_without_file);
|
||||||
} else if (!image_file && tempPath) {
|
} else if (!image_file && tempPath) {
|
||||||
return res.status(400).send("You must supply image file hash along with file");
|
fs.rmSync(tempPath);
|
||||||
|
throw new customError(`Abstract product update file without image hash`, responseCodes.responses.abstractproducts.file_without_hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category) await AbstractProductService.updateCategory(groupId, localId, category);
|
if (category) await AbstractProductService.updateCategory(groupId, localId, category);
|
||||||
|
|
||||||
if (unit) await AbstractProductService.updateUnit(groupId, localId, unit);
|
if (unit) await AbstractProductService.updateUnit(groupId, localId, unit);
|
||||||
|
|
||||||
return res.status(200).send("Successfull");
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
} catch (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case statuses.invalid_syntax:
|
|
||||||
log.error(e.original);
|
|
||||||
return res.status(400).send(e.message);
|
|
||||||
default:
|
|
||||||
log.error(e.original);
|
|
||||||
return res.status(500).send(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,23 @@
|
||||||
import CategoryService from "../services/category.js";
|
import CategoryService from "../services/category.js";
|
||||||
import log from "../utils/log.js";
|
import translate from "../utils/translate.js";
|
||||||
|
|
||||||
const TAG = "controllers/category.js";
|
const TAG = "controllers/category.js";
|
||||||
|
|
||||||
class CategoryController {
|
class CategoryController {
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
try {
|
|
||||||
const { localId, categoryName, groupId } = req.body;
|
const { localId, categoryName, groupId } = req.body;
|
||||||
|
|
||||||
await CategoryService.create(groupId, localId, categoryName);
|
await CategoryService.create(groupId, localId, categoryName);
|
||||||
return res.status(200).send("Success");
|
|
||||||
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/create: ${e}`)); }
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
try {
|
|
||||||
const { localId, groupId, name } = req.body;
|
const { localId, groupId, name } = req.body;
|
||||||
|
|
||||||
await CategoryService.update(groupId, localId, name);
|
await CategoryService.update(groupId, localId, name);
|
||||||
return res.status(200).send("Success");
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/update: ${e}`)); }
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,13 @@ import GroupService from '../services/group.js';
|
||||||
import UserService from '../services/user.js';
|
import UserService from '../services/user.js';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import config from '../../config.json' with {type: "json"};
|
import config from '../../config.json' with {type: "json"};
|
||||||
import statuses from '../utils/status.js';
|
|
||||||
import log from '../utils/log.js';
|
import log from '../utils/log.js';
|
||||||
|
import translate from '../utils/translate.js';
|
||||||
|
|
||||||
const TAG = "/controllers/group.js";
|
const TAG = "/controllers/group.js";
|
||||||
|
|
||||||
class GroupController {
|
class GroupController {
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
try {
|
|
||||||
let { groupName } = req.params;
|
let { groupName } = req.params;
|
||||||
|
|
||||||
let user = jwt.decode(req.headers.authorization.split(' ')[1], config.secret);
|
let user = jwt.decode(req.headers.authorization.split(' ')[1], config.secret);
|
||||||
|
@ -18,33 +17,26 @@ class GroupController {
|
||||||
log.info(`New group with name ${groupName} was just created by user ${user.login.username}`);
|
log.info(`New group with name ${groupName} was just created by user ${user.login.username}`);
|
||||||
|
|
||||||
UserService.joinGroup(user.login.id, status.id);
|
UserService.joinGroup(user.login.id, status.id);
|
||||||
return res.status(200).send("Successfull");
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/create: ${e}`)); }
|
|
||||||
}
|
}
|
||||||
async join(req, res) {
|
async join(req, res) {
|
||||||
try {
|
|
||||||
let { groupId } = req.params;
|
let { groupId } = req.params;
|
||||||
|
|
||||||
let user = jwt.decode(req.headers.authorization.split(' ')[1], config.secret);
|
let user = jwt.decode(req.headers.authorization.split(' ')[1], config.secret);
|
||||||
let status = await UserService.joinGroup(user.login.id, groupId);
|
await UserService.joinGroup(user.login.id, groupId);
|
||||||
|
|
||||||
if (status == statuses.duplicate) return res.status(400).send("Already in group");
|
|
||||||
|
|
||||||
log.info(`User ${user.login.username} has just joined group with ID ${groupId}`);
|
log.info(`User ${user.login.username} has just joined group with ID ${groupId}`);
|
||||||
return res.status(200).send("Successfull");
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/join: ${e}`)); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePassword(req, res) {
|
async updatePassword(req, res) {
|
||||||
try {
|
|
||||||
let { groupId } = req.params;
|
let { groupId } = req.params;
|
||||||
let { password } = req.body;
|
let { password } = req.body;
|
||||||
|
|
||||||
await GroupService.updatePassword(groupId, password);
|
await GroupService.updatePassword(groupId, password);
|
||||||
log.info(`Password for group with ID ${groupId} was updated`);
|
log.info(`Password for group with ID ${groupId} was updated`);
|
||||||
return res.status(200).send("Successfull");
|
|
||||||
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/updatePassword ${e}`)); }
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,19 @@
|
||||||
import ProductService from '../services/product.js';
|
import ProductService from '../services/product.js';
|
||||||
import statuses from '../utils/status.js';
|
import translate from '../utils/translate.js';
|
||||||
import log from '../utils/log.js';
|
|
||||||
|
|
||||||
const TAG = "/controllers/product.js"
|
const TAG = "/controllers/product.js"
|
||||||
|
|
||||||
class AbstractProductController {
|
class AbstractProductController {
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
try {
|
|
||||||
const { groupId, localId, abstract_product_id, amount, date_of_production, expiry_date } = req.body;
|
const { groupId, localId, abstract_product_id, amount, date_of_production, expiry_date } = req.body;
|
||||||
|
|
||||||
await ProductService.create(groupId, localId, abstract_product_id, amount, date_of_production, expiry_date);
|
await ProductService.create(groupId, localId, abstract_product_id, amount, date_of_production, expiry_date);
|
||||||
return res.status(200).send("Successfull");
|
|
||||||
} catch (e) {
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
switch (e.status) {
|
|
||||||
case statuses.duplicate:
|
|
||||||
return res.status(400).send(e.message);
|
|
||||||
default:
|
|
||||||
log.error(e.original)
|
|
||||||
return res.status(500).send(e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
try {
|
|
||||||
let { groupId, localId, abstract_product_id, amount, date_of_production, expiry_date } = req.body;
|
let { groupId, localId, abstract_product_id, amount, date_of_production, expiry_date } = req.body;
|
||||||
|
|
||||||
if (abstract_product_id) await ProductService.updateAbstractProductId(groupId, localId, abstract_product_id);
|
if (abstract_product_id) await ProductService.updateAbstractProductId(groupId, localId, abstract_product_id);
|
||||||
|
@ -33,15 +23,8 @@ class AbstractProductController {
|
||||||
if (date_of_production) await ProductService.updateDateOfProduction(groupId, localId, date_of_production);
|
if (date_of_production) await ProductService.updateDateOfProduction(groupId, localId, date_of_production);
|
||||||
|
|
||||||
if (expiry_date) await ProductService.updateExpiryDate(groupId, localId, expiry_date);
|
if (expiry_date) await ProductService.updateExpiryDate(groupId, localId, expiry_date);
|
||||||
} catch (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case statuses.invalid_syntax:
|
|
||||||
log.error(e.original);
|
|
||||||
return res.status(400).send(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send("Successfull");
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,35 +4,31 @@ import bcrypt from 'bcrypt';
|
||||||
import genToken from '../utils/jwt.js';
|
import genToken from '../utils/jwt.js';
|
||||||
import AbstractProductService from '../services/abstractproduct.js';
|
import AbstractProductService from '../services/abstractproduct.js';
|
||||||
import ProductService from '../services/product.js';
|
import ProductService from '../services/product.js';
|
||||||
|
import translate from '../utils/translate.js';
|
||||||
|
|
||||||
const TAG = "/controllers/userjs"
|
const TAG = "/controllers/userjs"
|
||||||
|
|
||||||
class UserController {
|
class UserController {
|
||||||
async register(req, res) {
|
async register(req, res) {
|
||||||
try {
|
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
await UserService.create(username, password);
|
await UserService.create(username, password);
|
||||||
|
|
||||||
log.info(`New user with name ${username} has just registered`);
|
log.info(`New user with name ${username} has just registered`);
|
||||||
return res.status(200).send("Successfull register");
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
} catch (e) { res.status(500).send(log.unknownError(`${TAG}/register: ${e}`)); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(req, res) {
|
async login(req, res) {
|
||||||
try {
|
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
const user = await UserService.getByUsername(username);
|
const user = await UserService.getByUsername(username);
|
||||||
if (!bcrypt.compareSync(password, user.password)) return res.status(401).send("Wrong password");
|
if (!bcrypt.compareSync(password, user.password)) return res.status(401).send("Wrong password");
|
||||||
|
|
||||||
const token = genToken(user);
|
const token = genToken(user);
|
||||||
return res.status(200).send(token);
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
} catch (e) { res.status(500).send(log.unknownError(`${TAG}/login: ${e}`)); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async synchronize(req, res) {
|
async synchronize(req, res) {
|
||||||
try {
|
|
||||||
const { groupId } = req.params;
|
const { groupId } = req.params;
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
|
@ -40,8 +36,7 @@ class UserController {
|
||||||
result.products = await ProductService.getAll(groupId);
|
result.products = await ProductService.getAll(groupId);
|
||||||
result.categories = await CategoryService.getAll(groupId);
|
result.categories = await CategoryService.getAll(groupId);
|
||||||
|
|
||||||
return res.status(200).json(result);
|
return res.status(200).send(translate(req.headers["accept-language"], responseCodes.responses.general.ok));
|
||||||
} catch (e) { res.status(500).send(log.unknownError(`${TAG}/synchronize: ${e}`)); }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,46 +3,41 @@ import jwt from 'jsonwebtoken';
|
||||||
import config from '../../config.json' with {type: "json"};
|
import config from '../../config.json' with {type: "json"};
|
||||||
import GroupService from '../services/group.js';
|
import GroupService from '../services/group.js';
|
||||||
import UserService from '../services/user.js';
|
import UserService from '../services/user.js';
|
||||||
|
import customError from '../response/customError.js';
|
||||||
|
import responseCodes from '../response/responseCodes.js';
|
||||||
|
|
||||||
const TAG = "/middlewares/auth.js";
|
const TAG = "/middlewares/auth.js";
|
||||||
|
|
||||||
const requireUsername = async (req, res, next) => {
|
const requireUsername = async (req, res, next) => {
|
||||||
if (req.method == "OPTIONS") next();
|
if (req.method == "OPTIONS") next();
|
||||||
|
|
||||||
try {
|
|
||||||
const { username } = req.body;
|
const { username } = req.body;
|
||||||
if (!username) return res.status(400).send("Username is required");
|
if (!username) throw new customError(`requireUsername username is required`, responseCodes.responses.usernames.required)
|
||||||
next();
|
next();
|
||||||
} catch (e) { return res.status(500).send(unknownError(`${TAG}/requireUsername: ${e}`)); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const requirePassword = async (req, res, next) => {
|
const requirePassword = async (req, res, next) => {
|
||||||
if (req.method == "OPTIONS") next();
|
if (req.method == "OPTIONS") next();
|
||||||
|
|
||||||
try {
|
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
if (!password) return res.status(400).send("Password is required");
|
if (!password) throw new customError(`requirePassword password is required`, responseCodes.responses.passwords.required);
|
||||||
next();
|
next();
|
||||||
} catch (e) { return res.status(500).send(unknownError(`${TAG}/requirePassword: ${e}`)); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const authenticate = async (req, res, next) => {
|
const authenticate = async (req, res, next) => {
|
||||||
if (req.method == "OPTIONS") next();
|
if (req.method == "OPTIONS") next();
|
||||||
|
|
||||||
try {
|
if (!req.headers.authorization) throw new customError(`authenticate no authorization header`, responseCodes.responses.authentication.not_found);
|
||||||
if (!req.headers.authorization) return res.status(403).send("No authorization header supplied");
|
|
||||||
const token = req.headers.authorization.split(' ')[1];
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
if (!token) return res.status(403).send("No authorization token supplied");
|
if (!token) throw new customError(`authenticate no authorization token in header`, responseCodes.responses.authentication.not_found);
|
||||||
if (!jwt.verify(token, config.secret)) return res.status(403).send("Authorization token is incorrect");
|
if (!jwt.verify(token, config.secret)) throw new customError(`authenticate token is invalid`, responseCodes.responses.authentication.invalid);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/authenticate: ${e}`)); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizeGroupOwner = async (req, res, next) => {
|
const authorizeGroupOwner = async (req, res, next) => {
|
||||||
if (req.method == "OPTIONS") next();
|
if (req.method == "OPTIONS") next();
|
||||||
|
|
||||||
try {
|
|
||||||
const token = req.headers.authorization.split(' ')[1];
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
|
|
||||||
const { groupId } = req.params;
|
const { groupId } = req.params;
|
||||||
|
@ -50,24 +45,21 @@ const authorizeGroupOwner = async (req, res, next) => {
|
||||||
let user = jwt.decode(token, config.secret);
|
let user = jwt.decode(token, config.secret);
|
||||||
|
|
||||||
let adminId = await GroupService.getAdminId(groupId);
|
let adminId = await GroupService.getAdminId(groupId);
|
||||||
if (user.login.id != adminId) return res.status(403).send("Not your group");
|
if (user.login.id != adminId) throw new customError(`authorizeGroupOwner not an owner`, responseCodes.responses.groups.not_an_owner)
|
||||||
next();
|
next();
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/authorizeGroupOwner: ${e}`)); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkGroupPassword = async (req, res, next) => {
|
const checkGroupPassword = async (req, res, next) => {
|
||||||
if (req.method == "OPTIONS") next();
|
if (req.method == "OPTIONS") next();
|
||||||
|
|
||||||
try {
|
|
||||||
const { groupId } = req.params;
|
const { groupId } = req.params;
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
|
|
||||||
const groupPassword = await GroupService.getPassword(groupId);
|
const groupPassword = await GroupService.getPassword(groupId);
|
||||||
|
|
||||||
if (groupPassword != password) return res.status(403).send("Wrong password");
|
if (groupPassword != password) throw new customError(`checkGroupPassword password is invalid`, responseCodes.responses.passwords.invalid);
|
||||||
next();
|
next();
|
||||||
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/checkGroupPassword: ${e}`)); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const userIsInGroup = async (req, res, next) => {
|
const userIsInGroup = async (req, res, next) => {
|
||||||
|
@ -77,7 +69,7 @@ const userIsInGroup = async (req, res, next) => {
|
||||||
|
|
||||||
const token = req.headers.authorization.split(' ')[1];
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
let user = jwt.decode(token, config.secret);
|
let user = jwt.decode(token, config.secret);
|
||||||
if (!await UserService.isInGroup(user.login.id, groupId)) return res.status(403).send("You are not a member of this group");
|
if (!await UserService.isInGroup(user.login.id, groupId)) throw new customError(`userIsInGroup not a member`, responseCodes.responses.groups.not_a_member)
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
import UserService from '../services/user.js';
|
|
||||||
import GroupService from '../services/group.js';
|
|
||||||
import AbstractProductService from '../services/abstractproduct.js';
|
|
||||||
import ProductService from '../services/product.js';
|
|
||||||
import CategoryService from '../services/category.js';
|
|
||||||
import log from '../utils/log.js';
|
|
||||||
import statuses from '../utils/status.js';
|
|
||||||
|
|
||||||
const TAG = "/middlewares/existance.js";
|
|
||||||
|
|
||||||
const usernameExists = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
let { username } = req.body;
|
|
||||||
|
|
||||||
let user = await UserService.getByUsername(username);
|
|
||||||
if (!user || user == statuses.not_found) return res.status(404).send("User not found");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/usernameExists: ${e}`)); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const usernameDoesntExist = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
let { username } = req.body;
|
|
||||||
|
|
||||||
let user = await UserService.getByUsername(username);
|
|
||||||
if (user != undefined && user != statuses.not_found) return res.status(400).send("Such username already taken");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/usernameDoesntExist: ${e}`)); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupExists = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
let groupId = req.params.groupId || req.body.groupId;
|
|
||||||
|
|
||||||
let group = await GroupService.getById(groupId);
|
|
||||||
|
|
||||||
if (!group || group == statuses.not_found) return res.status(404).send("Group not found");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/groupExists: ${e}`)) }
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupDoesntExist = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
let groupId = req.params.groupId || req.body.groupId;
|
|
||||||
|
|
||||||
let group = await GroupService.getById(groupId);
|
|
||||||
|
|
||||||
if (group || group != statuses.not_found) return res.status(400).send("Such group already exists");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/groupDoesntExist: ${e}`)) }
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupNameDoesntExist = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { groupName } = req.params;
|
|
||||||
|
|
||||||
let group = await GroupService.getByName(groupName);
|
|
||||||
if (group) return res.status(400).send("Such group name already exists");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/groupNameDoesntExist: ${e}`)); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const abstractProductExists = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { groupId, localId } = req.body;
|
|
||||||
|
|
||||||
let result = await AbstractProductService.exists(groupId, localId);
|
|
||||||
if (!result) return res.status(404).send("Abstract product not found");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/abstractProductExists: ${e}`)); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const productExists = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { groupId, localId } = req.body;
|
|
||||||
|
|
||||||
let result = await ProductService.exists(groupId, localId);
|
|
||||||
if (!result) return res.status(404).send("Product not found");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/productExists: ${e}`)); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const categoryNameDoesntExist = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { categoryName, localId, groupId } = req.body;
|
|
||||||
|
|
||||||
let result = await CategoryService.getByName(groupId, localId, categoryName);
|
|
||||||
if (result != statuses.not_found) return res.status(400).send("Such category name exists");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/categoryNameDoesntExist: ${e}`)); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const categoryExists = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { localId, groupId } = req.body;
|
|
||||||
|
|
||||||
let result = await CategoryService.getById(groupId, localId);
|
|
||||||
if (!result || result == statuses.not_found) return res.status(404).send("No such category");
|
|
||||||
next();
|
|
||||||
} catch (e) { return res.status(500).send(log.unknownError(`${TAG}/categoryExists: ${e}`)); }
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
usernameExists,
|
|
||||||
usernameDoesntExist,
|
|
||||||
|
|
||||||
groupExists,
|
|
||||||
groupDoesntExist,
|
|
||||||
groupNameDoesntExist,
|
|
||||||
|
|
||||||
abstractProductExists,
|
|
||||||
productExists,
|
|
||||||
|
|
||||||
categoryNameDoesntExist,
|
|
||||||
categoryExists
|
|
||||||
};
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
class customError extends Error {
|
||||||
|
constructor(message, code) {
|
||||||
|
super(message)
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, customError);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default customError;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import log from "../utils/log";
|
||||||
|
import customError from "./customError";
|
||||||
|
import responses from './responseCodes';
|
||||||
|
import translate from '../utils/translate.js';
|
||||||
|
|
||||||
|
const errorHandler = (err, req, res) => {
|
||||||
|
log.error(err);
|
||||||
|
|
||||||
|
let language = req.headers["accept-language"];
|
||||||
|
|
||||||
|
if (err instanceof customError) {
|
||||||
|
return res.status(responses.getHTTPCode(err.code)).send(translate(language, code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryHandler = (func) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await func(req, res, next);
|
||||||
|
} catch (err) {
|
||||||
|
errorHandler(err, req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tryHandler;
|
|
@ -0,0 +1,218 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const responses = {
|
||||||
|
authentication: {
|
||||||
|
not_found: "authentication not found",
|
||||||
|
invalid: "authentication invalid"
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
duplicate: "user duplicate",
|
||||||
|
invalid_syntax: "user invalid syntax",
|
||||||
|
already_in_group: "user already in group"
|
||||||
|
},
|
||||||
|
usernames: {
|
||||||
|
duplicate: "username taken",
|
||||||
|
not_found: "username not found",
|
||||||
|
required: "username required"
|
||||||
|
},
|
||||||
|
passwords: {
|
||||||
|
required: "password required",
|
||||||
|
invalid: "password invalid"
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
duplicate: "group name taken",
|
||||||
|
name_not_found: "group name not found",
|
||||||
|
id_not_found: "group id not found",
|
||||||
|
not_an_owner: "group not an owner",
|
||||||
|
not_a_member: "group not a member"
|
||||||
|
},
|
||||||
|
abstractproducts: {
|
||||||
|
not_found: "abstract product not found",
|
||||||
|
hash_without_file: "hash without file",
|
||||||
|
file_without_hash: "file without hash",
|
||||||
|
invalid_syntax: "abstract product invalid syntax"
|
||||||
|
},
|
||||||
|
barcode: {
|
||||||
|
not_found: "barcode not found",
|
||||||
|
duplicate: "barcode duplicate",
|
||||||
|
too_long: "barcode too long",
|
||||||
|
invalid_syntax: "barcode invalid syntax"
|
||||||
|
},
|
||||||
|
abstractproductname: {
|
||||||
|
not_found: "abstract product name not found",
|
||||||
|
duplicate: "abstract product name duplicate",
|
||||||
|
too_long: "abstract product name too long",
|
||||||
|
invalid_syntax: "abstract product name invalid syntax"
|
||||||
|
},
|
||||||
|
netweight: {
|
||||||
|
too_long: "net weight too long",
|
||||||
|
invalid_syntax: "net weight invalid syntax"
|
||||||
|
},
|
||||||
|
imagehash: {
|
||||||
|
too_long: "image hash too long",
|
||||||
|
invalid_syntax: "image hash invalid syntax"
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
too_long: "category too long",
|
||||||
|
invalid_syntax: "category invalid syntax",
|
||||||
|
not_found: "category not found"
|
||||||
|
},
|
||||||
|
unit: {
|
||||||
|
too_long: "unit too long",
|
||||||
|
invalid_syntax: "unit invalid syntax"
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
not_found: "product not found",
|
||||||
|
invalid_syntax: "product invalid syntax"
|
||||||
|
},
|
||||||
|
abstractproductid: {
|
||||||
|
not_found: "abstract product id not found",
|
||||||
|
invalid_syntax: "abstract product id invalid syntax"
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
too_long: "amount too long",
|
||||||
|
invalid_syntax: "amount invalid syntax"
|
||||||
|
},
|
||||||
|
dateofproduction: {
|
||||||
|
too_long: "date of production too long",
|
||||||
|
invalid_syntax: "date of production invalid syntax"
|
||||||
|
},
|
||||||
|
expirydate: {
|
||||||
|
too_long: "expiry date too long",
|
||||||
|
invalid_syntax: "expiry date invalid syntax"
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
duplicate: "categories duplicate",
|
||||||
|
not_found: "categories not found"
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
ok: "ok",
|
||||||
|
invalid_syntax: "invalid syntax",
|
||||||
|
png_only: "png only",
|
||||||
|
too_long: "too long",
|
||||||
|
unknown: "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHTTPCode = (type) => {
|
||||||
|
switch (code) {
|
||||||
|
case responses.general.ok:
|
||||||
|
return 200
|
||||||
|
case responses.general.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.general.unknown:
|
||||||
|
return 500
|
||||||
|
case responses.general.png_only:
|
||||||
|
return 400
|
||||||
|
case responses.general.too_long:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
case responses.authentication.invalid:
|
||||||
|
return 403
|
||||||
|
case responses.authentication.not_found:
|
||||||
|
return 403
|
||||||
|
|
||||||
|
case responses.user.already_in_group:
|
||||||
|
return 409
|
||||||
|
case responses.user.duplicate:
|
||||||
|
return 409
|
||||||
|
case responses.user.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
|
||||||
|
case responses.usernames.duplicate:
|
||||||
|
return 409
|
||||||
|
case responses.usernames.not_found:
|
||||||
|
return 404
|
||||||
|
case responses.usernames.required:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
case responses.passwords.required:
|
||||||
|
return 400
|
||||||
|
case responses.passwords.invalid:
|
||||||
|
return 403
|
||||||
|
|
||||||
|
case responses.groups.id_not_found:
|
||||||
|
return 404
|
||||||
|
case responses.groups.name_not_found:
|
||||||
|
return 404
|
||||||
|
case responses.groups.duplicate:
|
||||||
|
return 409
|
||||||
|
case responses.groups.not_an_owner:
|
||||||
|
return 403
|
||||||
|
case responses.groups.not_a_member:
|
||||||
|
return 403
|
||||||
|
|
||||||
|
case responses.abstractproducts.not_found:
|
||||||
|
return 404
|
||||||
|
case responses.abstractproducts.file_without_hash:
|
||||||
|
return 400
|
||||||
|
case responses.abstractproducts.hash_without_file:
|
||||||
|
return 400
|
||||||
|
case responses.abstractproducts.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
case responses.abstractproductid.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.abstractproductid.not_found:
|
||||||
|
return 404
|
||||||
|
|
||||||
|
case responses.abstractproductname.duplicate:
|
||||||
|
return 409
|
||||||
|
case responses.abstractproductname.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.abstractproductname.not_found:
|
||||||
|
return 404
|
||||||
|
case responses.abstractproductname.too_long:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
case responses.abstractproducts.file_without_hash:
|
||||||
|
return 400
|
||||||
|
case responses.abstractproducts.hash_without_file:
|
||||||
|
return 400
|
||||||
|
case responses.abstractproducts.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.abstractproducts.not_found:
|
||||||
|
return 404
|
||||||
|
|
||||||
|
case responses.amount.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.amount.too_long:
|
||||||
|
return 409
|
||||||
|
|
||||||
|
case responses.barcode.duplicate:
|
||||||
|
return 409
|
||||||
|
case responses.barcode.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.barcode.not_found:
|
||||||
|
return 404
|
||||||
|
case responses.barcode.too_long:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
case responses.categories.duplicate:
|
||||||
|
return 409
|
||||||
|
case responses.categories.not_found:
|
||||||
|
return 404
|
||||||
|
|
||||||
|
case responses.category.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.category.too_long:
|
||||||
|
return 400
|
||||||
|
case responses.category.not_found:
|
||||||
|
return 404
|
||||||
|
|
||||||
|
case responses.dateofproduction.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.dateofproduction.too_long:
|
||||||
|
return 400
|
||||||
|
|
||||||
|
case responses.expirydate.invalid_syntax:
|
||||||
|
return 400
|
||||||
|
case responses.expirydate.too_long:
|
||||||
|
return 400
|
||||||
|
default:
|
||||||
|
return 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { responses, getHTTPCode }
|
|
@ -1,9 +1,9 @@
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import auth from '../middlewares/auth.js';
|
import auth from '../middlewares/auth.js';
|
||||||
import AbstractProductController from '../controllers/abstractproduct.js';
|
import AbstractProductController from '../controllers/abstractproduct.js';
|
||||||
import existance from '../middlewares/existance.js';
|
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import tryHandler from '../response/errorHandler.js';
|
||||||
|
|
||||||
const upload = multer(({
|
const upload = multer(({
|
||||||
dest: path.join(path.resolve(path.dirname('')), "/temp")
|
dest: path.join(path.resolve(path.dirname('')), "/temp")
|
||||||
|
@ -11,7 +11,7 @@ const upload = multer(({
|
||||||
|
|
||||||
const AbstractProductRouter = new Router();
|
const AbstractProductRouter = new Router();
|
||||||
|
|
||||||
AbstractProductRouter.post('/create', upload.single("file"), auth.authenticate, existance.groupExists, auth.userIsInGroup, AbstractProductController.create);
|
AbstractProductRouter.post('/create', upload.single("file"), tryHandler(auth.authenticate), tryHandler(auth.userIsInGroup), tryHandler(AbstractProductController.create));
|
||||||
AbstractProductRouter.post('/update', upload.single("file"), auth.authenticate, existance.groupExists, auth.userIsInGroup, existance.abstractProductExists, AbstractProductController.update);
|
AbstractProductRouter.post('/update', upload.single("file"), tryHandler(auth.authenticate), tryHandler(auth.userIsInGroup), tryHandler(AbstractProductController.update));
|
||||||
|
|
||||||
export default AbstractProductRouter;
|
export default AbstractProductRouter;
|
|
@ -1,11 +1,11 @@
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import auth from '../middlewares/auth.js';
|
import auth from '../middlewares/auth.js';
|
||||||
import existance from '../middlewares/existance.js';
|
|
||||||
import CategoryController from '../controllers/category.js';
|
import CategoryController from '../controllers/category.js';
|
||||||
|
import tryHandler from '../response/errorHandler.js';
|
||||||
|
|
||||||
const CategoryRouter = new Router();
|
const CategoryRouter = new Router();
|
||||||
|
|
||||||
CategoryRouter.post('/create', auth.authenticate, existance.groupExists, existance.categoryNameDoesntExist, CategoryController.create);
|
CategoryRouter.post('/create', tryHandler(auth.authenticate), tryHandler(CategoryController.create));
|
||||||
CategoryRouter.post('/update', auth.authenticate, existance.groupExists, existance.categoryExists, CategoryController.update);
|
CategoryRouter.post('/update', tryHandler(auth.authenticate), tryHandler(CategoryController.update));
|
||||||
|
|
||||||
export default CategoryRouter;
|
export default CategoryRouter;
|
|
@ -1,12 +1,12 @@
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import auth from '../middlewares/auth.js';
|
import auth from '../middlewares/auth.js';
|
||||||
import GroupController from '../controllers/group.js';
|
import GroupController from '../controllers/group.js';
|
||||||
import existance from '../middlewares/existance.js';
|
import tryHandler from '../response/errorHandler.js';
|
||||||
|
|
||||||
const GroupRouter = new Router();
|
const GroupRouter = new Router();
|
||||||
|
|
||||||
GroupRouter.post('/create/:groupName', auth.authenticate, existance.groupNameDoesntExist, GroupController.create);
|
GroupRouter.post('/create/:groupName', tryHandler(auth.authenticate), tryHandler(GroupController.create));
|
||||||
GroupRouter.post('/join/:groupId', auth.authenticate, existance.groupExists, auth.requirePassword, auth.checkGroupPassword, GroupController.join);
|
GroupRouter.post('/join/:groupId', tryHandler(auth.authenticate), tryHandler(auth.requirePassword), tryHandler(auth.checkGroupPassword), tryHandler(GroupController.join));
|
||||||
GroupRouter.post('/password/:groupId', auth.authenticate, existance.groupExists, auth.authorizeGroupOwner, auth.requirePassword, GroupController.updatePassword);
|
GroupRouter.post('/password/:groupId', tryHandler(auth.authenticate), tryHandler(auth.authorizeGroupOwner), tryHandler(auth.requirePassword), tryHandler(GroupController.updatePassword));
|
||||||
|
|
||||||
export default GroupRouter;
|
export default GroupRouter;
|
|
@ -1,11 +1,11 @@
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import auth from '../middlewares/auth.js';
|
import auth from '../middlewares/auth.js';
|
||||||
import ProductController from '../controllers/product.js'
|
import ProductController from '../controllers/product.js'
|
||||||
import existance from '../middlewares/existance.js';
|
import tryHandler from '../response/errorHandler.js';
|
||||||
|
|
||||||
const ProductRouter = new Router();
|
const ProductRouter = new Router();
|
||||||
|
|
||||||
ProductRouter.post('/create', auth.authenticate, existance.groupExists, auth.userIsInGroup, ProductController.create);
|
ProductRouter.post('/create', tryHandler(auth.authenticate), tryHandler(auth.userIsInGroup), tryHandler(ProductController.create));
|
||||||
ProductRouter.post('/update', auth.authenticate, existance.groupExists, auth.userIsInGroup, existance.productExists, ProductController.update);
|
ProductRouter.post('/update', tryHandler(auth.authenticate), tryHandler(auth.userIsInGroup), tryHandler(ProductController.update));
|
||||||
|
|
||||||
export default ProductRouter;
|
export default ProductRouter;
|
|
@ -1,12 +1,12 @@
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import auth from '../middlewares/auth.js';
|
import auth from '../middlewares/auth.js';
|
||||||
import existance from '../middlewares/existance.js';
|
|
||||||
import UserController from '../controllers/user.js';
|
import UserController from '../controllers/user.js';
|
||||||
|
import tryHandler from '../response/errorHandler.js';
|
||||||
|
|
||||||
const UserRouter = new Router();
|
const UserRouter = new Router();
|
||||||
|
|
||||||
UserRouter.post('/register', auth.requireUsername, auth.requirePassword, existance.usernameDoesntExist, UserController.register);
|
UserRouter.post('/register', tryHandler(auth.requireUsername), tryHandler(auth.requirePassword), tryHandler(UserController.register));
|
||||||
UserRouter.post('/login', auth.requireUsername, auth.requirePassword, existance.usernameExists, UserController.login);
|
UserRouter.post('/login', tryHandler(auth.requireUsername), tryHandler(auth.requirePassword), tryHandler(UserController.login));
|
||||||
UserRouter.get('/synchronize/:groupId', auth.authenticate, existance.groupExists, auth.userIsInGroup, UserController.synchronize);
|
UserRouter.get('/synchronize/:groupId', tryHandler(auth.authenticate), tryHandler(auth.userIsInGroup), tryHandler(UserController.synchronize));
|
||||||
|
|
||||||
export default UserRouter;
|
export default UserRouter;
|
|
@ -1,12 +1,15 @@
|
||||||
import db from '../db.js';
|
import db from '../db.js';
|
||||||
import statuses from '../utils/status.js';
|
import statuses from '../utils/status.js';
|
||||||
import errorHandler from '../utils/pgerrorhandler.js';
|
import errorHandler from '../utils/pgerrorhandler.js';
|
||||||
|
import tryHandler from '../response/errorHandler.js';
|
||||||
|
import responses from '../response/responseCodes.js';
|
||||||
|
import customError from '../response/customError.js';
|
||||||
|
|
||||||
class AbstractProductService {
|
class AbstractProductService {
|
||||||
async create(groupid, localid, barcode, name, net_weight, image_filename, category, unit) {
|
async create(groupid, localid, barcode, name, net_weight, image_filename, category, unit) {
|
||||||
await db.query("INSERT INTO abstract_products (group_id, local_id, barcode, name, net_weight, image_filename, category, unit) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", [groupid, localid, barcode, name, net_weight, image_filename, category, unit])
|
await db.query("INSERT INTO abstract_products (group_id, local_id, barcode, name, net_weight, image_filename, category, unit) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", [groupid, localid, barcode, name, net_weight, image_filename, category, unit])
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
errorHandler(e, "abstract product")
|
errorHandler(e, "abstractproduct")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,21 +23,21 @@ class AbstractProductService {
|
||||||
async updateName(groupId, localId, name) {
|
async updateName(groupId, localId, name) {
|
||||||
await db.query("UPDATE abstract_products SET name = $1 WHERE group_id = $2 AND local_id = $3", [name, groupId, localId])
|
await db.query("UPDATE abstract_products SET name = $1 WHERE group_id = $2 AND local_id = $3", [name, groupId, localId])
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
errorHandler(e, "name")
|
errorHandler(e, "abstractproductname")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateNetWeight(groupId, localId, net_weight) {
|
async updateNetWeight(groupId, localId, net_weight) {
|
||||||
await db.query("UPDATE abstract_products SET net_weight = $1 WHERE group_id = $2 AND local_id = $3", [net_weight, groupId, localId]
|
await db.query("UPDATE abstract_products SET net_weight = $1 WHERE group_id = $2 AND local_id = $3", [net_weight, groupId, localId]
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
errorHandler(e, "net weight")
|
errorHandler(e, "netweight")
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateImageFilename(groupId, localId, image_filename) {
|
async updateImageFilename(groupId, localId, image_filename) {
|
||||||
await db.query("UPDATE abstract_products SET image_filename = $1 WHERE group_id = $2 AND local_id = $3", [image_filename, groupId, localId])
|
await db.query("UPDATE abstract_products SET image_filename = $1 WHERE group_id = $2 AND local_id = $3", [image_filename, groupId, localId])
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
errorHandler(e, "image filename")
|
errorHandler(e, "imagehash")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +57,7 @@ class AbstractProductService {
|
||||||
|
|
||||||
async getAll(groupId) {
|
async getAll(groupId) {
|
||||||
let result = (await db.query("SELECT local_id, barcode, name, net_weight, image_filename, category, unit FROM abstract_products WHERE group_id = $1", [groupId])).rows;
|
let result = (await db.query("SELECT local_id, barcode, name, net_weight, image_filename, category, unit FROM abstract_products WHERE group_id = $1", [groupId])).rows;
|
||||||
if (!result) return statuses.not_found;
|
if (!result) throw new customError("getAll abstract products not found", responses.responses.abstractproducts.not_found)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import db from '../db.js';
|
import db from '../db.js';
|
||||||
|
import customError from '../response/customError.js';
|
||||||
|
import responseCodes from '../response/responseCodes.js';
|
||||||
import statuses from '../utils/status.js';
|
import statuses from '../utils/status.js';
|
||||||
|
|
||||||
class CategoryService {
|
class CategoryService {
|
||||||
|
@ -11,14 +13,14 @@ class CategoryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(groupId, localId) {
|
async getById(groupId, localId) {
|
||||||
let result = (await db.query("SELECT * FROM categories WHERE group_id = $1 AND local_id = $2", [groupId, localId]))
|
let result = (await db.query("SELECT * FROM categories WHERE group_id = $1 AND local_id = $2", [groupId, localId]));
|
||||||
if (result.rowCount == 0) return statuses.not_found;
|
if (result.rowCount == 0) throw new customError(`getById categorirs not found`, responseCodes.responses.category.not_found)
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByName(groupId, localId, name) {
|
async getByName(groupId, localId, name) {
|
||||||
let result = (await db.query("SELECT * FROM categories WHERE group_id = $1 AND local_id = $2 AND name = $3", [groupId, localId, name]));
|
let result = (await db.query("SELECT * FROM categories WHERE group_id = $1 AND local_id = $2 AND name = $3", [groupId, localId, name]));
|
||||||
if (result.rowCount == 0) return statuses.not_found;
|
if (result.rowCount == 0) throw new customError(`getByName categories not found`, responseCodes.responses.category.not_found)
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import db from '../db.js';
|
import db from '../db.js';
|
||||||
|
import customError from '../response/customError.js';
|
||||||
|
import responseCodes from '../response/responseCodes.js';
|
||||||
import errorHandler from '../utils/pgerrorhandler.js';
|
import errorHandler from '../utils/pgerrorhandler.js';
|
||||||
import status from '../utils/status.js';
|
|
||||||
|
|
||||||
class GroupService {
|
class GroupService {
|
||||||
async create(name, creatorId) {
|
async create(name, creatorId) {
|
||||||
let res = await db.query("INSERT INTO groups (name, admin_id) VALUES ($1, $2) RETURNING ID", [name, creatorId]).catch(errorHandler);
|
let res = await db.query("INSERT INTO groups (name, admin_id) VALUES ($1, $2) RETURNING ID", [name, creatorId]).catch((e) => errorHandler(e, "group"));
|
||||||
|
|
||||||
return res.rows[0];
|
return res.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(id) {
|
async getById(id) {
|
||||||
let res = (await db.query("SELECT * FROM groups WHERE id = $1", [id]));
|
let res = (await db.query("SELECT * FROM groups WHERE id = $1", [id]));
|
||||||
if (res.rowCount == 0) return status.not_found;
|
if (res.rowCount == 0) throw new customError(`getByUd group not found`, responseCodes.responses.groups.id_not_found);
|
||||||
return res.rows[0];
|
return res.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +29,9 @@ class GroupService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByName(name) {
|
async getByName(name) {
|
||||||
return (await db.query("SELECT * FROM groups WHERE name = $1", [name])).rows[0];
|
let res = (await db.query("SELECT * FROM groups WHERE name = $1", [name]));
|
||||||
|
if (res.rowCount == 0) throw new customError(`getByName group not found`, responseCodes.responses.groups.name_not_found);
|
||||||
|
return res.rows[0];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import db from '../db.js';
|
import db from '../db.js';
|
||||||
import statuses from '../utils/status.js';
|
|
||||||
import errorHandler from '../utils/pgerrorhandler.js';
|
import errorHandler from '../utils/pgerrorhandler.js';
|
||||||
|
import customError from '../response/customError.js';
|
||||||
|
import responseCodes from '../response/responseCodes.js';
|
||||||
|
|
||||||
class ProductService {
|
class ProductService {
|
||||||
async create(groupid, localid, abstract_product_id, amount, date_of_production, expiry_date) {
|
async create(groupid, localid, abstract_product_id, amount, date_of_production, expiry_date) {
|
||||||
await db.query("INSERT INTO products (group_id, local_id, abstract_product_id, amount, date_of_production, expiry_date) VALUES ($1, $2, $3, $4, $5, $6)", [groupid, localid, abstract_product_id, amount, date_of_production, expiry_date])
|
await db.query("INSERT INTO products (group_id, local_id, abstract_product_id, amount, date_of_production, expiry_date) VALUES ($1, $2, $3, $4, $5, $6)", [groupid, localid, abstract_product_id, amount, date_of_production, expiry_date])
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
errorHandler(e, "Abstract Product")
|
errorHandler(e, "product")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAbstractProductId(groupId, localId, abstract_product_id) {
|
async updateAbstractProductId(groupId, localId, abstract_product_id) {
|
||||||
await db.query("UPDATE products SET abstract_product_id = $1 WHERE group_id = $2 AND local_id = $3", [abstract_product_id, groupId, localId])
|
await db.query("UPDATE products SET abstract_product_id = $1 WHERE group_id = $2 AND local_id = $3", [abstract_product_id, groupId, localId])
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
errorHandler(e, "abstract product id")
|
errorHandler(e, "abstractproductid")
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,20 +29,20 @@ class ProductService {
|
||||||
async updateDateOfProduction(groupId, localId, date_of_production) {
|
async updateDateOfProduction(groupId, localId, date_of_production) {
|
||||||
await db.query("UPDATE products SET date_of_production = $1 WHERE group_id = $2 AND local_id = $3", [date_of_production, groupId, localId])
|
await db.query("UPDATE products SET date_of_production = $1 WHERE group_id = $2 AND local_id = $3", [date_of_production, groupId, localId])
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
errorHandler(e, "date of production")
|
errorHandler(e, "dateofproduction")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExpiryDate(groupId, localId, expiry_date) {
|
async updateExpiryDate(groupId, localId, expiry_date) {
|
||||||
await db.query("UPDATE products SET expiry_date = $1 WHERE group_id = $2 AND local_id = $3", [expiry_date, groupId, localId])
|
await db.query("UPDATE products SET expiry_date = $1 WHERE group_id = $2 AND local_id = $3", [expiry_date, groupId, localId])
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
errorHandler(e, "expiry date")
|
errorHandler(e, "expirydate")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(groupId) {
|
async getAll(groupId) {
|
||||||
let result = (await db.query("SELECT local_id, abstract_product_id, amount, date_of_production, expiry_date FROM products WHERE group_id = $1", [groupId])).rows;
|
let result = (await db.query("SELECT local_id, abstract_product_id, amount, date_of_production, expiry_date FROM products WHERE group_id = $1", [groupId])).rows;
|
||||||
if (!result) return statuses.not_found;
|
if (!result) throw new customError(`getAll product not found`, responseCodes.responses.products.not_found);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import db from '../db.js'
|
import db from '../db.js'
|
||||||
|
import customError from '../response/customError.js';
|
||||||
|
import responseCodes from '../response/responseCodes.js';
|
||||||
import statuses from '../utils/status.js';
|
import statuses from '../utils/status.js';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
@ -12,12 +14,14 @@ class UserService {
|
||||||
|
|
||||||
async getByUsername(username) {
|
async getByUsername(username) {
|
||||||
let user = (await db.query("SELECT * FROM Users WHERE username = $1", [username])).rows;
|
let user = (await db.query("SELECT * FROM Users WHERE username = $1", [username])).rows;
|
||||||
if (user == undefined) return statuses.not_found;
|
if (user == undefined) throw new customError(`getByUsername user not found`, responseCodes.responses.usernames.not_found);
|
||||||
return (user[0]);
|
return (user[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll() {
|
async getAll() {
|
||||||
return (await db.query("SELECT * FROM Users")).rows;
|
let res = await db.query("SELECT * FROM Users");
|
||||||
|
if (res.rowCount == 0) throw new customError(`getAll user not found`, responseCodes.responses.usernames.not_found);
|
||||||
|
return res.rows[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async isInGroup(userId, groupId) {
|
async isInGroup(userId, groupId) {
|
||||||
|
@ -25,7 +29,7 @@ class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinGroup(userId, groupId) {
|
async joinGroup(userId, groupId) {
|
||||||
if (await (this.isInGroup(userId, groupId))) return statuses.duplicate;
|
if (await (this.isInGroup(userId, groupId))) throw new customError(`joinGroup user already in group`, responseCodes.responses.user.already_in_group);
|
||||||
await db.query("UPDATE Users SET groups = array_append(groups, $1::integer) WHERE ID = $2", [groupId, userId]);
|
await db.query("UPDATE Users SET groups = array_append(groups, $1::integer) WHERE ID = $2", [groupId, userId]);
|
||||||
return statuses.ok;
|
return statuses.ok;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,23 +2,23 @@ import config from '../../config.json' with {type: "json"};
|
||||||
|
|
||||||
const debug = (text) => {
|
const debug = (text) => {
|
||||||
if (config.debug) console.debug(`[D] [${Date()}]: ${text}`);
|
if (config.debug) console.debug(`[D] [${Date()}]: ${text}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
const info = (text) => {
|
const info = (text) => {
|
||||||
console.log(`[I] [${Date()}]: ${text}`);
|
console.log(`[I] [${Date()}]: ${text}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
const error = (text) => {
|
const error = (text) => {
|
||||||
console.error(`[E] [${Date()}]: ${text}`);
|
console.error(`[E] [${Date()}]: ${text}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
const warn = (text) => {
|
const warn = (text) => {
|
||||||
console.warn(`[W] [${Date()}]: ${text}`);
|
console.warn(`[W] [${Date()}]: ${text}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
const unknownError = (text) => {
|
const unknownError = (text) => {
|
||||||
error(text);
|
error(text);
|
||||||
return "Unknown server error. Please, report to the developer";
|
return "Unknown server error. Please, report to the developer";
|
||||||
}
|
};
|
||||||
|
|
||||||
export default { debug, info, error, warn, unknownError };
|
export default { debug, info, error, warn, unknownError };
|
|
@ -1,28 +1,16 @@
|
||||||
import statuses from "./status.js";
|
import customError from "../response/customError.js";
|
||||||
|
import responseCodes from "../response/responseCodes.js";
|
||||||
|
|
||||||
const errorHandler = (e, obj) => {
|
const errorHandler = (e, obj) => {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case '23505':
|
case '23505':
|
||||||
throw {
|
throw new customError(`Duplicate ${obj}`, responseCodes.responses[obs].duplicate)
|
||||||
status: statuses.duplicate,
|
|
||||||
message: `Such ${obj} already exists`
|
|
||||||
};
|
|
||||||
case '22007':
|
case '22007':
|
||||||
throw {
|
throw new customError(`Invalid syntax ${obj}`, responseCodes.responses.general.invalid_syntax)
|
||||||
status: statuses.invalid_syntax,
|
|
||||||
message: `Invalid syntax in ${obj}`
|
|
||||||
};
|
|
||||||
case '22001':
|
case '22001':
|
||||||
throw {
|
throw new customError(`Value too long ${obj}`, responseCodes.responses[obj].too_long)
|
||||||
status: statuses.invalid_syntax,
|
|
||||||
message: `Value too long (${obj})`
|
|
||||||
};
|
|
||||||
default:
|
default:
|
||||||
throw {
|
throw new customError(`Unknown error ${obj}`, responseCodes.responses.general.unknown)
|
||||||
status: statuses.unknown,
|
|
||||||
message: `Unknown error. Please, report to the developer`,
|
|
||||||
original: e
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
const statuses = {
|
|
||||||
ok: "ok",
|
|
||||||
duplicate: "duplicate",
|
|
||||||
not_found: "not found",
|
|
||||||
invalid_syntax: "invalid syntax",
|
|
||||||
unknown: "unknown"
|
|
||||||
};
|
|
||||||
|
|
||||||
export default statuses;
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
const translate = (language, code) => {
|
||||||
|
if (!language) language = "en-US"
|
||||||
|
if (!code) code = "unknown"
|
||||||
|
return JSON.parse(fs.readFileSync(`../../messages/${language}/msgs.json`).toString())[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate
|
Loading…
Reference in New Issue