diff --git a/models/account.py b/models/account.py index 8a0bf9e..fc2239a 100644 --- a/models/account.py +++ b/models/account.py @@ -16,6 +16,9 @@ class Account_User(Base): last_seen = Column(TIMESTAMP(timezone=True), default=lambda: datetime.now(timezone.utc)) password_reset_token = Column(TEXT, nullable=True) password_reset_expires = Column(TIMESTAMP(timezone=True), nullable=True) + confirmation_token = Column(TEXT, nullable=True) + confirmation_sent_at = Column(TIMESTAMP(timezone=True), nullable=True) + confirmed_at = Column(TIMESTAMP(timezone=True), nullable=True) admin = Column(Integer) admin_role = Column(Integer) confirmed = Column(Integer) @@ -35,6 +38,9 @@ class Account_User(Base): confirmed, active=1, user_id=None, + confirmation_token=None, + confirmation_sent_at=None, + confirmed_at=None ): self.username = username self.account_number = account_number @@ -48,6 +54,9 @@ class Account_User(Base): self.confirmed = confirmed self.active = active self.user_id = user_id + self.confirmation_token = confirmation_token + self.confirmation_sent_at = confirmation_sent_at + self.confirmed_at = confirmed_at def is_authenticated(self): return True diff --git a/routes/auth/__init__.py b/routes/auth/__init__.py index 74cc9f1..ced8c19 100644 --- a/routes/auth/__init__.py +++ b/routes/auth/__init__.py @@ -4,6 +4,7 @@ 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 +from .confirm import router as confirm_router router = APIRouter() router.include_router(login_router) @@ -11,3 +12,4 @@ router.include_router(register_router) router.include_router(new_router) router.include_router(current_user_router) router.include_router(lost_password_router) +router.include_router(confirm_router) diff --git a/routes/auth/confirm.py b/routes/auth/confirm.py new file mode 100644 index 0000000..e55e0fd --- /dev/null +++ b/routes/auth/confirm.py @@ -0,0 +1,25 @@ +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 datetime import datetime, timezone + +router = APIRouter() + +@router.get("/confirm-email") +async def confirm_email(token: str, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(Account_User).where(Account_User.confirmation_token == token)) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException(status_code=400, detail="Invalid token") + + if user.confirmed: + return {"message": "Account already confirmed"} + + user.confirmed = 1 + user.confirmed_at = datetime.now(timezone.utc) + await db.commit() + + return {"message": "Email confirmed successfully"} diff --git a/routes/auth/login.py b/routes/auth/login.py index 3353ad0..cf0432e 100644 --- a/routes/auth/login.py +++ b/routes/auth/login.py @@ -37,6 +37,12 @@ async def authenticate_user(db: AsyncSession, email: str, password: str): return False if not verify_password(password, user.password_hash): return False + if not user.confirmed: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Please confirm your email address before logging in.", + headers={"WWW-Authenticate": "Bearer"}, + ) # Update last_seen user.last_seen = datetime.utcnow() await db.commit() diff --git a/routes/auth/new.py b/routes/auth/new.py index 9c302b3..136cfe3 100644 --- a/routes/auth/new.py +++ b/routes/auth/new.py @@ -1,3 +1,4 @@ +import secrets import random import string import os @@ -99,6 +100,7 @@ async def register_new_customer(customer: NewCustomerCreate, db: AsyncSession = # Create account user username = account_number hashed_password = get_password_hash(customer.password) + token = secrets.token_urlsafe(32) db_user = Account_User( username=username, account_number=account_number, @@ -109,13 +111,21 @@ async def register_new_customer(customer: NewCustomerCreate, db: AsyncSession = last_seen=datetime.utcnow(), admin=0, admin_role=0, - confirmed=1, + confirmed=0, active=1, - user_id=db_customer.id + user_id=db_customer.id, + confirmation_token=token, + confirmation_sent_at=datetime.utcnow() ) db.add(db_user) await db.commit() await db.refresh(db_user) + + # In a real application, you would send an email here + # For now, we'll just print the confirmation URL to the console + confirmation_url = f"http://localhost:3000/confirm-email?token={token}" + print(f"Confirmation URL: {confirmation_url}") + return db_user @router.post("/step3") @@ -504,7 +514,7 @@ async def create_customer_step1(customer: CustomerCreateStep1, db: AsyncSession customer_data.pop('house_description') # Remove from customer data customer_data.update({ 'account_number': account_number, - 'customer_state': 1, # Default + 'customer_state': 0, # Default 'customer_automatic': 0, 'company_id': 1, # Default 'customer_latitude': '0', @@ -568,6 +578,7 @@ async def create_customer_account(account_data: CustomerAccountCreate, db: Async # Create account user username = account_data.account_number hashed_password = get_password_hash(account_data.password) + token = secrets.token_urlsafe(32) db_user = Account_User( username=username, account_number=account_data.account_number, @@ -578,11 +589,19 @@ async def create_customer_account(account_data: CustomerAccountCreate, db: Async last_seen=datetime.utcnow(), admin=0, admin_role=0, - confirmed=1, + confirmed=0, active=1, - user_id=customer.id + user_id=customer.id, + confirmation_token=token, + confirmation_sent_at=datetime.utcnow() ) db.add(db_user) await db.commit() await db.refresh(db_user) + + # In a real application, you would send an email here + # For now, we'll just print the confirmation URL to the console + confirmation_url = f"http://localhost:3000/confirm-email?token={token}" + print(f"Confirmation URL: {confirmation_url}") + return db_user diff --git a/routes/auth/register.py b/routes/auth/register.py index c434b12..5995d6e 100644 --- a/routes/auth/register.py +++ b/routes/auth/register.py @@ -1,12 +1,17 @@ +import secrets +from fastapi.responses import JSONResponse + +# Existing imports... 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 schemas import UserCreate from passlib.context import CryptContext -from datetime import datetime +from datetime import datetime, timezone +# Existing pwd_context and helper functions... pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") def get_password_hash(password): @@ -25,14 +30,13 @@ def escape_like_pattern(value: str) -> str: router = APIRouter() -@router.post("/register", response_model=UserResponse) +@router.post("/register") 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( @@ -51,21 +55,31 @@ async def register(user: UserCreate, db: AsyncSession = Depends(get_db)): username = f"{user.account_number}-{user.house_number}" hashed_password = get_password_hash(user.password) + token = secrets.token_urlsafe(32) + db_user = Account_User( username=username, account_number=user.account_number, house_number=user.house_number, password_hash=hashed_password, - member_since=datetime.utcnow(), + member_since=datetime.now(timezone.utc), email=user.email, - last_seen=datetime.utcnow(), + last_seen=datetime.now(timezone.utc), admin=0, admin_role=0, - confirmed=1, + confirmed=0, active=1, - user_id=customer.id + user_id=customer.id, + confirmation_token=token, + confirmation_sent_at=datetime.now(timezone.utc) ) db.add(db_user) await db.commit() await db.refresh(db_user) - return db_user + + # In a real application, you would send an email here + # For now, we'll just print the confirmation URL to the console + confirmation_url = f"http://localhost:3000/confirm-email?token={token}" + print(f"Confirmation URL: {confirmation_url}") + + return JSONResponse(status_code=201, content={"message": "Registration successful. Please check your email to confirm your account."}) diff --git a/routes/info/pricing.py b/routes/info/pricing.py index c64ce24..f2e9c41 100644 --- a/routes/info/pricing.py +++ b/routes/info/pricing.py @@ -19,5 +19,8 @@ async def get_current_pricing(db: AsyncSession = Depends(get_db)): return { "price_per_gallon": float(pricing.price_for_customer), + "price_same_day": float(pricing.price_same_day) if pricing.price_same_day else 0.00, + "price_prime": float(pricing.price_prime) if pricing.price_prime else 0.00, + "price_emergency": float(pricing.price_emergency) if pricing.price_emergency else 0.00, "date": pricing.date.isoformat() if pricing.date else None }