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("
+