added login page and started integrated of security

This commit is contained in:
GotthardG 2024-12-03 23:01:38 +01:00
parent 1798c480f6
commit 0d1374ded7
8 changed files with 302 additions and 101 deletions

0
backend/__init__.py Normal file
View File

View File

@ -5,8 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
from app import ssl_heidi from app import ssl_heidi
from pathlib import Path from pathlib import Path
from app.routers import address, contact, proposal, dewar, shipment, puck, spreadsheet, logistics, auth
from app.routers import address, contact, proposal, dewar, shipment, puck, spreadsheet, logistics
from app.database import Base, engine, SessionLocal, load_sample_data from app.database import Base, engine, SessionLocal, load_sample_data
app = FastAPI() app = FastAPI()
@ -40,6 +39,7 @@ def on_startup():
# Include routers with correct configuration # 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(contact.router, prefix="/contacts", tags=["contacts"])
app.include_router(address.router, prefix="/addresses", tags=["addresses"]) app.include_router(address.router, prefix="/addresses", tags=["addresses"])
app.include_router(proposal.router, prefix="/proposals", tags=["proposals"]) app.include_router(proposal.router, prefix="/proposals", tags=["proposals"])

View File

@ -3,5 +3,6 @@ from .contact import router as contact_router
from .proposal import router as proposal_router from .proposal import router as proposal_router
from .dewar import router as dewar_router from .dewar import router as dewar_router
from .shipment import router as shipment_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"] __all__ = ["address_router", "contact_router", "proposal_router", "dewar_router", "shipment_router", "auth_router"]

View File

@ -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}

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react'; 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 ResponsiveAppBar from './components/ResponsiveAppBar';
import ShipmentView from './pages/ShipmentView'; import ShipmentView from './pages/ShipmentView';
import HomePage from './pages/HomeView'; import HomePage from './pages/HomeView';
@ -8,6 +9,8 @@ import PlanningView from './pages/PlanningView';
import Modal from './components/Modal'; import Modal from './components/Modal';
import AddressManager from './pages/AddressManagerView'; import AddressManager from './pages/AddressManagerView';
import ContactsManager from './pages/ContactsManagerView'; import ContactsManager from './pages/ContactsManagerView';
import LoginView from './pages/LoginView';
import ProtectedRoute from './components/ProtectedRoute';
const App: React.FC = () => { const App: React.FC = () => {
const [openAddressManager, setOpenAddressManager] = useState(false); const [openAddressManager, setOpenAddressManager] = useState(false);
@ -36,10 +39,11 @@ const App: React.FC = () => {
onOpenContactsManager={handleOpenContactsManager} onOpenContactsManager={handleOpenContactsManager}
/> />
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/login" element={<LoginView />} />
<Route path="/shipments" element={<ShipmentView />} /> <Route path="/" element={<ProtectedRoute element={<HomePage />} />} />
<Route path="/planning" element={<PlanningView />} /> <Route path="/shipments" element={<ProtectedRoute element={<ShipmentView />} />} />
<Route path="/results" element={<ResultsView />} /> <Route path="/planning" element={<ProtectedRoute element={<PlanningView />} />} />
<Route path="/results" element={<ProtectedRoute element={<ResultsView />} />} />
{/* Other routes as necessary */} {/* Other routes as necessary */}
</Routes> </Routes>
<Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management"> <Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management">

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
interface ProtectedRouteProps {
element: JSX.Element;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ element }) => {
const isAuthenticated = () => {
const token = localStorage.getItem('token');
console.log("Is Authenticated: ", token !== null);
return token !== null;
};
return isAuthenticated() ? element : <Navigate to="/login" />;
};
export default ProtectedRoute;

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import AppBar from '@mui/material/AppBar'; import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
@ -20,24 +21,26 @@ interface ResponsiveAppBarProps {
onOpenContactsManager: () => void; 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<ResponsiveAppBarProps> = ({ onOpenAddressManager, onOpenContactsManager }) => { const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManager, onOpenContactsManager }) => {
const navigate = useNavigate();
const location = useLocation();
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null); const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null);
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null); const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(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<HTMLElement>) => { const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget); setAnchorElNav(event.currentTarget);
}; };
@ -54,81 +57,67 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
setAnchorElUser(null); setAnchorElUser(null);
}; };
const handleLogout = () => {
console.log("Performing logout...");
localStorage.removeItem('token');
navigate('/login');
};
const handleMenuItemClick = (action: string) => { const handleMenuItemClick = (action: string) => {
if (action === 'contacts') { if (action === 'contacts') {
onOpenContactsManager(); onOpenContactsManager();
} else if (action === 'addresses') { } else if (action === 'addresses') {
onOpenAddressManager(); onOpenAddressManager();
} else if (action === 'logout') {
handleLogout();
} }
handleCloseUserMenu(); handleCloseUserMenu();
}; };
return ( return (
<div> <div>
<AppBar position="static" sx={{ backgroundColor: '#2F4858' }}> <AppBar position="static" sx={{ backgroundColor: '#324A5F' }}>
<Container maxWidth="xl"> <Container maxWidth="xl">
<Toolbar disableGutters> <Toolbar disableGutters>
<Link to="/" style={{ textDecoration: 'none' }}> <Link to="/" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center' }}>
<img src={logo} height="50px" alt="PSI logo" /> <img src={logo} height="40px" alt="PSI logo" style={{ marginRight: 12 }}/>
<Typography
variant="h6"
noWrap
sx={{
display: 'flex',
fontFamily: 'Roboto, sans-serif',
fontWeight: 700,
color: 'inherit',
textDecoration: 'none',
transition: 'color 0.3s',
'&:hover': {
color: '#f0db4f',
},
}}
>
AARE v0.1
</Typography>
</Link> </Link>
<Typography
variant="h6"
noWrap
component="a"
href="#app-bar-with-responsive-menu"
sx={{
mr: 2,
display: { xs: 'none', md: 'flex' },
fontFamily: 'monospace',
fontWeight: 300,
color: 'inherit',
textDecoration: 'none',
}}
>
Heidi v2
</Typography>
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}> <Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' }, justifyContent: 'center' }}>
<IconButton
size="large"
aria-label="menu"
onClick={handleOpenNavMenu}
color="inherit"
>
<MenuIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
open={Boolean(anchorElNav)}
onClose={handleCloseNavMenu}
>
{pages.map((page) => (
<MenuItem key={page.name} onClick={handleCloseNavMenu}>
<Link to={page.path} style={{ textDecoration: 'none', color: 'inherit' }}>
{page.name}
</Link>
</MenuItem>
))}
</Menu>
</Box>
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
{pages.map((page) => ( {pages.map((page) => (
<Button <Button
key={page.name} key={page.name}
component={Link} component={Link}
to={page.path} to={page.path}
sx={{ my: 2, color: 'white', display: 'block', fontSize: '1rem', padding: '12px 24px' }} sx={{
mx: 2,
color: page.path === location.pathname ? '#f0db4f' : 'white',
fontWeight: page.path === location.pathname ? 700 : 500,
textTransform: 'none',
fontSize: '1rem',
transition: 'color 0.3s, background-color 0.3s',
'&:hover': {
color: '#f0db4f',
backgroundColor: 'rgba(255,255,255,0.1)',
},
}}
> >
{page.name} {page.name}
</Button> </Button>
@ -138,40 +127,24 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
<Box sx={{ flexGrow: 0 }}> <Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings"> <Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar /> <Avatar sx={{ bgcolor: 'yellowgreen' }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Menu <Menu
sx={{ mt: '45px' }} sx={{ mt: '45px' }}
id="menu-appbar" id="menu-appbar"
anchorEl={anchorElUser} anchorEl={anchorElUser}
anchorOrigin={{ anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
vertical: 'top',
horizontal: 'right',
}}
keepMounted keepMounted
transformOrigin={{ transformOrigin={{ vertical: 'top', horizontal: 'right' }}
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorElUser)} open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu} onClose={handleCloseUserMenu}
> >
{userMenuItems.map((item) => {userMenuItems.map((item) => (
item.action ? ( <MenuItem key={item.name} onClick={() => handleMenuItemClick(item.action ?? '')}>
<MenuItem key={item.name} onClick={() => handleMenuItemClick(item.action)}> {item.name}
{item.name} </MenuItem>
</MenuItem> ))}
) : (
item.path && (
<MenuItem key={item.name} onClick={handleCloseUserMenu}>
<Link to={item.path} style={{ textDecoration: 'none', color: 'inherit' }}>
{item.name}
</Link>
</MenuItem>
)
)
)}
</Menu> </Menu>
</Box> </Box>
</Toolbar> </Toolbar>

View File

@ -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 (
<div style={containerStyle}>
<form style={cardStyle} onSubmit={handleLogin}>
<h2>Welcome Back!</h2>
<input
style={{
...inputStyle,
borderColor: username ? '#764ba2' : '#ddd',
}}
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<input
style={{
...inputStyle,
borderColor: password ? '#764ba2' : '#ddd',
}}
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button
style={{
...buttonStyle,
backgroundColor: '#764ba2',
}}
type="submit"
onMouseOver={(e) =>
(e.currentTarget.style.backgroundColor = '#6a3a89')
}
onMouseOut={(e) =>
(e.currentTarget.style.backgroundColor = '#764ba2')
}
>
Login
</button>
{error && <p style={errorStyle}>{error}</p>}
</form>
</div>
);
};
export default LoginView;