| 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() |
|
|
| |
| |
| |
| _otp_store: dict[str, dict] = {} |
|
|
| |
| |
| |
| _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.", |
| ) |
|
|
| |
| existing = db.query(User).filter(User.email == email).first() |
|
|
| if existing: |
| raise HTTPException( |
| status_code=400, |
| detail="Email already registered.", |
| ) |
|
|
| |
| existing_username = ( |
| db.query(User) |
| .filter(User.username == username) |
| .first() |
| ) |
|
|
| if existing_username: |
| raise HTTPException( |
| status_code=400, |
| detail="Username already taken.", |
| ) |
|
|
| |
| otp_code = f"{random.randint(100000, 999999)}" |
|
|
| _otp_store[email] = { |
| "code": otp_code, |
| "expires_at": time.time() + 600, |
| "username": username, |
| "password": password, |
| } |
|
|
| |
| 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.", |
| ) |
|
|
| |
| existing = db.query(User).filter(User.email == email).first() |
|
|
| if existing: |
| del _otp_store[email] |
|
|
| raise HTTPException( |
| status_code=400, |
| detail="Email already registered.", |
| ) |
|
|
| |
| 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 |
|
|
| |
| demo_user = db.query(User).filter(User.email == "demo@docweave.io").first() |
|
|
| if not demo_user: |
| |
| populated_user = ( |
| db.query(User) |
| .join(Workspace, Workspace.created_by == User.id) |
| .first() |
| ) |
| if populated_user: |
| demo_user = populated_user |
| else: |
| |
| 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) |
|
|
| |
| 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", |
| ) |
|
|
| |
| token = secrets.token_urlsafe(32) |
|
|
| _reset_tokens[token] = { |
| "email": email, |
| "expires_at": time.time() + 3600, |
| } |
|
|
| |
| 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.", |
| ) |
|
|
| |
| del _reset_tokens[data.token] |
|
|
| return { |
| "message": "Password reset successful" |
| } |