ADD: added basic backend function plus a mockup for a cli interface

This commit is contained in:
2025-12-13 21:44:48 +01:00
parent c6de2481e6
commit a047d57824
21 changed files with 657 additions and 51 deletions

View File

@@ -9,8 +9,11 @@
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"jwt-decode": "^4.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router": "^7.10.1",
"react-router-dom": "^6.30.2",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
@@ -981,6 +984,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@remix-run/router": {
"version": "1.23.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.53",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
@@ -2121,6 +2133,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2761,6 +2786,15 @@
"node": ">=6"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3293,6 +3327,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3310,6 +3345,60 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
"integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.1",
"react-router": "6.30.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-router-dom/node_modules/react-router": {
"version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3377,6 +3466,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -11,8 +11,11 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"jwt-decode": "^4.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router": "^7.10.1",
"react-router-dom": "^6.30.2",
"tailwindcss": "^4.1.18"
},
"devDependencies": {

View File

@@ -1,19 +1,37 @@
import { useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./components/AuthContext";
import ProtectedRoute from "./components/ProtectedRoute";
import Landing from "./pages/Landing";
// import Dashboard from "./pages/Dashboard";
import Dashboard from "./pages/Dashboard"
import LoginModal from "./components/LoginModal";
export default function App() {
const [token, setToken] = useState(localStorage.getItem("token"));
const [showLogin, setShowLogin] = useState(false);
// const [showLogin, setShowLogin] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
if (!token)
return (
<>
<Landing onLogin={() => setShowLogin(true)} />
{showLogin && <LoginModal onSuccess={setToken} />}
</>
<AuthProvider>
<BrowserRouter>
<LoginModal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
<Routes>
<Route path="/" element={<Landing onLogin={() => setModalOpen(true)} />} />
<Route path="/dashboard" element={ <ProtectedRoute><Dashboard /></ProtectedRoute> }
/>
</Routes>
</BrowserRouter>
</AuthProvider>
// <>
// <Landing onLogin={() => setShowLogin(true)} />
// {showLogin && <LoginModal onSuccess={setToken} />}
// </>
);
return <Dashboard />;

View File

@@ -0,0 +1,20 @@
const API_URL = 'http://localhost:8080';
export async function loginUser(email:string, password: string) {
const res = await fetch(`${API_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error('Login fehlgeschlagen');
return res.json(); // { token: string }
}
export async function fetchUserProfile(token: string) {
const res = await fetch(`${API_URL}/profile`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!res.ok) throw new Error('Profil konnte nicht geladen werden');
return res.json(); // { id: number, email: string, name: string }
}

View File

@@ -0,0 +1,67 @@
import { createContext, useState, useContext, useEffect } from "react";
import type { JSX } from 'react';
import { getUserFromToken } from '../utils/jwt';
import { loginUser } from "../api/user";
type AuthUser = { token: string } | null;
type AuthContextType = {
token: string | null;
userId: string | null;
login: (token: string, userId: string, role?: string[]) => void;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: JSX.Element }) => {
const [token, setToken] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [userEmail, setuserEmail] = useState<string | null>(null);
useEffect(() => {
const storedToken = localStorage.getItem('token');
const storedUserId = localStorage.getItem('userId');
if (!storedToken) {
return;
}
if (storedToken && storedUserId) {
setToken(storedToken);
setUserId(storedUserId);
}
const user = getUserFromToken(storedToken);
if (!user) {
logout(); // z.B. localStorage.clear() + navigate("/login")
return;
}
// ⏳ Logout bei Ablauf
const timeout = setTimeout(() => {
logout();
}, user.exp * 1000 - Date.now());
return () => clearTimeout(timeout);
}, []);
const login = (token: string, userId: string, role: string[] = []) => {
setToken(token);
setUserId(userId);
localStorage.setItem('token', token);
localStorage.setItem('userId', userId);
// localStorage.setItem('role', JSON.stringify(role)); // Store array as string
};
const logout = () => {
setToken(null);
setUserId(null);
localStorage.removeItem('token');
localStorage.removeItem('userId');
};
return (
<AuthContext.Provider value={{ token, userId, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);

View File

@@ -1,23 +1,51 @@
import {loginUser} from "../api/user";
export default function LoginModal({ onSuccess }: any) {
const login = async () => {
const res = await fetch("http://localhost:8080/login", { method: "POST" });
const data = await res.json();
localStorage.setItem("token", data.token);
onSuccess(data.token);
};
export default function LoginModal({ isOpen, onSuccess }: any) {
if (!isOpen) return null; // 👈 THIS is the key
return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center">
<div className="bg-white rounded-2xl p-8 w-96 shadow-xl">
<h2 className="text-2xl font-bold mb-6">Login</h2>
<button
onClick={login}
className="w-full bg-indigo-600 text-white py-3 rounded-xl hover:bg-indigo-700"
>
Login as Demo
</button>
</div>
return(
<div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center">
<div className="bg-white rounded-2xl p-8 w-96 shadow-xl">
<h2 className="text-2xl font-bold mb-6">Login</h2>
<form
onSubmit={async (e: any) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const email = fd.get("email");
const password = fd.get("password");
const res = await loginUser(email as string, password as string);
const data = await res.json();
localStorage.setItem("token", data.token);
onSuccess(data.token);
}}
className="space-y-4"
>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full border rounded-xl px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full border rounded-xl px-3 py-2"
/>
<button
type="submit"
className="w-full bg-indigo-600 text-white py-3 rounded-xl hover:bg-indigo-700"
>
Login
</button>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { Navigate } from "react-router-dom";
import { useAuth } from "../components/AuthContext";
import type { JSX } from 'react';
const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
const { token } = useAuth() as { token?: string | null };
return token ? children : <Navigate to="/" />;
};
export default ProtectedRoute;

View File

@@ -0,0 +1,23 @@
// utils/jwt.ts
import { jwtDecode } from 'jwt-decode';
export interface TokenPayload {
userId: string;
email: string;
role: string[];
exp: number;
}
export function getUserFromToken(token: string): TokenPayload | null {
try {
const decoded = jwtDecode<TokenPayload>(token);
if (decoded.exp && decoded.exp < Date.now() / 1000) {
console.warn("Token ist abgelaufen");
return null;
}
return decoded;
} catch (error) {
console.error("Fehler beim Decodieren des Tokens:", error);
return null;
}
}