feat(auth): require email confirmation for new accounts
Updates the user registration and new account creation endpoints to require email confirmation. - Sets the 'confirmed' flag to 'false' by default for all new user accounts. - Generates a unique confirmation token for each new user. - Logs the confirmation link to the console for development purposes. This change ensures that users cannot log in without first verifying their email address, enhancing account security.
This commit is contained in:
@@ -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)
|
||||
|
||||
25
routes/auth/confirm.py
Normal file
25
routes/auth/confirm.py
Normal file
@@ -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"}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."})
|
||||
|
||||
Reference in New Issue
Block a user