This commit is contained in:
Vladislav 2026-08-02 12:08:08 -04:00
parent c1efb9d36f
commit d60dc1f8c2
68 changed files with 479 additions and 280 deletions

48
auth.py
View File

@ -266,6 +266,8 @@ async def get_user_details(access: str, scopes, response: Response, request: Req
{"hashed_password": 0}
)
if user:
if user.get("blocked"):
raise HTTPException(status_code=403, detail="Аккаунт заблокирован администратором")
user['scopes'] = scopes
user['id'] = user_id_str
user = bson_clear(user)
@ -302,6 +304,8 @@ async def get_user(
user = await users_collection.find_one({"_id": ObjectId(user_id_str)}, {"hashed_password": 0})
if not user:
return None
if user.get("blocked"):
return None
scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", []))
if request.cookies.get("admin_access_token"):
@ -315,3 +319,47 @@ async def get_user(
return None
except jwt.InvalidTokenError:
return None
async def issue_session(user_id: str, request: Request, response: Response,
client_id: str = 'web', scopes=None):
"""Выдаёт новую сессию: JWT access/refresh, запись в Redis и cookie.
Переиспользуемый хелпер (например, для входа/регистрации через VK).
Если scopes не переданы — вычисляются из ролей пользователя.
"""
if scopes is None:
user = await users_collection.find_one({"_id": ObjectId(user_id)})
scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", []))
client_ip = request.headers.get('X-Real-IP') or (request.client.host if request.client else None)
access_exp = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)
refresh_exp = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
access_token = create_jwt_token(
{"sub": user_id, "scopes": scopes, "client_id": client_id, "ip": client_ip},
access_exp,
)
refresh_token = create_jwt_token(
{"sub": user_id, "type": "refresh", "client_id": client_id, "ip": client_ip},
refresh_exp,
)
await redis_client.setex(f"access:{access_token}", int(access_exp.total_seconds()), user_id)
await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp.total_seconds()), user_id)
session_key = f"user_sessions:{user_id}"
await redis_client.sadd(session_key, f"access:{access_token}")
await redis_client.sadd(session_key, f"refresh:{hash_token(refresh_token)}")
response.set_cookie(
key="access_token", value=access_token, httponly=True, secure=PROD,
samesite="Strict", max_age=int(access_exp.total_seconds()),
)
response.set_cookie(
key="refresh_token", value=refresh_token, httponly=True, secure=PROD,
samesite="Strict", max_age=int(refresh_exp.total_seconds()),
)
return access_token, refresh_token

View File

@ -773,6 +773,74 @@
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "4c434852",
"metadata": {},
"source": [
"### local service"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "45bddd01",
"metadata": {
"vscode": {
"languageId": "shellscript"
}
},
"outputs": [],
"source": [
"%%bash\n",
"'tee /etc/systemd/system/staging.service >/dev/null' <<'EOF'\n",
"[Unit]\n",
"Description=FastAPI stag service\n",
"After=network.target\n",
"\n",
"[Service]\n",
"WorkingDirectory=$pwd\n",
"ExecStart=$pwd/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30082 --reload\n",
"Restart=always\n",
"EnvironmentFile=$pwd/env\n",
"\n",
"[Install]\n",
"WantedBy=default.target\n",
"EOF"
],
"display_name": ".venv"
},
{
"cell_type": "code",
"execution_count": null,
"id": "837698d9",
"metadata": {
"vscode": {
"languageId": "shellscript"
}
},
"outputs": [],
"source": [
"%%bash\n",
"ssh kultmost 'tee /etc/systemd/system/staging.service >/dev/null' <<'EOF'\n",
"[Unit]\n",
"Description=FastAPI stag service\n",
"After=network.target\n",
"\n",
"[Service]\n",
"WorkingDirectory=/app/staging\n",
"ExecStart=/app/staging/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30082 --reload\n",
"Restart=always\n",
"EnvironmentFile=/app/staging/env\n",
"\n",
"[Install]\n",
"WantedBy=default.target\n",
"EOF\n",
"\n",
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n"
],
"display_name": ".venv"
},
{
"cell_type": "markdown",
"id": "0474516d",

View File

@ -1056,12 +1056,18 @@
" identifier = safe_strip(login_data.identifier)\n",
" await check_bruteforce(identifier)\n",
"\n",
" users = [doc async for doc in users_collection.find({\n",
" \"$or\": [\n",
" or_conditions = [\n",
" {\"email\": identifier},\n",
" {\"email\": identifier.lower()}\n",
" {\"email\": identifier.lower()},\n",
" ]\n",
" })]\n",
" \n",
" try:\n",
" normalized_phone = normalize_phone(identifier)\n",
" if normalized_phone:\n",
" or_conditions.append({\"phone\": normalized_phone})\n",
" except:\n",
" pass\n",
" users = [doc async for doc in users_collection.find({\"$or\": or_conditions})]\n",
"\n",
"\n",
" if not users:\n",
@ -1790,42 +1796,76 @@
" request: Request,\n",
" restore_data: RestoreRequest = Body(...),\n",
"):\n",
" email = restore_data.identifier.strip().lower()\n",
" next = restore_data.next\n",
" # 1. Валидация и очистка входных данных\n",
" identifier = restore_data.identifier.strip().lower()\n",
" \n",
" user = await users_collection.find_one({'$or': [\n",
" {'email': email}, \n",
" {'email': email.lower()}\n",
" ]})\n",
" if not identifier or not is_valid_email(identifier):\n",
" raise HTTPException(status_code=400, detail=\"Некорректный формат email\")\n",
"\n",
" # 2. Проверка анти-спама / ограничения частоты запросов\n",
" client_ip = request.client.host\n",
" rate_limit_key = f\"rate_limit:restore:{identifier}:{client_ip}\"\n",
" \n",
" # Проверяем, запрашивал ли пользователь сброс недавно (например, за последние 60 секунд)\n",
" existing_request = await redis_client.get(rate_limit_key)\n",
" \n",
" if existing_request:\n",
" # Опционально: получаем TTL, чтобы сообщить пользователю, когда можно повторить попытку\n",
" ttl = await redis_client.ttl(rate_limit_key)\n",
" raise HTTPException(\n",
" status_code=429, \n",
" detail=f\"Слишком много запросов. Попробуйте через {ttl} секунд.\"\n",
" )\n",
"\n",
" # Устанавливаем флаг ограничения частоты (истекает через 60 секунд)\n",
" await redis_client.set(rate_limit_key, \"1\", ex=60)\n",
"\n",
" # 3. Поиск пользователя\n",
" user = await users_collection.find_one({\"email\": identifier})\n",
" \n",
" if not user:\n",
" raise HTTPException(status_code=404, detail='Email не найден')\n",
" # Безопасность: всегда возвращать одинаковое время ответа/структуру сообщения, если возможно\n",
" # Но для удобства использования мы можем сказать \"не найден\".\n",
" # Чтобы предотвратить перечисление, некоторые приложения всегда возвращают \"OK\", но отправляют письмо только если найдено.\n",
" # Здесь мы придерживаемся вашей оригинальной логики, но помните о риске перечисления.\n",
" raise HTTPException(status_code=404, detail=\"Пользователь с таким email не найден\")\n",
" \n",
" # 4. Генерация токена\n",
" token = secrets.token_urlsafe(32)\n",
" expires_at = datetime.utcnow() + timedelta(hours=1)\n",
" expires_at = datetime.now(timezone.utc) + timedelta(hours=1)\n",
" \n",
" token_data = {\n",
" \"user_id\": str(user[\"_id\"]),\n",
" \"email\": user[\"email\"],\n",
" \"next\": next,\n",
" \"next\": restore_data.next,\n",
" \"expires_at\": expires_at.isoformat()\n",
" }\n",
" \n",
" await redis_client.set(\n",
" f\"reset_token:{token}\", \n",
" json.dumps(token_data), \n",
" ex=3600 \n",
" )\n",
" \n",
" reset_link = f\"{str(request.base_url).rstrip('/')}/restore/{token}\"\n",
" # 5. Сохранение токена сброса в Redis\n",
" reset_token_key = f\"reset_token:{token}\"\n",
" await redis_client.set(reset_token_key, json.dumps(token_data), ex=3600)\n",
"\n",
" # 6. Генерация ссылки для сброса\n",
" base_url = str(request.base_url).rstrip('/')\n",
" reset_link = f\"{base_url}/restore/{token}\"\n",
"\n",
" # 7. Асинхронная отправка email\n",
" try:\n",
" email_task.notify.delay(\n",
" user['email'], \n",
" {'link': reset_link, 'email': user['email']}, \n",
" 'Восстановление пароля', \n",
" 'restore.html'\n",
" recipient=user['email'],\n",
" context={\n",
" 'link': reset_link, \n",
" 'email': user['email']\n",
" },\n",
" subject='Восстановление пароля',\n",
" template_name='restore.html'\n",
" )\n",
" except Exception as e:\n",
" # Логирование ошибки\n",
" print(f\"Не удалось отправить письмо: {e}\")\n",
" # В зависимости от политики, вы можете захотеть удалить токен, если письмо не отправлено\n",
" await redis_client.delete(reset_token_key)\n",
" raise HTTPException(status_code=500, detail=\"Ошибка отправки письма\")\n",
"\n",
" return {'ok': True}\n",
"\n",
@ -1880,7 +1920,7 @@
" )\n",
"\n",
"\n",
"app.include_router(router, tags=['Restore passwrod'])"
"app.include_router(router, tags=['Восстановление пароля'])"
],
"display_name": ".venv"
},
@ -1894,13 +1934,13 @@
"@app.get(\"/login\", response_class=HTMLResponse)\n",
"async def login_(\n",
" request: Request,\n",
" next: str | None = None,\n",
" next_url: str | None = None,\n",
"):\n",
" if not next:\n",
" next = request.headers.get(\"Referer\")\n",
" context={\"next\": next}\n",
" print(next)\n",
" if 'profile/competition' in next:\n",
" if not next_url:\n",
" next_url = request.headers.get(\"Referer\")\n",
" context={\"next\": next_url}\n",
" print(next_url)\n",
" if next_url and 'profile/competition' in next_url:\n",
" context.update({'notify':{'text':'Участвовать в конкурсе могут только пользователи сервиса! Пожалуйста, войдите или зарегистрируйтесь.', 'status':'info','duration':8000}})\n",
" return templates.TemplateResponse(\n",
" request=request,\n",
@ -2749,7 +2789,7 @@
" users = [doc async for doc in users_collection.find({'roles':'user'}).limit(1000)]\n",
" return templates.TemplateResponse(\n",
" request=request, name=\"users/index.html\", context={\"id\": 1, \"user\":user, \"users\": users}\n",
" )"
" )\n"
],
"display_name": ".venv"
},
@ -3448,7 +3488,7 @@
" \"serviceSocial\": payload.serviceSocial,\n",
" \"userSocial\": payload.userSocial,\n",
" \"short_desc\": safe_strip(payload.short_desc) if payload.short_desc else None,\n",
" \"poster_file_id\": poster_id,\n",
" \"poster_file_id\": po/api/restorster_id,\n",
" \"poster_orig_id\": poster_id,\n",
" \"rules_file_id\": rules_id,\n",
" \"rules_link\": payload.rules_link,\n",

25
main.py
View File

@ -788,6 +788,18 @@ async def login(
{"email": identifier.lower()}
]
})]
or_conditions = [
{"email": identifier},
{"email": identifier.lower()},
]
try:
normalized_phone = normalize_phone(identifier)
if normalized_phone:
or_conditions.append({"phone": normalized_phone})
except:
pass
users = [doc async for doc in users_collection.find({"$or": or_conditions})]
if not users:
@ -1417,13 +1429,13 @@ app.include_router(router, tags=['Restore passwrod'])
@app.get("/login", response_class=HTMLResponse)
async def login_(
request: Request,
next: str | None = None,
next_url: str | None = None,
):
if not next:
next = request.headers.get("Referer")
context={"next": next}
print(next)
if 'profile/competition' in next:
if not next_url:
next_url = request.headers.get("Referer")
context={"next": next_url}
print(next_url)
if next_url and 'profile/competition' in next_url:
context.update({'notify':{'text':'Участвовать в конкурсе могут только пользователи сервиса! Пожалуйста, войдите или зарегистрируйтесь.', 'status':'info','duration':8000}})
return templates.TemplateResponse(
request=request,
@ -2041,6 +2053,7 @@ async def artists(
request=request, name="users/index.html", context={"id": 1, "user":user, "users": users}
)
@app.get("/my-paints", response_class=HTMLResponse)
async def My_paints(
request: Request,

3
migrate.py Normal file
View File

@ -0,0 +1,3 @@
from db import *
await posts_collection.update_many({'status':'publish'},{'$set':{'status':'В каталоге'}})

View File

@ -74,7 +74,7 @@ class LoginRequest(BaseModel):
class RestoreRequest(BaseModel):
identifier: str
next: str
next: str | None = None
class ResetPasswordRequest(BaseModel):

View File

@ -5,7 +5,7 @@
viewBox="0 0 0.37 0.37"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
<style>
<![CDATA[
.str0 {stroke:#2B2A29;stroke-width:0.04;stroke-miterlimit:22.9256}
.fil0 {fill:none}

Before

Width:  |  Height:  |  Size: 891 B

After

Width:  |  Height:  |  Size: 875 B

View File

@ -5,7 +5,7 @@
viewBox="0 0 0.37 0.37"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
<style>
<![CDATA[
.str0 {stroke:#2B2A29;stroke-width:0.04;stroke-miterlimit:22.9256}
.fil0 {fill:none}

Before

Width:  |  Height:  |  Size: 891 B

After

Width:  |  Height:  |  Size: 875 B

View File

@ -7,13 +7,13 @@
{% block title %}Сервис Автопостинга{% endblock %}
{% block head %}
<meta hid="og:title" property="og:title" content="Сервис Автопостинга">
<meta hid="og:description" property="og:description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta hid="og:image" property="og:image" content="https://avtopost.kultmost.ru/favicon.ico">
<meta property="og:title" content="Сервис Автопостинга">
<meta property="og:description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta property="og:image" content="https://avtopost.kultmost.ru/favicon.ico">
<meta name="facebook-domain-verification" content="uwjj4p3kr1xlxvcux4ujv7b6qujhl8">
<meta hid="og:url" property="og:url" content="https://avtopost.kultmost.ru/">
<meta hid="description" name="description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta hid="og:site_name" property="og:site_name" content="KULTMOST.АВТОПОСТ">
<meta property="og:url" content="https://avtopost.kultmost.ru/">
<meta name="description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta property="og:site_name" content="KULTMOST.АВТОПОСТ">
{{ header.style() }}
<style>
.newAbout .logotip {

View File

@ -32,7 +32,7 @@
{%- endmacro %}
{% macro style() -%}
<style type="text/css">
<style>
h2,p {
max-width: 100%;
overflow-wrap: break-word;

File diff suppressed because one or more lines are too long

View File

@ -110,7 +110,7 @@
{%- endmacro %}
{% macro style() -%}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.c_aq9et {
cursor: pointer;
transition: transform 0.1s linear, box‑shadow 0.1s linear;

View File

@ -206,7 +206,8 @@
</article>
</div>
</form>
<script defer>
<script>
document.addEventListener('DOMContentLoaded', () => {
/* ---------- Utility helpers (removed) ---------- */
/* Toggle visibility of a block (flex / none) */
@ -340,9 +341,10 @@ function bindLinkValidation({
linkErrorSelector: '#link-error'
});
});
</script>
<script defer>
<script>
document.addEventListener('DOMContentLoaded', () => {
const competition = JSON.parse({{competition|tojson}})
const competitionId = competition._id
@ -429,6 +431,7 @@ document.getElementById('preview_poster').src = `/b/competition/${competition.po
// input.value = moscowInstant.toISOString().slice(0, 16);
// date_finish.value = date_finish_date.toISOString().slice(0, 10);
});
</script>
<script>
var type = competition.type_;
@ -447,7 +450,8 @@ document.getElementById('preview_poster').src = `/b/competition/${competition.po
}
}
</script>
<script defer>
<script>
document.addEventListener('DOMContentLoaded', () => {
const minInput = document.getElementById('min_val');
const maxInput = document.getElementById('max_val');
@ -633,15 +637,14 @@ document.getElementById('preview_poster').src = `/b/competition/${competition.po
}
});
// Инициализация
// updateRange();
});
</script>
{%- endmacro %}
{% macro style() -%}
{{ competition.style() }}
{{ checkbox.style() }}
<style type="text/css">
<style>
.toggle-group {
display: flex;
margin-bottom: 15px;

View File

@ -574,7 +574,7 @@ document.addEventListener('DOMContentLoaded', () => {
{% macro style() -%}
{{ competition.style() }}
{{ checkbox.style() }}
<style type="text/css">
<style>
.toggle-group {
display: flex;
margin-bottom: 15px;

View File

@ -190,15 +190,15 @@
{% endblock %}
{% block head %}
<meta hid="og:title" property="og:title" content="{{competition.title}}">
<meta hid="og:description" property="og:description" content="{{competition.short_desc}}">
<!-- <meta hid="og:type" property="og:type" content="product"> -->
<meta hid="og:image" property="og:image" content="https://avtopost.kultmost.ru/b/competition/{{competition.poster_file_id}}">
<meta hid="og:url" property="og:url" content="https://avtopost.kultmost.ru/competition/{{competition._id}}">
<meta hid="description" name="description" content="{{competition.short_desc}}">
<meta hid="og:site_name" property="og:site_name" content="Конкурсы | KULTMOST.АВТОПОСТ">
<meta property="og:title" content="{{competition.title}}">
<meta property="og:description" content="{{competition.short_desc}}">
<!-- <meta property="og:type" content="product"> -->
<meta property="og:image" content="https://avtopost.kultmost.ru/b/competition/{{competition.poster_file_id}}">
<meta property="og:url" content="https://avtopost.kultmost.ru/competition/{{competition._id}}">
<meta name="description" content="{{competition.short_desc}}">
<meta property="og:site_name" content="Конкурсы | KULTMOST.АВТОПОСТ">
{{ header.style() }}
<style type="text/css">
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
@ -271,7 +271,7 @@ picture>img{width: 100%;object-fit: contain;}
.c_fyi26 { align-content: flex-start; align-items: flex-start; align-self: stretch; display: flex; flex-wrap: wrap; gap: 20px; padding-left: 100px; }
.c_r75b6 { align-items: flex-start; background: #FFF; display: flex; justify-content: center; width: 1600px; }
}</style>
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.inptS{height: 48px;display: flex;align-items: center;gap: 10px; border-bottom: 1px solid #5F1798;}
.b_x41jk { fill: #5F1798; height: 12px; width: 12px; }
.b_rroqu { height: 18px; width: 18px; }

View File

@ -17,7 +17,7 @@
{% macro style() -%}
{{card.style()}}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.b_p0hpm { color: #5F1798; font-family: Inter; font-size: 42px; font-style: normal; font-weight: 700; line-height: normal; text-transform: uppercase; }
/* Mobile */
@media (max-width: 699px) {

View File

@ -147,7 +147,7 @@
{% block head %}
{{ header.style() }}
<style type="text/css">
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }

View File

@ -1,5 +1,5 @@
{% macro style() %}
<style type="text/css">
<style>
.offer-label {
display: flex;
align-items: center;

View File

@ -12,15 +12,15 @@
{% endif %}
<footer class="c_t5jtn">
<div class="c_00gkd"><span class="c_62bje">KULTMOST</span><span class="c_jtxml">.АВТОПОСТ </span></div>
<nav class="c_wmdgs"><a class="c_bygqa">Главная</a><a class="c_bygqa">Маркетплейс</a><a class="c_bygqa">Мои
картины</a><a class="c_bygqa">Художники</a><a class="c_bygqa">Партнеры</a><a class="c_bygqa">О
платформе</a><a class="c_bygqa">Новости</a><a class="c_bygqa">Админ-панель</a></nav>
<nav class="c_wmdgs"><a class="c_bygqa" href="/">Главная</a><a class="c_bygqa" href="https://kultmost.ru/post">Маркетплейс</a><a class="c_bygqa" href="/my-paints">Мои
картины</a><a class="c_bygqa" href="/artists">Художники</a><a class="c_bygqa" href="/partner">Партнеры</a><a class="c_bygqa" href="/about">О
платформе</a><a class="c_bygqa" href="/news">Новости</a></nav>
<p class="c_vii7u">Автономная некоммерческая организация развития художественного творчества и ремесел «Культурный мост «Передвижные выставки» © 2026</p>
</footer>
{%- endmacro %}
{% macro style() -%}
<style type="text/css">
<style>
.st {
position: sticky;
bottom: 0;

View File

@ -159,22 +159,10 @@ document.addEventListener('DOMContentLoaded', () => {
</script>
{% endif %}
{% endif %}
<style>
.nav-icon {display: flex;flex-direction: column;justify-content: space-around;height: 24px;cursor: pointer;position: relative;width: 100%;}
.line {right: 0;display: block;width: 42px;height: 0;transition: all 0.3s ease;position: absolute;border: 1px solid;}
.line1{top:3px}
.line2{width: 32px;top: 10px;}
.line3{top: 18px;}
.line1.active {max-width: 30px;top: 9px;transform: rotate(135deg);}
.line2.active {transform: translateX(-500px);}
.line3.active {max-width: 30px;top: 9px;transform: rotate(-135deg);}
</style>
<header class="c_xirl1">
<div class="c_uh9b6">
<a class="c_7dj6c" href="/">
<img src="/static/logo375.jpeg" class="c_vnonn b_cj7sg" crossorigin="anonymous" fetchpriority="high">
<img src="/static/logo375.jpeg" alt="logo avtopost kultmost" class="c_vnonn b_cj7sg" crossorigin="anonymous" fetchpriority="high">
</a>
<div class="avatar">
{% if user %}
@ -183,15 +171,14 @@ document.addEventListener('DOMContentLoaded', () => {
</a>
{% endif %}
<button id="menu_button" class="c_pcz1l b_oxx99" aria-expanded="false">
<div class="nav-icon">
<span class="nav-icon">
<span class="line line1"></span>
<span class="line line2"></span>
<span class="line line3"></span>
</div>
</span>
</button>
</div>
</div>
<nav id="menu" class="c_olmk1">
<a class="c_qmp95 b_fokm1" href="/">Главная</a>
<a class="c_qmp95 b_fokm1" href="https://kultmost.ru/post">Маркетплейс</a>
@ -210,7 +197,8 @@ document.addEventListener('DOMContentLoaded', () => {
{% endif %}
{% if user and 'back' in user.get('scopes',[]) %}
<span id="backBtn" class="c_qmp95 b_fokm1 btn">Переключиться на Админа</span>
<script defer>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('backBtn').addEventListener('click', async () => {
try {
const resp = await fetch('/api/admin/back', { method: 'POST' });
@ -224,6 +212,7 @@ document.addEventListener('DOMContentLoaded', () => {
console.error('Network error:', err);
}
});
});
</script>
{% endif %}
<span
@ -288,7 +277,17 @@ document.addEventListener('DOMContentLoaded', () => {
{%- endmacro %}
{% macro style() -%}
<style type="text/css">/* Base (common) styles */
<style>
.nav-icon {display: flex;flex-direction: column;justify-content: space-around;height: 24px;cursor: pointer;position: relative;width: 100%;}
.line {right: 0;display: block;width: 42px;height: 0;transition: all 0.3s ease;position: absolute;border: 1px solid;}
.line1{top:3px}
.line2{width: 32px;top: 10px;}
.line3{top: 18px;}
.line1.active {max-width: 30px;top: 9px;transform: rotate(135deg);}
.line2.active {transform: translateX(-500px);}
.line3.active {max-width: 30px;top: 9px;transform: rotate(-135deg);}
</style>
<style>/* Base (common) styles */
.closeNotify{display: none;position: fixed;
width: 100vw;
height: 100vh;

View File

@ -111,7 +111,7 @@
{% endmacro %}
{% macro style() -%}
<style type="text/css">
<style>
.b_xsvl1 { color: #5F1798; font-family: Inter; font-size: 16px; font-style: normal; font-weight: 700; line-height: normal; text-align: center; padding: 0 10px;}
.b_u9ode { align-items: center; align-self: stretch; background: #FFF; border-radius: 5px; display: flex; justify-content: center; padding: 10px 0; }
.b_al8e7 { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }

View File

@ -6,7 +6,7 @@
{% block head %}
{{ header.style() }}
<style type="text/css">
<style>
body {font-family: Arial, sans-serif;}
.modal {display: none;position: fixed;z-index: 1;left: 0;top: 0;width: 100%;height: 100%;overflow: auto;background-color: rgba(0, 0, 0, 0.7);}
.modal-content {background-color: #fff;margin: 15% auto;padding: 20px;border: 1px solid #888;width: 80%;}

View File

@ -148,15 +148,15 @@
{% endblock %}
{% block head %}
<meta hid="og:title" property="og:title" content="{{exhibition.title}}">
<meta hid="og:description" property="og:description" content="{{exhibition.short_desc}}">
<!-- <meta hid="og:type" property="og:type" content="product"> -->
<meta hid="og:image" property="og:image" content="https://avtopost.kultmost.ru/b/exhibition/{{exhibition.poster_file_id}}">
<meta hid="og:url" property="og:url" content="https://avtopost.kultmost.ru/exhibition/{{exhibition._id}}">
<meta hid="description" name="description" content="{{exhibition.short_desc}}">
<meta hid="og:site_name" property="og:site_name" content="Конкурсы | KULTMOST.АВТОПОСТ">
<meta property="og:title" content="{{exhibition.title}}">
<meta property="og:description" content="{{exhibition.short_desc}}">
<!-- <meta property="og:type" content="product"> -->
<meta property="og:image" content="https://avtopost.kultmost.ru/b/exhibition/{{exhibition.poster_file_id}}">
<meta property="og:url" content="https://avtopost.kultmost.ru/exhibition/{{exhibition._id}}">
<meta name="description" content="{{exhibition.short_desc}}">
<meta property="og:site_name" content="Конкурсы | KULTMOST.АВТОПОСТ">
{{ header.style() }}
<style type="text/css">
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
@ -229,7 +229,7 @@ picture>img{width: 100%;object-fit: contain;}
.c_fyi26 { align-content: flex-start; align-items: flex-start; align-self: stretch; display: flex; flex-wrap: wrap; gap: 20px; padding-left: 100px; }
.c_r75b6 { align-items: flex-start; background: #FFF; display: flex; justify-content: center; width: 1600px; }
}</style>
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.inptS{height: 48px;display: flex;align-items: center;gap: 10px; border-bottom: 1px solid #5F1798;}
.b_x41jk { fill: #5F1798; height: 12px; width: 12px; }
.b_rroqu { height: 18px; width: 18px; }

View File

@ -93,7 +93,7 @@
{% block head %}
{{ header.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.c_aq9et {
cursor: pointer;
transition: transform 0.1s linear, box‑shadow 0.1s linear;
@ -134,7 +134,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.b_ornr9{height: auto;}
}
</style>
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.b_p0hpm { color: #5F1798; font-family: Inter; font-size: 42px; font-style: normal; font-weight: 700; line-height: normal; text-transform: uppercase; }
/* Mobile */
@media (max-width: 699px) {

View File

@ -561,7 +561,7 @@ function bindLinkValidation({
{{ header.style() }}
{{ competition.style() }}
{{ checkbox.style() }}
<style type="text/css">
<style>
.toggle-group {
display: flex;
margin-bottom: 15px;

View File

@ -555,7 +555,7 @@ bindLinkValidation({
{{ header.style() }}
{{ competition.style() }}
{{ checkbox.style() }}
<style type="text/css">
<style>
.toggle-group {
display: flex;
margin-bottom: 15px;

View File

@ -7,13 +7,13 @@
{% block title %}Сервис Автопостинга{% endblock %}
{% block head %}
<meta hid="og:title" property="og:title" content="Сервис Автопостинга">
<meta hid="og:description" property="og:description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta hid="og:image" property="og:image" content="https://avtopost.kultmost.ru/favicon.ico">
<meta property="og:title" content="Сервис Автопостинга">
<meta property="og:description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta property="og:image" content="https://avtopost.kultmost.ru/favicon.ico">
<meta name="facebook-domain-verification" content="uwjj4p3kr1xlxvcux4ujv7b6qujhl8">
<meta hid="og:url" property="og:url" content="https://avtopost.kultmost.ru/">
<meta hid="description" name="description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta hid="og:site_name" property="og:site_name" content="KULTMOST.АВТОПОСТ">
<meta property="og:url" content="https://avtopost.kultmost.ru/">
<meta name="description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta property="og:site_name" content="KULTMOST.АВТОПОСТ">
<link rel="canonical" href="https://avtopost.kultmost.ru/"/>
{{ header.style() }}
{{ main.style() }}

View File

@ -20,7 +20,7 @@
<input type="password" id="password" name="password" class="c_xhd5k" required>
<label for="password" class="c_z3ybu floating-label">Пароль</label>
<div type="button" class="password-toggle" aria-label="Переключатель видимости пароля">
<button type="button" class="password-toggle" aria-label="Переключатель видимости пароля">
<svg class="eye eye-open" width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
@ -33,7 +33,7 @@
<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>
</button>
</div>
<span class="login-hint">Минимум 8 символов</span>
@ -59,7 +59,6 @@
</div>
</div>
</form>
<script>
function sendToNextPage(e) {
e.preventDefault();
@ -73,7 +72,8 @@ function sendToNextPage(e) {
window.location.href = "/restore{% if next %}?next={{next}}{% endif %}";
}
</script>
<script defer>
<script>
document.addEventListener('DOMContentLoaded', () => {
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const createBtn = document.getElementById('createBtn');
@ -209,7 +209,7 @@ function sendToNextPage(e) {
}
});
}
});
</script>
{{ footer.section() }}
<script>
@ -234,7 +234,7 @@ fetch('/api/me')
{% block head %}
{{ header.style() }}
<style type="text/css">
<style>
.poster-group {position: relative;}
.password-group { position: relative; }
a{color: #5F1798;}

View File

@ -12,7 +12,7 @@
class="c_ifykb">Видео-презентация</span> </button>
</div>
</div>
<picture class="c_qa0pb"><img crossorigin="anonymous" fetchpriority="high" class="c_qa0pb" src="/static/painter.svg"></picture>
<picture class="c_qa0pb"><img crossorigin="anonymous" fetchpriority="high" class="c_qa0pb" src="/static/painter.svg" alt=""></picture>
</main>
<div id="modalVideo" class="modal" style="display: none;">
<span class="close" onclick="videocloseModalButton()" style="z-index: 2;">&times;</span>
@ -37,7 +37,7 @@
{%- endmacro %}
{% macro style() -%}
<style type="text/css">
<style>
body {
font-family: Arial, sans-serif;
}

View File

@ -355,7 +355,7 @@ display: flex;
color: white;
}
</style>
<style type="text/css">
<style>
.poster-group {position: relative;}
.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;}

View File

@ -354,7 +354,7 @@ display: flex;
color: white;
}
</style>
<style type="text/css">
<style>
.poster-group {position: relative;}
.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;}

View File

@ -5,15 +5,15 @@
{% block title %}Сервис Автопостинга{% endblock %}
{% block head %}
<!-- <meta hid="og:title" property="og:title" content="Сервис Автопостинга">
<meta hid="og:description" property="og:description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta hid="og:image" property="og:image" content="https://avtopost.kultmost.ru/favicon.ico">
<!-- <meta property="og:title" content="Сервис Автопостинга">
<meta property="og:description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta property="og:image" content="https://avtopost.kultmost.ru/favicon.ico">
<meta name="facebook-domain-verification" content="uwjj4p3kr1xlxvcux4ujv7b6qujhl8">
<meta hid="og:url" property="og:url" content="https://avtopost.kultmost.ru/">
<meta hid="description" name="description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta hid="og:site_name" property="og:site_name" content="KULTMOST.АВТОПОСТ"> -->
<meta property="og:url" content="https://avtopost.kultmost.ru/">
<meta name="description" content="Сервис создан для художников, скульпторов, искусствоведов, журналистов, галеристов, музеев и разных выставочных пространств с возможностью выложить пост картины, скульптуры, новостей, анонсов выставок или любую другую информацию, разместив сразу в нескольких Ваших социальных сетях интернета, арт-маркетплейсах и сообществах партнёров сервиса одним нажатием!">
<meta property="og:site_name" content="KULTMOST.АВТОПОСТ"> -->
{{ header.style() }}
<style type="text/css">
<style>
/* Base (common) styles */
.b_0a7ub {
-webkit-box-orient: vertical;

View File

@ -6,13 +6,13 @@
{% block title %}{{paint.title}}{% endblock %}
{% block head %}
<meta hid="og:title" property="og:title" content="{{paint.title}}">
<meta hid="og:description" property="og:description" content="{{description}}">
<meta hid="og:type" property="og:type" content="product">
<meta hid="og:image" property="og:image" content="https://avtopost.kultmost.ru/b/posts/{{paint.medias[0]}}">
<meta hid="og:url" property="og:url" content="https://avtopost.kultmost.ru/paintings/{{paint._id}}">
<meta hid="description" name="description" content="{{description}}">
<meta hid="og:site_name" property="og:site_name" content="Картины | KULTMOST.АВТОПОСТ">
<meta property="og:title" content="{{paint.title}}">
<meta property="og:description" content="{{description}}">
<meta property="og:type" content="product">
<meta property="og:image" content="https://avtopost.kultmost.ru/b/posts/{{paint.medias[0]}}">
<meta property="og:url" content="https://avtopost.kultmost.ru/paintings/{{paint._id}}">
<meta name="description" content="{{description}}">
<meta property="og:site_name" content="Картины | KULTMOST.АВТОПОСТ">
{{ header.style() }}
<style>
@ -102,7 +102,7 @@
transform: scale(1.35);
}
</style>
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.b_mmlhf { border-radius: 1em;
height: 70px;
min-width: 70px;

View File

@ -29,7 +29,7 @@
{% block head %}
{{ header.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.c_o0nwu{
border-radius: 1em;
height: 100px;

View File

@ -28,7 +28,7 @@
{%- endmacro %}
{% macro style() -%}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.b_f7c7g { align-self: stretch; background: #D9D9D9; object-fit: contain;}
.b_jh0gq { align-items: flex-start; display: flex; flex-direction: column; width: 335px; }
.c_2e7ix{ margin-bottom: 20px; text-transform: uppercase;}

View File

@ -387,7 +387,7 @@ display: flex;
color: white;
}
</style>
<style type="text/css">
<style>
.poster-group {position: relative;}
.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;}

View File

@ -20,7 +20,7 @@
<input readonly name="grade" id="grade" class="c_xhd5k" required data-toast="Поле Звание должно быть заполнено" data-select="1" data-radio="0" data-list="['Без звания', 'Заслуженный художник Российской федерации', 'Народный художник Российской федерации', 'Академик Российской академии художеств', 'Почетный член Российской академии художеств', 'Действительный член Российской Академии художеств', 'Член-корреспондент Российской академии художеств', 'Член Союза художников России', 'Член творческого союза художников России', 'Член Московского союза художников', 'Член Подмосковного союза художников', 'Член Союза русских художников', 'Творческий союз профессиональных художников', 'Евразийский союз художников', 'Международный художественный фонд', 'Творческое объединение женщин-художников']" data-label="Сохранить" data-custom_placeholder="Напишите свой вариант" data-custom="1" data-title="Выберите звание из списка">
<label for="grade" class="c_z3ybu floating-label">Звание</label>
</div>
<script async>
<script>
document.addEventListener('DOMContentLoaded', () => {
showToast({message: 'Выберите из списка актуальное звание', status: 'info', duration:3000});
});
@ -521,7 +521,7 @@
{% block head %}
{{ header.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.b_xj1br { color: #FFF; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
.b_dc4rx { align-items: center; align-self: stretch; background: #5F1798; border-radius: 5px; display: flex; justify-content: center; padding: 12px 0; }
.b_5rck0 { align-items: center; align-self: stretch; background: #F39318; border-radius: 5px; display: flex; justify-content: center; padding: 12px 0; }
@ -625,7 +625,7 @@ display: flex;
color: white;
}
</style>
<style type="text/css">
<style>
.poster-group {position: relative;}
.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;}

View File

@ -70,7 +70,7 @@
</script>
{%- endmacro %}
{% macro style() -%}
<style type="text/css">
<style>
picture>img{ width: 100%; object-fit: scale-down; }
.c_eiyz0{cursor: pointer;}
.b_ya0t5 { align-self: stretch; border-radius: 5px; height: 222px; }

View File

@ -162,7 +162,7 @@ document.querySelectorAll('.main-row').forEach(row => {
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {

View File

@ -36,7 +36,7 @@
{{ header.style() }}
{{ card.style() }}
{{ profile_nav.style() }}
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {

View File

@ -413,7 +413,7 @@ document.addEventListener('DOMContentLoaded', () => {
{{ card.style() }}
{{ profile_nav.style() }}
{{ checkbox.style() }}
<style type="text/css">
<style>
.inptS{height: 48px;
display: flex; width: 335px;
align-items: center;gap: 10px; border-bottom: 1px solid #5F1798;}

View File

@ -51,7 +51,7 @@
{%- endmacro %}
{% macro style() -%}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.saves{padding: 8px 10px;
width: 270px;
justify-content: center;

View File

@ -328,7 +328,7 @@ async function btnSend(btn, itemId, sender, too){
{{ header.style() }}
{{ competition.style() }}
{{ profile_nav.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.c_aihm6{width: 100%;}
.mw{margin: 0 20px;
width: 100%;}
@ -462,7 +462,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.b_ornr9{height: auto;}
}
</style>
<style type="text/css">
<style>
.menua{width: 100%;
display: flex;
flex-direction: column;
@ -507,7 +507,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.c_nnsh6>a>img{display: none;}
}</style>
<style type="text/css">
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }

View File

@ -106,7 +106,7 @@
{{ header.style() }}
{{ competition.style() }}
{{ profile_nav.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.c_aihm6{width: 100%;}
.mw{margin: 0 20px;
width: 100%;}
@ -240,7 +240,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.b_ornr9{height: auto;}
}
</style>
<style type="text/css">
<style>
.menua{width: 100%;
display: flex;
flex-direction: column;
@ -285,7 +285,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.c_nnsh6>a>img{display: none;}
}</style>
<style type="text/css">
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }

View File

@ -66,7 +66,7 @@
{{ header.style() }}
{{ profile_nav.style() }}
{{ competition.style() }}
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {

View File

@ -351,7 +351,7 @@ window.location.reload();
width: 25%;
text-align: center;}
</style>
<style type="text/css">
<style>
.c_aq9et { position: sticky;
bottom: 115px;
z-index: 5;display: flex; padding: 19px 33px; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #5F1798; }

View File

@ -23,7 +23,7 @@
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {

View File

@ -315,7 +315,7 @@
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.c_aq9et {
cursor: pointer;
transition: transform 0.1s linear, box‑shadow 0.1s linear;
@ -356,7 +356,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.b_ornr9{height: auto;}
}
</style>
<style type="text/css">
<style>
.menua{width: 100%;
display: flex;
flex-direction: column;
@ -401,7 +401,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.c_nnsh6>a>img{display: none;}
}</style>
<style type="text/css">
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }

View File

@ -150,7 +150,7 @@
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.mw{margin: 0 20px;
width: 100%;}
.main__top{ display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:16px;}
@ -283,7 +283,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.b_ornr9{height: auto;}
}
</style>
<style type="text/css">
<style>
.menua{width: 100%;
display: flex;
flex-direction: column;

View File

@ -139,7 +139,7 @@ async function accept_inv(inv_id) {
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.c_aq9et {
cursor: pointer;
transition: transform 0.1s linear, box‑shadow 0.1s linear;
@ -180,7 +180,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.b_ornr9{height: auto;}
}
</style>
<style type="text/css">
<style>
.menua{width: 100%;
display: flex;
flex-direction: column;

View File

@ -63,7 +63,7 @@
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style type="text/css">
<style>
picture>img{ width: 100%; object-fit: scale-down; }
.b_ya0t5 { align-self: stretch; border-radius: 5px; height: 221px; }
@ -90,7 +90,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
@media (min-width: 901px) {
.c_5uh64 { color: rgba(58, 58, 58, 0.75); font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
}</style>
<style type="text/css">
<style>
.b_rhmvx { border-radius: 1em; height: 140px; min-width: 140px; max-width: 100%; object-fit: cover; object-position: top;}
.b_lgvu4 { align-items: center; background: #5F1798; border-radius: 25px; bottom: 0; display: flex; height: 50px; justify-content: center; position: absolute; right: 0; width: 50px; }

View File

@ -278,7 +278,7 @@ document.querySelectorAll('.b_k2cle button').forEach(button => {
{{ header.style() }}
{{card.style()}}
{{ profile_nav.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
/* Активное состояние */
.c_fx6lz.active,
.c_deket.active {
@ -375,7 +375,7 @@ document.querySelectorAll('.b_k2cle button').forEach(button => {
.c_b2jz8 { -webkit-box-orient: vertical; -webkit-line-clamp: 1; color: #000; display: -webkit-box; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; overflow: hidden; text-overflow: ellipsis; }
.c_jcujv { align-items: flex-start; background: #FFF; display: flex; flex-direction: column; gap: 30px; padding-left: 100px; width: 1600px; }
}</style>
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {

View File

@ -279,7 +279,7 @@ document.querySelectorAll('.b_k2cle button').forEach(button => {
{{ header.style() }}
{{card.style()}}
{{ profile_nav.style() }}
<style type="text/css">
<style>
.b_thlt1 { align-items: center; background: rgba(14, 214, 70, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
.b_5dr47 { color: #0ED646; font-family: Inter; font-size: 12px; font-style: normal; font-weight: 700; line-height: normal; }
.b_thlt1_2 { align-items: center; background: rgba(95, 23, 152, 0.20); border-radius: 30px; display: flex; justify-content: center; padding: 1.5px 10px; }
@ -404,7 +404,7 @@ document.querySelectorAll('.b_k2cle button').forEach(button => {
.c_b2jz8 { -webkit-box-orient: vertical; -webkit-line-clamp: 1; color: #000; display: -webkit-box; font-family: Inter; font-size: 24px; font-style: normal; font-weight: 700; line-height: normal; overflow: hidden; text-overflow: ellipsis; }
.c_jcujv { align-items: flex-start; background: #FFF; display: flex; flex-direction: column; gap: 30px; padding-left: 100px; width: 1600px; }
}</style>
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {

View File

@ -97,7 +97,7 @@ document.querySelectorAll('#rePublishCompetition').forEach(btn => {
{{ header.style() }}
{{card.style()}}
{{ profile_nav.style() }}
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {

View File

@ -500,7 +500,7 @@ genre = genres.map(g => {
{{ header.style() }}
{{ profile_nav.style() }}
{{ competition.style() }}
<style type="text/css">
<style>
{{label.green()}}
{{label.purple()}}
@media (min-width: 901px) {

View File

@ -149,7 +149,7 @@
{{ header.style() }}
{{ profile_nav.style() }}
{{ competition.style() }}
<style type="text/css">
<style>
{{label.green()}}
{{label.purple()}}
#preview_poster{min-height: 456px;}

View File

@ -83,7 +83,7 @@ async function deleteExhibition(btn, _id) {
{{ header.style() }}
{{ profile_nav.style() }}
{{ competition.style() }}
<style type="text/css">
<style>
body{background: #F6EBFF;}
#preview_poster{min-height: 456px;}
/* Mobile */

View File

@ -28,7 +28,7 @@
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {

View File

@ -34,7 +34,7 @@
{{ card.style() }}
{{ profile_nav.style() }}
<style type="text/css">
<style>
.b_rhmvx { border-radius: 1em; height: 140px; min-width: 140px; max-width: 100%; object-fit: cover; object-position: top;}

View File

@ -163,7 +163,7 @@ document.addEventListener('change', function (e) {
{{ header.style() }}
{{ profile_nav.style() }}
{{ checkbox.style() }}
<style type="text/css">
<style>
hr{width: 100%;
color: #5e179888;}
.b_v6dzl { height: 12px; stroke-width: 3px; stroke: #0ED646; width: 16px; }
@ -198,7 +198,7 @@ document.addEventListener('change', function (e) {
.c_jkbyo { align-items: flex-start; align-self: stretch; display: flex; flex-direction: column; gap: 5px; }
.c_fswiv { align-items: flex-start; background: #FFF; display: flex; flex-direction: column; width: 100%; }
}</style>
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.b_qgav8 { height: 12px; stroke-width: 3px; stroke: #0ED646; width: 16px; }
.b_h1rw4 { height: 16px; width: 20px; }
.b_bpfdu { flex-shrink: 0; height: 30px; width: 30px; }
@ -209,7 +209,7 @@ document.addEventListener('change', function (e) {
.b_jphak { align-items: center; background: #5F1798; border-radius: 5px; display: flex; justify-content: center; padding: 12px 30px; }
.b_2tr5g { align-items: center; display: flex; flex-direction: column; gap: 9px; }
</style>
<style type="text/css">
<style>
/* Mobile */
@media (max-width: 699px) {
.c_mrl59 {

File diff suppressed because one or more lines are too long

View File

@ -158,7 +158,7 @@ fetch('/api/me')
{% block head %}
{{ header.style() }}
<style type="text/css">
<style>
.poster-group {position: relative;}
.password-group { position: relative; }
a{color: #5F1798;}

View File

@ -116,7 +116,7 @@
{% block head %}
{{ header.style() }}
<style type="text/css">
<style>
.poster-group {position: relative;}
.password-group { position: relative; }
a{color: #5F1798;}

View File

@ -98,7 +98,7 @@
{% block head %}
{{ header.style() }}
{{ footer.style() }}
<style type="text/css">/* Base (common) styles */
<style>/* Base (common) styles */
.b_25g00 { align-items: flex-start; display: flex; flex-direction: column; gap: 20px; width: 335px; }
.b_7wywm { fill: #1F1F1F; height: 16px; width: 16.041px; }

View File

@ -97,7 +97,7 @@ document.addEventListener('DOMContentLoaded', () => {
{% block head %}
{{ header.style() }}
{{ card.style() }}
<style type="text/css">
<style>
.o{font-style: italic;color: #777;}
.it{padding: 0px 15px;}
.b_xsvl1 { color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 700; line-height: normal; text-align: center; padding: 0 10px;}

View File

@ -8,7 +8,7 @@ import httpx
import logging
from bson import ObjectId
from config import *
from db import mongo_client, redis_client, gridfs_map
from db import mongo_client, redis_client, gridfs_map, social_collection
import asyncio
import secrets
from utils import fetch_bytes, get_password_hash
@ -194,6 +194,9 @@ class TokenManager:
vk_id = vk_profile.get("id")
# 1) Уже привязанный VK-аккаунт
existing = await social_collection.find_one({'id':vk_id},{'user_id':1})
if existing:
return existing["user_id"], False
existing = await users.find_one({"vk_id": vk_id}, {"_id": 1})
if existing:
return str(existing["_id"]), False

View File

@ -18,6 +18,7 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
import base64
import json
import re
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
@ -261,6 +262,11 @@ def display_user(user):
}
def is_valid_email(email: str) -> bool:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
def normalize_phone(phone: Optional[str]) -> Optional[str]:
if not phone or len(phone) < 6:
return None

12
vk.py
View File

@ -89,12 +89,12 @@ async def oauth_callback(request: Request, response: Response, code: str, state:
return RedirectResponse(url='/login', status_code=status.HTTP_303_SEE_OTHER)
# Регистрация/вход через VK — выдаём нашу сессию и редиректим на next
# if result.get('purpose') == 'register':
# await issue_session(result['user_id'], request, response, client_id='vk')
# next_url = result.get('next') or '/profile'
# if not is_safe_redirect(next_url):
# next_url = '/profile'
# return RedirectResponse(url=next_url, status_code=status.HTTP_303_SEE_OTHER, headers=response.headers)
if result.get('purpose') == 'register':
await issue_session(result['user_id'], request, response, client_id='vk')
next_url = result.get('next') or '/profile'
if not is_safe_redirect(next_url):
next_url = '/profile'
return RedirectResponse(url=next_url, status_code=status.HTTP_303_SEE_OTHER, headers=response.headers)
# Привязка соцсети к уже залогиненному пользователю
return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)