From 60229a49b3bfa7cf18e350acf36d7debe33c433a62d7d725e0e003d55918e5d1 Mon Sep 17 00:00:00 2001 From: vladislav Date: Thu, 10 Sep 2026 06:31:12 -0400 Subject: [PATCH] feat --- main.ipynb | 135 +++++++++--- main.py | 135 +++++++++--- templates/login.html | 108 +++++++--- templates/profile/password.html | 364 +++++++++++++++++++++++++++++++- templates/registration.html | 21 +- templates/restore_token.html | 21 +- 6 files changed, 682 insertions(+), 102 deletions(-) diff --git a/main.ipynb b/main.ipynb index c13b577..dd0eb9f 100644 --- a/main.ipynb +++ b/main.ipynb @@ -1086,7 +1086,14 @@ "metadata": {}, "outputs": [], "source": [ - "@app.post(\"/api/login\")\n", + "import hashlib\n", + "\n", + "def sha256(text: str) -> str:\n", + " \"\"\"Server-side SHA-256 (hex)\"\"\"\n", + " return hashlib.sha256(text.encode(\"utf-8\")).hexdigest()\n", + "\n", + "\n", + "@router.post(\"/api/login\")\n", "async def login(\n", " request: Request,\n", " response: Response,\n", @@ -1096,57 +1103,86 @@ " await check_bruteforce(identifier)\n", "\n", " or_conditions = [\n", - " {\"email\": identifier},\n", - " {\"email\": identifier.lower()},\n", - " ]\n", - " \n", + " {\"email\": identifier},\n", + " {\"email\": identifier.lower()},\n", + " ]\n", + "\n", " try:\n", " normalized_phone = normalize_phone(identifier)\n", " if normalized_phone:\n", " or_conditions.append({\"phone\": normalized_phone})\n", - " except:\n", + " except Exception:\n", " pass\n", - " users = [doc async for doc in users_collection.find({\"$or\": or_conditions})]\n", "\n", + " users = [doc async for doc in users_collection.find({\"$or\": or_conditions})]\n", "\n", " if not users:\n", " await increment_failed_attempt(identifier)\n", - " raise HTTPException(401, \"Incorrect credentials\")\n", + " raise HTTPException(status_code=401, detail=\"Incorrect credentials\")\n", "\n", - " # Track if a valid password was found\n", - " valid_user_found = False\n", + " matched_user = None\n", + " new_hash = None\n", + " is_hash_login = login_data.client_id == \"web_hash\"\n", "\n", " for user in users:\n", " stored = user.get(\"hashed_password\")\n", - "\n", " if isinstance(stored, bytes):\n", " stored = stored.decode()\n", "\n", - " # Check if the password is hashed with Argon2\n", - " if stored.startswith(\"$argon2\"):\n", - " ok = verify_password(login_data.password, stored)\n", - " if ok:\n", - " valid_user_found = True\n", - " break # Password is valid, exit the loop\n", - " \n", - " # Check if the password is hashed with bcrypt\n", - " elif stored.startswith(\"$2y$\"):\n", - " stored = stored.replace(\"$2y$\", \"$2b$\", 1)\n", - " if bcrypt.checkpw(login_data.password.encode(), stored.encode()):\n", - " valid_user_found = True\n", - " new_hash = get_password_hash(login_data.password)\n", - " break # Password is valid, exit the loop\n", + " if not stored:\n", + " continue\n", "\n", - " # If no valid user was found, increment attempts and raise error\n", - " if not valid_user_found:\n", + " # ---------- 1. Client sent SHA-256 hash ----------\n", + " if is_hash_login:\n", + " # Stored value is expected to be Argon2(SHA256(password))\n", + " if stored.startswith(\"$argon2\"):\n", + " if verify_password(login_data.password, stored):\n", + " matched_user = user\n", + " break\n", + "\n", + " # ---------- 2. Client sent plaintext ----------\n", + " else:\n", + " # Modern: Argon2(SHA256(plain))\n", + " if stored.startswith(\"$argon2\"):\n", + " # Try verifying as if stored is Argon2(SHA256(plain))\n", + " hashed_input = sha256(login_data.password)\n", + " if verify_password(hashed_input, stored):\n", + " matched_user = user\n", + " break\n", + "\n", + " # Also try classic Argon2(plain) for very old accounts\n", + " if verify_password(login_data.password, stored):\n", + " matched_user = user\n", + " # Force migration to Argon2(SHA256)\n", + " new_hash = get_password_hash(sha256(login_data.password))\n", + " break\n", + "\n", + " # Legacy bcrypt(plain)\n", + " elif stored.startswith((\"$2y$\", \"$2b$\", \"$2a$\")):\n", + " stored_bcrypt = stored.replace(\"$2y$\", \"$2b$\", 1)\n", + " try:\n", + " if bcrypt.checkpw(\n", + " login_data.password.encode(\"utf-8\"),\n", + " stored_bcrypt.encode(\"utf-8\")\n", + " ):\n", + " matched_user = user\n", + " # Migrate to Argon2(SHA256(plain))\n", + " new_hash = get_password_hash(sha256(login_data.password))\n", + " break\n", + " except Exception:\n", + " continue\n", + "\n", + " if not matched_user:\n", " await increment_failed_attempt(identifier)\n", - " raise HTTPException(401, \"Incorrect credentials\")\n", + " raise HTTPException(status_code=401, detail=\"Incorrect credentials\")\n", "\n", - " # If we reach this point, authentication succeeded\n", - " # Proceed with further logic such as generating tokens or returning user details\n", + " # Apply migration if needed\n", + " if new_hash:\n", + " await users_collection.update_one(\n", + " {\"_id\": matched_user[\"_id\"]},\n", + " {\"$set\": {\"hashed_password\": new_hash}}\n", + " )\n", "\n", - " # await users_collection.update_one({\"_id\": user[\"_id\"]}, {\"$set\": {\"hashed_password\": new_hash}})\n", - " \n", " await reset_failed_attempts(identifier)\n", "\n", " user_id_str = str(user[\"_id\"])\n", @@ -1915,6 +1951,41 @@ " )\n", "\n", "\n", + "@router.post(\"/api/v2/reset-password\")\n", + "async def reset_password_user(\n", + " data: ResetPasswordRequest = Body(...),\n", + " current_user: Dict = Security(get_current_user)\n", + "):\n", + " user_id_str = current_user[\"id\"]\n", + "\n", + " session_key = f\"user_sessions:{user_id_str}\"\n", + " tokens = await redis_client.smembers(session_key)\n", + " for t in tokens:\n", + " await redis_client.delete(t)\n", + " await redis_client.delete(session_key)\n", + "\n", + " for cookie_name in request.cookies.keys():\n", + " if cookie_name in ['zoom']:\n", + " continue\n", + "\n", + " for dom in [None,'.kultmost.ru','www.kultmost.ru','avtopost.kultmost.ru','avtopost.kultmost.ru','kultmost.ru']:\n", + " response.delete_cookie(\n", + " key=cookie_name,\n", + " domain=dom,\n", + " path=\"/\", \n", + " secure=False, \n", + " httponly=False,\n", + " )\n", + " hashed_password = get_password_hash(data.new_password)\n", + " \n", + " await users_collection.update_one(\n", + " {\"_id\": ObjectId(user_id_str)},\n", + " {\"$set\": {\"hashed_password\": hashed_password}}\n", + " )\n", + " \n", + " return {\"ok\": True, \"message\": \"Пароль успешно изменён\", \"next\": token_data.get('next', '/profile')}\n", + "\n", + "\n", "@router.post(\"/api/reset-password\")\n", "async def reset_password(\n", " data: ResetPasswordRequest = Body(...),\n", diff --git a/main.py b/main.py index ad3aa46..d9c817d 100644 --- a/main.py +++ b/main.py @@ -797,7 +797,14 @@ async def login_token(token: str, request: Request, response: Response): return RedirectResponse(next_url,headers=response.headers) -@app.post("/api/login") +import hashlib + +def sha256(text: str) -> str: + """Server-side SHA-256 (hex)""" + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +@router.post("/api/login") async def login( request: Request, response: Response, @@ -807,57 +814,86 @@ async def login( await check_bruteforce(identifier) or_conditions = [ - {"email": identifier}, - {"email": identifier.lower()}, - ] - + {"email": identifier}, + {"email": identifier.lower()}, + ] + try: normalized_phone = normalize_phone(identifier) if normalized_phone: or_conditions.append({"phone": normalized_phone}) - except: + except Exception: pass - users = [doc async for doc in users_collection.find({"$or": or_conditions})] + users = [doc async for doc in users_collection.find({"$or": or_conditions})] if not users: await increment_failed_attempt(identifier) - raise HTTPException(401, "Incorrect credentials") + raise HTTPException(status_code=401, detail="Incorrect credentials") - # Track if a valid password was found - valid_user_found = False + matched_user = None + new_hash = None + is_hash_login = login_data.client_id == "web_hash" for user in users: stored = user.get("hashed_password") - if isinstance(stored, bytes): stored = stored.decode() - # Check if the password is hashed with Argon2 - if stored.startswith("$argon2"): - ok = verify_password(login_data.password, stored) - if ok: - valid_user_found = True - break # Password is valid, exit the loop - - # Check if the password is hashed with bcrypt - elif stored.startswith("$2y$"): - stored = stored.replace("$2y$", "$2b$", 1) - if bcrypt.checkpw(login_data.password.encode(), stored.encode()): - valid_user_found = True - new_hash = get_password_hash(login_data.password) - break # Password is valid, exit the loop + if not stored: + continue - # If no valid user was found, increment attempts and raise error - if not valid_user_found: + # ---------- 1. Client sent SHA-256 hash ---------- + if is_hash_login: + # Stored value is expected to be Argon2(SHA256(password)) + if stored.startswith("$argon2"): + if verify_password(login_data.password, stored): + matched_user = user + break + + # ---------- 2. Client sent plaintext ---------- + else: + # Modern: Argon2(SHA256(plain)) + if stored.startswith("$argon2"): + # Try verifying as if stored is Argon2(SHA256(plain)) + hashed_input = sha256(login_data.password) + if verify_password(hashed_input, stored): + matched_user = user + break + + # Also try classic Argon2(plain) for very old accounts + if verify_password(login_data.password, stored): + matched_user = user + # Force migration to Argon2(SHA256) + new_hash = get_password_hash(sha256(login_data.password)) + break + + # Legacy bcrypt(plain) + elif stored.startswith(("$2y$", "$2b$", "$2a$")): + stored_bcrypt = stored.replace("$2y$", "$2b$", 1) + try: + if bcrypt.checkpw( + login_data.password.encode("utf-8"), + stored_bcrypt.encode("utf-8") + ): + matched_user = user + # Migrate to Argon2(SHA256(plain)) + new_hash = get_password_hash(sha256(login_data.password)) + break + except Exception: + continue + + if not matched_user: await increment_failed_attempt(identifier) - raise HTTPException(401, "Incorrect credentials") + raise HTTPException(status_code=401, detail="Incorrect credentials") - # If we reach this point, authentication succeeded - # Proceed with further logic such as generating tokens or returning user details + # Apply migration if needed + if new_hash: + await users_collection.update_one( + {"_id": matched_user["_id"]}, + {"$set": {"hashed_password": new_hash}} + ) - # await users_collection.update_one({"_id": user["_id"]}, {"$set": {"hashed_password": new_hash}}) - await reset_failed_attempts(identifier) user_id_str = str(user["_id"]) @@ -1432,6 +1468,41 @@ async def restore_page(request: Request, token: str): ) +@router.post("/api/v2/reset-password") +async def reset_password_user( + data: ResetPasswordRequest = Body(...), + current_user: Dict = Security(get_current_user) +): + user_id_str = current_user["id"] + + session_key = f"user_sessions:{user_id_str}" + tokens = await redis_client.smembers(session_key) + for t in tokens: + await redis_client.delete(t) + await redis_client.delete(session_key) + + for cookie_name in request.cookies.keys(): + if cookie_name in ['zoom']: + continue + + for dom in [None,'.kultmost.ru','www.kultmost.ru','avtopost.kultmost.ru','avtopost.kultmost.ru','kultmost.ru']: + response.delete_cookie( + key=cookie_name, + domain=dom, + path="/", + secure=False, + httponly=False, + ) + hashed_password = get_password_hash(data.new_password) + + await users_collection.update_one( + {"_id": ObjectId(user_id_str)}, + {"$set": {"hashed_password": hashed_password}} + ) + + return {"ok": True, "message": "Пароль успешно изменён", "next": token_data.get('next', '/profile')} + + @router.post("/api/reset-password") async def reset_password( data: ResetPasswordRequest = Body(...), diff --git a/templates/login.html b/templates/login.html index 0b8fb12..8179ad9 100644 --- a/templates/login.html +++ b/templates/login.html @@ -72,6 +72,24 @@ function sendToNextPage(e) { // @license-end + + {{ footer.section(user) }} {% endblock %} + {% block head %} {{ header.style() }} {{ profile_nav.style() }} + {{ footer.style() }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/registration.html b/templates/registration.html index 58b0526..580a9c1 100644 --- a/templates/registration.html +++ b/templates/registration.html @@ -143,6 +143,24 @@ document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelector(" +