summaryrefslogtreecommitdiff
path: root/backend/src
diff options
context:
space:
mode:
Diffstat (limited to 'backend/src')
-rw-r--r--backend/src/controllers/generateAdmin.ts48
-rw-r--r--backend/src/controllers/loginUser.ts65
-rw-r--r--backend/src/database/postgres.ts34
-rw-r--r--backend/src/database/redis.ts49
-rw-r--r--backend/src/middleware/authCheck.ts36
-rw-r--r--backend/src/routes/router.ts12
-rw-r--r--backend/src/server.ts86
-rw-r--r--backend/src/types/database.ts7
-rw-r--r--backend/src/types/express.d.ts11
-rw-r--r--backend/src/types/request.ts4
-rw-r--r--backend/src/types/user.ts18
-rw-r--r--backend/src/utils/dbErrorCheck.ts16
12 files changed, 386 insertions, 0 deletions
diff --git a/backend/src/controllers/generateAdmin.ts b/backend/src/controllers/generateAdmin.ts
new file mode 100644
index 0000000..045fd2d
--- /dev/null
+++ b/backend/src/controllers/generateAdmin.ts
@@ -0,0 +1,48 @@
+import bcrypt from "bcrypt";
+import { Request, Response } from "express";
+import { QueryResult } from "pg";
+import db from "../database/postgres.js";
+import type { AdminCreationResult } from "../types/user.js";
+import { isPostgresError, PostgresErrorCodes } from "../utils/dbErrorCheck.js";
+
+/*
+ One-time function to generate an admin user with specific email & password in the DB
+ Reason: hash the password with bcrypt for future authentication
+*/
+async function generateAdmin(_req: Request, res: Response) {
+ const adminMail = "admin@test.com";
+ const adminPass = "test123";
+
+ try {
+ const hashedPassword = await bcrypt.hash(adminPass, 10);
+
+ const adminCreation: QueryResult<AdminCreationResult> = await db.query(
+ "INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id, email, created_at;",
+ [adminMail, hashedPassword]
+ );
+
+ return res.status(201).json({
+ message: "The admin was created successfully.",
+ admin: adminCreation.rows[0],
+ });
+ } catch (error) {
+ if (
+ isPostgresError(error) &&
+ error.code === PostgresErrorCodes.UNIQUE_VIOLATION
+ ) {
+ console.error("Error creating the admin: ", error);
+ return res.status(409).json({
+ message: "Admin already exists.",
+ error: error.message,
+ });
+ }
+
+ console.error("Error creating admin: ", error);
+ return res.status(500).json({
+ message: "Internal server error for admin creation.",
+ error,
+ });
+ }
+}
+
+export default generateAdmin;
diff --git a/backend/src/controllers/loginUser.ts b/backend/src/controllers/loginUser.ts
new file mode 100644
index 0000000..860bce2
--- /dev/null
+++ b/backend/src/controllers/loginUser.ts
@@ -0,0 +1,65 @@
+import bcrypt from "bcrypt";
+import { Request, Response } from "express";
+import jwt from "jsonwebtoken";
+import { QueryResult } from "pg";
+import db from "../database/postgres.js";
+import type { LoginRequest } from "../types/request.js";
+import type { AuthorizedUser, DatabaseUser } from "../types/user.js";
+
+async function loginUser(req: Request, res: Response) {
+ const { email, password } = req.body as LoginRequest;
+
+ if (!email || !password) {
+ return res.status(400).json({
+ message: "E-Mail und Passwort sind erforderlich.",
+ });
+ }
+
+ try {
+ // Get data for user with login email address
+ const queryResult: QueryResult<DatabaseUser> = await db.query(
+ "SELECT id, email, password_hash, created_at FROM users WHERE email = $1;",
+ [email]
+ );
+
+ const user = queryResult.rows[0];
+
+ if (!user) {
+ return res.status(401).json({ message: "Login Daten ungültig." });
+ }
+
+ // Check if password is correct
+ const isValidPassword = await bcrypt.compare(
+ password,
+ user.password_hash
+ );
+
+ if (!isValidPassword) {
+ return res.status(401).json({
+ message: "Das Passwort ist nicht korrekt.",
+ });
+ }
+
+ const userData: AuthorizedUser = {
+ id: user.id,
+ email: user.email,
+ createdAt: user.created_at,
+ };
+
+ // Create token for authentication
+ const token = jwt.sign(userData, process.env.JWT_SECRET!);
+
+ return res.status(200).json({
+ message: "Erfolgreiche Anmeldung.",
+ user: userData,
+ token,
+ });
+ } catch (error) {
+ console.error("Fehler beim Login: ", error);
+ return res
+ .status(500)
+ .json({ message: "Interner Serverfehler beim Login.", error });
+ }
+}
+
+export default loginUser;
diff --git a/backend/src/database/postgres.ts b/backend/src/database/postgres.ts
new file mode 100644
index 0000000..6f6b682
--- /dev/null
+++ b/backend/src/database/postgres.ts
@@ -0,0 +1,34 @@
+import { Pool } from "pg";
+
+const pool = new Pool({
+ user: process.env.DB_USER,
+ password: process.env.DB_PASSWORD,
+ database: process.env.DB_DATABASE,
+ host: process.env.DB_HOST,
+ port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
+});
+
+/* Too strict for Docker:
+pool.connect((error, _client, release) => {
+ if (error) {
+ console.error("Verbindung zur Datenbank fehlgeschlagen: ", error);
+ process.exit(1);
+ } else {
+ console.log("Erfolgreich mit der Datenbank verbunden.");
+ release();
+ }
+});
+ */
+
+console.log("Erfolgreich mit der Datenbank verbunden.");
+
+export async function closeDBConnection() {
+ try {
+ await pool.end();
+ console.log("PostgreSQL-Verbindung erfolgreich beendet.");
+ } catch (error) {
+ console.error("Fehler beim Beenden der PostgreSQL-Verbindung: ", error);
+ }
+}
+
+export default pool;
diff --git a/backend/src/database/redis.ts b/backend/src/database/redis.ts
new file mode 100644
index 0000000..06773b9
--- /dev/null
+++ b/backend/src/database/redis.ts
@@ -0,0 +1,49 @@
+import { createClient } from "redis";
+
+const redisClient = createClient({
+ username: process.env.REDIS_USER,
+ password: process.env.REDIS_PASSWORD,
+ socket: {
+ host: process.env.REDIS_HOST,
+ port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : 13080,
+ reconnectStrategy: (retries) => {
+ if (retries > 10) {
+ console.error("Redis Verbindunsversuche erschöpft.");
+ return new Error(
+ "Redis Verbindung nach 10 Versuchen Fehlgeschlagen."
+ );
+ }
+ console.log(`Redis Verbindungsversuch ${retries} von 10`);
+ return 3000;
+ },
+ },
+});
+
+redisClient.on("error", (error) => {
+ console.error("Redis Fehler: ", error);
+});
+
+async function connectRedis() {
+ try {
+ await redisClient.connect();
+ console.log("Erfolgreich mit Redis verbunden.");
+ } catch (error) {
+ // Server läuft ohne Cache weiter
+ console.error("Verbindung zu Redis fehlgeschlagen: ", error);
+ }
+}
+
+await connectRedis();
+
+export async function closeRedisConnection() {
+ try {
+ if (redisClient.isOpen) {
+ await redisClient.quit();
+ console.log("Redis-Verbindung erfolgreich beendet.");
+ }
+ } catch (error) {
+ console.error("Fehler beim Beenden der Redis-Verbindung: ", error);
+ }
+}
+
+export default redisClient;
diff --git a/backend/src/middleware/authCheck.ts b/backend/src/middleware/authCheck.ts
new file mode 100644
index 0000000..4ee0806
--- /dev/null
+++ b/backend/src/middleware/authCheck.ts
@@ -0,0 +1,36 @@
+import { NextFunction, Request, Response } from "express";
+import jwt from "jsonwebtoken";
+import type { AuthorizedUser } from "../types/user.js";
+
+async function authenticateUser(
+ req: Request,
+ res: Response,
+ next: NextFunction
+) {
+ const authHeader = req.headers.authorization;
+
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
+ return res.status(401).json({
+ message: "User authentication failed.",
+ });
+ }
+
+ const token = authHeader.split(" ")[1];
+
+ try {
+ const authorizedUser = jwt.verify(
+ token,
+ process.env.JWT_SECRET!
+ ) as AuthorizedUser;
+
+ req.user = authorizedUser;
+ next();
+ } catch (error) {
+ console.error("User authentication failed: ", error);
+ return res.status(403).json({
+ message: "User authentication failed.",
+ });
+ }
+}
+
+export default authenticateUser;
diff --git a/backend/src/routes/router.ts b/backend/src/routes/router.ts
new file mode 100644
index 0000000..325b08a
--- /dev/null
+++ b/backend/src/routes/router.ts
@@ -0,0 +1,12 @@
+import { Router } from "express";
+import generateAdmin from "../controllers/generateAdmin.js";
+import loginUser from "../controllers/loginUser.js";
+
+const router = Router();
+
+// One-time generation of admin user with pre-defined email & password
+router.get("/admin-generation", generateAdmin);
+// Login for registered users
+router.post("/auth/login", loginUser);
+
+export default router;
diff --git a/backend/src/server.ts b/backend/src/server.ts
new file mode 100644
index 0000000..d2df403
--- /dev/null
+++ b/backend/src/server.ts
@@ -0,0 +1,86 @@
+import cors from "cors";
+import express from "express";
+import http from "http";
+import { Server } from "socket.io";
+import { closeDBConnection } from "./database/postgres.js";
+import { closeRedisConnection } from "./database/redis.js";
+import router from "./routes/router.js";
+
+const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
+
+const app = express();
+const httpServer = http.createServer(app); // for Websocket connection
+
+const io = new Server(httpServer, {
+ cors: {
+ origin: "*",
+ methods: ["GET", "POST"],
+ },
+});
+
+// Global access to Socket.IO server for all controllers
+app.set("io", io);
+
+// Websocket connection handler
+io.on("connection", (socket) => {
+ console.log(`New Client connected: ${socket.id}`);
+ socket.on("disconnect", () => {
+ console.log(`Client disconnected: ${socket.id}`);
+ });
+});
+
+app.use(cors());
+app.use(express.json());
+
+app.use(router);
+
+let isShuttingDown = false;
+async function shutdown(exitCode: number) {
+ // Prevent function from being called repeatedly e.g. from nodemon or node --watch
+ if (isShuttingDown) return;
+
+ isShuttingDown = true;
+ console.log("[SHUTDOWN] Server shutdown initiated...");
+
+ // 1. Closing HTTP/WebSocket-Server
+ httpServer.close(async (error) => {
+ if (error) {
+ console.error("Error closing the HTTP server:", error);
+ } else {
+ console.log("Closed HTTP- and WebSocket server.");
+ }
+
+ // Closing connections to database & Redis
+ await closeDBConnection();
+ await closeRedisConnection();
+
+ // Closing the process
+ console.log("[SHUTDOWN] Process closed.");
+ process.exit(exitCode);
+ });
+
+ // Timeout as final fallback if server is unresponsive for 10 seconds
+ setTimeout(() => {
+ console.error("[SHUTDOWN] Timeout! Forcing shutdown.");
+ process.exit(1);
+ }, 10000);
+}
+
+// Listeners for shutdown signals
+process.on("SIGINT", () => shutdown(0)); // Ctrl+C
+process.on("SIGTERM", () => shutdown(0)); // Kubernetes, Docker, etc.
+
+httpServer.on("error", (error: NodeJS.ErrnoException) => {
+ console.error("Failed to connect to the server...", error);
+ if (error.code === "EADDRINUSE") {
+ console.error(`Port ${port} is already in use.`);
+ }
+ shutdown(1);
+});
+
+httpServer.listen(port, () => {
+ console.log(
+ `The Robot-Tracker API is activ and listening on port ${port}.`
+ );
+ console.log("The WebSocket server is active.");
+});
diff --git a/backend/src/types/database.ts b/backend/src/types/database.ts
new file mode 100644
index 0000000..fff88a1
--- /dev/null
+++ b/backend/src/types/database.ts
@@ -0,0 +1,7 @@
+export interface PostgresError extends Error {
+ code: string;
+ detail?: string;
+ schema?: string;
+ table?: string;
+ constraint?: string;
+}
diff --git a/backend/src/types/express.d.ts b/backend/src/types/express.d.ts
new file mode 100644
index 0000000..7ac6ca0
--- /dev/null
+++ b/backend/src/types/express.d.ts
@@ -0,0 +1,11 @@
+import type { AuthorizedUser } from "./user.js";
+
+declare global {
+ namespace Express {
+ interface Request {
+ user?: AuthorizedUser;
+ }
+ }
+}
+
+export {};
diff --git a/backend/src/types/request.ts b/backend/src/types/request.ts
new file mode 100644
index 0000000..ef80738
--- /dev/null
+++ b/backend/src/types/request.ts
@@ -0,0 +1,4 @@
+export type LoginRequest = {
+ email: string;
+ password: string;
+};
diff --git a/backend/src/types/user.ts b/backend/src/types/user.ts
new file mode 100644
index 0000000..875072c
--- /dev/null
+++ b/backend/src/types/user.ts
@@ -0,0 +1,18 @@
+export type AdminCreationResult = {
+ id: string;
+ email: string;
+ created_at: Date;
+};
+
+export type AuthorizedUser = {
+ id: string;
+ email: string;
+ createdAt: Date;
+};
+
+export type DatabaseUser = {
+ id: string;
+ email: string;
+ password_hash: string;
+ created_at: Date;
+};
diff --git a/backend/src/utils/dbErrorCheck.ts b/backend/src/utils/dbErrorCheck.ts
new file mode 100644
index 0000000..c5c6415
--- /dev/null
+++ b/backend/src/utils/dbErrorCheck.ts
@@ -0,0 +1,16 @@
+import type { PostgresError } from "../types/database.js";
+
+export function isPostgresError(error: unknown): error is PostgresError {
+ return (
+ error instanceof Error &&
+ "code" in error &&
+ typeof (error as PostgresError).code === "string"
+ );
+}
+
+export const PostgresErrorCodes = {
+ UNIQUE_VIOLATION: "23505",
+ FOREIGN_KEY_VIOLATION: "23503",
+ NOT_NULL_VIOLATION: "23502",
+ CHECK_VIOLATION: "23514",
+};