Implement backend structure with Docker, database schema, and user authentication
This commit is contained in:
@@ -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);
|
||||
+27
-25
@@ -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
|
||||
);
|
||||
Generated
+10
@@ -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",
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user