added login page and started integrated of security
This commit is contained in:
parent
1798c480f6
commit
0d1374ded7
0
backend/__init__.py
Normal file
0
backend/__init__.py
Normal 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"])
|
||||||
|
@ -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"]
|
82
backend/app/routers/auth.py
Normal file
82
backend/app/routers/auth.py
Normal 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}
|
@ -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">
|
||||||
|
18
frontend/src/components/ProtectedRoute.tsx
Normal file
18
frontend/src/components/ProtectedRoute.tsx
Normal 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;
|
@ -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>
|
||||||
|
123
frontend/src/pages/LoginView.tsx
Normal file
123
frontend/src/pages/LoginView.tsx
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user