Implement backend structure with Docker, database schema, and user authentication

This commit is contained in:
2026-05-26 13:01:11 +02:00
parent ac9679ab27
commit 4c0c441e92
14 changed files with 313 additions and 33 deletions
View File
+3
View File
@@ -44,3 +44,6 @@ Temporary Items
.apdisk
ToDo.txt
.env
.docker/volumes
+12
View File
@@ -0,0 +1,12 @@
FROM node:20-alpine
ENV NODE_ENV=production
WORKDIR /backend
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 8004
CMD ["npm", "start"]
+83
View File
@@ -0,0 +1,83 @@
-- ============================================================
-- StockHome – Mock Data
-- Run against the stockhome schema before executing this.
-- Passwords are stored in plain text (development only).
-- ============================================================
USE stockhome;
SET FOREIGN_KEY_CHECKS = 0;
DELETE FROM products;
DELETE FROM storage_locations;
DELETE FROM users;
SET FOREIGN_KEY_CHECKS = 1;
-- ============================================================
-- USERS
-- ============================================================
INSERT INTO users (username, first_name, last_name, email, password, is_admin, is_active, last_login) VALUES
('thomas.mueller', 'Thomas', 'Müller', 'thomas.mueller@example.com', 'Password123!', TRUE, TRUE, '2025-05-25 08:30:00'),
('sarah.mueller', 'Sarah', 'Müller', 'sarah.mueller@example.com', 'Password123!', FALSE, TRUE, '2025-05-24 19:15:00'),
('max.schmidt', 'Max', 'Schmidt', 'max.schmidt@example.com', 'Password123!', FALSE, FALSE, '2025-03-10 11:00:00');
-- ============================================================
-- STORAGE LOCATIONS
-- ============================================================
INSERT INTO storage_locations (name, description) VALUES
('Kühlschrank', 'Kühlschrank in der Küche'),
('Gefrierfach 1', 'Oberes Gefrierfach – Fleisch und Fisch'),
('Gefrierfach 2', 'Unteres Gefrierfach – Gemüse und Fertiggerichte'),
('Vorratskammer', 'Regal im Flur, Trockenware und Konserven'),
('Keller Regal A', 'Keller links – Getränke und Eingemachtes'),
('Keller Regal B', 'Keller rechts – Hygieneartikel und Reinigungsmittel'),
('Garage Regal', 'Garage – Werkzeug, Öle, Autopflege');
-- ============================================================
-- PRODUCTS
-- Storage locations are looked up by name since UUIDs are
-- auto-generated above and not known at insert time.
-- ============================================================
-- Gefrierfach 1
INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES
('Hähnchenbrust', 'Tiefgekühlte Hähnchenbrustfilets, vakuumverpackt', '6.49', 5, (SELECT uuid FROM storage_locations WHERE name = 'Gefrierfach 1'), '2026-01-10', '2025-01-10'),
('Lachs Filet', 'Tiefgefrorenes Lachsfilet, Atlantic, 500g', '12.99', 1, (SELECT uuid FROM storage_locations WHERE name = 'Gefrierfach 1'), '2025-12-31', '2025-02-20'),
('Hackfleisch', 'Rind und Schwein gemischt, 500g Portion', '3.99', 3, (SELECT uuid FROM storage_locations WHERE name = 'Gefrierfach 1'), '2026-02-15', '2025-01-28');
-- Gefrierfach 2
INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES
('Erbsen TK', 'Tiefkühlerbsen, 750g Packung', '1.99', 3, (SELECT uuid FROM storage_locations WHERE name = 'Gefrierfach 2'), '2026-06-01', '2025-03-05'),
('Spinat TK', 'Blattspinat tiefgefroren, 450g', '1.49', 2, (SELECT uuid FROM storage_locations WHERE name = 'Gefrierfach 2'), '2026-04-01', '2025-02-10'),
('Pizza Margherita', 'Tiefkühlpizza, 350g', '2.29', 4, (SELECT uuid FROM storage_locations WHERE name = 'Gefrierfach 2'), '2025-11-30', NULL);
-- Kühlschrank
INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES
('Vollmilch', '3,5% Fett, 1 Liter', '1.09', 2, (SELECT uuid FROM storage_locations WHERE name = 'Kühlschrank'), '2025-05-29', NULL),
('Butter', 'Deutsche Markenbutter, 250g', '1.89', 3, (SELECT uuid FROM storage_locations WHERE name = 'Kühlschrank'), '2025-07-01', NULL),
('Gouda am Stück', 'Junger Gouda, ca. 400g', '3.29', 1, (SELECT uuid FROM storage_locations WHERE name = 'Kühlschrank'), '2025-06-15', NULL);
-- Vorratskammer
INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES
('Spaghetti', 'Barilla Nr. 5, 500g', '1.29', 5, (SELECT uuid FROM storage_locations WHERE name = 'Vorratskammer'), '2027-02-01', NULL),
('Basmatireis', 'Uncle Ben''s, 1kg', '2.49', 2, (SELECT uuid FROM storage_locations WHERE name = 'Vorratskammer'), '2027-01-05', NULL),
('Tomaten gehackt', 'Mutti, 400g Dose', '0.89', 8, (SELECT uuid FROM storage_locations WHERE name = 'Vorratskammer'), '2027-01-01', '2024-11-01'),
('Kichererbsen', 'ja!, 400g Dose in Lake', '0.79', 4, (SELECT uuid FROM storage_locations WHERE name = 'Vorratskammer'), '2027-06-01', '2024-12-01'),
('Haferflocken', 'Kölln, zarte Haferflocken, 500g', '1.49', 2, (SELECT uuid FROM storage_locations WHERE name = 'Vorratskammer'), '2026-09-10', NULL),
('Thunfisch in Öl', 'Rio Mare, 3er Pack in Olivenöl', '3.49', 2, (SELECT uuid FROM storage_locations WHERE name = 'Vorratskammer'), '2026-08-01', '2025-01-20');
-- Keller Regal A
INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES
('Mineralwasser', 'Volvic still, 1,5L Flasche', '0.79', 12, (SELECT uuid FROM storage_locations WHERE name = 'Keller Regal A'), '2027-02-15', NULL),
('Apfelsaft', 'Valensina naturtrüb, 1L', '1.99', 3, (SELECT uuid FROM storage_locations WHERE name = 'Keller Regal A'), '2026-03-01', '2025-03-01'),
('Erdbeerkonfitüre', 'Selbst eingemacht, 250ml Glas', NULL, 6, (SELECT uuid FROM storage_locations WHERE name = 'Keller Regal A'), '2026-07-01', '2025-07-15');
-- Keller Regal B
INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES
('Toilettenpapier', 'Zewa, 3-lagig, 16 Rollen', '4.99', 3, (SELECT uuid FROM storage_locations WHERE name = 'Keller Regal B'), NULL, NULL),
('Handseife', 'Dove, pflegend, 250ml', '2.29', 0, (SELECT uuid FROM storage_locations WHERE name = 'Keller Regal B'), '2026-02-10', '2025-02-10'),
('Waschmittel', 'Persil Color, 20 Waschladungen', '8.99', 1, (SELECT uuid FROM storage_locations WHERE name = 'Keller Regal B'), NULL, NULL);
-- Garage Regal
INSERT INTO products (name, description, price, amount, storage_location, expiry_date, bottling_date) VALUES
('Motoröl 5W-30', 'Castrol EDGE, 1L Flasche', '14.99', 2, (SELECT uuid FROM storage_locations WHERE name = 'Garage Regal'), NULL, NULL),
('Fahrradkette', 'Shimano HG-54, 11-fach', '18.50', 1, (SELECT uuid FROM storage_locations WHERE name = 'Garage Regal'), NULL, NULL);
+5 -3
View File
@@ -1,8 +1,10 @@
USE stockhome;
CREATE TABLE IF NOT EXISTS users (
uuid BINARY(16) PRIMARY KEY,
uuid BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
username VARCHAR(255) NOT NULL UNIQUE,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -14,7 +16,7 @@ CREATE TABLE IF NOT EXISTS users (
);
CREATE TABLE IF NOT EXISTS storage_locations (
uuid BINARY(16) PRIMARY KEY,
uuid BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -22,7 +24,7 @@ CREATE TABLE IF NOT EXISTS storage_locations (
);
CREATE TABLE IF NOT EXISTS products (
uuid BINARY(16) PRIMARY KEY,
uuid BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(255) NOT NULL,
description TEXT DEFAULT NULL,
price VARCHAR(10) DEFAULT NULL,
+10
View File
@@ -13,6 +13,7 @@
"dotenv": "^17.2.3",
"ejs": "^3.1.10",
"express": "^5.2.1",
"jose": "^6.0.12",
"mysql2": "^3.16.0"
}
},
@@ -586,6 +587,15 @@
"node": ">=10"
}
},
"node_modules/jose": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+2 -1
View File
@@ -10,12 +10,13 @@
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"type": "module",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"ejs": "^3.1.10",
"express": "^5.2.1",
"jose": "^6.0.12",
"mysql2": "^3.16.0"
}
}
@@ -0,0 +1,12 @@
import mysql from "mysql2";
import dotenv from "dotenv";
dotenv.config();
const pool = mysql
.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
})
.promise();
@@ -0,0 +1,42 @@
import mysql from "mysql2";
import dotenv from "dotenv";
dotenv.config();
const pool = mysql
.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
})
.promise();
export const findUser = async (username, password) => {
const [result] = await pool.query(
"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],
);
if (result.length <= 0) {
return { code: "e001" }; // username or password is wrong
}
if (!result[0].is_active) {
return { code: "e002" }; // user is deactivated
}
return { code: "s001", data: result[0] }; // user found
};
export const loginUser = async (username) => {
const [result] = await pool.query(
"UPDATE users SET last_login = NOW() WHERE username = ?;",
[username],
);
if (result.affectedRows > 0) {
return { code: "s002" };
} else {
return { code: "e003" };
}
};
+6
View File
@@ -0,0 +1,6 @@
import express from "express";
import dotenv from "dotenv";
dotenv.config();
const router = express.Router();
export default router;
+56
View File
@@ -0,0 +1,56 @@
import express from "express";
import dotenv from "dotenv";
import { generateToken } from "../../services/tokenService.js";
import { findUser, loginUser } from "./database/users.database.js";
dotenv.config();
const router = express.Router();
router.post("/login", async (req, res) => {
const username = req.body.username;
const password = req.body.password;
const result = await findUser(username, password);
if (result.code === "e001") {
res.status(404).json({
success: false,
code: "e001",
data: null,
message: "username oder password is wrong",
});
}
if (result.code === "e002") {
res.status(403).json({
success: false,
code: "e002",
data: null,
message: "user is deactivated",
});
}
if (result.code === "s001") {
const token = await generateToken(result.data);
const login = await loginUser(result.data.username);
if (login.code === "e003") {
res.status(500).json({
success: false,
code: "e003",
data: null,
message: "Unexpected server error. Please contact system admin.",
});
}
res.status(202).json({
success: true,
code: "s001",
data: {
token,
},
message: "User token generated successfully",
});
}
});
export default router;
+29
View File
@@ -0,0 +1,29 @@
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
dotenv.config();
const app = express();
const PORT = process.env.PORT;
app.set("view engine", "ejs");
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// frontend routes
import userRouter from "./routes/app/users.route.js";
app.use("/users", userRouter);
import productRouter from "./routes/app/products.route.js";
app.use("/products", productRouter);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
// error handling code
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Something broke!");
});
+27
View File
@@ -0,0 +1,27 @@
import bodyParser from "body-parser";
import { SignJWT, jwtVerify } from "jose";
import env from "dotenv";
env.config();
const secret = new TextEncoder().encode(process.env.SECRET_KEY);
export async function generateToken(payload) {
const newToken = await new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("24h") // Token valid for 24 hours
.sign(secret);
console.log("Generated token: ", newToken);
return newToken;
}
export async function authenticate(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1]; // Bearer <token>
if (token == null) return res.sendStatus(401); // No token present
const { payload } = await jwtVerify(token, secret);
req.user = payload;
next();
}
+3 -6
View File
@@ -1,7 +1,7 @@
services:
database:
container_name: stockhome-mysql
image: mysql:latest
image: mysql:8.0
restart: unless-stopped
ports:
- "3312:3306"
@@ -10,7 +10,7 @@ services:
MYSQL_DATABASE: stockhome
TZ: Europe/Berlin
volumes:
- stockhome_mysql:/var/lib/mysql
- ./.docker/volumes/stockhome_mysql:/var/lib/mysql
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
backend:
@@ -20,11 +20,8 @@ services:
dockerfile: Dockerfile
ports:
- "8004:8004"
entrypoint:
environment:
NODE_ENV: production
depends_on:
- database
restart: unless-stopped
volumes:
stockhome_mysql: