From 0d1374ded76a437b2de1ff1b70a0247cb9cfde4f Mon Sep 17 00:00:00 2001
From: GotthardG <51994228+GotthardG@users.noreply.github.com>
Date: Tue, 3 Dec 2024 23:01:38 +0100
Subject: [PATCH] added login page and started integrated of security
---
backend/__init__.py | 0
backend/app/main.py | 4 +-
backend/app/routers/__init__.py | 3 +-
backend/app/routers/auth.py | 82 ++++++++++
frontend/src/App.tsx | 14 +-
frontend/src/components/ProtectedRoute.tsx | 18 +++
frontend/src/components/ResponsiveAppBar.tsx | 159 ++++++++-----------
frontend/src/pages/LoginView.tsx | 123 ++++++++++++++
8 files changed, 302 insertions(+), 101 deletions(-)
create mode 100644 backend/__init__.py
create mode 100644 backend/app/routers/auth.py
create mode 100644 frontend/src/components/ProtectedRoute.tsx
create mode 100644 frontend/src/pages/LoginView.tsx
diff --git a/backend/__init__.py b/backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/main.py b/backend/app/main.py
index 1e81c49..98c9913 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -5,8 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
from app import ssl_heidi
from pathlib import Path
-
-from app.routers import address, contact, proposal, dewar, shipment, puck, spreadsheet, logistics
+from app.routers import address, contact, proposal, dewar, shipment, puck, spreadsheet, logistics, auth
from app.database import Base, engine, SessionLocal, load_sample_data
app = FastAPI()
@@ -40,6 +39,7 @@ def on_startup():
# Include routers with correct configuration
+app.include_router(auth.router, prefix="/auth", tags=["auth"])
app.include_router(contact.router, prefix="/contacts", tags=["contacts"])
app.include_router(address.router, prefix="/addresses", tags=["addresses"])
app.include_router(proposal.router, prefix="/proposals", tags=["proposals"])
diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py
index b6653e3..9c86676 100644
--- a/backend/app/routers/__init__.py
+++ b/backend/app/routers/__init__.py
@@ -3,5 +3,6 @@ from .contact import router as contact_router
from .proposal import router as proposal_router
from .dewar import router as dewar_router
from .shipment import router as shipment_router
+from .auth import router as auth_router
-__all__ = ["address_router", "contact_router", "proposal_router", "dewar_router", "shipment_router"]
\ No newline at end of file
+__all__ = ["address_router", "contact_router", "proposal_router", "dewar_router", "shipment_router", "auth_router"]
\ No newline at end of file
diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py
new file mode 100644
index 0000000..4d46a0b
--- /dev/null
+++ b/backend/app/routers/auth.py
@@ -0,0 +1,82 @@
+from fastapi import APIRouter, HTTPException, status, Depends
+from fastapi.security import OAuth2PasswordRequestForm
+from fastapi.security import OAuth2AuthorizationCodeBearer
+from app.schemas import loginToken, loginData
+
+import jwt
+from datetime import datetime, timedelta, timezone
+
+# Define an APIRouter for authentication
+router = APIRouter()
+
+
+mock_users_db = {
+ "testuser": {
+ "username": "testuser",
+ "password": "testpass", # In a real scenario, store the hash of the password
+ "pgroups": [20000, 20001, 20003],
+ }
+}
+
+
+# https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/#hash-and-verify-the-passwords
+# SECRET_KEY taken from FastAPI documentation, so not that secret :D
+# openssl rand -hex 32
+SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = 30
+
+oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl="/login", tokenUrl="/token/login")
+
+def create_access_token(data: dict) -> str:
+ to_encode = data.copy()
+ expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ to_encode.update({"exp": expire})
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+
+async def get_current_user(token: str = Depends(oauth2_scheme)) -> loginData:
+ credentials_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ token_expired_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Token expired",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ try:
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+ username: str = payload.get("sub")
+ pgroups = payload.get("pgroups")
+
+ if username is None:
+ raise credentials_exception
+ token_data = loginData(username=username, pgroups=pgroups)
+ except jwt.ExpiredSignatureError:
+ raise token_expired_exception
+ except jwt.InvalidTokenError:
+ raise credentials_exception
+
+ return token_data
+
+@router.post("/token/login", response_model=loginToken)
+async def login(form_data: OAuth2PasswordRequestForm = Depends()):
+ user = mock_users_db.get(form_data.username)
+ if user is None or user["password"] != form_data.password:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect username or password",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ # Create token
+ access_token = create_access_token(
+ data={"sub": user["username"], "pgroups": user["pgroups"]}
+ )
+ return loginToken(access_token=access_token, token_type="bearer")
+
+
+@router.get("/protected-route")
+async def read_protected_data(current_user: loginData = Depends(get_current_user)):
+ return {"username": current_user.username, "pgroups": current_user.pgroups}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index fd8778d..8ec2c00 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
-import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+
import ResponsiveAppBar from './components/ResponsiveAppBar';
import ShipmentView from './pages/ShipmentView';
import HomePage from './pages/HomeView';
@@ -8,6 +9,8 @@ import PlanningView from './pages/PlanningView';
import Modal from './components/Modal';
import AddressManager from './pages/AddressManagerView';
import ContactsManager from './pages/ContactsManagerView';
+import LoginView from './pages/LoginView';
+import ProtectedRoute from './components/ProtectedRoute';
const App: React.FC = () => {
const [openAddressManager, setOpenAddressManager] = useState(false);
@@ -36,10 +39,11 @@ const App: React.FC = () => {
onOpenContactsManager={handleOpenContactsManager}
/>
- } />
- } />
- } />
- } />
+ } />
+ } />} />
+ } />} />
+ } />} />
+ } />} />
{/* Other routes as necessary */}
diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx
new file mode 100644
index 0000000..1de22da
--- /dev/null
+++ b/frontend/src/components/ProtectedRoute.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Navigate } from 'react-router-dom';
+
+interface ProtectedRouteProps {
+ element: JSX.Element;
+}
+
+const ProtectedRoute: React.FC = ({ element }) => {
+ const isAuthenticated = () => {
+ const token = localStorage.getItem('token');
+ console.log("Is Authenticated: ", token !== null);
+ return token !== null;
+ };
+
+ return isAuthenticated() ? element : ;
+};
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/frontend/src/components/ResponsiveAppBar.tsx b/frontend/src/components/ResponsiveAppBar.tsx
index f71d2b0..a2aefc0 100644
--- a/frontend/src/components/ResponsiveAppBar.tsx
+++ b/frontend/src/components/ResponsiveAppBar.tsx
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
@@ -20,24 +21,26 @@ interface ResponsiveAppBarProps {
onOpenContactsManager: () => void;
}
-const pages = [
- { name: 'Home', path: '/' },
- { name: 'Shipments', path: '/shipments' },
- { name: 'Planning', path: '/planning' },
- { name: 'Results', path: '/results' }
-];
-
-const userMenuItems = [
- { name: 'My Contacts', action: 'contacts' },
- { name: 'My Addresses', action: 'addresses' },
- { name: 'DUO', path: '/duo' },
- { name: 'Logout', path: '/logout' }
-];
-
const ResponsiveAppBar: React.FC = ({ onOpenAddressManager, onOpenContactsManager }) => {
+ const navigate = useNavigate();
+ const location = useLocation();
const [anchorElNav, setAnchorElNav] = useState(null);
const [anchorElUser, setAnchorElUser] = useState(null);
+ const pages = [
+ { name: 'Home', path: '/' },
+ { name: 'Shipments', path: '/shipments' },
+ { name: 'Planning', path: '/planning' },
+ { name: 'Results', path: '/results' }
+ ];
+
+ const userMenuItems = [
+ { name: 'My Contacts', action: 'contacts' },
+ { name: 'My Addresses', action: 'addresses' },
+ { name: 'DUO', path: '/duo' },
+ { name: 'Logout', action: 'logout' }
+ ];
+
const handleOpenNavMenu = (event: React.MouseEvent) => {
setAnchorElNav(event.currentTarget);
};
@@ -54,81 +57,67 @@ const ResponsiveAppBar: React.FC = ({ onOpenAddressManage
setAnchorElUser(null);
};
+ const handleLogout = () => {
+ console.log("Performing logout...");
+ localStorage.removeItem('token');
+ navigate('/login');
+ };
+
const handleMenuItemClick = (action: string) => {
if (action === 'contacts') {
onOpenContactsManager();
} else if (action === 'addresses') {
onOpenAddressManager();
+ } else if (action === 'logout') {
+ handleLogout();
}
handleCloseUserMenu();
};
return (
-
+
-
-
+
+
+
+ AARE v0.1
+
-
- Heidi v2
-
-
-
-
-
-
-
-
-
+
{pages.map((page) => (
@@ -138,40 +127,24 @@ const ResponsiveAppBar: React.FC = ({ onOpenAddressManage
-
+
diff --git a/frontend/src/pages/LoginView.tsx b/frontend/src/pages/LoginView.tsx
new file mode 100644
index 0000000..90da88b
--- /dev/null
+++ b/frontend/src/pages/LoginView.tsx
@@ -0,0 +1,123 @@
+// LoginView.tsx
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { AuthService } from '../../openapi'; // Adjust import path
+
+const containerStyle = {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '100vh',
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+};
+
+const cardStyle = {
+ padding: '50px',
+ width: '350px',
+ backgroundColor: '#fff',
+ boxShadow: '0 15px 30px rgba(0, 0, 0, 0.1)',
+ borderRadius: '8px',
+ textAlign: 'center' as const,
+ animation: 'fadeIn 1s ease-in-out',
+};
+
+const inputStyle = {
+ width: '100%',
+ padding: '15px',
+ margin: '10px 0',
+ borderRadius: '30px',
+ border: '1px solid #ddd',
+ fontSize: '16px',
+ transition: 'border-color 0.3s ease',
+};
+
+const buttonStyle = {
+ width: '100%',
+ padding: '15px',
+ margin: '20px 0',
+ borderRadius: '30px',
+ backgroundColor: '#764ba2',
+ color: '#fff',
+ border: 'none',
+ fontSize: '16px',
+ cursor: 'pointer',
+ transition: 'background 0.3s ease',
+};
+
+const errorStyle = {
+ color: 'red',
+ marginTop: '20px',
+ fontWeight: 'bold',
+};
+
+const LoginView: React.FC = () => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const navigate = useNavigate();
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ const response = await AuthService.loginAuthTokenLoginPost({
+ grant_type: 'password',
+ username: username,
+ password: password,
+ });
+
+ localStorage.setItem('token', response.access_token);
+ navigate('/'); // Redirect post-login
+ } catch (err) {
+ setError('Login failed. Please check your credentials.');
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default LoginView;
\ No newline at end of file