api/app/services/auth_service.py
Yoga Pangestu 104a58e6e7 Add password reset functionality with email notifications
- Implemented endpoints for forgot password and reset password
- Added PasswordResetToken model and relationships
- Integrated email service using Resend for sending reset links
- Updated configuration for email and frontend URL
- Enhanced user model to include password reset tokens
- Added necessary schemas for password reset requests
- Updated requirements to include Resend SDK
- Created comprehensive documentation for the new feature
2026-07-22 20:22:58 +07:00

117 lines
3.3 KiB
Python

import secrets
from datetime import datetime, timedelta, timezone
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app.config import settings
from app.models.password_reset_token import PasswordResetToken
from app.models.user import User
from app.security import hash_password, verify_password
from app.services.email_service import send_password_reset_email
def _generate_token() -> str:
return secrets.token_urlsafe(48)
def _hash_token(token: str) -> str:
return hash_password(token)
def _verify_token_hash(plain_token: str, hashed_token: str) -> bool:
return verify_password(plain_token, hashed_token)
def forgot_password(db: Session, email: str) -> dict:
user = db.query(User).filter(
User.email == email,
User.deleted_at.is_(None),
).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"message": "Email tidak ditemukan."},
)
now = datetime.now(timezone.utc)
active_token = db.query(PasswordResetToken).filter(
PasswordResetToken.user_id == user.id,
PasswordResetToken.used_at.is_(None),
PasswordResetToken.expires_at > now,
).first()
if active_token:
return {"message": "Link reset password sudah dikirim ke email Anda dan masih aktif. Silakan cek inbox atau folder spam Anda."}
db.query(PasswordResetToken).filter(
PasswordResetToken.user_id == user.id,
PasswordResetToken.used_at.is_(None),
).update({"used_at": now})
raw_token = _generate_token()
hashed = _hash_token(raw_token)
reset_token = PasswordResetToken(
user_id=user.id,
token=hashed,
expires_at=now + timedelta(hours=1),
)
db.add(reset_token)
db.commit()
reset_url = f"{settings.FRONTEND_URL}/auth/reset-password?token={raw_token}"
send_password_reset_email(user.email, reset_url)
return {"message": "Link reset password telah dikirim ke email Anda"}
def reset_password(db: Session, token: str, password: str) -> dict:
now = datetime.now(timezone.utc)
tokens = db.query(PasswordResetToken).filter(
PasswordResetToken.user_id.isnot(None),
PasswordResetToken.used_at.is_(None),
).all()
matched = None
for t in tokens:
if _verify_token_hash(token, t.token):
matched = t
break
if not matched:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"message": "Token tidak valid atau sudah kedaluwarsa."},
)
if matched.expires_at.replace(tzinfo=timezone.utc) < now:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"message": "Token tidak valid atau sudah kedaluwarsa."},
)
matched.used_at = now
user = db.query(User).filter(User.id == matched.user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"message": "Token tidak valid atau sudah kedaluwarsa."},
)
user.password = hash_password(password)
db.query(PasswordResetToken).filter(
PasswordResetToken.user_id == user.id,
PasswordResetToken.used_at.is_(None),
).update({"used_at": now})
db.commit()
return {"message": "Password berhasil diubah"}