Files
api/routes/auth/new.py
2026-01-17 15:21:41 -05:00

589 lines
23 KiB
Python

import random
import string
import os
import shutil
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
from models import Customer_Customer, Customer_Description, Customer_Tank_Inspection, Customer_Stats, Account_User
from schemas import NewCustomerCreate, UserResponse, CustomerCreateStep1, CustomerAccountCreate
from passlib.context import CryptContext
from datetime import datetime
from PIL import Image
import io
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
def generate_random_number_string(length):
if length < 1:
raise ValueError("Length must be at least 1")
random_number = ''.join(random.choices(string.digits, k=length))
return random_number
def get_password_hash(password):
# Truncate password to 72 bytes max for bcrypt compatibility
truncated_password = password.encode('utf-8')[:72].decode('utf-8', errors='ignore')
return pwd_context.hash(truncated_password)
def resize_image(image_data, max_size=(1024, 1024), quality=85):
"""
Resize image to fit within max_size while maintaining aspect ratio.
Convert to JPEG format and strip metadata for security.
"""
try:
# Open image from bytes
img = Image.open(io.BytesIO(image_data))
# Convert to RGB if necessary (for PNG with transparency, etc.)
if img.mode in ('RGBA', 'LA', 'P'):
img = img.convert('RGB')
# Resize if larger than max_size
if img.width > max_size[0] or img.height > max_size[1]:
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# Save as JPEG with specified quality
output = io.BytesIO()
img.save(output, format='JPEG', quality=quality, optimize=True)
output.seek(0)
return output.getvalue()
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}")
router = APIRouter()
@router.post("/new", response_model=UserResponse)
async def register_new_customer(customer: NewCustomerCreate, db: AsyncSession = Depends(get_db)):
# Verify passwords match
if customer.password != customer.confirm_password:
raise HTTPException(status_code=400, detail="Passwords do not match")
# Check if email already registered
result = await db.execute(select(Account_User).where(Account_User.email == customer.customer_email))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email already registered")
while True:
random_part = generate_random_number_string(6)
account_number = 'AO-' + random_part
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
existing_customer = result.scalar_one_or_none()
if not existing_customer:
break
# Create customer
customer_data = customer.model_dump()
customer_data.pop('password')
customer_data.pop('confirm_password')
customer_data.update({
'account_number': account_number,
'customer_state': 1, # Default
'customer_automatic': 0,
'company_id': 1, # Default
'customer_latitude': '0',
'customer_longitude': '0',
'correct_address': True,
'customer_first_call': datetime.utcnow()
})
db_customer = Customer_Customer(**customer_data)
db.add(db_customer)
await db.commit()
await db.refresh(db_customer)
# Extract house number from customer address (first part before space)
house_number = customer.customer_address.split()[0] if customer.customer_address else ''
# Create account user
username = account_number
hashed_password = get_password_hash(customer.password)
db_user = Account_User(
username=username,
account_number=account_number,
house_number=house_number,
password_hash=hashed_password,
member_since=datetime.utcnow(),
email=customer.customer_email,
last_seen=datetime.utcnow(),
admin=0,
admin_role=0,
confirmed=1,
active=1,
user_id=db_customer.id
)
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user
@router.post("/step3")
async def upload_tank_images(
account_number: str = Form(...),
tank_image_1: UploadFile = File(...),
tank_image_2: UploadFile = File(...),
tank_image_3: UploadFile = File(...),
db: AsyncSession = Depends(get_db)
):
print("=== STEP3 DEBUG START ===")
print(f"Account number received: '{account_number}'")
# Debug: Check all parameters received
images = [tank_image_1, tank_image_2, tank_image_3]
for i, image in enumerate(images, 1):
print(f"Image {i}: filename='{image.filename}', content_type='{image.content_type}', size={image.size}")
# Validate account number
if not account_number:
print("ERROR: Account number is empty")
raise HTTPException(status_code=400, detail="Account number is required")
# Get customer info for description record
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
customer = customer_result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=400, detail="Customer not found")
print(f"Creating directory: /images/{account_number}")
# Create directory for account number in the mounted images volume
account_dir = f"/images/{account_number}"
os.makedirs(account_dir, exist_ok=True)
current_datetime = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") # YYYY-MM-DD_HH-MM-SS format
# Track if images were uploaded successfully
images_uploaded_successfully = False
# Validate and save images
saved_files = []
try:
for i, image in enumerate(images, 1):
print(f"Processing image {i}...")
# Read image data
image_data = await image.read()
print(f"Image {i} data read: {len(image_data)} bytes")
# Validate file size (max 20MB before processing)
if len(image_data) > 20 * 1024 * 1024:
print(f"ERROR: Image {i} too large: {len(image_data)} bytes")
raise HTTPException(status_code=400, detail=f"File {i} is too large (max 20MB)")
# Resize and process image (this also validates it's a valid image)
print(f"Resizing image {i}...")
processed_image_data = resize_image(image_data, max_size=(1024, 1024), quality=85)
print(f"Image {i} resized: {len(processed_image_data)} bytes")
# Save processed image with datetime-based filename
filename = f"{current_datetime}-{i}.jpg"
file_path = os.path.join(account_dir, filename)
print(f"Saving image {i} to: {file_path}")
with open(file_path, "wb") as buffer:
buffer.write(processed_image_data)
saved_files.append(filename)
print(f"Image {i} saved successfully")
images_uploaded_successfully = True
print(f"All images processed successfully. Saved files: {saved_files}")
except Exception as e:
print(f"ERROR processing images: {str(e)}")
# Don't raise exception - we want to track the failure in the database
images_uploaded_successfully = False
# Update or create customer tank inspection record with tank_images status
print("Updating customer tank inspection record...")
tank_result = await db.execute(
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
)
existing_tank = tank_result.scalar_one_or_none()
if existing_tank:
# Update existing record
if images_uploaded_successfully:
existing_tank.tank_images += 1 # Increment count of image sets
# Append current datetime to upload dates list
current_dates = existing_tank.tank_image_upload_dates or []
current_dates.append(current_datetime)
existing_tank.tank_image_upload_dates = current_dates
await db.commit()
print(f"Updated existing tank inspection record with tank_images = {existing_tank.tank_images}, dates = {existing_tank.tank_image_upload_dates}")
else:
# Create new record
new_tank = Customer_Tank_Inspection(
customer_id=customer.id,
tank_images=1 if images_uploaded_successfully else 0,
tank_image_upload_dates=[current_datetime] if images_uploaded_successfully else []
# Other fields will be null/None initially
)
db.add(new_tank)
await db.commit()
print(f"Created new tank inspection record with tank_images = {new_tank.tank_images}, dates = {new_tank.tank_image_upload_dates}")
print("=== STEP3 DEBUG END ===")
if images_uploaded_successfully:
return {
"message": "Tank images uploaded successfully",
"account_number": account_number,
"uploaded_files": saved_files
}
else:
return {
"message": "Tank images upload skipped or failed",
"account_number": account_number,
"uploaded_files": []
}
@router.post("/upload-tank-images")
async def upload_additional_tank_images(
account_number: str = Form(...),
tank_image_1: UploadFile = File(...),
tank_image_2: UploadFile = File(...),
tank_image_3: UploadFile = File(...),
db: AsyncSession = Depends(get_db)
):
"""
Endpoint for customers to upload additional sets of 3 tank images after registration.
Similar to step3 but for existing customers.
"""
print("=== UPLOAD ADDITIONAL TANK IMAGES DEBUG START ===")
print(f"Account number received: '{account_number}'")
# Debug: Check all parameters received
images = [tank_image_1, tank_image_2, tank_image_3]
for i, image in enumerate(images, 1):
print(f"Image {i}: filename='{image.filename}', content_type='{image.content_type}', size={image.size}")
# Validate account number
if not account_number:
print("ERROR: Account number is empty")
raise HTTPException(status_code=400, detail="Account number is required")
# Get customer info
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
customer = customer_result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=400, detail="Customer not found")
print(f"Creating directory: /images/{account_number}")
# Create directory for account number in the mounted images volume
account_dir = f"/images/{account_number}"
os.makedirs(account_dir, exist_ok=True)
current_datetime = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") # YYYY-MM-DD_HH-MM-SS format
# Track if images were uploaded successfully
images_uploaded_successfully = False
# Validate and save images
saved_files = []
try:
for i, image in enumerate(images, 1):
print(f"Processing image {i}...")
# Read image data
image_data = await image.read()
print(f"Image {i} data read: {len(image_data)} bytes")
# Validate file size (max 20MB before processing)
if len(image_data) > 20 * 1024 * 1024:
print(f"ERROR: Image {i} too large: {len(image_data)} bytes")
raise HTTPException(status_code=400, detail=f"File {i} is too large (max 20MB)")
# Resize and process image (this also validates it's a valid image)
print(f"Resizing image {i}...")
processed_image_data = resize_image(image_data, max_size=(1024, 1024), quality=85)
print(f"Image {i} resized: {len(processed_image_data)} bytes")
# Save processed image with datetime-based filename
filename = f"{current_datetime}-{i}.jpg"
file_path = os.path.join(account_dir, filename)
print(f"Saving image {i} to: {file_path}")
with open(file_path, "wb") as buffer:
buffer.write(processed_image_data)
saved_files.append(filename)
print(f"Image {i} saved successfully")
images_uploaded_successfully = True
print(f"All images processed successfully. Saved files: {saved_files}")
except Exception as e:
print(f"ERROR processing images: {str(e)}")
# Don't raise exception - we want to track the failure in the database
images_uploaded_successfully = False
# Update customer tank inspection record
print("Updating customer tank inspection record...")
tank_result = await db.execute(
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
)
existing_tank = tank_result.scalar_one_or_none()
if existing_tank:
# Update existing record
if images_uploaded_successfully:
existing_tank.tank_images += 1 # Increment count of image sets
# Append current datetime to upload dates list
current_dates = existing_tank.tank_image_upload_dates or []
current_dates.append(current_datetime)
existing_tank.tank_image_upload_dates = current_dates
await db.commit()
print(f"Updated existing tank inspection record with tank_images = {existing_tank.tank_images}, dates = {existing_tank.tank_image_upload_dates}")
else:
# This shouldn't happen for additional uploads, but handle it just in case
new_tank = Customer_Tank_Inspection(
customer_id=customer.id,
tank_images=1 if images_uploaded_successfully else 0,
tank_image_upload_dates=[current_datetime] if images_uploaded_successfully else []
)
db.add(new_tank)
await db.commit()
print(f"Created new tank inspection record with tank_images = {new_tank.tank_images}, dates = {new_tank.tank_image_upload_dates}")
print("=== UPLOAD ADDITIONAL TANK IMAGES DEBUG END ===")
if images_uploaded_successfully:
return {
"message": "Additional tank images uploaded successfully",
"account_number": account_number,
"uploaded_files": saved_files,
"upload_date": current_datetime
}
else:
raise HTTPException(status_code=400, detail="Failed to upload tank images")
@router.get("/tank-images/{account_number}")
async def get_tank_images(account_number: str, db: AsyncSession = Depends(get_db)):
"""
Get tank images information for a customer including upload dates.
"""
# Get customer info
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
customer = customer_result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Get tank inspection record
tank_result = await db.execute(
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
)
tank_record = tank_result.scalar_one_or_none()
# Build image sets - first from database, then scan for any additional files
image_sets = []
upload_dates = tank_record.tank_image_upload_dates or [] if tank_record else []
# Add sets from database
for i, upload_date in enumerate(upload_dates):
# Handle backward compatibility: old format used date-only strings and tank_*.jpg files
# New format uses datetime strings and {datetime}-*.jpg files
if "_" in upload_date:
# New datetime format (YYYY-MM-DD_HH-MM-SS)
image_set = {
"date": upload_date,
"images": [
f"/images/{account_number}/{upload_date}-1.jpg",
f"/images/{account_number}/{upload_date}-2.jpg",
f"/images/{account_number}/{upload_date}-3.jpg"
]
}
else:
# Old date-only format (YYYY-MM-DD) - uses tank_*.jpg files
image_set = {
"date": upload_date,
"images": [
f"/images/{account_number}/tank_1.jpg",
f"/images/{account_number}/tank_2.jpg",
f"/images/{account_number}/tank_3.jpg"
]
}
image_sets.append(image_set)
# Scan for any additional image sets that might not be in database
account_dir = f"/images/{account_number}"
if os.path.exists(account_dir):
# Find all image files
all_files = os.listdir(account_dir)
image_files = [f for f in all_files if f.endswith('.jpg')]
# Group by datetime prefix
datetime_groups = {}
for file in image_files:
if file.startswith('tank_'):
# Old tank_*.jpg files - already handled above
continue
elif '_' in file and file.endswith('.jpg'): # datetime format like 2026-01-08_23-25-31-1.jpg
# Extract datetime prefix by removing the image number and .jpg
# Example: 2026-01-08_23-25-31-1.jpg -> 2026-01-08_23-25-31
datetime_prefix = file.rsplit('-', 1)[0] # Remove everything after last dash
if datetime_prefix not in datetime_groups:
datetime_groups[datetime_prefix] = []
datetime_groups[datetime_prefix].append(file)
# Add any datetime groups not already in database
for datetime_prefix, files in datetime_groups.items():
if datetime_prefix not in upload_dates and len(files) >= 3:
# Sort files to ensure correct order
sorted_files = sorted(files)
image_set = {
"date": datetime_prefix,
"images": [
f"/images/{account_number}/{sorted_files[0]}",
f"/images/{account_number}/{sorted_files[1]}",
f"/images/{account_number}/{sorted_files[2]}"
]
}
image_sets.append(image_set)
# Also check for date-prefixed files (like 2026-01-08-1.jpg)
date_groups = {}
for file in image_files:
if file.startswith('tank_') or '_' in file:
continue # Skip old format and datetime format
parts = file.split('-')
if len(parts) == 4 and parts[3] in ['1.jpg', '2.jpg', '3.jpg']:
date_prefix = '-'.join(parts[:3]) # 2026-01-08
if date_prefix not in date_groups:
date_groups[date_prefix] = []
date_groups[date_prefix].append(file)
# Add date groups not already in database
for date_prefix, files in date_groups.items():
if date_prefix not in upload_dates and len(files) >= 3:
# Sort files to ensure correct order
sorted_files = sorted(files)
image_set = {
"date": date_prefix,
"images": [
f"/images/{account_number}/{sorted_files[0]}",
f"/images/{account_number}/{sorted_files[1]}",
f"/images/{account_number}/{sorted_files[2]}"
]
}
image_sets.append(image_set)
# Sort image sets by date descending (newest first)
def sort_key(item):
date_str = item['date']
try:
if '_' in date_str:
return datetime.strptime(date_str, "%Y-%m-%d_%H-%M-%S")
else:
return datetime.strptime(date_str, "%Y-%m-%d")
except:
return datetime.min
image_sets.sort(key=sort_key, reverse=True)
return {
"account_number": account_number,
"image_sets": image_sets
}
@router.post("/step1")
async def create_customer_step1(customer: CustomerCreateStep1, db: AsyncSession = Depends(get_db)):
while True:
random_part = generate_random_number_string(6)
account_number = 'AO-' + random_part
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
existing_customer = result.scalar_one_or_none()
if not existing_customer:
break
# Extract house_description for separate table
house_description = customer.house_description
# Create customer
customer_data = customer.model_dump()
customer_data.pop('house_description') # Remove from customer data
customer_data.update({
'account_number': account_number,
'customer_state': 1, # Default
'customer_automatic': 0,
'company_id': 1, # Default
'customer_latitude': '0',
'customer_longitude': '0',
'correct_address': True,
'customer_first_call': datetime.utcnow()
})
db_customer = Customer_Customer(**customer_data)
db.add(db_customer)
await db.commit()
await db.refresh(db_customer)
# Create customer description if house_description provided
if house_description:
db_description = Customer_Description(
customer_id=db_customer.id,
account_number=account_number,
company_id=1, # Default
fill_location=None, # Will work on this later
description=house_description
# tank_images is now tracked in customer_tank table
)
db.add(db_description)
await db.commit()
# Create customer stats record for tracking metrics
db_stats = Customer_Stats(
customer_id=db_customer.id
# All other fields default to 0/0.00 as defined in the model
)
db.add(db_stats)
await db.commit()
return {"account_number": account_number}
@router.post("/step2", response_model=UserResponse)
async def create_customer_account(account_data: CustomerAccountCreate, db: AsyncSession = Depends(get_db)):
# Verify passwords match
if account_data.password != account_data.confirm_password:
raise HTTPException(status_code=400, detail="Passwords do not match")
# Check if customer exists
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_data.account_number))
customer = result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=400, detail="Customer not found")
# Check if email already registered
result = await db.execute(select(Account_User).where(Account_User.email == account_data.customer_email))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email already registered")
# Update customer email
customer.customer_email = account_data.customer_email
db.add(customer)
await db.commit()
# Extract house number from customer address (first part before space)
house_number = customer.customer_address.split()[0] if customer.customer_address else ''
# Create account user
username = account_data.account_number
hashed_password = get_password_hash(account_data.password)
db_user = Account_User(
username=username,
account_number=account_data.account_number,
house_number=house_number,
password_hash=hashed_password,
member_since=datetime.utcnow(),
email=account_data.customer_email,
last_seen=datetime.utcnow(),
admin=0,
admin_role=0,
confirmed=1,
active=1,
user_id=customer.id
)
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user