https and ssl integration on the backend, frontend and started integration of logistics app as a separate frontend

This commit is contained in:
GotthardG 2024-11-18 14:22:54 +01:00
parent 0eb0bc3486
commit a91d74b718
11 changed files with 176 additions and 50 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/frontend/.env.local

View File

@ -2,12 +2,20 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware 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
from app.database import Base, engine, SessionLocal, load_sample_data from app.database import Base, engine, SessionLocal, load_sample_data
app = FastAPI() app = FastAPI()
# Generate SSL Key and Certificate if not exist
Path("ssl").mkdir(parents=True, exist_ok=True)
if not Path("ssl/cert.pem").exists() or not Path("ssl/key.pem").exists():
ssl_heidi.generate_self_signed_cert("ssl/cert.pem", "ssl/key.pem")
# Apply CORS middleware # Apply CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@ -39,9 +47,11 @@ app.include_router(dewar.router, prefix="/dewars", tags=["dewars"])
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"]) app.include_router(shipment.router, prefix="/shipments", tags=["shipments"])
app.include_router(puck.router, prefix="/pucks", tags=["pucks"]) app.include_router(puck.router, prefix="/pucks", tags=["pucks"])
app.include_router(spreadsheet.router, tags=["spreadsheet"]) app.include_router(spreadsheet.router, tags=["spreadsheet"])
app.include_router(dewar.router, prefix="/logistics", tags=["logistics"]) app.include_router(logistics.router, prefix="/logistics", tags=["logistics"])
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(certfile="ssl/cert.pem", keyfile="ssl/key.pem")
uvicorn.run(app, host="127.0.0.1", port=8000, log_level="debug") uvicorn.run(app, host="127.0.0.1", port=8000, log_level="debug", ssl_context=ssl_context)

View File

@ -1,56 +1,96 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List from typing import List
from app.models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel from app.models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel
from app.schemas import LogisticsEventCreate, SlotCreate, Slot as SlotSchema from app.schemas import LogisticsEventCreate, SlotCreate, Slot as SlotSchema, Dewar as DewarSchema
from app.database import get_db from app.database import get_db
router = APIRouter() router = APIRouter()
@router.post("/scan-dewar") logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@router.get("/dewars", response_model=List[DewarSchema])
async def get_all_prouts(db: Session = Depends(get_db)):
try:
dewars = db.query(DewarModel).all()
logging.info(f"Retrieved {len(dewars)} dewars from the database")
return dewars
except Exception as e:
logger.error(f"An error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/dewar/{qr_code}", response_model=DewarSchema)
async def get_dewar_by_qr_code(qr_code: str, db: Session = Depends(get_db)):
logger.info(f"Received qr_code: {qr_code}")
trimmed_qr_code = qr_code.strip()
logger.info(f"Trimmed qr_code after stripping: {trimmed_qr_code}")
try:
dewar = db.query(DewarModel).filter(DewarModel.unique_id == trimmed_qr_code).first()
logger.info(f"Query Result: {dewar}")
if dewar:
return dewar
else:
logger.error(f"Dewar not found for unique_id: {trimmed_qr_code}")
raise HTTPException(status_code=404, detail="Dewar not found")
except Exception as e:
logger.error(f"An error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/dewar/scan", response_model=LogisticsEventCreate)
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)): async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)):
dewar_qr_code = event_data.dewar_qr_code dewar_qr_code = event_data.dewar_qr_code
location_qr_code = event_data.location_qr_code location_qr_code = event_data.location_qr_code
transaction_type = event_data.transaction_type transaction_type = event_data.transaction_type
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first() try:
if not dewar: dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
raise HTTPException(status_code=404, detail="Dewar not found") if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
if transaction_type == 'incoming': if transaction_type == 'incoming':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first() slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or slot.occupied: if not slot or slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or already occupied") raise HTTPException(status_code=404, detail="Slot not found or already occupied")
slot.occupied = True slot.occupied = True
log_event(db, dewar.id, slot.id, 'incoming') log_event(db, dewar.id, slot.id, 'incoming')
elif transaction_type == 'beamline': elif transaction_type == 'beamline':
log_event(db, dewar.id, None, 'beamline') log_event(db, dewar.id, None, 'beamline')
elif transaction_type == 'outgoing': elif transaction_type == 'outgoing':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first() slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or not slot.occupied: if not slot or not slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or not occupied") raise HTTPException(status_code=404, detail="Slot not found or not occupied")
slot.occupied = False slot.occupied = False
log_event(db, dewar.id, slot.id, 'outgoing') log_event(db, dewar.id, slot.id, 'outgoing')
elif transaction_type == 'release': elif transaction_type == 'release':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first() slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or not slot.occupied: if not slot or not slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or not occupied") raise HTTPException(status_code=404, detail="Slot not found or not occupied")
slot.occupied = False slot.occupied = False
log_event(db, dewar.id, slot.id, 'released') log_event(db, dewar.id, slot.id, 'released')
db.commit() db.commit()
return {"message": "Status updated successfully"} logger.info(f"Status updated successfully for Dewar ID: {dewar.id}")
return {"message": "Status updated successfully"}
except Exception as e:
logger.error(f"An error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
def log_event(db: Session, dewar_id: int, slot_id: int, event_type: str): def log_event(db: Session, dewar_id: int, slot_id: int, event_type: str):
new_event = LogisticsEventModel(dewar_id=dewar_id, slot_id=slot_id, event_type=event_type) new_event = LogisticsEventModel(dewar_id=dewar_id if dewar_id else None, slot_id=slot_id, event_type=event_type)
db.add(new_event) db.add(new_event)
db.commit()
logger.info(f"Logged event {event_type} for Dewar ID: {dewar_id}")
@router.get("/refill-status", response_model=List[SlotSchema]) @router.get("/slots/refill-status", response_model=List[SlotSchema])
async def refill_status(db: Session = Depends(get_db)): async def refill_status(db: Session = Depends(get_db)):
slots_needing_refill = db.query(SlotModel).filter(SlotModel.needs_refill == True).all() slots_needing_refill = db.query(SlotModel).filter(SlotModel.needs_refill == True).all()
result = [] result = []

52
backend/app/ssl_heidi.py Normal file
View File

@ -0,0 +1,52 @@
# Generate SSL Key and Certificate
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import Encoding
import datetime
def generate_self_signed_cert(cert_file: str, key_file: str):
# Generate private key
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# Write private key to file
with open(key_file, "wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
# Generate self-signed certificate
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u"CH"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"Argau"),
x509.NameAttribute(NameOID.LOCALITY_NAME, u"Villigen"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Paul Scherrer Institut"),
x509.NameAttribute(NameOID.COMMON_NAME, u"PSI.CH"),
])
cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
# Our certificate will be valid for 10 days
datetime.datetime.utcnow() + datetime.timedelta(days=10)
).add_extension(
x509.SubjectAlternativeName([x509.DNSName(u"localhost")]),
critical=False,
).sign(key, hashes.SHA256())
# Write certificate to file
with open(cert_file, "wb") as f:
f.write(cert.public_bytes(Encoding.PEM))

View File

@ -39,7 +39,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const [errorMessage, setErrorMessage] = React.useState<string | null>(null); const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
useEffect(() => { useEffect(() => {
OpenAPI.BASE = 'http://127.0.0.1:8000'; OpenAPI.BASE = 'https://127.0.0.1:8000';
const getContacts = async () => { const getContacts = async () => {
try { try {

View File

@ -46,7 +46,7 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose, selectedShip
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
OpenAPI.BASE = 'http://127.0.0.1:8000'; OpenAPI.BASE = 'https://127.0.0.1:8000';
}, []); }, []);
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -9,7 +9,7 @@ import { Grid, Container } from '@mui/material';
// Define props for Shipments View // Define props for Shipments View
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>; type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
const API_BASE_URL = 'http://127.0.0.1:8000'; const API_BASE_URL = 'https://127.0.0.1:8000';
OpenAPI.BASE = API_BASE_URL; OpenAPI.BASE = API_BASE_URL;
const ShipmentView: React.FC<ShipmentViewProps> = () => { const ShipmentView: React.FC<ShipmentViewProps> = () => {

View File

@ -1,7 +1,18 @@
import { defineConfig } from 'vite'; import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
export default defineConfig({ import path from 'path';
plugins: [
react(), export default defineConfig(({ mode }) => {
], const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [
react(),
],
server: {
https: {
key: env.VITE_SSL_KEY_PATH,
cert: env.VITE_SSL_CERT_PATH,
}
}}
}); });

View File

@ -16,12 +16,12 @@ export interface SlotData {
timeUntilRefill: string; timeUntilRefill: string;
} }
const SlotContainer = styled(Box)<{ occupied: boolean, isSelected: boolean }>` const SlotContainer = styled(Box)<{ isOccupied: boolean, isSelected: boolean }>`
width: 90px; width: 90px;
height: 180px; height: 180px;
margin: 10px; margin: 10px;
background-color: ${(props) => (props.occupied ? '#f0f0f0' : '#ffffff')}; background-color: ${({ isOccupied }) => (isOccupied ? '#ffebee' : '#e8f5e9')}; /* occupied = light red, free = light green */
border: ${(props) => (props.isSelected ? '3px solid blue' : '2px solid #aaaaaa')}; border: ${({ isSelected }) => (isSelected ? '3px solid blue' : '2px solid #aaaaaa')};
border-radius: 5px; border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex; display: flex;
@ -56,14 +56,16 @@ const ClockIcon = styled(AccessTime)`
`; `;
const Slot: React.FC<SlotProps> = ({ data, onSelect, isSelected }) => { const Slot: React.FC<SlotProps> = ({ data, onSelect, isSelected }) => {
const { id, occupied, needsRefill, timeUntilRefill } = data;
return ( return (
<SlotContainer occupied={data.occupied} onClick={() => onSelect(data)} isSelected={isSelected}> <SlotContainer isOccupied={occupied} onClick={() => onSelect(data)} isSelected={isSelected}>
<SlotNumber>{data.id}</SlotNumber> <SlotNumber>{id}</SlotNumber>
<QrCodeIcon /> <QrCodeIcon />
{data.occupied && ( {occupied && (
<> <>
{data.needsRefill && <RefillIcon titleAccess="Needs Refill" />} {needsRefill && <RefillIcon titleAccess="Needs Refill" />}
<ClockIcon titleAccess={`Time until refill: ${data.timeUntilRefill}`} /> <ClockIcon titleAccess={`Time until refill: ${timeUntilRefill}`} />
</> </>
)} )}
</SlotContainer> </SlotContainer>

View File

@ -96,7 +96,12 @@ const Storage: React.FC<StorageProps> = ({ name, selectedSlot }) => {
<Typography variant="h5">{name} Slots</Typography> <Typography variant="h5">{name} Slots</Typography>
<StorageWrapper> <StorageWrapper>
{storageSlotsData[name].map((slot) => ( {storageSlotsData[name].map((slot) => (
<Slot key={slot.id} data={slot} onSelect={handleSlotSelect} isSelected={selectedSlot === slot.id} /> <Slot
key={slot.id}
data={slot}
onSelect={handleSlotSelect}
isSelected={selectedSlot === slot.id}
/>
))} ))}
</StorageWrapper> </StorageWrapper>
{highlightedSlot && ( {highlightedSlot && (

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
SQLAlchemy~=2.0.36
fastapi~=0.115.4
pydantic~=2.9.2
openpyxl~=3.1.5
typing_extensions~=4.12.2