ADD: added basic backend function plus a mockup for a cli interface
This commit is contained in:
95
frontend/studia/package-lock.json
generated
95
frontend/studia/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
20
frontend/studia/src/api/user.tsx
Normal file
20
frontend/studia/src/api/user.tsx
Normal 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 }
|
||||
}
|
||||
67
frontend/studia/src/components/AuthContext.tsx
Normal file
67
frontend/studia/src/components/AuthContext.tsx
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
10
frontend/studia/src/components/ProtectedRoute.tsx
Normal file
10
frontend/studia/src/components/ProtectedRoute.tsx
Normal 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;
|
||||
23
frontend/studia/src/utils/jwt.tsx
Normal file
23
frontend/studia/src/utils/jwt.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user