feat(api,ui,db): add address, guest users, image URLs; update API
- Backend:
- Add address column to tickets and migration script
- Create guest users when user_id is missing; accept user_name and address
- Normalize stored image paths and expose absolute image_url
- Introduce utils for path normalization and ticket serialization
- Add CORS configuration for dashboard/emulator origins
- Tickets API:
- Serialize via ticket_to_dict with consistent schema
- Change status update to PATCH /api/tickets/{id}/status with JSON body
- Add DELETE /api/tickets/{id} with safe file removal
- Dashboard:
- Fetch tickets from backend, show thumbnails, absolute image URLs
- Status select + PATCH updates, toasts for feedback
- Add i18n key btn.viewDetails
- Mobile app:
- Persist device user_id via SharedPreferences
- Fetch and merge API tickets; prefer network imageUrl
- Submit user_name and address; delete via API when available
- Make location acquisition robust with fallbacks and non-blocking UX
- Android/deps:
- Disable Geolocator NMEA listener to prevent crashes
- Downgrade geolocator to ^11.0.0 for stability
BREAKING CHANGE:
- Status endpoint changed from PATCH /api/tickets/{id}?new_status=... to
PATCH /api/tickets/{id}/status with JSON body: {"status":"in_progress"}.
- /api/tickets and /api/tickets/{id} responses now use "id" (replacing
"ticket_id"), include "image_url", and normalize fields for clients. Update
consumers to use the new schema.
This commit is contained in:
Binary file not shown.
@@ -46,6 +46,7 @@ class Ticket(Base):
|
||||
category = Column(String, nullable=False)
|
||||
severity = Column(Enum(SeverityLevel), nullable=False, default=SeverityLevel.NA)
|
||||
description = Column(String, default="")
|
||||
address = Column(String, nullable=True)
|
||||
status = Column(Enum(TicketStatus), nullable=False, default=TicketStatus.NEW)
|
||||
latitude = Column(Float, nullable=False)
|
||||
longitude = Column(Float, nullable=False)
|
||||
|
||||
@@ -1,37 +1,53 @@
|
||||
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.services.ticket_service import TicketService, SeverityLevel
|
||||
from app.models.ticket_model import User
|
||||
from app.services.global_ai import get_ai_service
|
||||
import os, uuid, logging
|
||||
from app.utils import make_image_url, normalize_image_path_for_url
|
||||
from pathlib import Path
|
||||
import logging, uuid
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
UPLOAD_DIR = "static/uploads"
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
UPLOAD_DIR = Path("static") / "uploads"
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@router.post("/report")
|
||||
async def report_issue(
|
||||
user_id: str = Form(...),
|
||||
user_id: Optional[str] = Form(None),
|
||||
user_name: Optional[str] = Form(None),
|
||||
latitude: float = Form(...),
|
||||
longitude: float = Form(...),
|
||||
address: Optional[str] = Form(None),
|
||||
description: str = Form(""),
|
||||
image: UploadFile = File(...),
|
||||
request: Request = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
logger.debug("Received report request")
|
||||
ticket_service = TicketService(db)
|
||||
|
||||
# Validate user
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
# Validate or create user
|
||||
user = None
|
||||
if user_id:
|
||||
user = ticket_service.get_user(user_id)
|
||||
if not user:
|
||||
logger.error(f"User with id {user_id} not found")
|
||||
raise HTTPException(status_code=404, detail=f"User with id {user_id} not found")
|
||||
logger.debug(f"User found: {user.name} ({user.email})")
|
||||
# Create a guest user automatically
|
||||
guest_email = f"guest-{uuid.uuid4()}@example.local"
|
||||
guest_name = user_name or f"Guest-{str(uuid.uuid4())[:8]}"
|
||||
try:
|
||||
user = ticket_service.create_user(name=guest_name, email=guest_email)
|
||||
logger.info(f"Created guest user: {user}")
|
||||
except Exception as e:
|
||||
logger.exception("Failed to create guest user")
|
||||
raise HTTPException(status_code=500, detail="Failed to ensure user")
|
||||
|
||||
logger.debug(f"Using user: {user.name} ({user.email})")
|
||||
|
||||
# Validate file type
|
||||
allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
|
||||
@@ -40,7 +56,7 @@ async def report_issue(
|
||||
'application/octet-stream' # Some cameras/mobile devices use this
|
||||
}
|
||||
|
||||
file_ext = os.path.splitext(image.filename.lower())[1]
|
||||
file_ext = Path(image.filename).suffix.lower()
|
||||
if file_ext not in allowed_extensions:
|
||||
logger.error(f"Invalid file extension: {file_ext}")
|
||||
raise HTTPException(status_code=400, detail="Only image files are allowed")
|
||||
@@ -51,12 +67,11 @@ async def report_issue(
|
||||
|
||||
# Save uploaded image
|
||||
filename = f"{uuid.uuid4()}{file_ext}"
|
||||
file_path = os.path.join(UPLOAD_DIR, filename)
|
||||
file_path_obj = UPLOAD_DIR / filename
|
||||
try:
|
||||
content = await image.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
logger.debug(f"Saved image to {file_path} ({len(content)} bytes)")
|
||||
file_path_obj.write_bytes(content)
|
||||
logger.debug(f"Saved image to {file_path_obj} ({len(content)} bytes)")
|
||||
except Exception as e:
|
||||
logger.exception("Failed to save uploaded image")
|
||||
raise HTTPException(status_code=500, detail="Failed to save uploaded image")
|
||||
@@ -67,11 +82,11 @@ async def report_issue(
|
||||
|
||||
# Run AI predictions
|
||||
try:
|
||||
category = ai_service.classify_category(file_path)
|
||||
category = ai_service.classify_category(str(file_path_obj))
|
||||
logger.debug(f"Classification: {category}")
|
||||
|
||||
if category.lower() == "pothole":
|
||||
severity_str, annotated_path = ai_service.detect_pothole_severity(file_path)
|
||||
severity_str, annotated_path = ai_service.detect_pothole_severity(str(file_path_obj))
|
||||
logger.debug(f"Detection: severity={severity_str}, path={annotated_path}")
|
||||
severity = {
|
||||
"High": SeverityLevel.HIGH,
|
||||
@@ -87,20 +102,27 @@ async def report_issue(
|
||||
category = "Unknown"
|
||||
severity = SeverityLevel.NA
|
||||
|
||||
# Create ticket
|
||||
# Create ticket (store relative posix path)
|
||||
image_path_db = file_path_obj.as_posix()
|
||||
ticket = ticket_service.create_ticket(
|
||||
user_id=user.id,
|
||||
image_path=file_path,
|
||||
image_path=image_path_db,
|
||||
category=category,
|
||||
severity=severity,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
description=description
|
||||
description=description,
|
||||
address=address
|
||||
)
|
||||
logger.info(f"Ticket created: {ticket.id} for user {user.id}")
|
||||
|
||||
# Normalize stored path and build absolute URL
|
||||
rel_path = normalize_image_path_for_url(ticket.image_path)
|
||||
image_url = make_image_url(rel_path, request)
|
||||
|
||||
response = {
|
||||
"ticket_id": ticket.id,
|
||||
"id": ticket.id,
|
||||
"user_id": user.id,
|
||||
"user_name": user.name,
|
||||
"user_email": user.email,
|
||||
@@ -108,7 +130,9 @@ async def report_issue(
|
||||
"severity": ticket.severity.value,
|
||||
"status": ticket.status.value,
|
||||
"description": ticket.description,
|
||||
"image_path": ticket.image_path
|
||||
"image_path": rel_path,
|
||||
"image_url": image_url,
|
||||
"address": ticket.address
|
||||
}
|
||||
|
||||
logger.debug(f"Response: {response}")
|
||||
|
||||
@@ -1,96 +1,81 @@
|
||||
# app/routes/tickets.py
|
||||
from typing import Optional, List
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.services.ticket_service import TicketService, TicketStatus, SeverityLevel
|
||||
from pydantic import BaseModel
|
||||
from app.utils import ticket_to_dict
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class TicketStatusUpdate(BaseModel):
|
||||
new_status: TicketStatus
|
||||
status: TicketStatus
|
||||
|
||||
# ----------------------
|
||||
# GET /tickets
|
||||
# ----------------------
|
||||
@router.get("/tickets", response_model=List[dict])
|
||||
def list_tickets(
|
||||
request: Request,
|
||||
user_id: Optional[str] = Query(None, description="Filter by user ID"),
|
||||
category: Optional[str] = Query(None, description="Filter by category"),
|
||||
severity: Optional[SeverityLevel] = Query(None, description="Filter by severity"),
|
||||
status: Optional[TicketStatus] = Query(None, description="Filter by status"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Return all tickets by default. Optional query params may filter results.
|
||||
Each item is serialized using ticket_to_dict(...) which guarantees:
|
||||
- image_url is an absolute forward-slash URL
|
||||
- created_at is ISO-8601 string
|
||||
- consistent schema for dashboard & mobile clients
|
||||
"""
|
||||
service = TicketService(db)
|
||||
tickets = service.list_tickets(user_id=user_id, category=category, severity=severity, status=status)
|
||||
return [
|
||||
{
|
||||
"ticket_id": t.id,
|
||||
"user_id": t.user_id,
|
||||
"category": t.category,
|
||||
"severity": t.severity.value,
|
||||
"status": t.status.value,
|
||||
"description": t.description,
|
||||
"latitude": t.latitude,
|
||||
"longitude": t.longitude,
|
||||
"image_path": t.image_path,
|
||||
"created_at": t.created_at,
|
||||
"updated_at": t.updated_at
|
||||
} for t in tickets
|
||||
]
|
||||
return [ticket_to_dict(t, request) for t in tickets]
|
||||
|
||||
# ----------------------
|
||||
# GET /tickets/{ticket_id}
|
||||
# ----------------------
|
||||
@router.get("/tickets/{ticket_id}", response_model=dict)
|
||||
def get_ticket(ticket_id: str, db: Session = Depends(get_db)):
|
||||
def get_ticket(ticket_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
service = TicketService(db)
|
||||
ticket = service.get_ticket(ticket_id)
|
||||
if not ticket:
|
||||
raise HTTPException(status_code=404, detail=f"Ticket {ticket_id} not found")
|
||||
return {
|
||||
"ticket_id": ticket.id,
|
||||
"user_id": ticket.user_id,
|
||||
"category": ticket.category,
|
||||
"severity": ticket.severity.value,
|
||||
"status": ticket.status.value,
|
||||
"description": ticket.description,
|
||||
"latitude": ticket.latitude,
|
||||
"longitude": ticket.longitude,
|
||||
"image_path": ticket.image_path,
|
||||
"created_at": ticket.created_at,
|
||||
"updated_at": ticket.updated_at
|
||||
}
|
||||
return ticket_to_dict(ticket, request)
|
||||
|
||||
# ----------------------
|
||||
# PATCH /tickets/{ticket_id} - Update status
|
||||
# PATCH /tickets/{ticket_id}/status - Update status
|
||||
# ----------------------
|
||||
@router.patch("/tickets/{ticket_id}", response_model=dict)
|
||||
@router.patch("/tickets/{ticket_id}/status", response_model=dict)
|
||||
def update_ticket_status(
|
||||
ticket_id: str,
|
||||
status_update: TicketStatusUpdate, # JSON body with new_status
|
||||
status_update: TicketStatusUpdate, # JSON body with status
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
service = TicketService(db)
|
||||
try:
|
||||
ticket = service.update_ticket_status(ticket_id, status_update.new_status)
|
||||
ticket = service.update_ticket_status(ticket_id, status_update.status)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update ticket status: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {
|
||||
"ticket_id": ticket.id,
|
||||
"user_id": ticket.user_id,
|
||||
"category": ticket.category,
|
||||
"severity": ticket.severity.value,
|
||||
"status": ticket.status.value,
|
||||
"description": ticket.description,
|
||||
"latitude": ticket.latitude,
|
||||
"longitude": ticket.longitude,
|
||||
"image_path": ticket.image_path,
|
||||
"created_at": ticket.created_at,
|
||||
"updated_at": ticket.updated_at
|
||||
}
|
||||
return ticket_to_dict(ticket, request)
|
||||
|
||||
# ----------------------
|
||||
# DELETE /tickets/{ticket_id} - Delete ticket + image
|
||||
# ----------------------
|
||||
@router.delete("/tickets/{ticket_id}", response_model=dict)
|
||||
def delete_ticket(ticket_id: str, db: Session = Depends(get_db)):
|
||||
service = TicketService(db)
|
||||
try:
|
||||
service.delete_ticket(ticket_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete ticket {ticket_id}: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {"deleted": True, "id": ticket_id}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# app/services/ticket_service.py
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from app.models.ticket_model import User, Ticket, TicketAudit, TicketStatus, SeverityLevel
|
||||
from app.utils import normalize_image_path_for_url, UPLOADS_DIR_RESOLVED
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -26,8 +28,6 @@ class TicketService:
|
||||
self.db.refresh(user)
|
||||
logger.info(f"Created user {user}")
|
||||
return user # <-- return User object
|
||||
|
||||
|
||||
|
||||
def get_user(self, user_id: str) -> Optional[User]:
|
||||
return self.db.query(User).filter(User.id == user_id).first()
|
||||
@@ -44,16 +44,27 @@ class TicketService:
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
description: str = "",
|
||||
address: Optional[str] = None,
|
||||
) -> Ticket:
|
||||
"""
|
||||
Create a Ticket record.
|
||||
|
||||
image_path should be a relative POSIX path (e.g. 'static/uploads/uuid.jpg').
|
||||
report.route uses Path.as_posix() to ensure forward slashes on save.
|
||||
"""
|
||||
# Normalize stored path to POSIX
|
||||
image_path_posix = Path(str(image_path)).as_posix() if image_path else None
|
||||
|
||||
ticket = Ticket(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
image_path=image_path,
|
||||
image_path=image_path_posix,
|
||||
category=category,
|
||||
severity=severity,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
description=description,
|
||||
address=address,
|
||||
)
|
||||
self.db.add(ticket)
|
||||
self.db.commit()
|
||||
@@ -91,6 +102,9 @@ class TicketService:
|
||||
severity: Optional[SeverityLevel] = None,
|
||||
status: Optional[TicketStatus] = None
|
||||
) -> List[Ticket]:
|
||||
"""
|
||||
Return tickets. By default returns all tickets unless optional filters are provided.
|
||||
"""
|
||||
query = self.db.query(Ticket)
|
||||
if user_id:
|
||||
query = query.filter(Ticket.user_id == user_id)
|
||||
@@ -101,3 +115,57 @@ class TicketService:
|
||||
if status:
|
||||
query = query.filter(Ticket.status == status)
|
||||
return query.order_by(Ticket.created_at.desc()).all()
|
||||
|
||||
def delete_ticket(self, ticket_id: str) -> bool:
|
||||
"""
|
||||
Delete a ticket and its associated image file if it exists.
|
||||
|
||||
Safety:
|
||||
- Normalize stored image_path to a relative POSIX path under the static/ mount using
|
||||
normalize_image_path_for_url().
|
||||
- Resolve the resulting path and only delete if the resolved path is under the configured
|
||||
uploads directory (UPLOADS_DIR_RESOLVED) to prevent path traversal.
|
||||
"""
|
||||
ticket = self.db.query(Ticket).filter(Ticket.id == ticket_id).first()
|
||||
if not ticket:
|
||||
raise NoResultFound(f"Ticket with id {ticket_id} not found")
|
||||
|
||||
# Attempt to delete the image file if present
|
||||
try:
|
||||
rel = normalize_image_path_for_url(ticket.image_path)
|
||||
if rel:
|
||||
file_path = Path(rel)
|
||||
# Resolve to absolute path safely (works if file missing too)
|
||||
try:
|
||||
absolute = file_path.resolve()
|
||||
except Exception:
|
||||
absolute = (Path.cwd() / file_path).resolve()
|
||||
|
||||
# Ensure the file is inside the uploads directory
|
||||
try:
|
||||
absolute.relative_to(UPLOADS_DIR_RESOLVED)
|
||||
inside_uploads = True
|
||||
except Exception:
|
||||
inside_uploads = False
|
||||
|
||||
if inside_uploads and absolute.exists():
|
||||
try:
|
||||
absolute.unlink()
|
||||
logger.info(f"Deleted image file: {absolute}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete image file {absolute}: {e}")
|
||||
else:
|
||||
logger.debug(f"Image file not deleted (missing or outside uploads): {absolute}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error while attempting to remove image for ticket {ticket_id}: {e}")
|
||||
|
||||
# Delete ticket record
|
||||
try:
|
||||
self.db.delete(ticket)
|
||||
self.db.commit()
|
||||
logger.info(f"Deleted ticket {ticket_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to delete ticket {ticket_id} from DB: {e}")
|
||||
self.db.rollback()
|
||||
raise
|
||||
100
backend/app/utils.py
Normal file
100
backend/app/utils.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Path where uploaded images are stored (relative to project root)
|
||||
UPLOADS_DIR = Path("static") / "uploads"
|
||||
# Resolved absolute path for safety checks
|
||||
try:
|
||||
UPLOADS_DIR_RESOLVED = UPLOADS_DIR.resolve()
|
||||
except Exception:
|
||||
UPLOADS_DIR_RESOLVED = UPLOADS_DIR
|
||||
|
||||
def normalize_image_path_for_url(image_path: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Normalize stored image_path to a web-accessible relative path under the /static/ mount.
|
||||
Examples:
|
||||
r"static\\uploads\\uuid.jpg" -> "static/uploads/uuid.jpg"
|
||||
"C:\\project\\static\\uploads\\uuid.jpg" -> "static/uploads/uuid.jpg"
|
||||
"uploads/uuid.jpg" -> "static/uploads/uuid.jpg"
|
||||
"""
|
||||
if not image_path:
|
||||
return None
|
||||
|
||||
p = str(image_path).replace("\\", "/")
|
||||
# strip leading './' and leading slashes
|
||||
while p.startswith("./"):
|
||||
p = p[2:]
|
||||
p = p.lstrip("/")
|
||||
|
||||
# prefer existing 'static/' segment if present
|
||||
if "static/" in p:
|
||||
p = p[p.find("static/"):]
|
||||
elif p.startswith("uploads/"):
|
||||
p = f"static/{p}"
|
||||
else:
|
||||
# fallback to treating as filename only
|
||||
p = f"static/uploads/{Path(p).name}"
|
||||
|
||||
# collapse accidental duplicates
|
||||
p = p.replace("static/static", "static").replace("uploads/uploads", "uploads")
|
||||
return p
|
||||
|
||||
def make_image_url(image_path: Optional[str], request) -> Optional[str]:
|
||||
"""
|
||||
Build an absolute URL for the given stored image_path using the FastAPI request.
|
||||
Returns None if image_path is falsy.
|
||||
"""
|
||||
rel = normalize_image_path_for_url(image_path)
|
||||
if not rel:
|
||||
return None
|
||||
base = str(request.base_url).rstrip("/")
|
||||
return f"{base}/{rel.lstrip('/')}"
|
||||
|
||||
def ticket_to_dict(ticket, request=None) -> dict:
|
||||
"""
|
||||
Serialize a Ticket ORM object to the normalized schema expected by clients.
|
||||
|
||||
Schema:
|
||||
id, category, severity, status, description,
|
||||
user_id, user_name, user_email,
|
||||
created_at (ISO8601), latitude, longitude, address,
|
||||
image_url (absolute), image_path (relative POSIX under static/)
|
||||
"""
|
||||
created = None
|
||||
try:
|
||||
if getattr(ticket, "created_at", None):
|
||||
created = ticket.created_at.isoformat()
|
||||
except Exception:
|
||||
created = None
|
||||
|
||||
# Normalize stored image path to a safe relative POSIX path under 'static/'
|
||||
normalized_path = normalize_image_path_for_url(getattr(ticket, "image_path", None))
|
||||
|
||||
image_url = None
|
||||
if request is not None:
|
||||
try:
|
||||
image_url = make_image_url(normalized_path, request)
|
||||
except Exception:
|
||||
logger.exception("Failed to build image_url")
|
||||
image_url = None
|
||||
|
||||
return {
|
||||
"id": ticket.id,
|
||||
"category": ticket.category,
|
||||
"severity": ticket.severity.value if getattr(ticket, "severity", None) else None,
|
||||
"status": ticket.status.value if getattr(ticket, "status", None) else None,
|
||||
"description": ticket.description,
|
||||
"user_id": ticket.user_id,
|
||||
"user_name": ticket.user.name if getattr(ticket, "user", None) else None,
|
||||
"user_email": ticket.user.email if getattr(ticket, "user", None) else None,
|
||||
"created_at": created,
|
||||
"latitude": ticket.latitude,
|
||||
"longitude": ticket.longitude,
|
||||
"address": ticket.address,
|
||||
"image_url": image_url,
|
||||
"image_path": normalized_path
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.database import Base, engine
|
||||
from app.routes import report, tickets, analytics, users
|
||||
from app.services.global_ai import init_ai_service
|
||||
@@ -44,6 +45,25 @@ UPLOAD_DIR = "static/uploads"
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
# ----------------------
|
||||
# CORS - allow dashboard & emulator origins
|
||||
# ----------------------
|
||||
DEFAULT_ORIGINS = "http://localhost:3000,http://127.0.0.1:3000,http://10.0.2.2:3000,http://192.168.100.59:3000"
|
||||
origins_env = os.environ.get("FIXMATE_CORS_ORIGINS", DEFAULT_ORIGINS)
|
||||
allowed_origins = [o.strip() for o in origins_env.split(",") if o.strip()]
|
||||
# Ensure common development origins are always allowed (localhost, emulator, LAN)
|
||||
for origin in ("http://localhost:3000", "http://127.0.0.1:3000", "http://10.0.2.2:3000", "http://192.168.100.59:3000"):
|
||||
if origin not in allowed_origins:
|
||||
allowed_origins.append(origin)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=allowed_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ----------------------
|
||||
# Include routers
|
||||
# ----------------------
|
||||
|
||||
33
backend/scripts/add_address_column.py
Normal file
33
backend/scripts/add_address_column.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import sys
|
||||
|
||||
def main():
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
db_path = os.path.normpath(os.path.join(script_dir, '..', 'app', 'db', 'fixmate.db'))
|
||||
print(f"Using database: {db_path}")
|
||||
if not os.path.exists(db_path):
|
||||
print(f"DB not found: {db_path}")
|
||||
return 2
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("PRAGMA table_info(tickets);")
|
||||
cols = [row[1] for row in cur.fetchall()]
|
||||
if 'address' in cols:
|
||||
print("Column 'address' already exists")
|
||||
return 0
|
||||
|
||||
cur.execute("ALTER TABLE tickets ADD COLUMN address TEXT;")
|
||||
conn.commit()
|
||||
print("Added 'address' column to 'tickets' table")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print("Failed to add 'address' column:", e)
|
||||
return 1
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
BIN
backend/static/uploads/bdb41802-b2a9-4d4b-ad60-5491be7987c8.jpg
Normal file
BIN
backend/static/uploads/bdb41802-b2a9-4d4b-ad60-5491be7987c8.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
backend/static/uploads/d35f86a2-08b5-47d6-bcfd-c59c8ccee275.jpg
Normal file
BIN
backend/static/uploads/d35f86a2-08b5-47d6-bcfd-c59c8ccee275.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Reference in New Issue
Block a user