Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 8 additions & 25 deletions backend/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def register(req: UserRegisterRequest, request: Request):
expires_at=expires_at,
)

print(f"\n[AUTH] Generated verification link for {user.email}:\n{cfg.app_url}/verify-email?token={verify_token}\n")
print(f"[AUTH] Generated email verification link for {user.email}")
try:
email_service.send_verification(recipient=user.email, token=verify_token, app_url=cfg.app_url)
print(f"[AUTH] Successfully dispatched verification email to {user.email}")
Expand Down Expand Up @@ -355,20 +355,11 @@ def confirm_email_verification(req: EmailVerificationConfirm):
clean_token = req.token.strip()
token_hash = hashlib.sha256(clean_token.encode("utf-8")).hexdigest()
storage = get_storage()
record = storage.get_auth_token(token_hash, "verify_email")
user_id = storage.consume_verify_token_and_verify_user(token_hash)

if not record:
if not user_id:
raise HTTPException(status_code=400, detail="Invalid or expired verification link.")

now_iso = datetime.now(timezone.utc).isoformat()
if record["expires_at"] < now_iso:
raise HTTPException(status_code=400, detail="Verification link has expired. Please request a new one.")

storage.consume_auth_token(record["id"])
storage.invalidate_prior_auth_tokens(record["user_id"], "verify_email")
storage.update_user_verification(record["user_id"], verified=True)
storage.increment_user_session_version(record["user_id"])

return {"success": True, "message": "Email verified successfully. You may now log in."}


Expand Down Expand Up @@ -407,14 +398,14 @@ def request_password_reset(req: PasswordResetRequest, request: Request):
storage.invalidate_prior_auth_tokens(raw_user["id"], "reset_password")
storage.create_auth_token(raw_user["id"], token_hash, "reset_password", expires_at)

print(f"\n[AUTH] Generated password reset link for {email}:\n{cfg.app_url}/reset-password?token={reset_token}\n")
print(f"[AUTH] Generated password reset link for {email}")
try:
email_service.send_password_reset(recipient=email, token=reset_token, app_url=cfg.app_url)
print(f"[AUTH] Successfully dispatched password reset email to {email}")
except Exception as e:
print(f"[AUTH ERROR] Failed to send password reset email: {e}")
else:
print(f"\n[AUTH] Password reset requested for '{email}', but no user account exists with this email address in the database.\n")
pass

# Generic response to prevent account enumeration
return {"message": "If an account exists with that email, a password reset link has been sent."}
Expand All @@ -425,20 +416,12 @@ def confirm_password_reset(req: PasswordResetConfirm):
clean_token = req.token.strip()
token_hash = hashlib.sha256(clean_token.encode("utf-8")).hexdigest()
storage = get_storage()
record = storage.get_auth_token(token_hash, "reset_password")
hashed = hash_password(req.new_password)
user_id = storage.consume_reset_token_and_update_password(token_hash, hashed)

if not record:
if not user_id:
raise HTTPException(status_code=400, detail="Invalid or expired password reset link.")

now_iso = datetime.now(timezone.utc).isoformat()
if record["expires_at"] < now_iso:
raise HTTPException(status_code=400, detail="Password reset link has expired. Please request a new one.")

hashed = hash_password(req.new_password)
storage.consume_auth_token(record["id"])
storage.invalidate_prior_auth_tokens(record["user_id"], "reset_password")
storage.update_user_password(record["user_id"], hashed, increment_session=True)

return {"success": True, "message": "Password updated successfully. Please log in with your new password."}


Expand Down
41 changes: 19 additions & 22 deletions backend/services/document_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,30 +123,27 @@ def extract_text_from_file(file_bytes: bytes, filename: str) -> str:

# 2. Word (.docx, .doc)
if ext in [".docx", ".doc"]:
if ext == ".doc":
# Attempt docx parse in case it was a misnamed OOXML file; otherwise reject binary .doc
try:
import docx
doc = docx.Document(io.BytesIO(file_bytes))
except Exception:
# Guard against decompression bombs in zip-based docx and doc files
try:
with zipfile.ZipFile(io.BytesIO(file_bytes)) as zf:
total_uncompressed = sum(info.file_size for info in zf.infolist())
if total_uncompressed > MAX_DOCX_DECOMPRESSED_BYTES:
raise ValueError(
f"Document exceeds maximum decompressed size limit of 50 MB (got {total_uncompressed // (1024*1024)} MB)."
)
except zipfile.BadZipFile:
if ext == ".doc":
raise ValueError("Legacy .doc binary format is not supported. Please convert and save your resume as .docx or .pdf.")
else:
# Guard against decompression bombs in zip-based docx files
try:
with zipfile.ZipFile(io.BytesIO(file_bytes)) as zf:
total_uncompressed = sum(info.file_size for info in zf.infolist())
if total_uncompressed > MAX_DOCX_DECOMPRESSED_BYTES:
raise ValueError(
f"Document exceeds maximum decompressed size limit of 50 MB (got {total_uncompressed // (1024*1024)} MB)."
)
except zipfile.BadZipFile:
pass

try:
import docx
doc = docx.Document(io.BytesIO(file_bytes))
except Exception as e:
raise ValueError(f"Failed to read Word document: {str(e)}. Please save as .docx or .pdf.")
try:
import docx
doc = docx.Document(io.BytesIO(file_bytes))
except ValueError:
raise
except Exception as e:
if ext == ".doc":
raise ValueError("Legacy .doc binary format is not supported. Please convert and save your resume as .docx or .pdf.")
raise ValueError(f"Failed to read Word document: {str(e)}. Please save as .docx or .pdf.")

lines = []
total_chars = 0
Expand Down
45 changes: 28 additions & 17 deletions backend/services/resume_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,35 @@ def reserve_and_upload(
max_resumes: int = 10,
max_storage_bytes: int = 50 * 1024 * 1024,
) -> Resume:
current_bytes = self.storage.get_user_upload_bytes(user_id)
if current_bytes + len(content_bytes) > max_storage_bytes:
raise ValueError("Maximum total storage limit of 50MB reached for resumes.")

file_key = self.object_storage.upload_file(
content_bytes=content_bytes,
filename=filename,
content_type=content_type,
user_id=user_id,
)

storage_backend = "r2" if self.object_storage.is_configured else "local"
attachment = self.storage.create_attachment(

attachment = self.storage.reserve_upload_bytes(
user_id=user_id,
storage_backend=storage_backend,
object_key=file_key,
size_bytes=len(content_bytes),
max_storage_bytes=max_storage_bytes,
max_resumes=max_resumes,
original_filename=filename,
content_type=content_type,
size_bytes=len(content_bytes),
storage_backend=storage_backend,
)

final_name = resume_name.strip() if (resume_name and resume_name.strip()) else filename
file_key = None
try:
file_key = self.object_storage.upload_file(
content_bytes=content_bytes,
filename=filename,
content_type=content_type,
user_id=user_id,
)

self.storage.finalize_attachment(
attachment_id=attachment.id,
object_key=file_key,
storage_backend=storage_backend,
user_id=user_id,
)

final_name = resume_name.strip() if (resume_name and resume_name.strip()) else filename
resume = self.storage.add_resume(
name=final_name,
content=text_content,
Expand All @@ -64,10 +70,15 @@ def reserve_and_upload(
)
return resume
except Exception:
self.object_storage.delete_file(file_key, user_id=user_id)
if file_key:
try:
self.object_storage.delete_file(file_key, user_id=user_id)
except Exception:
pass
self.storage.update_attachment_deletion_state(attachment.id, "failed", user_id=user_id)
raise


def delete_resume(self, user_id: str, resume_id: str) -> bool:
resume = self.storage.get_resume(resume_id, user_id=user_id)
if not resume:
Expand Down
Loading
Loading