Merge pull request 'production' (#11) from production into main
This commit is contained in:
commit
db0cd8395c
147
main.ipynb
147
main.ipynb
@ -1086,6 +1086,13 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
|
"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",
|
||||||
"@app.post(\"/api/login\")\n",
|
"@app.post(\"/api/login\")\n",
|
||||||
"async def login(\n",
|
"async def login(\n",
|
||||||
" request: Request,\n",
|
" request: Request,\n",
|
||||||
@ -1104,48 +1111,77 @@
|
|||||||
" normalized_phone = normalize_phone(identifier)\n",
|
" normalized_phone = normalize_phone(identifier)\n",
|
||||||
" if normalized_phone:\n",
|
" if normalized_phone:\n",
|
||||||
" or_conditions.append({\"phone\": normalized_phone})\n",
|
" or_conditions.append({\"phone\": normalized_phone})\n",
|
||||||
" except:\n",
|
" except Exception:\n",
|
||||||
" pass\n",
|
" pass\n",
|
||||||
" users = [doc async for doc in users_collection.find({\"$or\": or_conditions})]\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
|
" users = [doc async for doc in users_collection.find({\"$or\": or_conditions})]\n",
|
||||||
"\n",
|
"\n",
|
||||||
" if not users:\n",
|
" if not users:\n",
|
||||||
" await increment_failed_attempt(identifier)\n",
|
" await increment_failed_attempt(identifier)\n",
|
||||||
" raise HTTPException(401, \"Incorrect credentials\")\n",
|
" raise HTTPException(status_code=401, detail=\"Incorrect credentials\")\n",
|
||||||
"\n",
|
"\n",
|
||||||
" # Track if a valid password was found\n",
|
" matched_user = None\n",
|
||||||
" valid_user_found = False\n",
|
" new_hash = None\n",
|
||||||
|
" is_hash_login = login_data.client_id == \"web_hash\"\n",
|
||||||
"\n",
|
"\n",
|
||||||
" for user in users:\n",
|
" for user in users:\n",
|
||||||
" stored = user.get(\"hashed_password\")\n",
|
" stored = user.get(\"hashed_password\")\n",
|
||||||
"\n",
|
|
||||||
" if isinstance(stored, bytes):\n",
|
" if isinstance(stored, bytes):\n",
|
||||||
" stored = stored.decode()\n",
|
" stored = stored.decode()\n",
|
||||||
"\n",
|
"\n",
|
||||||
" # Check if the password is hashed with Argon2\n",
|
" if not stored:\n",
|
||||||
|
" continue\n",
|
||||||
|
"\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 stored.startswith(\"$argon2\"):\n",
|
||||||
" ok = verify_password(login_data.password, stored)\n",
|
" if verify_password(login_data.password, stored):\n",
|
||||||
" if ok:\n",
|
" matched_user = user\n",
|
||||||
" valid_user_found = True\n",
|
" break\n",
|
||||||
" break # Password is valid, exit the loop\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
" # Check if the password is hashed with bcrypt\n",
|
" # ---------- 2. Client sent plaintext ----------\n",
|
||||||
" elif stored.startswith(\"$2y$\"):\n",
|
" else:\n",
|
||||||
" stored = stored.replace(\"$2y$\", \"$2b$\", 1)\n",
|
" # Modern: Argon2(SHA256(plain))\n",
|
||||||
" if bcrypt.checkpw(login_data.password.encode(), stored.encode()):\n",
|
" if stored.startswith(\"$argon2\"):\n",
|
||||||
" valid_user_found = True\n",
|
" # Try verifying as if stored is Argon2(SHA256(plain))\n",
|
||||||
" new_hash = get_password_hash(login_data.password)\n",
|
" hashed_input = sha256(login_data.password)\n",
|
||||||
" break # Password is valid, exit the loop\n",
|
" if verify_password(hashed_input, stored):\n",
|
||||||
|
" matched_user = user\n",
|
||||||
|
" break\n",
|
||||||
"\n",
|
"\n",
|
||||||
" # If no valid user was found, increment attempts and raise error\n",
|
" # Also try classic Argon2(plain) for very old accounts\n",
|
||||||
" if not valid_user_found:\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",
|
" await increment_failed_attempt(identifier)\n",
|
||||||
" raise HTTPException(401, \"Incorrect credentials\")\n",
|
" raise HTTPException(status_code=401, detail=\"Incorrect credentials\")\n",
|
||||||
"\n",
|
"\n",
|
||||||
" # If we reach this point, authentication succeeded\n",
|
" # Apply migration if needed\n",
|
||||||
" # Proceed with further logic such as generating tokens or returning user details\n",
|
" if new_hash:\n",
|
||||||
"\n",
|
" await users_collection.update_one(\n",
|
||||||
" # await users_collection.update_one({\"_id\": user[\"_id\"]}, {\"$set\": {\"hashed_password\": new_hash}})\n",
|
" {\"_id\": matched_user[\"_id\"]},\n",
|
||||||
|
" {\"$set\": {\"hashed_password\": new_hash}}\n",
|
||||||
|
" )\n",
|
||||||
"\n",
|
"\n",
|
||||||
" await reset_failed_attempts(identifier)\n",
|
" await reset_failed_attempts(identifier)\n",
|
||||||
"\n",
|
"\n",
|
||||||
@ -1915,6 +1951,43 @@
|
|||||||
" )\n",
|
" )\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
"@app.post(\"/api/reset-hash-password\")\n",
|
||||||
|
"async def reset_hash_password_user(\n",
|
||||||
|
" request: Request,\n",
|
||||||
|
" response: Response,\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\": '/profile'}\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
"@router.post(\"/api/reset-password\")\n",
|
"@router.post(\"/api/reset-password\")\n",
|
||||||
"async def reset_password(\n",
|
"async def reset_password(\n",
|
||||||
" data: ResetPasswordRequest = Body(...),\n",
|
" data: ResetPasswordRequest = Body(...),\n",
|
||||||
@ -4146,6 +4219,30 @@
|
|||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "c7c3772b",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"@app.post(\"/api/admin/delete-user\")\n",
|
||||||
|
"async def delete_user(\n",
|
||||||
|
" payload: DeleteUserRequest,\n",
|
||||||
|
" user: Dict = Security(get_current_user, scopes=[\"admin\"]),\n",
|
||||||
|
"):\n",
|
||||||
|
" try:\n",
|
||||||
|
" result = users_collection.delete_one({\"_id\": ObjectId(payload._id)})\n",
|
||||||
|
" except Exception:\n",
|
||||||
|
" raise HTTPException(status_code=400, detail=\"Неверный ID пользователя\")\n",
|
||||||
|
"\n",
|
||||||
|
" if result.deleted_count == 0:\n",
|
||||||
|
" raise HTTPException(status_code=404, detail=\"Пользователь не найден\")\n",
|
||||||
|
"\n",
|
||||||
|
" return {\"ok\": True}\n"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
|
|||||||
139
main.py
139
main.py
@ -797,6 +797,13 @@ async def login_token(token: str, request: Request, response: Response):
|
|||||||
return RedirectResponse(next_url,headers=response.headers)
|
return RedirectResponse(next_url,headers=response.headers)
|
||||||
|
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def sha256(text: str) -> str:
|
||||||
|
"""Server-side SHA-256 (hex)"""
|
||||||
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/login")
|
@app.post("/api/login")
|
||||||
async def login(
|
async def login(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -815,48 +822,77 @@ async def login(
|
|||||||
normalized_phone = normalize_phone(identifier)
|
normalized_phone = normalize_phone(identifier)
|
||||||
if normalized_phone:
|
if normalized_phone:
|
||||||
or_conditions.append({"phone": normalized_phone})
|
or_conditions.append({"phone": normalized_phone})
|
||||||
except:
|
except Exception:
|
||||||
pass
|
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:
|
if not users:
|
||||||
await increment_failed_attempt(identifier)
|
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
|
matched_user = None
|
||||||
valid_user_found = False
|
new_hash = None
|
||||||
|
is_hash_login = login_data.client_id == "web_hash"
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
stored = user.get("hashed_password")
|
stored = user.get("hashed_password")
|
||||||
|
|
||||||
if isinstance(stored, bytes):
|
if isinstance(stored, bytes):
|
||||||
stored = stored.decode()
|
stored = stored.decode()
|
||||||
|
|
||||||
# Check if the password is hashed with Argon2
|
if not stored:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ---------- 1. Client sent SHA-256 hash ----------
|
||||||
|
if is_hash_login:
|
||||||
|
# Stored value is expected to be Argon2(SHA256(password))
|
||||||
if stored.startswith("$argon2"):
|
if stored.startswith("$argon2"):
|
||||||
ok = verify_password(login_data.password, stored)
|
if verify_password(login_data.password, stored):
|
||||||
if ok:
|
matched_user = user
|
||||||
valid_user_found = True
|
break
|
||||||
break # Password is valid, exit the loop
|
|
||||||
|
|
||||||
# Check if the password is hashed with bcrypt
|
# ---------- 2. Client sent plaintext ----------
|
||||||
elif stored.startswith("$2y$"):
|
else:
|
||||||
stored = stored.replace("$2y$", "$2b$", 1)
|
# Modern: Argon2(SHA256(plain))
|
||||||
if bcrypt.checkpw(login_data.password.encode(), stored.encode()):
|
if stored.startswith("$argon2"):
|
||||||
valid_user_found = True
|
# Try verifying as if stored is Argon2(SHA256(plain))
|
||||||
new_hash = get_password_hash(login_data.password)
|
hashed_input = sha256(login_data.password)
|
||||||
break # Password is valid, exit the loop
|
if verify_password(hashed_input, stored):
|
||||||
|
matched_user = user
|
||||||
|
break
|
||||||
|
|
||||||
# If no valid user was found, increment attempts and raise error
|
# Also try classic Argon2(plain) for very old accounts
|
||||||
if not valid_user_found:
|
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)
|
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
|
# Apply migration if needed
|
||||||
# Proceed with further logic such as generating tokens or returning user details
|
if new_hash:
|
||||||
|
await users_collection.update_one(
|
||||||
# await users_collection.update_one({"_id": user["_id"]}, {"$set": {"hashed_password": new_hash}})
|
{"_id": matched_user["_id"]},
|
||||||
|
{"$set": {"hashed_password": new_hash}}
|
||||||
|
)
|
||||||
|
|
||||||
await reset_failed_attempts(identifier)
|
await reset_failed_attempts(identifier)
|
||||||
|
|
||||||
@ -1432,6 +1468,43 @@ async def restore_page(request: Request, token: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/reset-hash-password")
|
||||||
|
async def reset_hash_password_user(
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
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": '/profile'}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/reset-password")
|
@router.post("/api/reset-password")
|
||||||
async def reset_password(
|
async def reset_password(
|
||||||
data: ResetPasswordRequest = Body(...),
|
data: ResetPasswordRequest = Body(...),
|
||||||
@ -3178,6 +3251,22 @@ async def unsubscribe(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/admin/delete-user")
|
||||||
|
async def delete_user(
|
||||||
|
payload: DeleteUserRequest,
|
||||||
|
user: Dict = Security(get_current_user, scopes=["admin"]),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
result = users_collection.delete_one({"_id": ObjectId(payload._id)})
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=400, detail="Неверный ID пользователя")
|
||||||
|
|
||||||
|
if result.deleted_count == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/profile/admin/users", response_class=HTMLResponse)
|
@app.get("/profile/admin/users", response_class=HTMLResponse)
|
||||||
async def profile_admin_statistics(
|
async def profile_admin_statistics(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@ -70,6 +70,10 @@ class UserLoginForm(OAuth2PasswordRequestForm):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteUserRequest(BaseModel):
|
||||||
|
_id: str
|
||||||
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BaseModel):
|
||||||
identifier: str
|
identifier: str
|
||||||
password: str
|
password: str
|
||||||
|
|||||||
@ -72,6 +72,24 @@ function sendToNextPage(e) {
|
|||||||
// @license-end
|
// @license-end
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
|
async function getSHA256Hash(message) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const msgBuffer = encoder.encode(message);
|
||||||
|
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
||||||
|
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
|
||||||
|
const hashHex = hashArray
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
// @license-end
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const emailInput = document.getElementById('email');
|
const emailInput = document.getElementById('email');
|
||||||
@ -114,46 +132,84 @@ function sendToNextPage(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No client-side validation errors — send request
|
const identifier = emailInput ? emailInput.value.trim() : '';
|
||||||
try {
|
const plainPassword = passwordInput ? passwordInput.value : '';
|
||||||
const payload = {
|
|
||||||
identifier: emailInput ? emailInput.value : '',
|
|
||||||
password: passwordInput ? passwordInput.value : '',
|
|
||||||
client_id: 'web'
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await fetch('/api/login', {
|
if (!identifier || !plainPassword) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. First attempt: send SHA-256 hash with client_id = 'web_hash'
|
||||||
|
const hash = await getSHA256Hash(plainPassword);
|
||||||
|
console.log('Trying with hash:', hash); // for debug, remove later
|
||||||
|
|
||||||
|
let res = await fetch('/api/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
identifier: identifier,
|
||||||
|
password: hash,
|
||||||
|
client_id: 'web_hash'
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2. If hash attempt failed → try plain password with client_id = 'web'
|
||||||
|
if (!res.ok && res.status === 401) {
|
||||||
|
console.log('Hash login failed, trying plain password...');
|
||||||
|
res = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
identifier: identifier,
|
||||||
|
password: plainPassword,
|
||||||
|
client_id: 'web'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Handle final result
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
// success (HTTP 2xx). Redirect to /form
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const next = params.get('next') || '/profile';
|
const next = params.get('next') || '/profile';
|
||||||
window.location.href = next;
|
window.location.href = next;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
// Error handling
|
||||||
const allFields = document.querySelectorAll('.floating-group');
|
const allFields = document.querySelectorAll('.floating-group');
|
||||||
let msg;
|
let msg;
|
||||||
|
|
||||||
if (res.status === 403) {
|
if (res.status === 403) {
|
||||||
// Аккаунт заблокирован — поля ввода ни при чём, показываем причину
|
|
||||||
msg = 'Ваш аккаунт заблокирован администратором.';
|
msg = 'Ваш аккаунт заблокирован администратором.';
|
||||||
try { const d = await res.json(); if (d && typeof d.detail === 'string') msg = d.detail; } catch (e) {}
|
try {
|
||||||
|
const d = await res.json();
|
||||||
|
if (d && typeof d.detail === 'string') msg = d.detail;
|
||||||
|
} catch (_) {}
|
||||||
allFields.forEach(g => g.classList.remove('error'));
|
allFields.forEach(g => g.classList.remove('error'));
|
||||||
} else {
|
} else {
|
||||||
msg = 'Неверный логин или пароль';
|
msg = 'Неверный логин или пароль';
|
||||||
allFields.forEach(g => g.classList.add('error'));
|
allFields.forEach(g => g.classList.add('error'));
|
||||||
}
|
}
|
||||||
if (typeof showToast === 'function') showToast({ message: msg, status: 'error', duration: 9000 });
|
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast({ message: msg, status: 'error', duration: 9000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Network or other unexpected error — mark fields as error
|
console.error('Request failed', err);
|
||||||
const allFields = document.querySelectorAll('.floating-group');
|
const allFields = document.querySelectorAll('.floating-group');
|
||||||
allFields.forEach(g => g.classList.add('error'));
|
allFields.forEach(g => g.classList.add('error'));
|
||||||
console.error('Request failed', err);
|
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast({
|
||||||
|
message: 'Ошибка сети. Попробуйте позже.',
|
||||||
|
status: 'error',
|
||||||
|
duration: 9000
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Убираем красную подсветку сразу при исправлении пользователем
|
// Убираем красную подсветку сразу при исправлении пользователем
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
<th>Телефон</th>
|
<th>Телефон</th>
|
||||||
<th>Место проживания</th>
|
<th>Место проживания</th>
|
||||||
<th>Дата рождения</th>
|
<th>Дата рождения</th>
|
||||||
|
<th>...</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -54,11 +55,36 @@
|
|||||||
<td>{{doc.phone or ""}}</td>
|
<td>{{doc.phone or ""}}</td>
|
||||||
<td>{{doc.living_place or ""}}</td>
|
<td>{{doc.living_place or ""}}</td>
|
||||||
<td>{{doc.birthday or ""}}</td>
|
<td>{{doc.birthday or ""}}</td>
|
||||||
|
<td><button class="ba" onclick="deleteUser('{{doc._id}}')">Удалить</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<script>
|
||||||
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
|
async function deleteUser(params) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/delete-user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
_id: params,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showToast({ message: "Пользователь удален", status: 'success' });
|
||||||
|
} else {
|
||||||
|
showToast({ message: data.detail || "Ошибка", status: 'error' });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast({ message: "Ошибка соединения с сервером", status: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @license-end
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
function normalizeText(str) {
|
function normalizeText(str) {
|
||||||
@ -131,5 +157,14 @@ function normalizeText(str) {
|
|||||||
justify-content: unset;
|
justify-content: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
td>button{padding: 13px 15px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #5F1798;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 13px;}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -16,40 +16,386 @@
|
|||||||
<div class="c_ru432">
|
<div class="c_ru432">
|
||||||
{{ profile_nav.profile_nav(user, 'password') }}
|
{{ profile_nav.profile_nav(user, 'password') }}
|
||||||
<div class="c_tihtk" id="main">
|
<div class="c_tihtk" id="main">
|
||||||
|
<section class="c_sbrxk">
|
||||||
|
<div class="c_dtjoh">
|
||||||
|
<h2 style="text-align: center; margin-bottom: 24px; color: #5F1798;">Придумайте новый пароль</h2>
|
||||||
|
|
||||||
|
<div class="c_aihm6 floating-group">
|
||||||
|
<input type="password" id="new_password" class="c_xhd5k" required minlength="6">
|
||||||
|
<label for="new_password" class="c_z3ybu floating-label">Новый пароль</label>
|
||||||
|
<div type="button" class="password-toggle" aria-label="Toggle password visibility">
|
||||||
|
<svg class="eye eye-open" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.1875 14.6875C16.0625 13.8125 16.5 12.75 16.5 11.5C16.5 10.25 16.0625 9.1875 15.1875 8.3125C14.3125 7.4375 13.25 7 12 7C10.75 7 9.6875 7.4375 8.8125 8.3125C7.9375 9.1875 7.5 10.25 7.5 11.5C7.5 12.75 7.9375 13.8125 8.8125 14.6875C9.6875 15.5625 10.75 16 12 16C13.25 16 14.3125 15.5625 15.1875 14.6875ZM10.0875 13.4125C9.5625 12.8875 9.3 12.25 9.3 11.5C9.3 10.75 9.5625 10.1125 10.0875 9.5875C10.6125 9.0625 11.25 8.8 12 8.8C12.75 8.8 13.3875 9.0625 13.9125 9.5875C14.4375 10.1125 14.7 10.75 14.7 11.5C14.7 12.25 14.4375 12.8875 13.9125 13.4125C13.3875 13.9375 12.75 14.2 12 14.2C11.25 14.2 10.6125 13.9375 10.0875 13.4125ZM5.35 16.9625C3.35 15.6042 1.9 13.7833 1 11.5C1.9 9.21667 3.35 7.39583 5.35 6.0375C7.35 4.67917 9.56667 4 12 4C14.4333 4 16.65 4.67917 18.65 6.0375C20.65 7.39583 22.1 9.21667 23 11.5C22.1 13.7833 20.65 15.6042 18.65 16.9625C16.65 18.3208 14.4333 19 12 19C9.56667 19 7.35 18.3208 5.35 16.9625ZM17.1875 15.5125C18.7625 14.5208 19.9667 13.1833 20.8 11.5C19.9667 9.81667 18.7625 8.47917 17.1875 7.4875C15.6125 6.49583 13.8833 6 12 6C10.1167 6 8.3875 6.49583 6.8125 7.4875C5.2375 8.47917 4.03333 9.81667 3.2 11.5C4.03333 13.1833 5.2375 14.5208 6.8125 15.5125C8.3875 16.5042 10.1167 17 12 17C13.8833 17 15.6125 16.5042 17.1875 15.5125Z" fill="#5F1798"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="eye eye-closed" style="display:none" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5F1798">
|
||||||
|
<path d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="c_aihm6 floating-group password-group" style="margin-bottom: 20px;">
|
||||||
|
<input type="password" id="confirm_password" class="c_xhd5k" required minlength="6">
|
||||||
|
<label for="confirm_password" class="c_z3ybu floating-label">Подтвердите пароль</label>
|
||||||
|
<div type="button" class="password-toggle" aria-label="Toggle password visibility">
|
||||||
|
<svg class="eye eye-open" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.1875 14.6875C16.0625 13.8125 16.5 12.75 16.5 11.5C16.5 10.25 16.0625 9.1875 15.1875 8.3125C14.3125 7.4375 13.25 7 12 7C10.75 7 9.6875 7.4375 8.8125 8.3125C7.9375 9.1875 7.5 10.25 7.5 11.5C7.5 12.75 7.9375 13.8125 8.8125 14.6875C9.6875 15.5625 10.75 16 12 16C13.25 16 14.3125 15.5625 15.1875 14.6875ZM10.0875 13.4125C9.5625 12.8875 9.3 12.25 9.3 11.5C9.3 10.75 9.5625 10.1125 10.0875 9.5875C10.6125 9.0625 11.25 8.8 12 8.8C12.75 8.8 13.3875 9.0625 13.9125 9.5875C14.4375 10.1125 14.7 10.75 14.7 11.5C14.7 12.25 14.4375 12.8875 13.9125 13.4125C13.3875 13.9375 12.75 14.2 12 14.2C11.25 14.2 10.6125 13.9375 10.0875 13.4125ZM5.35 16.9625C3.35 15.6042 1.9 13.7833 1 11.5C1.9 9.21667 3.35 7.39583 5.35 6.0375C7.35 4.67917 9.56667 4 12 4C14.4333 4 16.65 4.67917 18.65 6.0375C20.65 7.39583 22.1 9.21667 23 11.5C22.1 13.7833 20.65 15.6042 18.65 16.9625C16.65 18.3208 14.4333 19 12 19C9.56667 19 7.35 18.3208 5.35 16.9625ZM17.1875 15.5125C18.7625 14.5208 19.9667 13.1833 20.8 11.5C19.9667 9.81667 18.7625 8.47917 17.1875 7.4875C15.6125 6.49583 13.8833 6 12 6C10.1167 6 8.3875 6.49583 6.8125 7.4875C5.2375 8.47917 4.03333 9.81667 3.2 11.5C4.03333 13.1833 5.2375 14.5208 6.8125 15.5125C8.3875 16.5042 10.1167 17 12 17C13.8833 17 15.6125 16.5042 17.1875 15.5125Z" fill="#5F1798"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="eye eye-closed" style="display:none" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5F1798">
|
||||||
|
<path d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="c_aq9et" id="resetBtn" style="margin-bottom: 16px;">
|
||||||
|
<span class="c_som9s">Сменить пароль</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
|
async function getSHA256Hash(message) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const msgBuffer = encoder.encode(message);
|
||||||
|
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
||||||
|
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
|
||||||
|
const hashHex = hashArray
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
// @license-end
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
|
// Password toggle functionality (same as login)
|
||||||
|
document.querySelectorAll('.password-toggle').forEach(toggle => {
|
||||||
|
const input = toggle.parentElement.querySelector('input');
|
||||||
|
const eyeOpen = toggle.querySelector('.eye-open');
|
||||||
|
const eyeClosed = toggle.querySelector('.eye-closed');
|
||||||
|
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
eyeOpen.style.display = 'none';
|
||||||
|
eyeClosed.style.display = 'inline';
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
eyeOpen.style.display = 'inline';
|
||||||
|
eyeClosed.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submit
|
||||||
|
document.getElementById('resetBtn').addEventListener('click', async () => {
|
||||||
|
const newPass = document.getElementById('new_password').value;
|
||||||
|
const confirmPass = document.getElementById('confirm_password').value;
|
||||||
|
|
||||||
|
if (newPass.length < 6) {
|
||||||
|
showToast({ message: "Пароль должен быть не менее 6 символов", status: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPass !== confirmPass) {
|
||||||
|
showToast({ message: "Пароли не совпадают", status: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hash = await getSHA256Hash(newPass);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/reset-hash-password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
new_password: hash,
|
||||||
|
token: 'access_token'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showToast({ message: "Пароль успешно изменён!", status: 'success' });
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = `/login?next=${data.next}`;
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
showToast({ message: data.detail || "Ошибка при смене пароля", status: 'error' });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast({ message: "Ошибка соединения с сервером", status: 'error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// @license-end
|
||||||
|
</script>
|
||||||
{{ footer.section(user) }}
|
{{ footer.section(user) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ header.style() }}
|
{{ header.style() }}
|
||||||
{{ profile_nav.style() }}
|
{{ profile_nav.style() }}
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
/* Mobile */
|
/* Mobile */
|
||||||
@media (max-width: 699px) {
|
@media (max-width: 699px) {
|
||||||
.c_mrl59 { align-items: center; background: #5F1798; border-radius: 25px; bottom: -15px; display: flex; height: 50px; justify-content: center; position: absolute; right: -15px; width: 50px; }
|
.c_mrl59 { align-items: center; background: #5F1798; border-radius: 25px; bottom: -15px; display: flex; height: 50px; justify-content: center; position: absolute; right: -15px; width: 50px; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tablet */
|
/* Tablet */
|
||||||
@media (min-width: 700px) and (max-width: 900px) {
|
@media (min-width: 700px) and (max-width: 900px) {
|
||||||
.c_iiehk { align-items: center; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 335px; }
|
.c_iiehk { align-items: center; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 335px; }
|
||||||
.c_nnsh6>a>img{display: none;}
|
.c_nnsh6>a>img{display: none;}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop */
|
/* Desktop */
|
||||||
@media (min-width: 901px) {
|
@media (min-width: 901px) {
|
||||||
.c_iiehk { align-items: flex-end; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 310px; }
|
.c_iiehk { align-items: flex-end; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 310px; }
|
||||||
.c_nnsh6>a>img{display: none;}
|
.c_nnsh6>a>img{display: none;}
|
||||||
|
|
||||||
}</style>
|
}</style>
|
||||||
|
<style>
|
||||||
|
.poster-group {position: relative;}
|
||||||
|
.password-group { position: relative; }
|
||||||
|
a{color: #5F1798;}
|
||||||
|
.password-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* remove default button outline but keep focus visible */
|
||||||
|
.password-toggle:focus { outline: 2px solid rgba(95,23,152,0.25); outline-offset: 2px; }
|
||||||
|
|
||||||
|
/* ensure svg size fits */
|
||||||
|
.password-toggle .eye { width: 24px; height: 24px; display: block; }
|
||||||
|
|
||||||
|
.file-name-display {width: 100%;padding: 16px 50px 16px 16px;border: 2px solid #ddd;border-radius: 8px;font-size: 16px;color: #555;background: #fff;min-height: 56px;display: flex;align-items: center;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;transition: all 0.3s ease;cursor: pointer;}
|
||||||
|
|
||||||
|
/* Hover эффект */
|
||||||
|
.c_aihm6:hover input, .c_nb6gy:hover textarea, .poster-group:hover .file-name-display {border-color: #5F1798;}
|
||||||
|
|
||||||
|
/* Когда файл выбран — делаем текст чуть темнее */
|
||||||
|
.poster-group.has-file .file-name-display {color: #222;font-weight: 500;}
|
||||||
|
|
||||||
|
/* Скрытый input file */
|
||||||
|
.hidden-file {position: absolute;inset: 0;opacity: 0;cursor: pointer;z-index: 2;}
|
||||||
|
|
||||||
|
/* Плавающий лейбл */
|
||||||
|
.poster-group .floating-label {position: absolute;left: 16px;top: 16px;font-size: 16px;color: #666;background: #fff;padding: 0 6px;z-index: 3;transition: all 0.3s ease;pointer-events: none;}
|
||||||
|
|
||||||
|
/* Когда лейбл должен подняться */
|
||||||
|
.poster-group input:focus + .floating-label,
|
||||||
|
.poster-group.has-file .floating-label {top: -8px;font-size: 13px;color: #5F1798;}
|
||||||
|
|
||||||
|
/* Иконка справа */
|
||||||
|
.file-icon {position: absolute;right: 16px;top: 50%;transform: translateY(-50%);z-index: 3;pointer-events: none;}
|
||||||
|
|
||||||
|
/* Красная ошибка */
|
||||||
|
.poster-group.error .file-name-display {border-color: #e74c3c !important;box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.15);}
|
||||||
|
|
||||||
|
.poster-group.error .floating-label {color: #e74c3c !important;}
|
||||||
|
/* ====================== FLOATING LABELS ====================== */
|
||||||
|
.floating-group {position: relative;margin-bottom: 1.5rem;}
|
||||||
|
|
||||||
|
.floating-label {position: absolute;left: 16px;top: 16px;font-size: 16px;color: #666;transition: all 0.3s ease;pointer-events: none;background: #fff;padding: 0 6px;z-index: 2;}
|
||||||
|
|
||||||
|
/* Плавающий лейбл */
|
||||||
|
.floating-group input:focus + .floating-label, .floating-group textarea:focus + .floating-label,
|
||||||
|
.floating-group input:not(:placeholder-shown) + .floating-label, .floating-group textarea:not(:placeholder-shown) + .floating-label {top: -8px;font-size: 15px;color: #5F1798;}
|
||||||
|
|
||||||
|
/* Красная подсветка — ТОЛЬКО после попытки отправки */
|
||||||
|
.floating-group.error input, .floating-group.error textarea,
|
||||||
|
.floating-group.error input[type="file"] {border-color: #e74c3c !important;box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.38);}
|
||||||
|
|
||||||
|
.floating-group.error .floating-label {color: #e74c3c !important;}
|
||||||
|
|
||||||
|
/* Основные стили полей */
|
||||||
|
.c_xhd5k, .c_4ykpn {width: 100%;padding: 16px 16px;border: 2px solid #ddd;border-radius: 8px;font-size: 16px;transition: all 0.3s ease;background: #fff;}
|
||||||
|
|
||||||
|
.c_xhd5k:focus, .c_4ykpn:focus {border-color: #5F1798;outline: none;box-shadow: 0 0 0 3px rgba(95, 23, 152, 0.15);}
|
||||||
|
|
||||||
|
/* Файловые поля */
|
||||||
|
.file-group input[type="file"] {height: 43px;}
|
||||||
|
|
||||||
|
|
||||||
|
.file-group .floating-label {top: 12px;}
|
||||||
|
/* ====================== СКРЫТЬ СТРЕЛОЧКИ У INPUT TYPE=NUMBER ====================== */
|
||||||
|
|
||||||
|
/* Chrome, Edge, Safari, Opera */
|
||||||
|
input[type="number"]::-webkit-outer-spin-button,
|
||||||
|
input[type="number"]::-webkit-inner-spin-button {-webkit-appearance: none;margin: 0;display: none;}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type="number"] {-moz-appearance: textfield;}
|
||||||
|
|
||||||
|
/* Дополнительно: чтобы стрелочки не появлялись при фокусе (на всякий случай) */
|
||||||
|
input[type="number"]:hover::-webkit-outer-spin-button,
|
||||||
|
input[type="number"]:focus::-webkit-outer-spin-button,
|
||||||
|
input[type="number"]:hover::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]:focus::-webkit-inner-spin-button {display: none;}
|
||||||
|
.c_xc07u {white-space: pre;}
|
||||||
|
.c_5qh93{cursor: pointer;}
|
||||||
|
.c_965ia{object-fit: cover;}
|
||||||
|
@media (min-width: 901px) {
|
||||||
|
.c_abq53 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_ydot9 { display: flex; padding: 1.5px 10px; justify-content: center; align-items: center; border-radius: 30px; background: rgba(14, 214, 70, 0.20); }
|
||||||
|
.c_w5koo { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; flex: 1 0 0; overflow: hidden; color: #000; text-overflow: ellipsis; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_fudgr { display: flex; justify-content: center; align-items: center; align-self: stretch; }
|
||||||
|
.c_n5lj4 { align-self: stretch; color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_bffhf { width: 50px; height: 50px; aspect-ratio: 1/1; border-radius: 50px; }
|
||||||
|
.c_sya8w { align-self: stretch; color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_o5cc2 { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; flex: 1 0 0; }
|
||||||
|
.c_zxyme { display: flex; height: 50px; align-items: center; gap: 13px; align-self: stretch; }
|
||||||
|
.c_rq5iq { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; align-self: stretch; overflow: hidden; color: rgba(58, 58, 58, 0.75); text-overflow: ellipsis; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_sa311 { align-self: stretch; color: rgba(58, 58, 58, 0.75); text-align: right; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_yhst8 { display: flex; padding: 10px 20px; flex-direction: column; justify-content: center; align-items: flex-start; gap: 10px; align-self: stretch; border-radius: 10px; background: #FFF; }
|
||||||
|
.c_jz1u6 { display: flex; width: 335px; height: 418px; padding: 120px 20px 20px 20px; flex-direction: column; align-items: flex-start; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.30), 0 4px 20px 0 rgba(0, 0, 0, 0.20); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) and (max-width: 900px) {
|
||||||
|
.c_abq53 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_ydot9 { display: flex; padding: 1.5px 10px; justify-content: center; align-items: center; border-radius: 30px; background: rgba(14, 214, 70, 0.20); }
|
||||||
|
.c_w5koo { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; flex: 1 0 0; overflow: hidden; color: #000; text-overflow: ellipsis; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_fudgr { display: flex; justify-content: center; align-items: center; align-self: stretch; }
|
||||||
|
.c_n5lj4 { align-self: stretch; color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_bffhf { width: 50px; height: 50px; aspect-ratio: 1/1; border-radius: 50px; }
|
||||||
|
.c_sya8w { align-self: stretch; color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_o5cc2 { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; flex: 1 0 0; }
|
||||||
|
.c_zxyme { display: flex; height: 50px; align-items: center; gap: 13px; align-self: stretch; }
|
||||||
|
.c_rq5iq { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; align-self: stretch; overflow: hidden; color: rgba(58, 58, 58, 0.75); text-overflow: ellipsis; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_sa311 { align-self: stretch; color: rgba(58, 58, 58, 0.75); text-align: right; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_yhst8 { display: flex; padding: 10px 20px; flex-direction: column; justify-content: center; align-items: flex-start; gap: 10px; align-self: stretch; border-radius: 10px; background: #FFF; }
|
||||||
|
.c_jz1u6 { display: flex; width: calc(50% - 10px); height: 418px; padding: 120px 20px 20px 20px; flex-direction: column; align-items: flex-start; flex-shrink: 0; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.30), 0 4px 20px 0 rgba(0, 0, 0, 0.20); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 699px) {
|
||||||
|
.c_abq53 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_ydot9 { display: flex; padding: 1.5px 10px; justify-content: center; align-items: center; border-radius: 30px; background: rgba(14, 214, 70, 0.20); }
|
||||||
|
.c_w5koo { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; flex: 1 0 0; overflow: hidden; color: #000; text-overflow: ellipsis; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_fudgr { display: flex; justify-content: center; align-items: center; align-self: stretch; }
|
||||||
|
.c_n5lj4 { align-self: stretch; color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_bffhf { width: 50px; height: 50px; aspect-ratio: 1/1; border-radius: 50px; }
|
||||||
|
.c_sya8w { align-self: stretch; color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_o5cc2 { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; flex: 1 0 0; }
|
||||||
|
.c_zxyme { display: flex; height: 50px; align-items: center; gap: 13px; align-self: stretch; }
|
||||||
|
.c_rq5iq { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; align-self: stretch; overflow: hidden; color: rgba(58, 58, 58, 0.75); text-overflow: ellipsis; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_sa311 { align-self: stretch; color: rgba(58, 58, 58, 0.75); text-align: right; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_yhst8 { display: flex; padding: 10px 20px; flex-direction: column; justify-content: center; align-items: flex-start; gap: 10px; align-self: stretch; border-radius: 10px; background: #FFF; }
|
||||||
|
.c_jz1u6 { display: flex; width: 335px; padding: 120px 20px 20px 20px; flex-direction: column; align-items: flex-start; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.30), 0 4px 20px 0 rgba(0, 0, 0, 0.20); }
|
||||||
|
}
|
||||||
|
@media (min-width: 901px) {
|
||||||
|
.c_xhd5k { display: flex; height: 42px; padding: 5px 10px; align-items: center; flex: 1 0 0; }
|
||||||
|
.c_z3ybu { position: absolute; left: 13px; top: 12px; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_vphtp { width: 18px; height: 18px; fill: #5F1798; }
|
||||||
|
.c_ffjwp { width: 24px; height: 24px; flex-shrink: 0; }
|
||||||
|
.c_aihm6 { display: flex; width: 323px; justify-content: center; align-items: center; border-radius: 5px; background: #FFF; }
|
||||||
|
.c_4ykpn { display: flex; height: 104px; padding: 10px 10px; align-items: center; flex: 1 0 0; }
|
||||||
|
.c_nb6gy { display: flex; width: 323px; justify-content: center; align-items: center; border-radius: 5px; background: #FFF; }
|
||||||
|
.c_q00x6 { min-width: 230px; flex: 1 0 0; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_mkcow { color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_uklyi { width: 20px; color: #000; text-align: center; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_l8kil {display: flex;padding: 5px 00px;align-items: center;border-radius: 5px;border: 1px solid rgba(95, 23, 152, 0.20);background: #FDFAFF;width: 35px;text-align: center;}
|
||||||
|
.c_5qh93 { display: flex; height: 7px; justify-content: center; align-items: center; flex: 1 0 0; border-radius: 5px; background: rgba(95, 23, 152, 0.20); }
|
||||||
|
.c_jups4 { display: flex; width: 323px; padding: 10px; align-items: center; align-content: center; gap: 10px; flex-wrap: wrap; border-radius: 5px; background: #FFF; border: 1px solid #5F1798; }
|
||||||
|
.c_som9s { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_aq9et { display: flex; padding: 19px 33px; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #5F1798; }
|
||||||
|
.c_dtjoh { display: flex; width: 323px; flex-direction: column; align-items: flex-start; gap: 12px; flex-shrink: 0; }
|
||||||
|
.c_1gai0 { align-self: stretch; color: #000; text-align: center; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_med8n { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_g8o3y { display: flex; padding: 1.5px 10px; justify-content: center; align-items: center; border-radius: 30px; background: rgba(95, 23, 152, 0.20); }
|
||||||
|
.c_1pdx0 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; flex: 1 0 0; overflow: hidden; color: #000; text-overflow: ellipsis; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_r5tzr { display: flex; justify-content: center; align-items: center; align-self: stretch; }
|
||||||
|
.c_965ia { width: 50px; height: 50px; aspect-ratio: 1/1; border-radius: 50px; }
|
||||||
|
.c_t9q2z { width: 189px; color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_zruk7 { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }
|
||||||
|
.c_k3nyf { display: flex; height: 50px; align-items: center; gap: 13px; align-self: stretch; }
|
||||||
|
.c_xc07u { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; align-self: stretch; overflow: hidden; color: rgba(58, 58, 58, 0.75); text-overflow: ellipsis; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_hni7n { align-self: stretch; color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_qfcvm { align-self: stretch; color: rgba(58, 58, 58, 0.75); text-align: right; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_rtopc { display: flex; width: 355px; flex-direction: column; justify-content: center; align-items: flex-start; gap: 20px; flex-shrink: 0; }
|
||||||
|
.c_sbrxk { padding: 50px 0px 100px; display: flex; box-sizing: unset; justify-content: center; align-items: flex-start; align-content: flex-start; gap: 200px; flex-wrap: wrap; background: #FFF; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) and (max-width: 900px) {
|
||||||
|
.c_xhd5k { display: flex; height: 42px; padding: 5px 10px; align-items: center; flex: 1 0 0; }
|
||||||
|
.c_z3ybu { position: absolute; left: 13px; top: 12px; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_vphtp { width: 18px; height: 18px; fill: #5F1798; }
|
||||||
|
.c_ffjwp { width: 24px; height: 24px; flex-shrink: 0; }
|
||||||
|
.c_aihm6 { display: flex; width: 323px; justify-content: center; align-items: center; border-radius: 5px; background: #FFF; }
|
||||||
|
.c_4ykpn { display: flex; height: 104px; padding: 10px 10px; align-items: center; flex: 1 0 0; }
|
||||||
|
.c_nb6gy { display: flex; width: 323px; justify-content: center; align-items: center; border-radius: 5px; background: #FFF; }
|
||||||
|
.c_q00x6 { min-width: 230px; flex: 1 0 0; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_mkcow { color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_uklyi { width: 20px; color: #000; text-align: center; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_l8kil {display: flex;padding: 5px 00px;align-items: center;border-radius: 5px;border: 1px solid rgba(95, 23, 152, 0.20);background: #FDFAFF;width: 35px;text-align: center;}
|
||||||
|
.c_5qh93 { display: flex; height: 7px; justify-content: center; align-items: center; flex: 1 0 0; border-radius: 5px; background: rgba(95, 23, 152, 0.20); }
|
||||||
|
.c_jups4 { display: flex; width: 323px; padding: 10px; align-items: center; align-content: center; gap: 10px; flex-wrap: wrap; border-radius: 5px; background: #FFF; border: 1px solid #5F1798; }
|
||||||
|
.c_som9s { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_aq9et { display: flex; padding: 19px 33px; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #5F1798; }
|
||||||
|
.c_dtjoh { display: flex; width: 323px; flex-direction: column; align-items: flex-start; gap: 12px; flex-shrink: 0; }
|
||||||
|
.c_1gai0 { align-self: stretch; color: #000; text-align: center; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_med8n { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_g8o3y { display: flex; padding: 1.5px 10px; justify-content: center; align-items: center; border-radius: 30px; background: rgba(95, 23, 152, 0.20); }
|
||||||
|
.c_1pdx0 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; flex: 1 0 0; overflow: hidden; color: #000; text-overflow: ellipsis; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_r5tzr { display: flex; justify-content: center; align-items: center; align-self: stretch; }
|
||||||
|
.c_965ia { width: 50px; height: 50px; aspect-ratio: 1/1; border-radius: 50px; }
|
||||||
|
.c_t9q2z { width: 189px; color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_zruk7 { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }
|
||||||
|
.c_k3nyf { display: flex; height: 50px; align-items: center; gap: 13px; align-self: stretch; }
|
||||||
|
.c_xc07u { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; align-self: stretch; overflow: hidden; color: rgba(58, 58, 58, 0.75); text-overflow: ellipsis; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_hni7n { align-self: stretch; color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_qfcvm { align-self: stretch; color: rgba(58, 58, 58, 0.75); text-align: right; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_rtopc { display: flex; width: 355px; flex-direction: column; justify-content: center; align-items: flex-start; gap: 20px; flex-shrink: 0; }
|
||||||
|
.c_sbrxk { padding: 50px 0px 100px; display: flex; box-sizing: unset; justify-content: center; align-items: flex-start; gap: 50px; background: #FFF; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 699px) {
|
||||||
|
.c_xhd5k { display: flex; height: 42px; padding: 5px 10px; align-items: center; flex: 1 0 0; }
|
||||||
|
.c_z3ybu { position: absolute; left: 13px; top: 12px; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_vphtp { width: 18px; height: 18px; fill: #5F1798; }
|
||||||
|
.c_ffjwp { width: 24px; height: 24px; }
|
||||||
|
.c_aihm6 { display: flex; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #FFF; }
|
||||||
|
.c_4ykpn { display: flex; height: 104px; padding: 10px 10px; align-items: center; flex: 1 0 0; }
|
||||||
|
.c_nb6gy { display: flex; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #FFF; }
|
||||||
|
.c_q00x6 { min-width: 230px; flex: 1 0 0; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_mkcow { color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_uklyi { width: 20px; color: #000; text-align: center; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_l8kil {display: flex;padding: 5px 00px;align-items: center;border-radius: 5px;border: 1px solid rgba(95, 23, 152, 0.20);background: #FDFAFF;width: 35px;text-align: center;}
|
||||||
|
.c_5qh93 { height: 7px; flex: 1 0 0; fill: rgba(95, 23, 152, 0.20); background: rgba(95, 23, 152, 0.20);}
|
||||||
|
.c_jups4 { display: flex; padding: 10px; align-items: center; align-content: center; gap: 10px; align-self: stretch; flex-wrap: wrap; border-radius: 5px; background: #FFF; border: 1px solid #5F1798; }
|
||||||
|
.c_som9s { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_aq9et { display: flex; padding: 19px 33px; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #5F1798; }
|
||||||
|
.c_dtjoh { display: flex; width: 323px; flex-direction: column; align-items: flex-start; gap: 12px; flex-shrink: 0; }
|
||||||
|
.c_1gai0 { align-self: stretch; color: #000; text-align: center; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_med8n { color: #5F1798; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_g8o3y { display: flex; padding: 1.5px 10px; justify-content: center; align-items: center; border-radius: 30px; background: rgba(95, 23, 152, 0.20); }
|
||||||
|
.c_1pdx0 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; flex: 1 0 0; overflow: hidden; color: #000; text-overflow: ellipsis; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; }
|
||||||
|
.c_r5tzr { display: flex; justify-content: center; align-items: center; align-self: stretch; }
|
||||||
|
.c_965ia { width: 50px; height: 50px; aspect-ratio: 1/1; border-radius: 50px; }
|
||||||
|
.c_t9q2z { width: 189px; color: #000; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_zruk7 { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }
|
||||||
|
.c_k3nyf { display: flex; height: 50px; align-items: center; gap: 13px; align-self: stretch; }
|
||||||
|
.c_xc07u { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; align-self: stretch; overflow: hidden; color: rgba(58, 58, 58, 0.75); text-overflow: ellipsis; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_hni7n { align-self: stretch; color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_qfcvm { align-self: stretch; color: rgba(58, 58, 58, 0.75); text-align: right; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
|
||||||
|
.c_rtopc { display: flex; width: 335px; flex-direction: column; justify-content: center; align-items: flex-start; gap: 20px; flex-shrink: 0; }
|
||||||
|
.c_sbrxk { padding: 50px 0px 100px; display: flex; box-sizing: unset; justify-content: center; align-items: flex-start; align-content: flex-start; gap: 50px 0; flex-wrap: wrap; background: #FFF; }
|
||||||
|
}
|
||||||
|
.c_sbrxk{z-index: 1000;}
|
||||||
|
#preview_poster{width: 335px;}
|
||||||
|
</style>
|
||||||
{{ footer.style() }}
|
{{ footer.style() }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -143,6 +143,24 @@ document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelector("
|
|||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
|
async function getSHA256Hash(message) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const msgBuffer = encoder.encode(message);
|
||||||
|
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
||||||
|
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
|
||||||
|
const hashHex = hashArray
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
// @license-end
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
function showOfferModal(offerHtml = '<p>Текст оферты...</p>') {
|
function showOfferModal(offerHtml = '<p>Текст оферты...</p>') {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const backdrop = document.createElement('div');
|
const backdrop = document.createElement('div');
|
||||||
@ -284,11 +302,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// No client-side validation errors — send request
|
// No client-side validation errors — send request
|
||||||
try {
|
try {
|
||||||
|
const hash = await getSHA256Hash(document.getElementById('password').value);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('img', document.getElementById('avatar').files[0]);
|
formData.append('img', document.getElementById('avatar').files[0]);
|
||||||
formData.append('living_place', document.getElementById('living_place').value);
|
formData.append('living_place', document.getElementById('living_place').value);
|
||||||
formData.append('Female_Name_Lastame', document.getElementById('fio').value);
|
formData.append('Female_Name_Lastame', document.getElementById('fio').value);
|
||||||
formData.append('password', document.getElementById('password').value);
|
formData.append('password', hash);
|
||||||
formData.append('email', document.getElementById('email').value);
|
formData.append('email', document.getElementById('email').value);
|
||||||
formData.append('phone', document.getElementById('phone').value);
|
formData.append('phone', document.getElementById('phone').value);
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<script>
|
||||||
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
|
async function getSHA256Hash(message) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const msgBuffer = encoder.encode(message);
|
||||||
|
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
||||||
|
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
|
||||||
|
const hashHex = hashArray
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
// @license-end
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
|
||||||
// Password toggle functionality (same as login)
|
// Password toggle functionality (same as login)
|
||||||
@ -85,14 +102,14 @@
|
|||||||
showToast({ message: "Пароли не совпадают", status: 'error' });
|
showToast({ message: "Пароли не совпадают", status: 'error' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const hash = await getSHA256Hash(newPass);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/reset-password', {
|
const res = await fetch('/api/reset-password', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
token: "{{ token }}",
|
token: "{{ token }}",
|
||||||
new_password: newPass
|
new_password: hash
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user