22from fastapi import APIRouter , Depends , HTTPException , status
33from fastapi .security import HTTPBearer
44from sqlalchemy .orm import Session
5- from app .schemas .user import UserLogin , Token , UserCreate , UserOut
5+ from app .schemas .user import (
6+ UserLogin ,
7+ Token ,
8+ UserCreate ,
9+ UserOut ,
10+ PasswordResetRequest ,
11+ PasswordResetVerify ,
12+ PasswordResetConfirm
13+ )
614from app .core .auth import (
715 verify_password ,
816 get_password_hash ,
1624from app .core .email import (
1725 send_verification_email ,
1826 send_welcome_email ,
27+ send_reset_password_email ,
1928 generate_verification_code ,
2029 generate_verification_token ,
2130 get_verification_code_expiry ,
22- get_verification_token_expiry
31+ get_verification_token_expiry ,
32+ get_reset_code_expiry ,
33+ get_reset_token_expiry
2334)
2435from app .database import get_db
2536from app .models .user import User
@@ -352,4 +363,163 @@ def resend_verification_email(email: str, db: Session = Depends(get_db)):
352363 return {
353364 "message" : "Verification email sent successfully" ,
354365 "email" : user .email
366+ }
367+
368+
369+ @router .post ("/password-reset/request" )
370+ def request_password_reset (request : PasswordResetRequest , db : Session = Depends (get_db )):
371+ """
372+ Request password reset by email. Sends reset code and magic link.
373+ Returns success even if email doesn't exist (security best practice).
374+ """
375+ # Find user by email
376+ user = db .query (User ).filter (User .email == request .email ).first ()
377+
378+ # Always return success (don't reveal if email exists)
379+ if not user :
380+ return {
381+ "message" : "If an account exists with this email, you will receive a password reset link." ,
382+ "email" : request .email
383+ }
384+
385+ # Generate reset code and token
386+ reset_code = generate_verification_code () # Reuse existing function (6 digits)
387+ reset_token = generate_verification_token () # Reuse existing function
388+
389+ # Update user with reset data
390+ user .reset_code = reset_code
391+ user .reset_code_expires = get_reset_code_expiry () # 15 minutes
392+ user .reset_token = reset_token
393+ user .reset_token_expires = get_reset_token_expiry () # 15 minutes
394+
395+ db .commit ()
396+
397+ # Send reset email
398+ send_reset_password_email (
399+ to_email = user .email ,
400+ username = user .username or user .id ,
401+ code = reset_code ,
402+ token = reset_token
403+ )
404+
405+ return {
406+ "message" : "If an account exists with this email, you will receive a password reset link." ,
407+ "email" : request .email
408+ }
409+
410+
411+ @router .post ("/password-reset/verify-code" )
412+ def verify_reset_code (request : PasswordResetVerify , db : Session = Depends (get_db )):
413+ """
414+ Verify that the reset code is valid without resetting password.
415+ This allows the frontend to show the new password form.
416+ """
417+ user = db .query (User ).filter (User .email == request .email ).first ()
418+
419+ if not user :
420+ raise HTTPException (
421+ status_code = status .HTTP_404_NOT_FOUND ,
422+ detail = "User not found"
423+ )
424+
425+ # Check if code matches
426+ if user .reset_code != request .code :
427+ raise HTTPException (
428+ status_code = status .HTTP_400_BAD_REQUEST ,
429+ detail = "Invalid reset code"
430+ )
431+
432+ # Check if code has expired
433+ if user .reset_code_expires and user .reset_code_expires < datetime .utcnow ():
434+ raise HTTPException (
435+ status_code = status .HTTP_400_BAD_REQUEST ,
436+ detail = "Reset code has expired. Please request a new one."
437+ )
438+
439+ return {
440+ "message" : "Code verified successfully" ,
441+ "email" : user .email
442+ }
443+
444+
445+ @router .post ("/password-reset/confirm" )
446+ def reset_password_with_code (request : PasswordResetConfirm , db : Session = Depends (get_db )):
447+ """
448+ Reset password using verified code and new password.
449+ """
450+ user = db .query (User ).filter (User .email == request .email ).first ()
451+
452+ if not user :
453+ raise HTTPException (
454+ status_code = status .HTTP_404_NOT_FOUND ,
455+ detail = "User not found"
456+ )
457+
458+ # Check if code matches
459+ if user .reset_code != request .code :
460+ raise HTTPException (
461+ status_code = status .HTTP_400_BAD_REQUEST ,
462+ detail = "Invalid reset code"
463+ )
464+
465+ # Check if code has expired
466+ if user .reset_code_expires and user .reset_code_expires < datetime .utcnow ():
467+ raise HTTPException (
468+ status_code = status .HTTP_400_BAD_REQUEST ,
469+ detail = "Reset code has expired. Please request a new one."
470+ )
471+
472+ # Validate new password
473+ if len (request .new_password ) < 6 :
474+ raise HTTPException (
475+ status_code = status .HTTP_400_BAD_REQUEST ,
476+ detail = "Password must be at least 6 characters long"
477+ )
478+
479+ # Update password
480+ user .hashed_password = get_password_hash (request .new_password )
481+
482+ # Clear reset fields
483+ user .reset_code = None
484+ user .reset_code_expires = None
485+ user .reset_token = None
486+ user .reset_token_expires = None
487+
488+ db .commit ()
489+
490+ return {
491+ "message" : "Password reset successfully. You can now login with your new password." ,
492+ "user" : {
493+ "id" : user .id ,
494+ "email" : user .email ,
495+ "username" : user .username
496+ }
497+ }
498+
499+
500+ @router .get ("/password-reset/verify-token" )
501+ def verify_reset_token (token : str , db : Session = Depends (get_db )):
502+ """
503+ Verify reset token from magic link.
504+ Returns user email if valid, for the frontend to show password reset form.
505+ """
506+ user = db .query (User ).filter (User .reset_token == token ).first ()
507+
508+ if not user :
509+ raise HTTPException (
510+ status_code = status .HTTP_404_NOT_FOUND ,
511+ detail = "Invalid reset link"
512+ )
513+
514+ # Check if token has expired
515+ if user .reset_token_expires and user .reset_token_expires < datetime .utcnow ():
516+ raise HTTPException (
517+ status_code = status .HTTP_400_BAD_REQUEST ,
518+ detail = "Reset link has expired. Please request a new one."
519+ )
520+
521+ return {
522+ "message" : "Reset link is valid" ,
523+ "email" : user .email ,
524+ "code" : user .reset_code # Return code so frontend can use it for password reset
355525 }
0 commit comments