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"}