Explorar el Código

feat: complete password reset flow with 10m expiry and token cleanup

unknown hace 2 días
padre
commit
4042f1a17d
Se han modificado 3 ficheros con 66 adiciones y 13 borrados
  1. 19 8
      backend/routers/auth.py
  2. 45 1
      backend/services/email_service.py
  3. 2 4
      src/lib/api.ts

+ 19 - 8
backend/routers/auth.py

@@ -12,7 +12,7 @@ import locales
 from dependencies import get_current_user, require_admin
 import config
 import secrets
-from services.email_service import send_verification_email
+from services.email_service import send_verification_email, send_password_reset_email
 
 try:
     from google.oauth2 import id_token
@@ -178,22 +178,33 @@ async def logout(user: dict = Depends(get_current_user)):
     return {"message": "Successfully logged out"}
 
 @router.post("/forgot-password")
-async def forgot_password(request: schemas.ForgotPassword):
-    user = db.execute_query("SELECT id FROM users WHERE email = %s", (request.email,))
+async def forgot_password(request: schemas.ForgotPassword, lang: str = "en"):
+    user = db.execute_query("SELECT id, preferred_language FROM users WHERE email = %s", (request.email,))
     if not user: raise HTTPException(status_code=404, detail="Email not found")
-    token = str(uuid.uuid4())
-    expires_at = datetime.utcnow() + timedelta(minutes=15)
+    
+    token = secrets.token_urlsafe(32)
+    expires_at = datetime.utcnow() + timedelta(minutes=10)
     db.execute_commit("INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (%s, %s, %s)", (user[0]['id'], token, expires_at))
-    return {"message": "Reset instructions sent to your email", "demo_token": token}
+    
+    # Send Email
+    user_lang = user[0]['preferred_language'] or lang
+    send_password_reset_email(request.email, token, user_lang)
+    
+    return {"message": "Reset instructions sent to your email"}
 
 @router.post("/reset-password")
 async def reset_password(request: schemas.ResetPassword):
     reset_data = db.execute_query("SELECT user_id, expires_at FROM password_reset_tokens WHERE token = %s", (request.token,))
     if not reset_data: raise HTTPException(status_code=400, detail="Invalid token")
     if reset_data[0]['expires_at'] < datetime.utcnow(): raise HTTPException(status_code=400, detail="Token expired")
+    
+    user_id = reset_data[0]['user_id']
     hashed_password = auth_utils.get_password_hash(request.new_password)
-    db.execute_commit("UPDATE users SET password_hash = %s WHERE id = %s", (hashed_password, reset_data[0]['user_id']))
-    db.execute_commit("DELETE FROM password_reset_tokens WHERE token = %s", (request.token,))
+    db.execute_commit("UPDATE users SET password_hash = %s WHERE id = %s", (hashed_password, user_id))
+    
+    # IMPORTANT: Delete ALL tokens for this user after successful reset
+    db.execute_commit("DELETE FROM password_reset_tokens WHERE user_id = %s", (user_id,))
+    
     return {"message": "Password reset successfully"}
 
 @router.get("/me", response_model=schemas.UserResponse)

+ 45 - 1
backend/services/email_service.py

@@ -26,7 +26,6 @@ def send_email(to_email: str, subject: str, body_html: str):
 def send_verification_email(to_email: str, token: str, lang: str = "en"):
     verify_url = f"{config.FRONTEND_URL}/{lang}/auth?verify_token={token}"
     
-    # Simple templates based on language
     templates = {
         "en": {
             "subject": "Verify your Radionica 3D account",
@@ -68,3 +67,48 @@ def send_verification_email(to_email: str, token: str, lang: str = "en"):
     
     tpl = templates.get(lang, templates["en"])
     return send_email(to_email, tpl["subject"], tpl["body"])
+
+def send_password_reset_email(to_email: str, token: str, lang: str = "en"):
+    reset_url = f"{config.FRONTEND_URL}/{lang}/auth?token={token}"
+    
+    templates = {
+        "en": {
+            "subject": "Reset your Radionica 3D password",
+            "body": f"""
+                <h2>Password Reset Request</h2>
+                <p>You requested a password reset. Click the button below to set a new password. This link is valid for 10 minutes.</p>
+                <p><a href="{reset_url}" style="padding: 10px 20px; background-color: #000; color: #fff; text-decoration: none; border-radius: 5px;">Reset Password</a></p>
+                <p>Or copy this link: {reset_url}</p>
+            """
+        },
+        "me": {
+            "subject": "Resetovanje lozinke na Radionica 3D",
+            "body": f"""
+                <h2>Zahtjev za resetovanje lozinke</h2>
+                <p>Zatražili ste resetovanje lozinke. Kliknite na dugme ispod kako biste postavili novu lozinku. Ovaj link važi 10 minuta.</p>
+                <p><a href="{reset_url}" style="padding: 10px 20px; background-color: #000; color: #fff; text-decoration: none; border-radius: 5px;">Resetuj lozinku</a></p>
+                <p>Ili kopirajte ovaj link: {reset_url}</p>
+            """
+        },
+        "ru": {
+            "subject": "Сброс пароля Radionica 3D",
+            "body": f"""
+                <h2>Запрос на сброс пароля</h2>
+                <p>Вы запросили сброс пароля. Нажмите на кнопку ниже, чтобы установить новый пароль. Ссылка действительна в течение 10 минут.</p>
+                <p><a href="{reset_url}" style="padding: 10px 20px; background-color: #000; color: #fff; text-decoration: none; border-radius: 5px;">Сбросить пароль</a></p>
+                <p>Или скопируйте эту ссылку: {reset_url}</p>
+            """
+        },
+        "ua": {
+            "subject": "Скидання пароля Radionica 3D",
+            "body": f"""
+                <h2>Запит на скидання пароля</h2>
+                <p>Ви запросили скидання пароля. Натисніть на кнопку нижче, щоб встановити новий пароль. Посилання дійсне протягом 10 хвилин.</p>
+                <p><a href="{reset_url}" style="padding: 10px 20px; background-color: #000; color: #fff; text-decoration: none; border-radius: 5px;">Скинути пароль</a></p>
+                <p>Або скопіюйте це посилання: {reset_url}</p>
+            """
+        }
+    }
+    
+    tpl = templates.get(lang, templates["en"])
+    return send_email(to_email, tpl["subject"], tpl["body"])

+ 2 - 4
src/lib/api.ts

@@ -194,10 +194,8 @@ export const updateProfile = async (userData: any) => {
 
 export const forgotPassword = async (email: string) => {
   const response = await fetch(`${API_BASE_URL}/auth/forgot-password?lang=${i18n.global.locale.value}`, {
-    method: 'POST',
-    headers: { 
-      'Content-Type': 'application/json'
-    },
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
     body: JSON.stringify({ email }),
   });
   if (!response.ok) {