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 (
- + - - PSI logo + + PSI logo + + AARE v0.1 + - - Heidi v2 - - - - - - - {pages.map((page) => ( - - - {page.name} - - - ))} - - - - + {pages.map((page) => ( @@ -138,40 +127,24 @@ const ResponsiveAppBar: React.FC = ({ onOpenAddressManage - + - {userMenuItems.map((item) => - item.action ? ( - handleMenuItemClick(item.action)}> - {item.name} - - ) : ( - item.path && ( - - - {item.name} - - - ) - ) - )} + {userMenuItems.map((item) => ( + handleMenuItemClick(item.action ?? '')}> + {item.name} + + ))} 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 ( +
+
+

Welcome Back!

+ setUsername(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + + {error &&

{error}

} +
+
+ ); +}; + +export default LoginView; \ No newline at end of file