import secrets import random import time from fastapi import APIRouter, Depends, HTTPException from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session from app.database.session import get_db from app.models.user import User from app.schemas.auth import UserCreate from app.schemas.password import ForgotPasswordRequest, ResetPasswordRequest from app.core.security import hash_password, verify_password, create_access_token from app.core.dependencies import get_current_user router = APIRouter() # Signup OTP store: # email -> {code, expires_at, username, password} # In production this should eventually move to Redis/DB-backed storage. _otp_store: dict[str, dict] = {} # Password reset token store: # token -> {email, expires_at} # In production this should eventually move to Redis/DB-backed storage. _reset_tokens: dict[str, dict] = {} @router.post("/send-otp") def send_otp(payload: dict, db: Session = Depends(get_db)): """ Generate and send a 6-digit OTP for email verification during signup. """ from app.services.email_service import send_otp_email email = payload.get("email", "").strip().lower() username = payload.get("username", "").strip() password = payload.get("password", "") if not email: raise HTTPException( status_code=422, detail="Email is required.", ) if not username: raise HTTPException( status_code=422, detail="Username is required.", ) if not password: raise HTTPException( status_code=422, detail="Password is required.", ) if "@" not in email or "." not in email.split("@")[-1]: raise HTTPException( status_code=400, detail="Invalid email address.", ) # Check email existing = db.query(User).filter(User.email == email).first() if existing: raise HTTPException( status_code=400, detail="Email already registered.", ) # Check username existing_username = ( db.query(User) .filter(User.username == username) .first() ) if existing_username: raise HTTPException( status_code=400, detail="Username already taken.", ) # Generate 6-digit OTP otp_code = f"{random.randint(100000, 999999)}" _otp_store[email] = { "code": otp_code, "expires_at": time.time() + 600, "username": username, "password": password, } # Send verification email email_sent = send_otp_email(email, otp_code) if not email_sent: del _otp_store[email] raise HTTPException( status_code=502, detail="Unable to send verification email. Please try again.", ) return { "message": f"Verification code sent to {email}" } @router.post("/verify-otp") def verify_otp(payload: dict, db: Session = Depends(get_db)): """ Verify signup OTP and create the user. """ email = payload.get("email", "").strip().lower() code = payload.get("code", "").strip() if not email or not code: raise HTTPException( status_code=422, detail="Email and OTP code are required.", ) stored = _otp_store.get(email) if not stored: raise HTTPException( status_code=400, detail="No OTP found for this email. Request a new one.", ) if time.time() > stored["expires_at"]: del _otp_store[email] raise HTTPException( status_code=400, detail="OTP expired. Request a new one.", ) if stored["code"] != code: raise HTTPException( status_code=400, detail="Invalid OTP code.", ) # Check email again in case it was registered after OTP was sent existing = db.query(User).filter(User.email == email).first() if existing: del _otp_store[email] raise HTTPException( status_code=400, detail="Email already registered.", ) # Check username again existing_username = ( db.query(User) .filter(User.username == stored["username"]) .first() ) if existing_username: del _otp_store[email] raise HTTPException( status_code=400, detail="Username already taken.", ) new_user = User( username=stored["username"], email=email, hashed_password=hash_password(stored["password"]), ) db.add(new_user) try: db.commit() db.refresh(new_user) except Exception: db.rollback() raise HTTPException( status_code=400, detail="Unable to create account. Please try again.", ) del _otp_store[email] return { "message": "Account verified and created successfully." } @router.post("/signup") def signup(user: UserCreate, db: Session = Depends(get_db)): """ Direct signup endpoint. """ existing = db.query(User).filter(User.email == user.email).first() if existing: raise HTTPException( status_code=400, detail="Email already registered", ) existing_username = ( db.query(User) .filter(User.username == user.username) .first() ) if existing_username: raise HTTPException( status_code=400, detail="Username already taken", ) new_user = User( username=user.username, email=user.email, hashed_password=hash_password(user.password), ) db.add(new_user) try: db.commit() db.refresh(new_user) except Exception: db.rollback() raise HTTPException( status_code=400, detail="Unable to create account. Please try again.", ) return { "message": "User created successfully" } @router.post("/login") def login( form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db), ): db_user = ( db.query(User) .filter(User.email == form_data.username) .first() ) if not db_user or not verify_password( form_data.password, db_user.hashed_password, ): raise HTTPException( status_code=401, detail="Invalid credentials", ) access_token = create_access_token( data={"sub": db_user.email} ) return { "access_token": access_token, "token_type": "bearer", } @router.post("/demo-login") def demo_login(db: Session = Depends(get_db)): """ Evaluator demo login endpoint. Directly logs into an active populated workspace or demo user account without requiring email verification, allowing instant project exploration. """ from app.models.workspace import Workspace # 1. Try to find dedicated demo user first demo_user = db.query(User).filter(User.email == "demo@docweave.io").first() if not demo_user: # Check if there is an active user with workspaces in the DB populated_user = ( db.query(User) .join(Workspace, Workspace.created_by == User.id) .first() ) if populated_user: demo_user = populated_user else: # Create fresh demo user demo_user = User( username="Demo Evaluator", email="demo@docweave.io", hashed_password=hash_password("docweave123"), role="operator", ) db.add(demo_user) db.commit() db.refresh(demo_user) # Create default demo workspace ws = Workspace( name="Cardiology Research", description="Clinical cardiology documentation & protocol verification", created_by=demo_user.id, ) db.add(ws) db.commit() access_token = create_access_token(data={"sub": demo_user.email}) return { "access_token": access_token, "token_type": "bearer", "email": demo_user.email, "username": demo_user.username, } @router.get("/me") def get_me(current_user: User = Depends(get_current_user)): return { "id": current_user.id, "username": current_user.username, "email": current_user.email, "role": current_user.role, } @router.post("/forgot-password") def forgot_password( email_data: ForgotPasswordRequest, db: Session = Depends(get_db), ): """ Generate a password reset token and email a reset link. """ from app.services.email_service import send_reset_email email = email_data.email.strip().lower() user = ( db.query(User) .filter(User.email == email) .first() ) if not user: raise HTTPException( status_code=404, detail="User not found", ) # Secure reset token token = secrets.token_urlsafe(32) _reset_tokens[token] = { "email": email, "expires_at": time.time() + 3600, } # Send reset link email_sent = send_reset_email(email, token) if not email_sent: del _reset_tokens[token] raise HTTPException( status_code=502, detail="Unable to send password reset email. Please try again.", ) return { "message": "Password reset instructions sent to your email." } @router.post("/reset-password") def reset_password( data: ResetPasswordRequest, db: Session = Depends(get_db), ): """ Reset password using the token received by email. """ stored = _reset_tokens.get(data.token) if not stored: raise HTTPException( status_code=400, detail="Invalid or expired reset link.", ) if time.time() > stored["expires_at"]: del _reset_tokens[data.token] raise HTTPException( status_code=400, detail="Reset link expired. Please request a new one.", ) email = stored["email"] user = ( db.query(User) .filter(User.email == email) .first() ) if not user: del _reset_tokens[data.token] raise HTTPException( status_code=404, detail="User not found", ) user.hashed_password = hash_password(data.new_password) try: db.commit() except Exception: db.rollback() raise HTTPException( status_code=400, detail="Unable to reset password. Please try again.", ) # Token can only be used once del _reset_tokens[data.token] return { "message": "Password reset successful" }