**Commit Message:**
Enhance app with active pgroup handling and token updates Added active pgroup state management across the app for user-specific settings. Improved token handling with decoding, saving user data, and setting OpenAPI authorization. Updated components, API calls, and forms to support dynamic pgroup selection and user-specific features.
This commit is contained in:
@ -110,36 +110,56 @@ contacts = [
|
||||
return_addresses = [
|
||||
Address(
|
||||
id=1,
|
||||
street="123 Hobbiton St",
|
||||
city="Shire",
|
||||
pgroups="p20000, p20002",
|
||||
status="active",
|
||||
house_number="123",
|
||||
street="Hobbiton St",
|
||||
city="Hobbitbourg",
|
||||
state="Shire",
|
||||
zipcode="12345",
|
||||
country="Middle Earth",
|
||||
),
|
||||
Address(
|
||||
id=2,
|
||||
street="456 Rohan Rd",
|
||||
pgroups="p20000, p20001",
|
||||
status="active",
|
||||
house_number="456",
|
||||
street="Rohan Rd",
|
||||
city="Edoras",
|
||||
state="Rohan",
|
||||
zipcode="67890",
|
||||
country="Middle Earth",
|
||||
),
|
||||
Address(
|
||||
id=3,
|
||||
street="789 Greenwood Dr",
|
||||
pgroups="p20001, p20002",
|
||||
status="active",
|
||||
house_number="789",
|
||||
street="Greenwood Dr",
|
||||
city="Mirkwood",
|
||||
state="Greenwood",
|
||||
zipcode="13579",
|
||||
country="Middle Earth",
|
||||
),
|
||||
Address(
|
||||
id=4,
|
||||
street="321 Gondor Ave",
|
||||
pgroups="p20001, p20002, p20003",
|
||||
status="active",
|
||||
house_number="321",
|
||||
street="Gondor Ave",
|
||||
city="Minas Tirith",
|
||||
state="Gondor",
|
||||
zipcode="24680",
|
||||
country="Middle Earth",
|
||||
),
|
||||
Address(
|
||||
id=5,
|
||||
street="654 Falgorn Pass",
|
||||
pgroups="p20004, p20005",
|
||||
status="active",
|
||||
house_number="654",
|
||||
street="Falgorn Pass",
|
||||
city="Rivendell",
|
||||
state="Rivendell",
|
||||
zipcode="11223",
|
||||
country="Middle Earth",
|
||||
),
|
||||
@ -234,11 +254,11 @@ dewars = [
|
||||
|
||||
# Define proposals
|
||||
proposals = [
|
||||
Proposal(id=1, number="p20000"),
|
||||
Proposal(id=2, number="p20001"),
|
||||
Proposal(id=3, number="p20002"),
|
||||
Proposal(id=4, number="p20003"),
|
||||
Proposal(id=5, number="p20004"),
|
||||
Proposal(id=1, number="202400125"),
|
||||
Proposal(id=2, number="202400235"),
|
||||
Proposal(id=3, number="202400237"),
|
||||
Proposal(id=4, number="202400336"),
|
||||
Proposal(id=5, number="202400255"),
|
||||
]
|
||||
|
||||
# Define shipment specific dewars
|
||||
|
@ -46,10 +46,14 @@ class Address(Base):
|
||||
__tablename__ = "addresses"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
street = Column(String(255))
|
||||
city = Column(String(255))
|
||||
zipcode = Column(String(255))
|
||||
country = Column(String(255))
|
||||
status = Column(String(255), default="active")
|
||||
pgroups = Column(String(255), nullable=False)
|
||||
street = Column(String(255), nullable=False)
|
||||
house_number = Column(String(255), nullable=True)
|
||||
city = Column(String(255), nullable=False)
|
||||
state = Column(String(255), nullable=True)
|
||||
zipcode = Column(String(255), nullable=False)
|
||||
country = Column(String(255), nullable=False)
|
||||
|
||||
shipments = relationship("Shipment", back_populates="return_address")
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
from .address import router as address_router
|
||||
from .address import protected_router as address_router
|
||||
from .contact import router as contact_router
|
||||
from .proposal import router as proposal_router
|
||||
from .dewar import router as dewar_router
|
||||
from .shipment import router as shipment_router
|
||||
from .auth import router as auth_router
|
||||
from .protected_router import protected_router as protected_router
|
||||
|
||||
__all__ = [
|
||||
"address_router",
|
||||
@ -12,4 +13,5 @@ __all__ = [
|
||||
"dewar_router",
|
||||
"shipment_router",
|
||||
"auth_router",
|
||||
"protected_router",
|
||||
]
|
||||
|
@ -1,20 +1,63 @@
|
||||
from fastapi import APIRouter, HTTPException, status, Depends
|
||||
from fastapi import Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from typing import List
|
||||
from app.schemas import Address as AddressSchema, AddressCreate, AddressUpdate
|
||||
|
||||
from app.routers.auth import get_current_user
|
||||
from app.schemas import (
|
||||
Address as AddressSchema,
|
||||
AddressCreate,
|
||||
AddressUpdate,
|
||||
loginData,
|
||||
)
|
||||
from app.models import Address as AddressModel
|
||||
from app.dependencies import get_db
|
||||
|
||||
router = APIRouter()
|
||||
from app.routers.protected_router import protected_router
|
||||
|
||||
|
||||
@router.get("/", response_model=List[AddressSchema])
|
||||
async def get_return_addresses(db: Session = Depends(get_db)):
|
||||
return db.query(AddressModel).all()
|
||||
@protected_router.get("/", response_model=List[AddressSchema])
|
||||
async def get_return_addresses(
|
||||
active_pgroup: str = Query(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: loginData = Depends(get_current_user),
|
||||
):
|
||||
if active_pgroup not in current_user.pgroups:
|
||||
raise HTTPException(status_code=400, detail="Invalid pgroup provided.")
|
||||
|
||||
# Return only active addresses
|
||||
user_addresses = (
|
||||
db.query(AddressModel)
|
||||
.filter(
|
||||
AddressModel.pgroups.like(f"%{active_pgroup}%"),
|
||||
AddressModel.status == "active",
|
||||
)
|
||||
.all()
|
||||
)
|
||||
return user_addresses
|
||||
|
||||
|
||||
@router.post("/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED)
|
||||
@protected_router.get("/all", response_model=List[AddressSchema])
|
||||
async def get_all_addresses(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: loginData = Depends(get_current_user),
|
||||
):
|
||||
# Fetch all active addresses associated with the user's pgroups
|
||||
user_pgroups = current_user.pgroups
|
||||
filters = [AddressModel.pgroups.like(f"%{pgroup}%") for pgroup in user_pgroups]
|
||||
user_addresses = (
|
||||
db.query(AddressModel)
|
||||
.filter(AddressModel.status == "active", or_(*filters))
|
||||
.all()
|
||||
)
|
||||
return user_addresses
|
||||
|
||||
|
||||
@protected_router.post(
|
||||
"/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def create_return_address(address: AddressCreate, db: Session = Depends(get_db)):
|
||||
print("Payload received by backend:", address.dict()) # Log incoming payload
|
||||
|
||||
if db.query(AddressModel).filter(AddressModel.city == address.city).first():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@ -22,10 +65,14 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge
|
||||
)
|
||||
|
||||
db_address = AddressModel(
|
||||
pgroups=address.pgroups,
|
||||
house_number=address.house_number,
|
||||
street=address.street,
|
||||
city=address.city,
|
||||
state=address.state,
|
||||
zipcode=address.zipcode,
|
||||
country=address.country,
|
||||
status="active",
|
||||
)
|
||||
|
||||
db.add(db_address)
|
||||
@ -34,29 +81,74 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge
|
||||
return db_address
|
||||
|
||||
|
||||
@router.put("/{address_id}", response_model=AddressSchema)
|
||||
@protected_router.put("/{address_id}", response_model=AddressSchema)
|
||||
async def update_return_address(
|
||||
address_id: int, address: AddressUpdate, db: Session = Depends(get_db)
|
||||
):
|
||||
# Retrieve the existing address
|
||||
db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
|
||||
if not db_address:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Address not found."
|
||||
)
|
||||
for key, value in address.dict(exclude_unset=True).items():
|
||||
setattr(db_address, key, value)
|
||||
|
||||
# Normalize existing and new pgroups (remove whitespace, handle case
|
||||
# sensitivity if needed)
|
||||
existing_pgroups = (
|
||||
set(p.strip() for p in db_address.pgroups.split(",") if p.strip())
|
||||
if db_address.pgroups
|
||||
else set()
|
||||
)
|
||||
new_pgroups = (
|
||||
set(p.strip() for p in address.pgroups.split(",") if p.strip())
|
||||
if address.pgroups
|
||||
else set()
|
||||
)
|
||||
|
||||
# Check if any old pgroups are being removed (strict validation against removal)
|
||||
if not new_pgroups.issuperset(existing_pgroups):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Modifying pgroups to remove existing ones is not allowed.",
|
||||
)
|
||||
|
||||
# Combine existing and new pgroups (only additions are allowed)
|
||||
combined_pgroups = existing_pgroups.union(new_pgroups)
|
||||
|
||||
# Mark the current address as obsolete
|
||||
db_address.status = "inactive"
|
||||
db.commit()
|
||||
db.refresh(db_address)
|
||||
return db_address
|
||||
|
||||
# Create a new address with updated values and the combined pgroups
|
||||
new_address = AddressModel(
|
||||
pgroups=",".join(combined_pgroups), # Join set back into comma-separated string
|
||||
house_number=address.house_number or db_address.house_number,
|
||||
street=address.street or db_address.street,
|
||||
city=address.city or db_address.city,
|
||||
state=address.state or db_address.state,
|
||||
zipcode=address.zipcode or db_address.zipcode,
|
||||
country=address.country or db_address.country,
|
||||
status="active", # Newly created address will be active
|
||||
)
|
||||
|
||||
# Save the new address
|
||||
db.add(new_address)
|
||||
db.commit()
|
||||
db.refresh(new_address)
|
||||
|
||||
return new_address
|
||||
|
||||
|
||||
@router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@protected_router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_return_address(address_id: int, db: Session = Depends(get_db)):
|
||||
db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
|
||||
if not db_address:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Address not found."
|
||||
)
|
||||
db.delete(db_address)
|
||||
|
||||
# Mark the address as obsolete instead of deleting it
|
||||
db_address.status = "inactive"
|
||||
db.commit()
|
||||
return
|
||||
|
@ -14,8 +14,13 @@ mock_users_db = {
|
||||
"testuser": {
|
||||
"username": "testuser",
|
||||
"password": "testpass", # In a real scenario, store the hash of the password
|
||||
"pgroups": [20000, 20001, 20003],
|
||||
}
|
||||
"pgroups": ["p20000", "p20001", "p20002", "p20003"],
|
||||
},
|
||||
"testuser2": {
|
||||
"username": "testuser2",
|
||||
"password": "testpass2", # In a real scenario, store the hash of the password
|
||||
"pgroups": ["p20004", "p20005", "p20006"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -39,30 +44,17 @@ def create_access_token(data: dict) -> str:
|
||||
|
||||
|
||||
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)
|
||||
print(f"[DEBUG] Username decoded from token: {username}") # Add debug log here
|
||||
return loginData(username=username, pgroups=payload.get("pgroups"))
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise token_expired_exception
|
||||
print("[DEBUG] Token expired")
|
||||
raise HTTPException(status_code=401, detail="Token expired")
|
||||
except jwt.InvalidTokenError:
|
||||
raise credentials_exception
|
||||
|
||||
return token_data
|
||||
print("[DEBUG] Invalid token")
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
|
||||
@router.post("/token/login", response_model=loginToken)
|
||||
|
7
backend/app/routers/protected_router.py
Normal file
7
backend/app/routers/protected_router.py
Normal file
@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.routers.auth import get_current_user
|
||||
|
||||
protected_router = APIRouter(
|
||||
dependencies=[Depends(get_current_user)] # Applies to all routes
|
||||
)
|
@ -7,7 +7,6 @@ import shutil
|
||||
from app.schemas import (
|
||||
Puck as PuckSchema,
|
||||
Sample as SampleSchema,
|
||||
SampleEventResponse,
|
||||
SampleEventCreate,
|
||||
Sample,
|
||||
)
|
||||
@ -90,106 +89,69 @@ async def create_sample_event(
|
||||
return sample # Return the sample, now including `mount_count`
|
||||
|
||||
|
||||
# Route to fetch the last (most recent) sample event
|
||||
@router.get("/samples/{sample_id}/events/last", response_model=SampleEventResponse)
|
||||
async def get_last_sample_event(sample_id: int, db: Session = Depends(get_db)):
|
||||
# Ensure the sample exists
|
||||
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
|
||||
if not sample:
|
||||
raise HTTPException(status_code=404, detail="Sample not found")
|
||||
|
||||
# Get the most recent event for the sample
|
||||
last_event = (
|
||||
db.query(SampleEventModel)
|
||||
.filter(SampleEventModel.sample_id == sample_id)
|
||||
.order_by(SampleEventModel.timestamp.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not last_event:
|
||||
raise HTTPException(status_code=404, detail="No events found for the sample")
|
||||
|
||||
return SampleEventResponse(
|
||||
id=last_event.id,
|
||||
sample_id=last_event.sample_id,
|
||||
event_type=last_event.event_type,
|
||||
timestamp=last_event.timestamp,
|
||||
) # Response will automatically use the SampleEventResponse schema
|
||||
|
||||
|
||||
@router.post("/samples/{sample_id}/upload-images")
|
||||
async def upload_sample_images(
|
||||
sample_id: int,
|
||||
uploaded_files: List[UploadFile] = File(...), # Accept multiple files
|
||||
uploaded_files: list[UploadFile] = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Uploads images for a sample and stores them in a directory structure:
|
||||
images/user/date/dewar_name/puck_name/position/.
|
||||
logging.info(f"Received files: {[file.filename for file in uploaded_files]}")
|
||||
|
||||
"""
|
||||
Uploads images for a given sample and saves them to a directory structure.
|
||||
Args:
|
||||
sample_id (int): ID of the sample.
|
||||
uploaded_files (List[UploadFile]): List of image files to be uploaded.
|
||||
db (Session): SQLAlchemy database session.
|
||||
uploaded_files (list[UploadFile]): A list of files uploaded with the request.
|
||||
db (Session): Database session.
|
||||
"""
|
||||
# Fetch sample details from the database
|
||||
|
||||
# 1. Validate Sample
|
||||
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
|
||||
if not sample:
|
||||
raise HTTPException(status_code=404, detail="Sample not found")
|
||||
|
||||
# Retrieve associated dewar_name, puck_name and position
|
||||
puck = sample.puck
|
||||
if not puck:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No puck associated with sample ID {sample_id}"
|
||||
)
|
||||
|
||||
dewar_name = puck.dewar.dewar_name if puck.dewar else None
|
||||
if not dewar_name:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No dewar associated with puck ID {puck.id}"
|
||||
)
|
||||
|
||||
puck_name = puck.puck_name
|
||||
position = sample.position
|
||||
|
||||
# Retrieve username (hardcoded for now—can be fetched dynamically if needed)
|
||||
username = "e16371"
|
||||
|
||||
# Today's date in the format YYYY-MM-DD
|
||||
# 2. Define Directory Structure
|
||||
username = "e16371" # Hardcoded username; replace with dynamic logic if applicable
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Generate the directory path based on the structure
|
||||
base_dir = (
|
||||
Path("images") / username / today / dewar_name / puck_name / str(position)
|
||||
dewar_name = (
|
||||
sample.puck.dewar.dewar_name
|
||||
if sample.puck and sample.puck.dewar
|
||||
else "default_dewar"
|
||||
)
|
||||
|
||||
# Create directories if they don't exist
|
||||
puck_name = sample.puck.puck_name if sample.puck else "default_puck"
|
||||
position = sample.position if sample.position else "default_position"
|
||||
base_dir = Path(f"images/{username}/{today}/{dewar_name}/{puck_name}/{position}")
|
||||
base_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save each uploaded image to the directory
|
||||
# 3. Process and Save Each File
|
||||
saved_files = []
|
||||
for file in uploaded_files:
|
||||
# Validate file content type
|
||||
# Validate MIME type
|
||||
if not file.content_type.startswith("image/"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid file type: {file.filename}. Must be an image.",
|
||||
detail=f"Invalid file type: {file.filename}. Only images are accepted.",
|
||||
)
|
||||
|
||||
# Create a file path for storing the uploaded file
|
||||
# Save file to the base directory
|
||||
file_path = base_dir / file.filename
|
||||
|
||||
# Save the file from the file stream
|
||||
try:
|
||||
# Save the file
|
||||
with file_path.open("wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
saved_files.append(str(file_path)) # Track saved file paths
|
||||
except Exception as e:
|
||||
logging.error(f"Error saving file {file.filename}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error saving file {file.filename}: {str(e)}",
|
||||
detail=f"Could not save file {file.filename}."
|
||||
f" Ensure the server has correct permissions.",
|
||||
)
|
||||
|
||||
# 4. Return Saved Files Information
|
||||
logging.info(f"Uploaded {len(saved_files)} files for sample {sample_id}.")
|
||||
return {
|
||||
"message": f"{len(uploaded_files)} images uploaded successfully.",
|
||||
"path": str(base_dir), # Return the base directory for reference
|
||||
"message": f"{len(saved_files)} images uploaded successfully.",
|
||||
"files": saved_files,
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ class loginToken(BaseModel):
|
||||
|
||||
class loginData(BaseModel):
|
||||
username: str
|
||||
pgroups: List[int]
|
||||
pgroups: List[str]
|
||||
|
||||
|
||||
class DewarTypeBase(BaseModel):
|
||||
@ -392,22 +392,29 @@ class ContactPersonUpdate(BaseModel):
|
||||
|
||||
|
||||
class AddressCreate(BaseModel):
|
||||
pgroups: str
|
||||
house_number: Optional[str] = None
|
||||
street: str
|
||||
city: str
|
||||
state: Optional[str] = None
|
||||
zipcode: str
|
||||
country: str
|
||||
|
||||
|
||||
class Address(AddressCreate):
|
||||
id: int
|
||||
status: str = "active"
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AddressUpdate(BaseModel):
|
||||
pgroups: str
|
||||
house_number: Optional[str] = None
|
||||
street: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
zipcode: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
|
||||
|
Reference in New Issue
Block a user