ADD: added basic backend function plus a mockup for a cli interface
This commit is contained in:
@@ -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