Implement backend structure with Docker, database schema, and user authentication
This commit is contained in:
@@ -44,3 +44,6 @@ Temporary Items
|
|||||||
.apdisk
|
.apdisk
|
||||||
|
|
||||||
ToDo.txt
|
ToDo.txt
|
||||||
|
|
||||||
|
.env
|
||||||
|
.docker/volumes
|
||||||
@@ -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"]
|
||||||
@@ -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);
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
USE stockhome;
|
USE stockhome;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
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,
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
first_name VARCHAR(255) NOT NULL,
|
||||||
|
last_name VARCHAR(255) NOT NULL,
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
password VARCHAR(255) NOT NULL,
|
password VARCHAR(255) NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -14,7 +16,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS storage_locations (
|
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,
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
description TEXT DEFAULT NULL,
|
description TEXT DEFAULT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -22,7 +24,7 @@ CREATE TABLE IF NOT EXISTS storage_locations (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS products (
|
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,
|
name VARCHAR(255) NOT NULL,
|
||||||
description TEXT DEFAULT NULL,
|
description TEXT DEFAULT NULL,
|
||||||
price VARCHAR(10) DEFAULT NULL,
|
price VARCHAR(10) DEFAULT NULL,
|
||||||
|
|||||||
Generated
+10
@@ -13,6 +13,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"jose": "^6.0.12",
|
||||||
"mysql2": "^3.16.0"
|
"mysql2": "^3.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -586,6 +587,15 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/long": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
|||||||
@@ -10,12 +10,13 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"jose": "^6.0.12",
|
||||||
"mysql2": "^3.16.0"
|
"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" };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import express from "express";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -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;
|
||||||
@@ -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!");
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
container_name: stockhome-mysql
|
container_name: stockhome-mysql
|
||||||
image: mysql:latest
|
image: mysql:8.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "3312:3306"
|
- "3312:3306"
|
||||||
@@ -10,7 +10,7 @@ services:
|
|||||||
MYSQL_DATABASE: stockhome
|
MYSQL_DATABASE: stockhome
|
||||||
TZ: Europe/Berlin
|
TZ: Europe/Berlin
|
||||||
volumes:
|
volumes:
|
||||||
- stockhome_mysql:/var/lib/mysql
|
- ./.docker/volumes/stockhome_mysql:/var/lib/mysql
|
||||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
@@ -20,11 +20,8 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8004:8004"
|
- "8004:8004"
|
||||||
entrypoint:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
|
||||||
stockhome_mysql:
|
|
||||||
|
|||||||
Reference in New Issue
Block a user