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.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.database import Base, engine, SessionLocal, load_sample_data
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
app.add_middleware(
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(puck.router, prefix="/pucks", tags=["pucks"])
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__":
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 sqlalchemy.orm import Session
import logging
from datetime import datetime, timedelta
from typing import List
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
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)):
dewar_qr_code = event_data.dewar_qr_code
location_qr_code = event_data.location_qr_code
transaction_type = event_data.transaction_type
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
try:
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
if transaction_type == 'incoming':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or already occupied")
slot.occupied = True
log_event(db, dewar.id, slot.id, 'incoming')
if transaction_type == 'incoming':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or already occupied")
slot.occupied = True
log_event(db, dewar.id, slot.id, 'incoming')
elif transaction_type == 'beamline':
log_event(db, dewar.id, None, 'beamline')
elif transaction_type == 'beamline':
log_event(db, dewar.id, None, 'beamline')
elif transaction_type == 'outgoing':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or not slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or not occupied")
slot.occupied = False
log_event(db, dewar.id, slot.id, 'outgoing')
elif transaction_type == 'outgoing':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or not slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or not occupied")
slot.occupied = False
log_event(db, dewar.id, slot.id, 'outgoing')
elif transaction_type == 'release':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or not slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or not occupied")
slot.occupied = False
log_event(db, dewar.id, slot.id, 'released')
elif transaction_type == 'release':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or not slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or not occupied")
slot.occupied = False
log_event(db, dewar.id, slot.id, 'released')
db.commit()
return {"message": "Status updated successfully"}
db.commit()
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):
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.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)):
slots_needing_refill = db.query(SlotModel).filter(SlotModel.needs_refill == True).all()
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);
useEffect(() => {
OpenAPI.BASE = 'http://127.0.0.1:8000';
OpenAPI.BASE = 'https://127.0.0.1:8000';
const getContacts = async () => {
try {

View File

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

View File

@ -9,7 +9,7 @@ import { Grid, Container } from '@mui/material';
// Define props for Shipments View
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;
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';
export default defineConfig({
plugins: [
react(),
],
import path from 'path';
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;
}
const SlotContainer = styled(Box)<{ occupied: boolean, isSelected: boolean }>`
const SlotContainer = styled(Box)<{ isOccupied: boolean, isSelected: boolean }>`
width: 90px;
height: 180px;
margin: 10px;
background-color: ${(props) => (props.occupied ? '#f0f0f0' : '#ffffff')};
border: ${(props) => (props.isSelected ? '3px solid blue' : '2px solid #aaaaaa')};
background-color: ${({ isOccupied }) => (isOccupied ? '#ffebee' : '#e8f5e9')}; /* occupied = light red, free = light green */
border: ${({ isSelected }) => (isSelected ? '3px solid blue' : '2px solid #aaaaaa')};
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
@ -56,14 +56,16 @@ const ClockIcon = styled(AccessTime)`
`;
const Slot: React.FC<SlotProps> = ({ data, onSelect, isSelected }) => {
const { id, occupied, needsRefill, timeUntilRefill } = data;
return (
<SlotContainer occupied={data.occupied} onClick={() => onSelect(data)} isSelected={isSelected}>
<SlotNumber>{data.id}</SlotNumber>
<SlotContainer isOccupied={occupied} onClick={() => onSelect(data)} isSelected={isSelected}>
<SlotNumber>{id}</SlotNumber>
<QrCodeIcon />
{data.occupied && (
{occupied && (
<>
{data.needsRefill && <RefillIcon titleAccess="Needs Refill" />}
<ClockIcon titleAccess={`Time until refill: ${data.timeUntilRefill}`} />
{needsRefill && <RefillIcon titleAccess="Needs Refill" />}
<ClockIcon titleAccess={`Time until refill: ${timeUntilRefill}`} />
</>
)}
</SlotContainer>

View File

@ -96,7 +96,12 @@ const Storage: React.FC<StorageProps> = ({ name, selectedSlot }) => {
<Typography variant="h5">{name} Slots</Typography>
<StorageWrapper>
{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>
{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