diff --git a/backend/auth.py b/backend/auth.py index 502c759..dc2225b 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -1,7 +1,7 @@ from database import execute, fetch_one, fetch_all import bcrypt from jose import jwt -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from dotenv import load_dotenv import os from fastapi import Depends @@ -10,6 +10,7 @@ from email_service import ( create_verification, send_password_reset_email, create_password_reset, + hash_token, ) import secrets @@ -268,4 +269,67 @@ def reset_password(email: str): return { "status": "sent", + } + +def reset_password_with_token(token: str, new_password: str): + token_hash = hash_token(token) + + reset = fetch_one( + """ + SELECT id, user_id, expires_at + FROM password_reset_tokens + WHERE token_hash = ? + """, + (token_hash,), + ) + + if reset is None: + return { + "status": "failed", + "reason": "invalid_token", + } + + expires_at = datetime.fromisoformat(reset["expires_at"]) + + if datetime.now(timezone.utc) >= expires_at: + execute( + """ + DELETE FROM password_reset_tokens + WHERE id = ? + """, + (reset["id"],), + ) + + return { + "status": "failed", + "reason": "token_expired", + } + + password_hash = bcrypt.hashpw( + new_password.encode(), + bcrypt.gensalt(), + ).decode() + + execute( + """ + UPDATE users + SET password_hash = ? + WHERE id = ? + """, + ( + password_hash, + reset["user_id"], + ), + ) + + execute( + """ + DELETE FROM password_reset_tokens + WHERE id = ? + """, + (reset["id"],), + ) + + return { + "status": "updated", } \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index e2c84d0..1647d73 100644 --- a/backend/main.py +++ b/backend/main.py @@ -30,6 +30,7 @@ from auth import ( change_password, delete_me, reset_password, + reset_password_with_token, ) from permissions import ( @@ -126,6 +127,10 @@ class RoomCreate(BaseModel): class ForgotPassword(BaseModel): email: EmailStr +class PasswordReset(BaseModel): + token: str + new_password: str = Field(min_length=8) + class RoomSearch(BaseModel): invite_code: str @@ -1134,6 +1139,27 @@ def api_forgot_password( ): return reset_password(data.email) +@app.post( + "/update-password", + summary="Reset password with token", + description="Sets a new password using a password reset token.", +) +def api_update_password( + data: PasswordReset, +): + result = reset_password_with_token( + data.token, + data.new_password, + ) + + if result["status"] == "failed": + raise HTTPException( + status_code=400, + detail=result["reason"], + ) + + return result + # easter egg @app.get(