first commit
This commit is contained in:
13
routes/auth/__init__.py
Normal file
13
routes/auth/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from fastapi import APIRouter
|
||||
from .login import router as login_router
|
||||
from .register import router as register_router
|
||||
from .new import router as new_router
|
||||
from .current_user import router as current_user_router, oauth2_scheme
|
||||
from .lost_password import router as lost_password_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(login_router)
|
||||
router.include_router(register_router)
|
||||
router.include_router(new_router)
|
||||
router.include_router(current_user_router)
|
||||
router.include_router(lost_password_router)
|
||||
191
routes/auth/current_user.py
Normal file
191
routes/auth/current_user.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from fastapi import Depends, HTTPException, status, APIRouter
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
import logging
|
||||
from database import get_db
|
||||
from models import Account_User, Customer_Customer, Customer_Description, Card
|
||||
from schemas import TokenData, CustomerUpdate
|
||||
from jose import JWTError, jwt
|
||||
from config import load_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load JWT configuration from environment
|
||||
ApplicationConfig = load_config()
|
||||
SECRET_KEY = ApplicationConfig.JWT_SECRET_KEY
|
||||
ALGORITHM = ApplicationConfig.JWT_ALGORITHM
|
||||
|
||||
async def get_current_user(token: str = Depends(OAuth2PasswordBearer(tokenUrl="auth/login")), db: AsyncSession = Depends(get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(username=username)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = await db.execute(select(Account_User).where(Account_User.username == token_data.username))
|
||||
user = user.scalar_one_or_none()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/me")
|
||||
async def read_users_me(current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
# Get customer details for the current user using user_id
|
||||
customer = None
|
||||
if current_user.user_id:
|
||||
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.id == current_user.user_id))
|
||||
customer = customer_result.scalar_one_or_none()
|
||||
|
||||
# Get house description if exists
|
||||
house_description = None
|
||||
if customer:
|
||||
description_record = await db.execute(
|
||||
select(Customer_Description).where(Customer_Description.customer_id == customer.id)
|
||||
)
|
||||
description_record = description_record.scalar_one_or_none()
|
||||
if description_record:
|
||||
house_description = description_record.description
|
||||
|
||||
# Map state code to name
|
||||
state_mapping = {0: "MA", 1: "NH"} # Add more as needed
|
||||
state_name = state_mapping.get(customer.customer_state, str(customer.customer_state)) if customer else None
|
||||
|
||||
return {
|
||||
"id": current_user.id,
|
||||
"username": current_user.username,
|
||||
"email": current_user.email,
|
||||
"account_number": current_user.account_number,
|
||||
"customer_first_name": customer.customer_first_name if customer else None,
|
||||
"customer_last_name": customer.customer_last_name if customer else None,
|
||||
"customer_address": customer.customer_address if customer else None,
|
||||
"customer_apt": customer.customer_apt if customer else None,
|
||||
"customer_town": customer.customer_town if customer else None,
|
||||
"customer_state": state_name,
|
||||
"customer_zip": customer.customer_zip if customer else None,
|
||||
"customer_phone_number": customer.customer_phone_number if customer else None,
|
||||
"customer_home_type": customer.customer_home_type if customer else None,
|
||||
"customer_email": customer.customer_email if customer else None,
|
||||
"house_description": house_description,
|
||||
"member_since": current_user.member_since,
|
||||
"last_seen": current_user.last_seen
|
||||
}
|
||||
|
||||
@router.put("/update-customer")
|
||||
async def update_customer_info(
|
||||
customer_data: CustomerUpdate,
|
||||
current_user: Account_User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if not current_user.user_id:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
try:
|
||||
# Update Customer_Customer table
|
||||
customer_update_data = customer_data.dict(exclude_unset=True)
|
||||
customer_fields = {
|
||||
'customer_first_name', 'customer_last_name', 'customer_address', 'customer_apt',
|
||||
'customer_town', 'customer_state', 'customer_zip', 'customer_phone_number',
|
||||
'customer_home_type', 'customer_email'
|
||||
}
|
||||
|
||||
customer_update_dict = {k: v for k, v in customer_update_data.items() if k in customer_fields}
|
||||
|
||||
if customer_update_dict:
|
||||
await db.execute(
|
||||
update(Customer_Customer)
|
||||
.where(Customer_Customer.id == current_user.user_id)
|
||||
.values(**customer_update_dict)
|
||||
)
|
||||
|
||||
# Update house description if provided
|
||||
if 'house_description' in customer_update_data and customer_update_data['house_description']:
|
||||
# Get customer record
|
||||
customer = await db.execute(
|
||||
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
|
||||
)
|
||||
customer = customer.scalar_one_or_none()
|
||||
|
||||
if customer:
|
||||
# Check if description record exists
|
||||
description_record = await db.execute(
|
||||
select(Customer_Description).where(Customer_Description.customer_id == customer.id)
|
||||
)
|
||||
description_record = description_record.scalar_one_or_none()
|
||||
|
||||
if description_record:
|
||||
# Update existing
|
||||
await db.execute(
|
||||
update(Customer_Description)
|
||||
.where(Customer_Description.customer_id == customer.id)
|
||||
.values(description=customer_update_data['house_description'])
|
||||
)
|
||||
else:
|
||||
# Create new
|
||||
new_description = Customer_Description(
|
||||
customer_id=customer.id,
|
||||
account_number=current_user.account_number,
|
||||
company_id=customer.company_id or 1,
|
||||
fill_location=0,
|
||||
description=customer_update_data['house_description']
|
||||
)
|
||||
db.add(new_description)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Sync billing info to Authorize.net if address-related fields were updated
|
||||
billing_fields = {
|
||||
'customer_first_name', 'customer_last_name', 'customer_address',
|
||||
'customer_town', 'customer_state', 'customer_zip', 'customer_phone_number',
|
||||
'customer_email'
|
||||
}
|
||||
if any(k in customer_update_dict for k in billing_fields):
|
||||
# Late import to avoid circular dependency
|
||||
from routes.payment.routes import update_customer_profile, update_payment_profile_billing
|
||||
|
||||
# Refetch the updated customer
|
||||
customer_result = await db.execute(
|
||||
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
|
||||
)
|
||||
updated_customer = customer_result.scalar_one_or_none()
|
||||
|
||||
if updated_customer and updated_customer.auth_net_profile_id:
|
||||
# Update customer profile in Authorize.net
|
||||
update_customer_profile(updated_customer.auth_net_profile_id, updated_customer)
|
||||
|
||||
# Update all saved payment profiles with new billing info
|
||||
cards_result = await db.execute(
|
||||
select(Card).where(Card.user_id == updated_customer.id)
|
||||
)
|
||||
cards = cards_result.scalars().all()
|
||||
|
||||
for card in cards:
|
||||
if card.auth_net_payment_profile_id:
|
||||
update_payment_profile_billing(
|
||||
customer_profile_id=updated_customer.auth_net_profile_id,
|
||||
payment_profile_id=card.auth_net_payment_profile_id,
|
||||
customer=updated_customer,
|
||||
card=card
|
||||
)
|
||||
|
||||
logger.info(f"Synced billing info to Authorize.net for customer {updated_customer.id}")
|
||||
|
||||
return {"message": "Customer information updated successfully"}
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update customer information: {str(e)}"
|
||||
)
|
||||
60
routes/auth/login.py
Normal file
60
routes/auth/login.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from database import get_db
|
||||
from models import Account_User
|
||||
from schemas import UserLogin, Token
|
||||
from passlib.context import CryptContext
|
||||
from jose import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from config import load_config
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
# Load JWT configuration from environment
|
||||
ApplicationConfig = load_config()
|
||||
SECRET_KEY = ApplicationConfig.JWT_SECRET_KEY
|
||||
ALGORITHM = ApplicationConfig.JWT_ALGORITHM
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = ApplicationConfig.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def create_access_token(data: dict, expires_delta = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
async def authenticate_user(db: AsyncSession, email: str, password: str):
|
||||
user = await db.execute(select(Account_User).where(Account_User.email == email))
|
||||
user = user.scalar_one_or_none()
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.password_hash):
|
||||
return False
|
||||
# Update last_seen
|
||||
user.last_seen = datetime.utcnow()
|
||||
await db.commit()
|
||||
return user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(user: UserLogin, db: AsyncSession = Depends(get_db)):
|
||||
user_obj = await authenticate_user(db, user.email, user.password)
|
||||
if not user_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user_obj.username}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
126
routes/auth/lost_password.py
Normal file
126
routes/auth/lost_password.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import secrets
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from database import get_db
|
||||
from models import Account_User
|
||||
from schemas import ForgotPasswordRequest, ResetPasswordRequest
|
||||
from passlib.context import CryptContext
|
||||
from config import load_config
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
# Load configuration from environment
|
||||
ApplicationConfig = load_config()
|
||||
|
||||
# Email configuration from environment variables
|
||||
SMTP_SERVER = ApplicationConfig.SMTP_SERVER
|
||||
SMTP_PORT = ApplicationConfig.SMTP_PORT
|
||||
SMTP_USERNAME = ApplicationConfig.SMTP_USERNAME
|
||||
SMTP_PASSWORD = ApplicationConfig.SMTP_PASSWORD
|
||||
FROM_EMAIL = ApplicationConfig.SMTP_FROM_EMAIL
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def generate_reset_token():
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
def send_reset_email(to_email: str, reset_token: str):
|
||||
"""Send password reset email with link"""
|
||||
reset_url = f"{ApplicationConfig.FRONTEND_URL}/reset-password?token={reset_token}"
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = FROM_EMAIL
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = "Password Reset - Oil Customer Gateway"
|
||||
|
||||
body = f"""
|
||||
You have requested a password reset for your Oil Customer Gateway account.
|
||||
|
||||
Click the following link to reset your password:
|
||||
{reset_url}
|
||||
|
||||
This link will expire in 24 hours.
|
||||
|
||||
If you didn't request this reset, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
Oil Customer Gateway Team
|
||||
"""
|
||||
|
||||
msg.attach(MIMEText(body, 'plain'))
|
||||
|
||||
try:
|
||||
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
|
||||
server.starttls()
|
||||
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
||||
text = msg.as_string()
|
||||
server.sendmail(FROM_EMAIL, to_email, text)
|
||||
server.quit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Email sending failed: {e}")
|
||||
return False
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Request password reset - sends email with reset link"""
|
||||
# Find user by email
|
||||
result = await db.execute(select(Account_User).where(Account_User.email == request.email))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
# Don't reveal if email exists or not for security
|
||||
return {"message": "If an account with that email exists, a password reset link has been sent."}
|
||||
|
||||
# Generate reset token and expiry (24 hours)
|
||||
reset_token = generate_reset_token()
|
||||
reset_expires = datetime.utcnow() + timedelta(hours=24)
|
||||
|
||||
# Update user with reset token
|
||||
user.password_reset_token = reset_token
|
||||
user.password_reset_expires = reset_expires
|
||||
await db.commit()
|
||||
|
||||
# Send email
|
||||
if send_reset_email(user.email, reset_token):
|
||||
return {"message": "Password reset link sent to your email."}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Failed to send reset email")
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(request: ResetPasswordRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Reset password using token"""
|
||||
# Verify passwords match
|
||||
if request.password != request.confirm_password:
|
||||
raise HTTPException(status_code=400, detail="Passwords do not match")
|
||||
|
||||
# Find user by reset token
|
||||
result = await db.execute(
|
||||
select(Account_User).where(
|
||||
Account_User.password_reset_token == request.token,
|
||||
Account_User.password_reset_expires > datetime.utcnow()
|
||||
)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Invalid or expired reset token")
|
||||
|
||||
# Update password
|
||||
hashed_password = get_password_hash(request.password)
|
||||
user.password_hash = hashed_password
|
||||
user.password_reset_token = None
|
||||
user.password_reset_expires = None
|
||||
user.last_seen = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Password reset successfully"}
|
||||
588
routes/auth/new.py
Normal file
588
routes/auth/new.py
Normal file
@@ -0,0 +1,588 @@
|
||||
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
|
||||
71
routes/auth/register.py
Normal file
71
routes/auth/register.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from database import get_db
|
||||
from models import Account_User, Customer_Customer
|
||||
from schemas import UserCreate, UserResponse
|
||||
from passlib.context import CryptContext
|
||||
from datetime import datetime
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def escape_like_pattern(value: str) -> str:
|
||||
"""Escape special characters for SQL LIKE patterns.
|
||||
|
||||
Escapes %, _, and \ which have special meaning in LIKE clauses
|
||||
to prevent SQL injection via wildcards.
|
||||
"""
|
||||
# Escape backslash first, then the wildcards
|
||||
return value.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
# Verify passwords match
|
||||
if user.password != user.confirm_password:
|
||||
raise HTTPException(status_code=400, detail="Passwords do not match")
|
||||
|
||||
# Check if customer exists in Customer_Customer table
|
||||
# Escape SQL LIKE wildcards to prevent injection attacks
|
||||
escaped_house_number = escape_like_pattern(user.house_number)
|
||||
customer_result = await db.execute(
|
||||
select(Customer_Customer).where(
|
||||
(Customer_Customer.account_number == user.account_number) &
|
||||
(Customer_Customer.customer_address.like(f'{escaped_house_number} %', escape='\\'))
|
||||
)
|
||||
)
|
||||
customer = customer_result.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=400, detail="Customer not found with provided account and house number")
|
||||
|
||||
# Check if email already registered
|
||||
result = await db.execute(select(Account_User).where(Account_User.email == user.email))
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
username = f"{user.account_number}-{user.house_number}"
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = Account_User(
|
||||
username=username,
|
||||
account_number=user.account_number,
|
||||
house_number=user.house_number,
|
||||
password_hash=hashed_password,
|
||||
member_since=datetime.utcnow(),
|
||||
email=user.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
|
||||
Reference in New Issue
Block a user