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
+4 -1
View File
@@ -43,4 +43,7 @@ Network Trash Folder
Temporary Items
.apdisk
ToDo.txt
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);
+27 -25
View File
@@ -1,37 +1,39 @@
USE stockhome;
CREATE TABLE IF NOT EXISTS users (
uuid BINARY(16) PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
is_admin BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
last_login TIMESTAMP NULL,
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,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
is_admin BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
last_login TIMESTAMP NULL,
profile_picture VARCHAR(500) NULL
);
CREATE TABLE IF NOT EXISTS storage_locations (
uuid BINARY(16) PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
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,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS products (
uuid BINARY(16) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT DEFAULT NULL,
price VARCHAR(10) DEFAULT NULL,
amount INT NOT NULL,
storage_location BINARY(16) NOT NULL,
expiry_date DATE DEFAULT NULL,
bottling_date DATE DEFAULT NULL,
picture VARCHAR(500) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
uuid BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(255) NOT NULL,
description TEXT DEFAULT NULL,
price VARCHAR(10) DEFAULT NULL,
amount INT NOT NULL,
storage_location BINARY(16) NOT NULL,
expiry_date DATE DEFAULT NULL,
bottling_date DATE DEFAULT NULL,
picture VARCHAR(500) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (storage_location) REFERENCES storage_locations(uuid) ON DELETE CASCADE
);
+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: