refactor: edit tailwindcss classes and change code formatting, therefore added .prettierrc file

fix: frontend bug where the passwords won't be checked when the password is changed
This commit is contained in:
2026-07-08 14:56:43 +02:00
parent 9c50c3f300
commit 6be34c93aa
36 changed files with 1280 additions and 1145 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2
}
+12 -1
View File
@@ -2,34 +2,45 @@ FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
# Workspace manifests (npm workspaces need these present)
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
COPY backend/package.json backend/ COPY backend/package.json backend/
COPY frontend/package.json frontend/ COPY frontend/package.json frontend/
COPY shared/package.json shared/ COPY shared/package.json shared/
# Install all deps for the monorepo (includes devDeps for building)
RUN npm ci RUN npm ci
# Copy sources required to build shared + backend
COPY shared/ shared/ COPY shared/ shared/
COPY backend/ backend/ COPY backend/ backend/
# Build shared (now produces dist/esm + dist/cjs + dist/types) and then backend
RUN npm run build --workspace=shared RUN npm run build --workspace=shared
RUN npm run build --workspace=backend RUN npm run build --workspace=backend
FROM node:22-alpine AS runner FROM node:22-alpine AS runner
ENV NODE_ENV=production ENV NODE_ENV=production
WORKDIR /app WORKDIR /app
# Workspace manifests again (so npm can install prod deps for backend + shared)
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
COPY backend/package.json backend/ COPY backend/package.json backend/
COPY frontend/package.json frontend/ COPY frontend/package.json frontend/
COPY shared/package.json shared/ COPY shared/package.json shared/
# Install prod deps only
RUN npm ci --omit=dev RUN npm ci --omit=dev
# Copy runtime artifacts
COPY --from=builder /app/backend/dist backend/dist COPY --from=builder /app/backend/dist backend/dist
COPY --from=builder /app/backend/database.scheme.sql backend/ COPY --from=builder /app/backend/database.scheme.sql backend/database.scheme.sql
# Copy the full shared dist output (esm/cjs/types)
COPY --from=builder /app/shared/dist shared/dist COPY --from=builder /app/shared/dist shared/dist
COPY shared/package.json shared/package.json
EXPOSE 8004 EXPOSE 8004
WORKDIR /app/backend WORKDIR /app/backend
+1 -1
View File
@@ -11,4 +11,4 @@
], ],
"@babel/preset-typescript" "@babel/preset-typescript"
] ]
} }
+1 -1
View File
@@ -27,4 +27,4 @@
"typescript": "~6.0.2", "typescript": "~6.0.2",
"tsx": "^4.23.0" "tsx": "^4.23.0"
} }
} }
+124 -112
View File
@@ -1,48 +1,61 @@
import mysql, {type ResultSetHeader, type RowDataPacket} from "mysql2/promise"; import mysql, {
type ResultSetHeader,
type RowDataPacket,
} from "mysql2/promise";
import dotenv from "dotenv"; import dotenv from "dotenv";
import {returnErrorCode} from "../../../services/helperFuncs.js"; import { returnErrorCode } from "../../../services/helperFuncs.js";
import {PRODUCT_ERROR_CODE} from "@stockhome/shared"; import { PRODUCT_ERROR_CODE } from "@stockhome/shared";
dotenv.config(); dotenv.config();
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST!, host: process.env.DB_HOST!,
user: process.env.DB_USER!, user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!, password: process.env.DB_PASSWORD!,
database: process.env.DB_NAME!, database: process.env.DB_NAME!,
}); });
export const newProduct = async ( export const newProduct = async (
name: string, name: string,
description: string, description: string,
price: string, price: string,
amount: string, amount: string,
storage_location: string, storage_location: string,
expiry_date: string, expiry_date: string,
bottling_date: string, bottling_date: string,
): Promise<boolean | { success: boolean; code: string; data: null; message: string }> => { ): Promise<
const newPrice = price !== "" ? price : null; boolean | { success: boolean; code: string; data: null; message: string }
> => {
const newPrice = price !== "" ? price : null;
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
"INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES (?, ?, ?, ?, UUID_TO_BIN(?), ?, ?)", "INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES (?, ?, ?, ?, UUID_TO_BIN(?), ?, ?)",
[name, description, newPrice, amount, storage_location, expiry_date, bottling_date], [
); name,
description,
newPrice,
amount,
storage_location,
expiry_date,
bottling_date,
],
);
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SP001", code: "SP001",
data: null, data: null,
message: "New Product successfully created.", message: "New Product successfully created.",
}; };
} }
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_CREATED); return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_CREATED);
}; };
export const productDetails = async (uuid: string) => { export const productDetails = async (uuid: string) => {
const [result] = await pool.query<RowDataPacket[]>( const [result] = await pool.query<RowDataPacket[]>(
`SELECT BIN_TO_UUID(p.uuid) AS uuid, `SELECT BIN_TO_UUID(p.uuid) AS uuid,
p.name, p.name,
p.description, p.description,
p.price, p.price,
@@ -55,23 +68,23 @@ export const productDetails = async (uuid: string) => {
FROM products p FROM products p
JOIN storage_locations s ON p.storage_location = s.uuid JOIN storage_locations s ON p.storage_location = s.uuid
WHERE p.uuid = UUID_TO_BIN(?)`, WHERE p.uuid = UUID_TO_BIN(?)`,
[uuid], [uuid],
); );
if (result.length > 0) { if (result.length > 0) {
return { return {
success: true, success: true,
code: "SP003", code: "SP003",
data: result[0], data: result[0],
message: "Product details successfully loaded.", message: "Product details successfully loaded.",
}; };
} }
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_FOUND); return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_FOUND);
}; };
export const allProducts = async () => { export const allProducts = async () => {
const [result] = await pool.query<RowDataPacket[]>(` const [result] = await pool.query<RowDataPacket[]>(`
SELECT BIN_TO_UUID(p.uuid) AS uuid, SELECT BIN_TO_UUID(p.uuid) AS uuid,
p.name, p.name,
p.description, p.description,
@@ -89,52 +102,52 @@ export const allProducts = async () => {
WHERE p.deleted = 0 WHERE p.deleted = 0
`); `);
if (result.length > 0) { if (result.length > 0) {
return { return {
success: true, success: true,
code: "SP002", code: "SP002",
data: result, data: result,
message: "All products successfully loaded.", message: "All products successfully loaded.",
}; };
} }
return returnErrorCode(PRODUCT_ERROR_CODE.NO_PRODUCTS_FOUND); return returnErrorCode(PRODUCT_ERROR_CODE.NO_PRODUCTS_FOUND);
}; };
export const setAmount = async (itemUUID: string, amount: number) => { export const setAmount = async (itemUUID: string, amount: number) => {
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
`UPDATE products `UPDATE products
SET amount = ? SET amount = ?
WHERE uuid = UUID_TO_BIN(?)`, WHERE uuid = UUID_TO_BIN(?)`,
[amount, itemUUID], [amount, itemUUID],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SP004", code: "SP004",
data: null, data: null,
message: "Amount successfully updated.", message: "Amount successfully updated.",
}; };
} }
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_AMOUNT_NOT_UPDATED); return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_AMOUNT_NOT_UPDATED);
}; };
export const updateItem = async ( export const updateItem = async (
itemUUID: string, itemUUID: string,
newValues: { newValues: {
name: string; name: string;
description: string; description: string;
price: string; price: string;
amount: string; amount: string;
storage_location_uuid: string; storage_location_uuid: string;
expiry_date: string; expiry_date: string;
bottling_date: string; bottling_date: string;
}, },
) => { ) => {
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
`UPDATE products `UPDATE products
SET name = ?, SET name = ?,
description = ?, description = ?,
price = ?, price = ?,
@@ -143,47 +156,46 @@ export const updateItem = async (
expiry_date = ?, expiry_date = ?,
bottling_date = ? bottling_date = ?
WHERE uuid = UUID_TO_BIN(?);`, WHERE uuid = UUID_TO_BIN(?);`,
[ [
newValues.name, newValues.name,
newValues.description, newValues.description,
newValues.price, newValues.price,
newValues.amount, newValues.amount,
newValues.storage_location_uuid, newValues.storage_location_uuid,
newValues.expiry_date, newValues.expiry_date,
newValues.bottling_date, newValues.bottling_date,
itemUUID, itemUUID,
], ],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SP005", code: "SP005",
data: null, data: null,
message: "Item updated successfully.", message: "Item updated successfully.",
}; };
} }
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_UPDATED); return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_UPDATED);
}; };
export const deleteProduct = async (uuid: string) => { export const deleteProduct = async (uuid: string) => {
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
`UPDATE products `UPDATE products
SET deleted = 1 SET deleted = 1
WHERE uuid = UUID_TO_BIN(?);`, WHERE uuid = UUID_TO_BIN(?);`,
[uuid], [uuid],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SP006", code: "SP006",
data: null, data: null,
message: "Product deleted successfully.", message: "Product deleted successfully.",
}; };
; }
}
return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED); return returnErrorCode(PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED);
}; };
+64 -61
View File
@@ -1,87 +1,90 @@
import mysql, {type ResultSetHeader, type RowDataPacket} from "mysql2/promise"; import mysql, {
type ResultSetHeader,
type RowDataPacket,
} from "mysql2/promise";
import dotenv from "dotenv"; import dotenv from "dotenv";
import {STORAGE_ERROR_CODE} from "@stockhome/shared"; import { STORAGE_ERROR_CODE } from "@stockhome/shared";
import {returnErrorCode} from "../../../services/helperFuncs.js"; import { returnErrorCode } from "../../../services/helperFuncs.js";
dotenv.config(); dotenv.config();
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST!, host: process.env.DB_HOST!,
user: process.env.DB_USER!, user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!, password: process.env.DB_PASSWORD!,
database: process.env.DB_NAME!, database: process.env.DB_NAME!,
}); });
export const allStorages = async () => { export const allStorages = async () => {
const [result] = await pool.query<RowDataPacket[]>( const [result] = await pool.query<RowDataPacket[]>(
"SELECT BIN_TO_UUID(uuid) AS uuid, name, description, created_at, updated_at FROM storage_locations;", "SELECT BIN_TO_UUID(uuid) AS uuid, name, description, created_at, updated_at FROM storage_locations;",
); );
if (result.length > 0) { if (result.length > 0) {
return { return {
success: true, success: true,
code: "SS001", code: "SS001",
data: result, data: result,
message: "Successfully fetched all storage locations.", message: "Successfully fetched all storage locations.",
}; };
} }
return returnErrorCode(STORAGE_ERROR_CODE.NO_STORAGE_LOCATIONS_FOUND); return returnErrorCode(STORAGE_ERROR_CODE.NO_STORAGE_LOCATIONS_FOUND);
}; };
export const newStorage = async (name: string, description: string) => { export const newStorage = async (name: string, description: string) => {
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
"INSERT INTO storage_locations (name, description) VALUES (?, ?)", "INSERT INTO storage_locations (name, description) VALUES (?, ?)",
[name, description], [name, description],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SS002", code: "SS002",
data: null, data: null,
message: "Successfully created new storage location.", message: "Successfully created new storage location.",
}; };
} }
return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_CREATED); return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_CREATED);
}; };
export const updateStorage = async ( export const updateStorage = async (
uuid: string, uuid: string,
values: { name: string; description: string }, values: { name: string; description: string },
) => { ) => {
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
"UPDATE storage_locations SET name = ?, description = ? WHERE uuid = UUID_TO_BIN(?);", "UPDATE storage_locations SET name = ?, description = ? WHERE uuid = UUID_TO_BIN(?);",
[values.name, values.description, uuid], [values.name, values.description, uuid],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SS003", code: "SS003",
data: null, data: null,
message: "Successfully updated storage location.", message: "Successfully updated storage location.",
}; };
} }
return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_UPDATED); return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_UPDATED);
}; };
export const deleteStorage = async (uuid: string) => { export const deleteStorage = async (uuid: string) => {
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
"DELETE FROM storage_locations WHERE uuid = UUID_TO_BIN(?);", "DELETE FROM storage_locations WHERE uuid = UUID_TO_BIN(?);",
[uuid], [uuid],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SS004", code: "SS004",
data: null, data: null,
message: "Successfully deleted storage location.", message: "Successfully deleted storage location.",
}; };
} }
return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_DELETED); return returnErrorCode(STORAGE_ERROR_CODE.STORAGE_NOT_DELETED);
}; };
+92 -89
View File
@@ -1,132 +1,135 @@
import mysql, {type ResultSetHeader, type RowDataPacket} from "mysql2/promise"; import mysql, {
type ResultSetHeader,
type RowDataPacket,
} from "mysql2/promise";
import dotenv from "dotenv"; import dotenv from "dotenv";
import {GENERAL_ERROR_CODE, USER_ERROR_CODE} from "@stockhome/shared"; import { GENERAL_ERROR_CODE, USER_ERROR_CODE } from "@stockhome/shared";
import {returnErrorCode} from "../../../services/helperFuncs.js"; import { returnErrorCode } from "../../../services/helperFuncs.js";
import type { AuthTokenPayload } from "../../../services/tokenService"; import type { AuthTokenPayload } from "../../../services/tokenService";
dotenv.config(); dotenv.config();
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST!, host: process.env.DB_HOST!,
user: process.env.DB_USER!, user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!, password: process.env.DB_PASSWORD!,
database: process.env.DB_NAME!, database: process.env.DB_NAME!,
}); });
export const findUser = async (username: string, password: string) => { export const findUser = async (username: string, password: string) => {
const [result] = await pool.query<RowDataPacket[]>( const [result] = await pool.query<RowDataPacket[]>(
"SELECT BIN_TO_UUID(uuid) AS uuid, username, first_name, last_name, email, is_admin, is_active, last_login FROM users WHERE username = ? AND password = ?;", "SELECT BIN_TO_UUID(uuid) AS uuid, username, first_name, last_name, email, is_admin, is_active, last_login FROM users WHERE username = ? AND password = ?;",
[username, password], [username, password],
); );
const userRow = result[0]; const userRow = result[0];
if (!userRow) { if (!userRow) {
return returnErrorCode(USER_ERROR_CODE.WRONG_USERNAME_PASSWORD); return returnErrorCode(USER_ERROR_CODE.WRONG_USERNAME_PASSWORD);
} }
// Cast DB row to AuthTokenPayload for token generation / typing // Cast DB row to AuthTokenPayload for token generation / typing
const user = userRow as unknown as AuthTokenPayload; const user = userRow as unknown as AuthTokenPayload;
if (!user.is_active) { if (!user.is_active) {
return returnErrorCode(USER_ERROR_CODE.USER_IS_DEACTIVATED); return returnErrorCode(USER_ERROR_CODE.USER_IS_DEACTIVATED);
} }
return { return {
success: true, success: true,
code: "SU001", code: "SU001",
data: user, data: user,
message: "Successfully found user.", message: "Successfully found user.",
}; };
}; };
export const loginUser = async (username: string) => { export const loginUser = async (username: string) => {
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
"UPDATE users SET last_login = NOW() WHERE username = ?;", "UPDATE users SET last_login = NOW() WHERE username = ?;",
[username], [username],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SU002", code: "SU002",
data: null, data: null,
message: "Successfully logged in user.", message: "Successfully logged in user.",
}; };
} else { } else {
return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR); return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR);
} }
}; };
export const updateSettings = async (payload: { export const updateSettings = async (payload: {
"app-name": string; "app-name": string;
currency: string; currency: string;
}) => { }) => {
const appName = payload["app-name"]; const appName = payload["app-name"];
const currency = payload.currency; const currency = payload.currency;
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
`UPDATE app_settings `UPDATE app_settings
SET value = CASE name SET value = CASE name
WHEN "app-name" THEN ? WHEN "app-name" THEN ?
WHEN "currency" THEN ? WHEN "currency" THEN ?
ELSE value ELSE value
END END
WHERE name IN ("app-name", "currency");`, WHERE name IN ("app-name", "currency");`,
[appName, currency], [appName, currency],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SU003", code: "SU003",
data: null, data: null,
message: "Successfully updated settings.", message: "Successfully updated settings.",
}; };
} else { } else {
return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR); return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR);
} }
}; };
export const getSettings = async () => { export const getSettings = async () => {
const [result] = await pool.query<RowDataPacket[]>( const [result] = await pool.query<RowDataPacket[]>(
`SELECT * `SELECT *
FROM app_settings;`, FROM app_settings;`,
); );
if (result.length > 0) { if (result.length > 0) {
return { return {
success: true, success: true,
code: "SU004", code: "SU004",
data: result, data: result,
message: "Successfully fetched settings.", message: "Successfully fetched settings.",
}; };
} else { } else {
return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR); return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR);
} }
}; };
export const changePassword = async ( export const changePassword = async (
username: string, username: string,
currentPasswordUser: string, currentPasswordUser: string,
newPassword: string, newPassword: string,
) => { ) => {
const [result] = await pool.query<ResultSetHeader>( const [result] = await pool.query<ResultSetHeader>(
`UPDATE users `UPDATE users
SET password = ? SET password = ?
WHERE username = ? WHERE username = ?
AND password = ?;`, AND password = ?;`,
[newPassword, username, currentPasswordUser], [newPassword, username, currentPasswordUser],
); );
if (result.affectedRows > 0) { if (result.affectedRows > 0) {
return { return {
success: true, success: true,
code: "SU005", code: "SU005",
data: null, data: null,
message: "Successfully fetched settings.", message: "Successfully fetched settings.",
}; };
} else { } else {
return returnErrorCode(GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR); return returnErrorCode(USER_ERROR_CODE.PASSWORD_CHANGE_FAILED);
} }
}; };
+245 -245
View File
@@ -1,284 +1,284 @@
import express from "express"; import express from "express";
import dotenv from "dotenv"; import dotenv from "dotenv";
import {authenticate} from "../../services/tokenService"; import { authenticate } from "../../services/tokenService";
import { import {
allProducts, allProducts,
deleteProduct, deleteProduct,
newProduct, newProduct,
productDetails, productDetails,
setAmount, setAmount,
updateItem, updateItem,
} from "./database/products.database"; } from "./database/products.database";
import {GENERAL_ERROR_CODE, PRODUCT_ERROR_CODE} from "@stockhome/shared"; import { GENERAL_ERROR_CODE, PRODUCT_ERROR_CODE } from "@stockhome/shared";
dotenv.config(); dotenv.config();
const router = express.Router(); const router = express.Router();
router.post("/new-product", authenticate, async (req, res) => { router.post("/new-product", authenticate, async (req, res) => {
const { const {
name, name,
description, description,
price, price,
amount, amount,
storage_location, storage_location,
expiry_date, expiry_date,
bottling_date, bottling_date,
} = req.body; } = req.body;
if (!name) { if (!name) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[0], code: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[0],
data: null, data: null,
message: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[1], message: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[1],
});
}
const result = await newProduct(
name,
description,
price,
amount,
storage_location,
expiry_date,
bottling_date,
);
if (!result || typeof result !== "object") {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP001") {
return res.status(201).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
const result = await newProduct(
name,
description,
price,
amount,
storage_location,
expiry_date,
bottling_date,
);
if (!result || typeof result !== "object") {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP001") {
return res.status(201).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.get("/all-products", authenticate, async (req, res) => { router.get("/all-products", authenticate, async (req, res) => {
const result = await allProducts(); const result = await allProducts();
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP002") {
return res.status(200).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
if (result.code === PRODUCT_ERROR_CODE.NO_PRODUCTS_FOUND[0]) {
return res.status(404).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
if (!result) {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0], code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null, data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1], message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
if (result.code === "SP002") {
return res.status(200).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
if (result.code === PRODUCT_ERROR_CODE.NO_PRODUCTS_FOUND[0]) {
return res.status(404).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.get("/view", async (req, res) => { router.get("/view", async (req, res) => {
const uuid = req.query.uuid; const uuid = req.query.uuid;
if (typeof uuid !== "string") { if (typeof uuid !== "string") {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0], code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0],
data: null, data: null,
message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1], message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1],
});
}
const result = await productDetails(uuid);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP003") {
return res.status(200).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
if (result.code === PRODUCT_ERROR_CODE.PRODUCT_NOT_FOUND[0]) {
return res.status(404).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
const result = await productDetails(uuid);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP003") {
return res.status(200).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
if (result.code === PRODUCT_ERROR_CODE.PRODUCT_NOT_FOUND[0]) {
return res.status(404).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.put("/mutate/set-amount", authenticate, async (req, res) => { router.put("/mutate/set-amount", authenticate, async (req, res) => {
const amount = req.query.amount; const amount = req.query.amount;
const itemUUID = req.query.item; const itemUUID = req.query.item;
if (typeof itemUUID !== "string" || typeof amount !== "number") { if (typeof itemUUID !== "string" || typeof amount !== "number") {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0], code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0],
data: null, data: null,
message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1], message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1],
});
}
const result = await setAmount(itemUUID, amount);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP004") {
return res.status(200).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
const result = await setAmount(itemUUID, amount);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP004") {
return res.status(200).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.post("/mutate/update-item", authenticate, async (req, res) => { router.post("/mutate/update-item", authenticate, async (req, res) => {
const itemUUID = req.query.item; const itemUUID = req.query.item;
const newValues = req.body; const newValues = req.body;
if (typeof itemUUID !== "string" || !newValues) { if (typeof itemUUID !== "string" || !newValues) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0], code: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[0],
data: null, data: null,
message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1], message: PRODUCT_ERROR_CODE.INVALID_PARAMETERS[1],
});
}
const result = await updateItem(itemUUID, newValues);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP005") {
return res.status(200).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
const result = await updateItem(itemUUID, newValues);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SP005") {
return res.status(200).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.post("/delete-selection", authenticate, async (req, res) => { router.post("/delete-selection", authenticate, async (req, res) => {
const uuidArray = req.body; const uuidArray = req.body;
if (!Array.isArray(uuidArray) || uuidArray.length === 0) { if (!Array.isArray(uuidArray) || uuidArray.length === 0) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[0], code: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[0],
data: null, data: null,
message: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[1], message: PRODUCT_ERROR_CODE.INVALID_REQUEST_BODY[1],
});
}
for (const uuid of uuidArray) {
const response = await deleteProduct(uuid);
if (!response) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (response.code !== "SP006") {
return res.status(500).json({
success: false,
code: response.code || PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED[0],
data: null,
message: response.message || PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED[1],
});
}
}
return res.status(202).json({
success: true,
code: "SP006",
data: null,
message: "All selected products deleted successfully.",
}); });
}
for (const uuid of uuidArray) {
const response = await deleteProduct(uuid);
if (!response) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (response.code !== "SP006") {
return res.status(500).json({
success: false,
code: response.code || PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED[0],
data: null,
message: response.message || PRODUCT_ERROR_CODE.PRODUCT_NOT_DELETED[1],
});
}
}
return res.status(202).json({
success: true,
code: "SP006",
data: null,
message: "All selected products deleted successfully.",
});
}); });
export default router; export default router;
+160 -155
View File
@@ -1,184 +1,189 @@
import express from "express"; import express from "express";
import dotenv from "dotenv"; import dotenv from "dotenv";
import {authenticate} from "../../services/tokenService"; import { authenticate } from "../../services/tokenService";
import {allStorages, deleteStorage, newStorage, updateStorage,} from "./database/storage.database"; import {
import {GENERAL_ERROR_CODE, STORAGE_ERROR_CODE} from "@stockhome/shared"; allStorages,
deleteStorage,
newStorage,
updateStorage,
} from "./database/storage.database";
import { GENERAL_ERROR_CODE, STORAGE_ERROR_CODE } from "@stockhome/shared";
dotenv.config(); dotenv.config();
const router = express.Router(); const router = express.Router();
router.get("/all-storages", authenticate, async (req, res) => { router.get("/all-storages", authenticate, async (req, res) => {
const result = await allStorages(); const result = await allStorages();
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR,
data: null,
message: "Unknown error",
});
}
if (result.code === "SS001") {
return res.status(200).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
if (result.code === STORAGE_ERROR_CODE.NO_STORAGE_LOCATIONS_FOUND[0]) {
return res.status(404).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
if (!result) {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0], code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR,
data: null, data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1], message: "Unknown error",
}); });
}
if (result.code === "SS001") {
return res.status(200).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
if (result.code === STORAGE_ERROR_CODE.NO_STORAGE_LOCATIONS_FOUND[0]) {
return res.status(404).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.post("/new-storage", authenticate, async (req, res) => { router.post("/new-storage", authenticate, async (req, res) => {
const {name, description} = req.body; const { name, description } = req.body;
let desc = description; let desc = description;
if (!name) { if (!name) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: STORAGE_ERROR_CODE.INVALID_REQUEST_BODY[0], code: STORAGE_ERROR_CODE.INVALID_REQUEST_BODY[0],
data: null, data: null,
message: STORAGE_ERROR_CODE.INVALID_REQUEST_BODY[1], message: STORAGE_ERROR_CODE.INVALID_REQUEST_BODY[1],
});
}
if (description == "") {
desc = null;
}
const result = await newStorage(name, desc);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
});
}
if (result.code === "SS002") {
return res.status(201).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
if (description == "") {
desc = null;
}
const result = await newStorage(name, desc);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
});
}
if (result.code === "SS002") {
return res.status(201).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.post("/update-storage", authenticate, async (req, res) => { router.post("/update-storage", authenticate, async (req, res) => {
const storageUUID = req.query.storageUUID; const storageUUID = req.query.storageUUID;
const values = req.body; const values = req.body;
if (!storageUUID || !values) { if (!storageUUID || !values) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0], code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0],
data: null, data: null,
message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1], message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1],
});
}
if (typeof storageUUID !== "string") {
return res.status(400).json({
success: false,
code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0],
data: null,
message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1],
});
}
const result = await updateStorage(storageUUID, values);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SS003") {
return res.status(201).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
if (typeof storageUUID !== "string") {
return res.status(400).json({
success: false,
code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0],
data: null,
message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1],
});
}
const result = await updateStorage(storageUUID, values);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SS003") {
return res.status(201).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.delete("/delete", authenticate, async (req, res) => { router.delete("/delete", authenticate, async (req, res) => {
const uuid = req.query.uuid; const uuid = req.query.uuid;
if (typeof uuid !== "string") { if (typeof uuid !== "string") {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0], code: STORAGE_ERROR_CODE.INVALID_PARAMETERS[0],
data: null, data: null,
message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1], message: STORAGE_ERROR_CODE.INVALID_PARAMETERS[1],
});
}
const result = await deleteStorage(uuid);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SS004") {
return res.status(201).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
const result = await deleteStorage(uuid);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SS004") {
return res.status(201).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
export default router; export default router;
+220 -214
View File
@@ -1,245 +1,251 @@
import express from "express"; import express from "express";
import dotenv from "dotenv"; import dotenv from "dotenv";
import {authenticate, generateToken} from "../../services/tokenService"; import { authenticate, generateToken } from "../../services/tokenService";
import {changePassword, findUser, getSettings, loginUser, updateSettings,} from "./database/users.database"; import {
import {GENERAL_ERROR_CODE, USER_ERROR_CODE} from "@stockhome/shared"; changePassword,
findUser,
getSettings,
loginUser,
updateSettings,
} from "./database/users.database";
import { GENERAL_ERROR_CODE, USER_ERROR_CODE } from "@stockhome/shared";
dotenv.config(); dotenv.config();
const router = express.Router(); const router = express.Router();
router.post("/verify-token", authenticate, async (req, res) => { router.post("/verify-token", authenticate, async (req, res) => {
res.sendStatus(200); res.sendStatus(200);
}); });
router.post("/update-app-settings", authenticate, async (req, res) => { router.post("/update-app-settings", authenticate, async (req, res) => {
const values = req.body; const values = req.body;
if (!values || Object.keys(values).length === 0) { if (!values || Object.keys(values).length === 0) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0], code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0],
data: null, data: null,
message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1], message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1],
});
}
const result = await updateSettings(values);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SU003") {
return res.status(201).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
const result = await updateSettings(values);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SU003") {
return res.status(201).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.get("/settings", authenticate, async (req, res) => { router.get("/settings", authenticate, async (req, res) => {
const result = await getSettings(); const result = await getSettings();
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SU004") {
return res.status(200).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
if (!result) {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0], code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null, data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1], message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
if (result.code === "SU004") {
return res.status(200).json({
success: true,
code: result.code,
data: result.data,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.post("/login", async (req, res) => { router.post("/login", async (req, res) => {
const username = req.body.username; const username = req.body.username;
const password = req.body.password; const password = req.body.password;
if (!username || !password) { if (!username || !password) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0], code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0],
data: null, data: null,
message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1], message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1],
});
}
const result = await findUser(username, password);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === USER_ERROR_CODE.WRONG_USERNAME_PASSWORD[0]) {
return res.status(404).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
if (result.code === USER_ERROR_CODE.USER_IS_DEACTIVATED[0]) {
return res.status(403).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
if (result.code === "SU001") {
if (!result.data) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
const token = await generateToken(result.data);
if (!token) {
return res.status(500).json({
success: false,
code: USER_ERROR_CODE.TOKEN_GENERATION_FAILED[0],
data: null,
message: USER_ERROR_CODE.TOKEN_GENERATION_FAILED[1],
});
}
if (!result.data) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
const login = await loginUser(result.data.username);
if (!login) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (login.code !== "SU002") {
return res.status(500).json({
success: false,
code: login.code || USER_ERROR_CODE.LOGIN_FAILED[0],
data: null,
message: login.message || USER_ERROR_CODE.LOGIN_FAILED[1],
});
}
return res.status(202).json({
success: true,
code: "SU001",
data: {
token,
},
message: "User token generated successfully",
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
const result = await findUser(username, password);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === USER_ERROR_CODE.WRONG_USERNAME_PASSWORD[0]) {
return res.status(404).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
if (result.code === USER_ERROR_CODE.USER_IS_DEACTIVATED[0]) {
return res.status(403).json({
success: false,
code: result.code,
data: null,
message: result.message,
});
}
if (result.code === "SU001") {
if (!result.data) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
const token = await generateToken(result.data);
if (!token) {
return res.status(500).json({
success: false,
code: USER_ERROR_CODE.TOKEN_GENERATION_FAILED[0],
data: null,
message: USER_ERROR_CODE.TOKEN_GENERATION_FAILED[1],
});
}
if (!result.data) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
const login = await loginUser(result.data.username);
if (!login) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (login.code !== "SU002") {
return res.status(500).json({
success: false,
code: login.code || USER_ERROR_CODE.LOGIN_FAILED[0],
data: null,
message: login.message || USER_ERROR_CODE.LOGIN_FAILED[1],
});
}
return res.status(202).json({
success: true,
code: "SU001",
data: {
token,
},
message: "User token generated successfully",
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
router.post("/change-password", authenticate, async (req, res) => { router.post("/change-password", authenticate, async (req, res) => {
const currentPassword = req.body.currentPassword; const currentPassword = req.body.currentPassword;
const newPassword = req.body.newPassword; const newPassword = req.body.newPassword;
if (!req.user) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
const username = req.user.username;
if (!currentPassword || !newPassword) {
return res.status(400).json({
success: false,
code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0],
data: null,
message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1],
});
}
const result = await changePassword(username, currentPassword, newPassword);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SU005") {
return res.status(202).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
if (!req.user) {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0], code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null, data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1], message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
}); });
}
const username = req.user.username;
if (!currentPassword || !newPassword) {
return res.status(400).json({
success: false,
code: USER_ERROR_CODE.INVALID_REQUEST_BODY[0],
data: null,
message: USER_ERROR_CODE.INVALID_REQUEST_BODY[1],
});
}
const result = await changePassword(username, currentPassword, newPassword);
if (!result) {
return res.status(500).json({
success: false,
code: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}
if (result.code === "SU005") {
return res.status(202).json({
success: true,
code: result.code,
data: null,
message: result.message,
});
}
return res.status(500).json({
success: false,
code: result.code || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[0],
data: null,
message: result.message || GENERAL_ERROR_CODE.UNEXPECTED_SERVER_ERROR[1],
});
}); });
export default router; export default router;
+90 -87
View File
@@ -1,10 +1,9 @@
import type {NextFunction, Request, Response} from "express"; import type { NextFunction, Request, Response } from "express";
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import dotenv from "dotenv"; import dotenv from "dotenv";
import mysql, {type ResultSetHeader, type RowDataPacket} from "mysql2"; import mysql, { type ResultSetHeader, type RowDataPacket } from "mysql2";
import {readFile} from "fs/promises"; import { readFile } from "fs/promises"; // frontend routes
// frontend routes
import userRouter from "./routes/app/users.route"; import userRouter from "./routes/app/users.route";
import productRouter from "./routes/app/products.route"; import productRouter from "./routes/app/products.route";
import storageRouter from "./routes/app/storage.route"; import storageRouter from "./routes/app/storage.route";
@@ -18,16 +17,16 @@ const port = 8004;
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({extended: true})); app.use(express.urlencoded({ extended: true }));
const pool = mysql const pool = mysql
.createPool({ .createPool({
host: process.env.DB_HOST!, host: process.env.DB_HOST!,
user: process.env.DB_USER!, user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!, password: process.env.DB_PASSWORD!,
database: process.env.DB_NAME!, database: process.env.DB_NAME!,
}) })
.promise(); .promise();
app.use("/users", userRouter); app.use("/users", userRouter);
@@ -36,113 +35,117 @@ app.use("/products", productRouter);
app.use("/storage", storageRouter); app.use("/storage", storageRouter);
app.listen(port, () => { app.listen(port, () => {
runStartup(port); runStartup(port);
}); });
// Startup code // Startup code
const runStartup = async (port: number) => { const runStartup = async (port: number) => {
let firstStartupValue: string | null = null; let firstStartupValue: string | null = null;
try { try {
const [firstResponse] = await pool.query<RowDataPacket[]>( const [firstResponse] = await pool.query<RowDataPacket[]>(
`SELECT value `SELECT value
FROM app_settings FROM app_settings
WHERE name = "first-startup";`, WHERE name = "first-startup";`,
); );
firstStartupValue = firstResponse[0]?.value ?? null; firstStartupValue = firstResponse[0]?.value ?? null;
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof Error && (err as NodeJS.ErrnoException & { code?: string }).code !== "ER_NO_SUCH_TABLE") { if (
throw err; err instanceof Error &&
} (err as NodeJS.ErrnoException & { code?: string }).code !==
"ER_NO_SUCH_TABLE"
) {
throw err;
}
}
if (firstStartupValue !== "false") {
const schemaPath = path.join(__dirname, "database.scheme.sql");
const schemaSql = await readFile(schemaPath, "utf8");
const statements = schemaSql
.split(";")
.map((statement: string) => statement.trim())
.filter((statement: string) => statement.length > 0);
for (const statement of statements) {
await pool.query(statement);
} }
if (firstStartupValue !== "false") { const [result] = await pool.query<RowDataPacket[]>(
const schemaPath = path.join(__dirname, "database.scheme.sql"); `SELECT value
const schemaSql = await readFile(schemaPath, "utf8");
const statements = schemaSql
.split(";")
.map((statement: string) => statement.trim())
.filter((statement: string) => statement.length > 0);
for (const statement of statements) {
await pool.query(statement);
}
const [result] = await pool.query<RowDataPacket[]>(
`SELECT value
FROM app_settings FROM app_settings
WHERE name = "first-startup";`, WHERE name = "first-startup";`,
); );
if (result[0]?.value === "true") { if (result[0]?.value === "true") {
const insertResult = await insertFirstData(); const insertResult = await insertFirstData();
if (insertResult.affectedRows > 0) { if (insertResult.affectedRows > 0) {
console.log("Successfully created admin user!"); console.log("Successfully created admin user!");
console.log("Username: admin"); console.log("Username: admin");
console.log("Password: admin"); console.log("Password: admin");
const [scndResponse] = await pool.query<ResultSetHeader>( const [scndResponse] = await pool.query<ResultSetHeader>(
`UPDATE app_settings `UPDATE app_settings
SET value = "false" SET value = "false"
WHERE name = "first-startup";`, WHERE name = "first-startup";`,
); );
if (scndResponse.affectedRows > 0) { if (scndResponse.affectedRows > 0) {
console.log("Database settet up successfully!"); console.log("Database settet up successfully!");
} else { } else {
console.error("There was an error while setting up the database!"); console.error("There was an error while setting up the database!");
}
} else {
console.error("Error while creating admin user.");
}
} }
} else {
console.error("Error while creating admin user.");
}
} }
}
console.log("Everything is settet up successfully!"); console.log("Everything is settet up successfully!");
console.log(`Backend is running on http://localhost:${port}`); console.log(`Backend is running on http://localhost:${port}`);
}; };
const insertFirstData = async (): Promise<ResultSetHeader> => { const insertFirstData = async (): Promise<ResultSetHeader> => {
const [insertResult] = await pool.query<ResultSetHeader>( const [insertResult] = await pool.query<ResultSetHeader>(
`INSERT INTO users (username, first_name, last_name, email, password, is_admin) `INSERT INTO users (username, first_name, last_name, email, password, is_admin)
VALUES ("admin", "admin", "admin", "admin@example.com", "admin", 1)`, VALUES ("admin", "admin", "admin", "admin@example.com", "admin", 1)`,
); );
await pool.query( await pool.query(
`INSERT INTO storage_locations (name, description) `INSERT INTO storage_locations (name, description)
VALUES (?, ?);`, VALUES (?, ?);`,
["Default Storage", "Initial storage location"], ["Default Storage", "Initial storage location"],
); );
const [storageRows] = await pool.query<RowDataPacket[]>( const [storageRows] = await pool.query<RowDataPacket[]>(
`SELECT uuid `SELECT uuid
FROM storage_locations FROM storage_locations
WHERE name = ? LIMIT 1;`, WHERE name = ? LIMIT 1;`,
["Default Storage"], ["Default Storage"],
); );
const storageUuid: string | null = storageRows[0]?.uuid ?? null; const storageUuid: string | null = storageRows[0]?.uuid ?? null;
if (storageUuid) { if (storageUuid) {
await pool.query( await pool.query(
`INSERT INTO products (name, description, price, amount, storage_location, picture) `INSERT INTO products (name, description, price, amount, storage_location, picture)
VALUES (?, ?, ?, ?, ?, ?);`, VALUES (?, ?, ?, ?, ?, ?);`,
[ [
"Welcome Product", "Welcome Product",
"Your first item in Stockhome", "Your first item in Stockhome",
"0.00", "0.00",
1, 1,
storageUuid, storageUuid,
null, null,
], ],
); );
} }
console.log("Welcome product is ready..."); console.log("Welcome product is ready...");
return insertResult; return insertResult;
}; };
// error handling code // error handling code
app.use((err: Error, req: Request, res: Response, next: NextFunction) => { app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack); console.error(err.stack);
res.status(500).send("Something broke!"); res.status(500).send("Something broke!");
}); });
+11 -11
View File
@@ -1,14 +1,14 @@
import type {ErrorCode} from "../misc/types.ts"; import type { ErrorCode } from "../misc/types.ts";
export const returnErrorCode = (errorKey: ErrorCode) => { export const returnErrorCode = (errorKey: ErrorCode) => {
if (!Array.isArray(errorKey) || errorKey.length < 2) { if (!Array.isArray(errorKey) || errorKey.length < 2) {
return false; return false;
} }
return { return {
success: false, success: false,
code: errorKey[0], code: errorKey[0],
data: null, data: null,
message: errorKey[1] message: errorKey[1],
} };
}; };
+23 -19
View File
@@ -1,36 +1,40 @@
import type {NextFunction, Request, Response} from "express"; import type { NextFunction, Request, Response } from "express";
import {type JWTPayload, jwtVerify, SignJWT} from "jose"; import { type JWTPayload, jwtVerify, SignJWT } from "jose";
import env from "dotenv"; import env from "dotenv";
env.config(); env.config();
const secret = new TextEncoder().encode(process.env.SECRET_KEY); const secret = new TextEncoder().encode(process.env.SECRET_KEY);
export type AuthTokenPayload = JWTPayload & { export type AuthTokenPayload = JWTPayload & {
username: string; username: string;
}; };
declare module "express-serve-static-core" { declare module "express-serve-static-core" {
interface Request { interface Request {
user?: AuthTokenPayload; user?: AuthTokenPayload;
} }
} }
export async function generateToken(payload: AuthTokenPayload) { export async function generateToken(payload: AuthTokenPayload) {
return await new SignJWT(payload) return await new SignJWT(payload)
.setProtectedHeader({alg: "HS256"}) .setProtectedHeader({ alg: "HS256" })
.setIssuedAt() .setIssuedAt()
.setExpirationTime("24h") .setExpirationTime("24h")
.sign(secret); .sign(secret);
} }
export async function authenticate(req: Request, res: Response, next: NextFunction) { export async function authenticate(
const authHeader = req.headers["authorization"]; req: Request,
const token = authHeader && authHeader.split(" ")[1]; res: Response,
next: NextFunction,
) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (token == null) return res.sendStatus(401); if (token == null) return res.sendStatus(401);
const {payload} = await jwtVerify<AuthTokenPayload>(token, secret); const { payload } = await jwtVerify<AuthTokenPayload>(token, secret);
req.user = payload; req.user = payload;
next(); next();
} }
+3 -7
View File
@@ -9,17 +9,13 @@
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"ignoreDeprecations": "6.0", "ignoreDeprecations": "6.0",
"types": [ "types": ["node"],
"node"
],
"sourceMap": true, "sourceMap": true,
"declaration": true, "declaration": true,
"strict": true, "strict": true,
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": ["./**/*.ts"],
"./**/*.ts"
],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"dist", "dist",
@@ -33,4 +29,4 @@
"path": "../shared" "path": "../shared"
} }
] ]
} }
+22 -22
View File
@@ -1,29 +1,29 @@
import {defineConfig} from 'vite' import { defineConfig } from "vite";
import path from 'path' import path from "path";
import {dirname} from "node:path" import { dirname } from "node:path";
import {fileURLToPath} from "node:url" import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
export default defineConfig({ export default defineConfig({
root: 'src', root: "src",
build: { build: {
outDir: '../dist', outDir: "../dist",
emptyOutDir: true, emptyOutDir: true,
sourcemap: true, sourcemap: true,
minify: 'esbuild', minify: "esbuild",
assetsDir: 'assets', assetsDir: "assets",
rollupOptions: { rollupOptions: {
input: path.resolve(__dirname, 'src/index.html'), input: path.resolve(__dirname, "src/index.html"),
output: { output: {
assetFileNames: 'assets/[name]-[hash][extname]', assetFileNames: "assets/[name]-[hash][extname]",
}, },
},
}, },
resolve: { },
alias: { resolve: {
'@': path.resolve(__dirname, 'src'), alias: {
}, "@": path.resolve(__dirname, "src"),
}, },
}) },
});
+1 -1
View File
@@ -1,5 +1,5 @@
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { Input, Button, Alert } from "@mui/joy"; import { Alert, Button, Input } from "@mui/joy";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { signInUser } from "../utils/api/auth"; import { signInUser } from "../utils/api/auth";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
+1 -1
View File
@@ -9,7 +9,7 @@ import ExitToAppIcon from "@mui/icons-material/ExitToApp";
import TranslateIcon from "@mui/icons-material/Translate"; import TranslateIcon from "@mui/icons-material/Translate";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { useNavigate, useMatchRoute } from "@tanstack/react-router"; import { useMatchRoute, useNavigate } from "@tanstack/react-router";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { changeTranslation } from "../utils/uxFncs"; import { changeTranslation } from "../utils/uxFncs";
+2 -2
View File
@@ -1,8 +1,8 @@
import { useQueryClient, useMutation } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { deleteStorage, updateStorage } from "../utils/api/storages"; import { deleteStorage, updateStorage } from "../utils/api/storages";
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { useStore } from "@tanstack/react-store"; import { useStore } from "@tanstack/react-store";
import { Input, Button } from "@mui/joy"; import { Button, Input } from "@mui/joy";
import type { Storage } from "../misc/interfaces"; import type { Storage } from "../misc/interfaces";
import { formatDate } from "../utils/uxFncs"; import { formatDate } from "../utils/uxFncs";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -1,17 +1,17 @@
import { import {
Alert,
Button,
DialogContent,
DialogTitle,
Input,
Modal, Modal,
ModalDialog, ModalDialog,
DialogTitle,
DialogContent,
Stack, Stack,
Input,
Button,
Alert,
} from "@mui/joy"; } from "@mui/joy";
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { NewStorage, AlertInterface } from "../../misc/interfaces"; import type { AlertInterface, NewStorage } from "../../misc/interfaces";
import { mutateNewStorage } from "../../utils/api/storages"; import { mutateNewStorage } from "../../utils/api/storages";
import { useState } from "react"; import { useState } from "react";
@@ -1,22 +1,24 @@
import { import {
Alert,
Button,
DialogTitle,
Input,
Modal, Modal,
ModalDialog, ModalDialog,
DialogTitle,
Stack, Stack,
Input,
Button,
Alert,
} from "@mui/joy"; } from "@mui/joy";
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { ChangePasswordIntf, AlertInterface } from "../../misc/interfaces"; import type { AlertInterface, ChangePasswordIntf } from "../../misc/interfaces";
import { mutatePassword } from "../../utils/api/auth"; import { mutatePassword } from "../../utils/api/auth";
import { useState } from "react"; import { useState } from "react";
import { USER_ERROR_CODE } from "@stockhome/shared";
interface ChangePasswordProps { interface ChangePasswordProps {
isOpen: boolean; isOpen: boolean;
setOpen: (value: boolean) => void; setOpen: (value: boolean) => void;
alert: (alert: AlertInterface) => void;
} }
export const ChangePasswordModal = (props: ChangePasswordProps) => { export const ChangePasswordModal = (props: ChangePasswordProps) => {
@@ -35,7 +37,18 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
newPasswordRep: "", newPasswordRep: "",
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
mutate(value); console.log(value);
if (value.newPassword === value.newPasswordRep) {
mutate(value);
} else {
setAlert({
isAlert: true,
type: "danger",
header: t("error"),
text: t(USER_ERROR_CODE.PASSWORDS_NOT_MATCHED[0]),
});
}
}, },
}); });
@@ -50,6 +63,21 @@ export const ChangePasswordModal = (props: ChangePasswordProps) => {
}); });
}, },
onSuccess: () => { onSuccess: () => {
// Sets the success alert in the settings page via component props
props.alert({
isAlert: true,
type: "success",
header: t("success"),
text: t("SU005"),
});
// Sets the success alert locally (in the modal itself)
setAlert({
isAlert: true,
type: "success",
header: t("success"),
text: t("SU005"),
});
props.setOpen(false); props.setOpen(false);
}, },
}); });
+3 -2
View File
@@ -16,8 +16,8 @@ import { useForm } from "@tanstack/react-form";
import { createProduct } from "../utils/api/products"; import { createProduct } from "../utils/api/products";
import { getStorages } from "../utils/api/storages"; import { getStorages } from "../utils/api/storages";
import type { import type {
ProductFormValues,
AlertInterface, AlertInterface,
ProductFormValues,
Storage, Storage,
} from "../misc/interfaces"; } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
@@ -272,10 +272,11 @@ export const AddProduct = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex gap-3 items-center">
<Typography level="body-sm" className="text-slate-500"> <Typography level="body-sm" className="text-slate-500">
{t("product-details")} {t("product-details")}
</Typography> </Typography>
<div className="grow"></div>
<Button <Button
type="submit" type="submit"
loading={isPending} loading={isPending}
+6 -7
View File
@@ -1,14 +1,14 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
Typography, Alert,
Avatar,
Button, Button,
Checkbox,
Chip,
CircularProgress, CircularProgress,
Sheet, Sheet,
Table, Table,
Avatar, Typography,
Chip,
Checkbox,
Alert,
} from "@mui/joy"; } from "@mui/joy";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -17,8 +17,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { deleteSelectedProducts, getProducts } from "../utils/api/products"; import { deleteSelectedProducts, getProducts } from "../utils/api/products";
import { formatDate } from "../utils/uxFncs"; import { formatDate } from "../utils/uxFncs";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import type { ProductRow } from "../misc/interfaces"; import type { AlertInterface, ProductRow } from "../misc/interfaces";
import type { AlertInterface } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
export const InventoryPage = () => { export const InventoryPage = () => {
+10 -10
View File
@@ -1,26 +1,26 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getStorages } from "../utils/api/storages.ts"; import { getStorages } from "../utils/api/storages.ts";
import { import {
CircularProgress, Alert,
Typography, Box,
Select,
Option,
Input,
Button, Button,
Chip, Chip,
CircularProgress,
Divider, Divider,
Box, Input,
Alert, Option,
Select,
Typography,
} from "@mui/joy"; } from "@mui/joy";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { mutateProduct, getProductDetails } from "../utils/api/products.ts"; import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
import { toInputDate } from "../utils/uxFncs"; import { toInputDate } from "../utils/uxFncs";
import type { import type {
ProductFormValues,
productDetailsInterface,
AlertInterface, AlertInterface,
productDetailsInterface,
ProductFormValues,
Storage, Storage,
} from "../misc/interfaces"; } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
+10 -11
View File
@@ -1,21 +1,20 @@
import { import {
Input,
Button,
CircularProgress,
Typography,
Sheet,
Chip,
Divider,
Alert, Alert,
Button,
Chip,
CircularProgress,
Divider,
Input,
Sheet,
Typography,
} from "@mui/joy"; } from "@mui/joy";
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { SettingsIntf } from "../misc/interfaces"; import type { AlertInterface, SettingsIntf } from "../misc/interfaces";
import type { AlertInterface } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
import { mutateSettings, fetchSettings } from "../utils/api/settings"; import { fetchSettings, mutateSettings } from "../utils/api/settings";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ChangePasswordModal } from "../components/modals/ChangePasswordModal"; import { ChangePasswordModal } from "../components/modals/ChangePasswordModal";
@@ -90,7 +89,7 @@ export const Settings = () => {
return ( return (
<> <>
<ChangePasswordModal isOpen={modal} setOpen={setModal} /> <ChangePasswordModal alert={setAlert} isOpen={modal} setOpen={setModal} />
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="space-y-1"> <div className="space-y-1">
+4 -5
View File
@@ -1,16 +1,15 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getStorages } from "../utils/api/storages"; import { getStorages } from "../utils/api/storages";
import { import {
Sheet, Alert,
Table,
Button, Button,
CircularProgress, CircularProgress,
Sheet,
Table,
Typography, Typography,
Alert,
} from "@mui/joy"; } from "@mui/joy";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { Storage } from "../misc/interfaces"; import type { AlertInterface, Storage } from "../misc/interfaces";
import type { AlertInterface } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
import { StorageRow } from "../components/StorageRow"; import { StorageRow } from "../components/StorageRow";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
+35 -31
View File
@@ -1,26 +1,26 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getStorages } from "../utils/api/storages.ts"; import { getStorages } from "../utils/api/storages.ts";
import { import {
CircularProgress, Alert,
Typography, Box,
Select,
Option,
Input,
Button, Button,
Chip, Chip,
CircularProgress,
Divider, Divider,
Box, Input,
Alert, Option,
Select,
Typography,
} from "@mui/joy"; } from "@mui/joy";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { mutateProduct, getProductDetails } from "../utils/api/products.ts"; import { getProductDetails, mutateProduct } from "../utils/api/products.ts";
import { toInputDate } from "../utils/uxFncs"; import { toInputDate } from "../utils/uxFncs";
import type { import type {
ProductFormValues,
productDetailsInterface,
AlertInterface, AlertInterface,
productDetailsInterface,
ProductFormValues,
Storage, Storage,
} from "../misc/interfaces"; } from "../misc/interfaces";
import type { ApiError } from "../utils/api/apiError"; import type { ApiError } from "../utils/api/apiError";
@@ -342,28 +342,32 @@ export const ViewProduct = (props: ViewProductProps) => {
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center justify-between gap-3"> <div>
<Typography level="body-sm" className="text-slate-500"> <div className="flex gap-3 items-center">
{t("product-details")} <Typography level="body-sm" className="text-slate-500">
</Typography> {t("product-details")}
<Button </Typography>
startDecorator={<QrCodeIcon />} <div className="grow"></div>
loading={isPending} <Button
onClick={() => downloadQRcode()} startDecorator={<QrCodeIcon />}
size="lg" loading={isPending}
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" onClick={() => downloadQRcode()}
> size="lg"
{t("download-qr-code")} className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
</Button> >
<Button {t("download-qr-code")}
type="submit" </Button>
loading={isPending} <Button
size="lg" type="submit"
className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]" loading={isPending}
> size="lg"
{t("save")} className="rounded-2xl bg-[#0b6bcb] text-white shadow-[0_16px_36px_rgba(11,107,203,0.35)] transition hover:-translate-y-0.5 hover:bg-[#095aa7]"
</Button> >
{t("save")}
</Button>
</div>
</div> </div>
{alert.isAlert && ( {alert.isAlert && (
<Alert <Alert
color={alert.type} color={alert.type}
@@ -57,6 +57,7 @@
"error": "Fehler", "error": "Fehler",
"success": "Erfolg", "success": "Erfolg",
"submit": "Absenden", "submit": "Absenden",
"SU005": "Das Passwort wurde erfolgreich geändert!",
"EG001": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.", "EG001": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.",
"EU001": "Falscher Benutzername oder Passwort!", "EU001": "Falscher Benutzername oder Passwort!",
"EU002": "Ihr Konto wurde deaktiviert. Bitte wenden Sie sich an einen Administrator.", "EU002": "Ihr Konto wurde deaktiviert. Bitte wenden Sie sich an einen Administrator.",
@@ -67,6 +68,7 @@
"EU007": "Passwort konnte nicht geändert werden. Bitte versuchen Sie es erneut.", "EU007": "Passwort konnte nicht geändert werden. Bitte versuchen Sie es erneut.",
"EU008": "Ihre Einstellungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.", "EU008": "Ihre Einstellungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.",
"EU009": "Ihre Einstellungen konnten nicht geladen werden. Bitte versuchen Sie es erneut.", "EU009": "Ihre Einstellungen konnten nicht geladen werden. Bitte versuchen Sie es erneut.",
"EU010": "Sie müssen Ihr neues Passwort zweimal korrekt eingeben.",
"ES001": "Bitte füllen Sie alle erforderlichen Felder für den Lagerort aus.", "ES001": "Bitte füllen Sie alle erforderlichen Felder für den Lagerort aus.",
"ES002": "Einige erforderliche Lagerinformationen fehlen. Bitte überprüfen Sie Ihre Eingabe.", "ES002": "Einige erforderliche Lagerinformationen fehlen. Bitte überprüfen Sie Ihre Eingabe.",
"ES003": "Der Lagerort konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.", "ES003": "Der Lagerort konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
@@ -57,6 +57,7 @@
"error": "Error", "error": "Error",
"success": "Success", "success": "Success",
"submit": "Submit", "submit": "Submit",
"SU005": "Your password is updated successfully!",
"EG001": "An unexpected error occurred. Please try again later.", "EG001": "An unexpected error occurred. Please try again later.",
"EU001": "Wrong username or password!", "EU001": "Wrong username or password!",
"EU002": "Your account has been deactivated. Please contact an administrator.", "EU002": "Your account has been deactivated. Please contact an administrator.",
@@ -67,6 +68,7 @@
"EU007": "Failed to change your password. Please try again.", "EU007": "Failed to change your password. Please try again.",
"EU008": "Your settings could not be saved. Please try again.", "EU008": "Your settings could not be saved. Please try again.",
"EU009": "Your settings could not be loaded. Please try again.", "EU009": "Your settings could not be loaded. Please try again.",
"EU010": "You must enter your new password twice correctly.",
"ES001": "Please fill in all required fields for the storage location.", "ES001": "Please fill in all required fields for the storage location.",
"ES002": "Some required storage information is missing. Please check your input.", "ES002": "Some required storage information is missing. Please check your input.",
"ES003": "The storage location could not be updated. Please try again.", "ES003": "The storage location could not be updated. Please try again.",
+4 -1
View File
@@ -19,7 +19,10 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": true,
"declarationMap": true
}, },
"include": ["src"] "include": ["src"]
} }
+6 -3
View File
@@ -9,12 +9,15 @@
"version": "0.1.0-dev", "version": "0.1.0-dev",
"description": "", "description": "",
"scripts": { "scripts": {
"build:shared": "npm run build --workspace=@stockhome/shared",
"build:backend": "npm run build --workspace=backend",
"build:frontend": "npm run build --workspace=frontend",
"dev:frontend": "npm run dev --workspace=frontend", "dev:frontend": "npm run dev --workspace=frontend",
"dev:backend": "npm run dev --workspace=backend", "dev:backend": "npm run build:shared && npm run dev --workspace=backend",
"dev:docker": "docker compose -f docker-compose.dev.yml up -d --wait", "dev:docker": "docker compose -f docker-compose.dev.yml up -d --wait",
"dev": "npm run dev:docker && concurrently --kill-others \"npm:dev:frontend\" \"npm:dev:backend\"", "dev": "npm run dev:docker && concurrently --kill-others \"npm:dev:frontend\" \"npm:dev:backend\"",
"dev:stop": "docker compose -f docker-compose.dev.yml down", "dev:stop": "docker compose -f docker-compose.dev.yml down",
"buildProd": "npm run build --workspace=frontend && npm run build --workspace=backend" "buildProd": "npm run build:shared && npm run build:backend && npm run build:frontend"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -33,4 +36,4 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"concurrently": "^10.0.3" "concurrently": "^10.0.3"
} }
} }
+13 -5
View File
@@ -2,16 +2,24 @@
"name": "@stockhome/shared", "name": "@stockhome/shared",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "commonjs", "type": "module",
"scripts": { "scripts": {
"prebuild": "barrelsby --directory src --delete --single", "prebuild": "barrelsby --directory src --delete --single",
"build": "tsc -b", "build:esm": "tsc -p tsconfig.esm.json",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build": "npm run prebuild && npm run build:esm && npm run build:cjs",
"lint": "eslint ." "lint": "eslint ."
}, },
"main": "./dist/src/index.js", "exports": {
"types": "./dist/src/index.d.ts", ".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"types": "./dist/types/index.d.ts",
"devDependencies": { "devDependencies": {
"barrelsby": "^2.8.1", "barrelsby": "^2.8.1",
"typescript": "~6.0.2" "typescript": "~6.0.2"
} }
} }
+32 -25
View File
@@ -1,35 +1,42 @@
export const GENERAL_ERROR_CODE = { export const GENERAL_ERROR_CODE = {
UNEXPECTED_SERVER_ERROR: ["EG001", "Unexpected server error"], UNEXPECTED_SERVER_ERROR: ["EG001", "Unexpected server error"],
} as const; } as const;
export const USER_ERROR_CODE = { export const USER_ERROR_CODE = {
WRONG_USERNAME_PASSWORD: ["EU001", "Username or password is wrong"], WRONG_USERNAME_PASSWORD: ["EU001", "Username or password is wrong"],
USER_IS_DEACTIVATED: ["EU002", "User is deactivated"], USER_IS_DEACTIVATED: ["EU002", "User is deactivated"],
INVALID_REQUEST_BODY: ["EU003", "Invalid request body. Username and password are required."], INVALID_REQUEST_BODY: [
INVALID_PARAMETERS: ["EU004", "Invalid or missing parameters."], "EU003",
TOKEN_GENERATION_FAILED: ["EU005", "Failed to generate user token."], "Invalid request body. Username and password are required.",
LOGIN_FAILED: ["EU006", "Failed to log in user."], ],
PASSWORD_CHANGE_FAILED: ["EU007", "Failed to change password."], INVALID_PARAMETERS: ["EU004", "Invalid or missing parameters."],
SETTINGS_NOT_UPDATED: ["EU008", "Settings could not be updated."], TOKEN_GENERATION_FAILED: ["EU005", "Failed to generate user token."],
SETTINGS_NOT_FOUND: ["EU009", "Settings could not be retrieved."], LOGIN_FAILED: ["EU006", "Failed to log in user."],
PASSWORD_CHANGE_FAILED: ["EU007", "Failed to change password."],
SETTINGS_NOT_UPDATED: ["EU008", "Settings could not be updated."],
SETTINGS_NOT_FOUND: ["EU009", "Settings could not be retrieved."],
PASSWORDS_NOT_MATCHED: ["EU010", "The two passwords are not matching."],
} as const; } as const;
export const STORAGE_ERROR_CODE = { export const STORAGE_ERROR_CODE = {
INVALID_REQUEST_BODY: ["ES001", "Invalid request body"], INVALID_REQUEST_BODY: ["ES001", "Invalid request body"],
INVALID_PARAMETERS: ["ES002", "Parameter storageUUID or/and parameter values are/is missing."], INVALID_PARAMETERS: [
STORAGE_NOT_UPDATED: ["ES003", "Storage is not updated."], "ES002",
STORAGE_NOT_DELETED: ["ES004", "Storage is not deleted."], "Parameter storageUUID or/and parameter values are/is missing.",
NO_STORAGE_LOCATIONS_FOUND: ["ES005", "No storage locations found"], ],
STORAGE_NOT_CREATED: ["ES006", "Storage is not created."], STORAGE_NOT_UPDATED: ["ES003", "Storage is not updated."],
STORAGE_NOT_DELETED: ["ES004", "Storage is not deleted."],
NO_STORAGE_LOCATIONS_FOUND: ["ES005", "No storage locations found"],
STORAGE_NOT_CREATED: ["ES006", "Storage is not created."],
} as const; } as const;
export const PRODUCT_ERROR_CODE = { export const PRODUCT_ERROR_CODE = {
PRODUCT_NOT_CREATED: ["EP001", "Error while creating product"], PRODUCT_NOT_CREATED: ["EP001", "Error while creating product"],
NO_PRODUCTS_FOUND: ["EP002", "No products found"], NO_PRODUCTS_FOUND: ["EP002", "No products found"],
PRODUCT_NOT_FOUND: ["EP003", "Product not found"], PRODUCT_NOT_FOUND: ["EP003", "Product not found"],
PRODUCT_AMOUNT_NOT_UPDATED: ["EP004", "Error while updating product amount"], PRODUCT_AMOUNT_NOT_UPDATED: ["EP004", "Error while updating product amount"],
PRODUCT_NOT_UPDATED: ["EP005", "Error while updating product"], PRODUCT_NOT_UPDATED: ["EP005", "Error while updating product"],
PRODUCT_NOT_DELETED: ["EP006", "Error while deleting product"], PRODUCT_NOT_DELETED: ["EP006", "Error while deleting product"],
INVALID_REQUEST_BODY: ["EP007", "Invalid request body. Name is required."], INVALID_REQUEST_BODY: ["EP007", "Invalid request body. Name is required."],
INVALID_PARAMETERS: ["EP008", "Invalid or missing parameters."], INVALID_PARAMETERS: ["EP008", "Invalid or missing parameters."],
} as const; } as const;
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"rootDir": "./src",
"target": "ES2022",
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/cjs",
"module": "CommonJS",
"moduleResolution": "Node",
"declaration": false,
"declarationMap": false,
"ignoreDeprecations": "6.0"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist/esm",
"module": "ESNext",
"moduleResolution": "Bundler",
"declaration": true,
"declarationDir": "./dist/types",
"declarationMap": true
}
}
+2 -4
View File
@@ -11,7 +11,5 @@
"skipLibCheck": true, "skipLibCheck": true,
"ignoreDeprecations": "6.0" "ignoreDeprecations": "6.0"
}, },
"include": [ "include": ["src/**/*.ts"]
"src/**/*.ts" }
]
}