https and ssl integration on the backend, frontend and started integration of logistics app as a separate frontend
This commit is contained in:
parent
0eb0bc3486
commit
a91d74b718
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/frontend/.env.local
|
@ -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)
|
||||
|
@ -1,20 +1,54 @@
|
||||
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
|
||||
|
||||
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")
|
||||
@ -44,13 +78,19 @@ async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get
|
||||
log_event(db, dewar.id, slot.id, 'released')
|
||||
|
||||
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
52
backend/app/ssl_heidi.py
Normal 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))
|
@ -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 {
|
||||
|
@ -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>) => {
|
||||
|
@ -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> = () => {
|
||||
|
@ -1,7 +1,18 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
export default defineConfig({
|
||||
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,
|
||||
}
|
||||
}}
|
||||
});
|
@ -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>
|
||||
|
@ -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
5
requirements.txt
Normal 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
|
Loading…
x
Reference in New Issue
Block a user