# `main.py` generate from `main.ipynb` by `build.ipynb` # FastAPI ## App from fastapi import FastAPI, APIRouter, UploadFile, File, Depends, HTTPException, Body, Security, Form, Request, Response, BackgroundTasks, Query from motor.motor_asyncio import AsyncIOMotorCollection import jwt from datetime import datetime, timedelta, timezone, date import io import asyncio from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, StreamingResponse, RedirectResponse from starlette.datastructures import UploadFile as StarletteUploadFile from fastapi.staticfiles import StaticFiles import mimetypes from urllib.parse import quote from starlette.middleware.gzip import GZipMiddleware import uuid import bcrypt from typing import List, Any, Dict, Optional, Union from datetime import timedelta from urllib.parse import urlencode from bson import ObjectId from PIL import Image from io import BytesIO from dateutil.parser import isoparse import httpx import string from db import * import vk as vk import tg as tg from delivery_router import router as delivery_router from auth import get_user, get_current_user, get_bearer_token, get_access_token, get_token from roles import * from utils import * from tasks import * from models import * from config import * import logging import json from templates import templates import email_task as email_task from pydantic import EmailStr import secrets from pymongo import ReturnDocument logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) ### Конфигурация app = FastAPI(title="KULTMOST") app.mount("/static", StaticFiles(directory="static"), name="static") app.add_middleware(GZipMiddleware, minimum_size=500) app.add_middleware( CORSMiddleware, allow_origins=['*'], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(delivery_router) app.include_router(vk.router, prefix='/api/v1/social', tags=["Auth"]) ### Хелперы RUS_TO_LAT = { 'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'yo','ж':'zh','з':'z', 'и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r', 'с':'s','т':'t','у':'u','ф':'f','х':'kh','ц':'ts','ч':'ch','ш':'sh','щ':'shch', 'ъ':'"','ы':'y','ь':"'",'э':'e','ю':'yu','я':'ya', ' ':'-', 'А':'A','Б':'B','В':'V','Г':'G','Д':'D','Е':'E','Ё':'Yo','Ж':'Zh','З':'Z', 'И':'I','Й':'Y','К':'K','Л':'L','М':'M','Н':'N','О':'O','П':'P','Р':'R', 'С':'S','Т':'T','У':'U','Ф':'F','Х':'Kh','Ц':'Ts','Ч':'Ch','Ш':'Sh','Щ':'Shch', 'Ъ':'"','Ы':'Y','Ь':"'",'Э':'E','Ю':'Yu','Я':'Ya' } def transliterate(text: str) -> str: return ''.join(RUS_TO_LAT.get(ch, '' if ch not in string.ascii_letters else ch) for ch in text) async def get_latest_news() -> List[dict]: cursor = news_collection.find({'publish_on_platform': True}).sort("_id", -1).limit(30) docs: List[dict] = [] async for doc in cursor: published = doc.get("published_at") if published is not None and hasattr(published, "isoformat"): doc["published_at"] = published.isoformat() docs.append(doc) return docs async def logo_user_task(user: dict): img_id = user.get('img') if user else None new_id = None if img_id: # download original image bytes file_data = await gridfs_map['users'].open_download_stream(ObjectId(img_id)) original_image_data = await file_data.read() # store original in GridFS (keep a copy) original_img_id = await gridfs_map['users'].upload_from_stream(f'original_{img_id}', BytesIO(original_image_data)) # open image with PIL img = Image.open(BytesIO(original_image_data)) w, h = img.size # resize if needed if w > 500: factor = 500 / w new_size = (500, int(h * factor)) img = img.resize(new_size, resample=Image.LANCZOS) # ensure image mode is compatible with JPEG if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info): # composite onto white background to remove alpha background = Image.new('RGB', img.size, (255, 255, 255)) alpha = img.split()[-1] if img.mode != 'P' else img.convert('RGBA').split()[-1] background.paste(img.convert('RGBA'), mask=alpha) img = background else: img = img.convert('RGB') # save to buffer as JPEG buf = BytesIO() # img.save(buf, format='JPEG', quality=85) img.save(buf, format="WEBP", quality=85, method=6) buf.seek(0) # upload processed avatar to GridFS new_id = await gridfs_map['users'].upload_from_stream('500_avatar.jpg', buf) # update user doc with new image ids (store as strings to match your original code) await users_collection.update_one( {'_id': ObjectId(user.get('_id'))}, {'$set': {'img': str(new_id), 'original_img': str(original_img_id)}} ) async def poster_competition(competition): img_id = competition.get('poster_orig_id') try: stream = await gridfs_map['competition'].open_download_stream(ObjectId(img_id)) data = await stream.read() buf = await build_composite(data, 0.75, 800) new_id = await gridfs_map['competition'].upload_from_stream(f"500_{stream.filename}", buf) except Exception: new_id = img_id print(f'Ошибка обработки id {img_id} в выставке {competition["_id"]}') if new_id: await competitions_col.update_one( {'_id': ObjectId(competition.get('_id'))}, {'$set': { 'poster_file_id': new_id }} ) async def poster_exhibition(exhibition): img_id = exhibition.get('poster_orig_id') try: stream = await gridfs_map['exhibition'].open_download_stream(ObjectId(img_id)) data = await stream.read() buf = await build_composite(data, 0.73, 800) new_id = await gridfs_map['exhibition'].upload_from_stream(f"500_{stream.filename}", buf) except Exception: new_id = img_id print(f'Ошибка обработки id {img_id} в выставке {exhibition["_id"]}') if new_id: await exhibition_collection.update_one( {'_id': ObjectId(exhibition.get('_id'))}, {'$set': { 'poster_file_id': new_id }} ) async def replace_new_medias(post, links_media): img_ids = post.get('medias_original', []) if not isinstance(img_ids, list): img_ids = [img_ids] new_ids = [] for img_id in img_ids: if img_id in links_media: new_ids.append(links_media['img_id']) continue try: stream = await gridfs_map['posts'].open_download_stream(ObjectId(img_id)) data = await stream.read() buf = await build_composite(data) new_id = await gridfs_map['posts'].upload_from_stream(f"500_{stream.filename}", buf) new_ids.append(str(new_id)) except Exception: new_ids.append(str(img_id)) print(f'Ошибка обработки id {img_id} в посте {post["_id"]}') if new_ids: await posts_collection.update_one( {'_id': ObjectId(post.get('_id'))}, {'$set': { 'medias': new_ids }} ) async def replace_post_medias(post): img_ids = post.get('medias_original', []) if not isinstance(img_ids, list): img_ids = [img_ids] new_ids = [] for img_id in img_ids: try: stream = await gridfs_map['posts'].open_download_stream(ObjectId(img_id)) data = await stream.read() buf = await build_composite(data) new_id = await gridfs_map['posts'].upload_from_stream(f"500_{stream.filename}", buf) new_ids.append(str(new_id)) except Exception: new_ids.append(str(img_id)) print(f'Ошибка обработки id {img_id} в посте {post["_id"]}') if new_ids: await posts_collection.update_one( {'_id': ObjectId(post.get('_id'))}, {'$set': { 'medias': new_ids }} ) async def replace_news_medias(news): img_ids = news.get('orig_media', []) if not isinstance(img_ids, list): img_ids = [img_ids] new_ids = [] for img_id in img_ids: try: stream = await gridfs_map['news'].open_download_stream(ObjectId(img_id)) data = await stream.read() buf = await build_composite(data) new_id = await gridfs_map['news'].upload_from_stream(f"500_{stream.filename}", buf) new_ids.append(str(new_id)) except Exception: new_ids.append(str(img_id)) print(f'Ошибка обработки id {img_id} в посте {news["_id"]}') if new_ids: await news_collection.update_one( {'_id': ObjectId(news.get('_id'))}, {'$set': { 'media': new_ids }} ) async def save_to_gridfs(upload: UploadFile, bucket_name: str): if upload is None: return None grid = gridfs_map.get(bucket_name) if grid is None: return None filename = upload.filename or str(uuid.uuid4()) contents = await upload.read() stream = io.BytesIO(contents) try: file_id = await grid.upload_from_stream(filename, stream) return str(file_id) finally: await upload.close() async def posts_temp(user,request): data=[] async for i in posts_collection.find({'user._id': user.get('_id')}).sort('_id',-1).limit(1000): data.append(i) return templates.TemplateResponse( request=request, name="profile/posts.html", context={ "user": user, 'cards': data} ) @app.get("/cron/day") async def cron_every_day(token: str = Security(get_token)): res = await competitions_col.update_many( { "date_finish": {"$lt": datetime.now(timezone.utc) + timedelta(hours=3)}, "status": {"$lt": 2}, }, {"$set": {"status": 2}}, ) return {} # await notify_collection.delete_many({'title':'Доставка картины'}) # await notify_collection.update_many({'title':'Доставка картины'},{'$set':{'type':'delivery'}}) # async for existing in delivery_collection.find({ # 'exhibition_id': # '6a55f0e90154435ba37a7c25' # , # 'status': # 'Ожидает получения' # }): # print(str(existing['receiver'].get('_id'))) # notify = await notify_in_profile(NotifyItem( # user_id=str(existing['receiver'].get('_id')), # type_item='delivery', # title='Доставка картины', # link=f'/profile/delivery', # text=f'{str(existing['sender']['username'])} прислал вам картину — {existing['title']}' # )) # print(notify) # await delivery_collection.update_one({'_id': existing.get('_id')},{'$set':{'notify': notify.upserted_id}}) @app.get("/cron/5") async def cron_every_five_min(token: str = Security(get_token)): now = datetime.now(timezone.utc) limit_dt = now + timedelta(hours=3) prod_filter = {"prod": PROD} # 1) If there's an exhibition that is going to be finished soon -> move deliveries to "Возврат художнику" exhibition_finished = await exhibition_collection.find_one_and_update( {**prod_filter, "status": 1, "date_finish": {"$lt": limit_dt}}, {"$set": {"status": 2}}, return_document=ReturnDocument.AFTER, ) if exhibition_finished: exhibition_id_str = str(exhibition_finished["_id"]) async for doc in delivery_collection.find( {"exhibition_id": exhibition_id_str, "status": "Получено", **prod_filter}, {"_id": 0}, ): sender = doc.get("sender") post_id = doc.get("post_id") exhibition_id = doc.get("exhibition_id") new_doc = { **doc, "delivery_type": "", "track": "", "status": "Возврат художнику", "receiver": sender, "sender": doc.get("receiver"), } await delivery_collection.update_one( {**prod_filter, "status": "Возврат художнику", "post_id": post_id, "exhibition_id": exhibition_id}, {"$set": new_doc}, upsert=True, ) return {} tasks = [] async for exhibition_to_start in exhibition_collection.find( {**prod_filter} ): exhibition_id_str = str(exhibition_to_start["_id"]) exhibition_title = exhibition_to_start.get("title") notified_user_id = None notified_post_user_id = None async for doc in delivery_collection.find( {"exhibition_id": exhibition_id_str, "status": "Получено", **prod_filter}, {"_id": 0}, ): post_id = doc.get("post_id") exhibition_id = doc.get("exhibition_id") post = await posts_collection.find_one({"_id": ObjectId(post_id)}) # Use user from post; keep them even if multiple deliveries exist if post and post.get("user") and not notified_post_user_id: notified_post_user_id = str(post["user"].get("_id")) new_doc = { **doc, "exhibition": exhibition_to_start, "status": "На выставке", "user": post.get("user") if post else None, "post": post, **prod_filter } tasks.append(delivery_collection.update_one( {**prod_filter, "status": "На выставке", "post_id": post_id, "exhibition_id": exhibition_id}, {"$set": new_doc}, upsert=True, )) tasks.append(posts_collection.update_one( {"_id": ObjectId(post_id)}, {"$set": {"status": "На выставке"}} )) # NOTE: your original code used `post` from inside the loop (could be last item / even undefined). # This version notifies once using the first found post user. if notified_post_user_id: await notify_in_profile( NotifyItem( user_id=notified_post_user_id, type_item="exhibtion", title=f"Выставка {exhibition_title}", link="/profile/exhibition", text="Ваши картины на выставке", ) ) if tasks: asyncio.gather(*tasks) # 2) Otherwise find an exhibition that should start soon -> move deliveries to "На выставке" exhibition_to_start = await exhibition_collection.find_one_and_update( {**prod_filter, "status": 0, "datetime_start": {"$lt": limit_dt}}, {"$set": {"status": 1}}, return_document=ReturnDocument.AFTER, ) if not exhibition_to_start: return {} # If vk/tg arrays are empty -> send social invitation if exhibition_to_start.get("vk") == [] or exhibition_to_start.get("tg") == []: await service_social_exhibition(bson_clear(exhibition_to_start)) return {} ### Эндпоинты @app.post("/api/register", response_model=dict) async def register( background_tasks: BackgroundTasks, username: Optional[str] = Form(None), phone: Optional[str] = Form(None), email: Optional[str] = Form(None), password: str = Form(...), client_id: str = Form(...), roles: List[str] = Form(...), birthday: Optional[date] = Form(None), grade: Optional[str] = Form(None), Female_Name_Lastame: Optional[str] = Form(None), living_place: Optional[str] = Form(None), offer_accepted: Optional[bool] = Form(False), img: Optional[UploadFile] = File(None), request: Request = None, response: Response = None ): user = UserRegister( username=username, phone=phone, email=email, password=password, client_id=client_id, roles=roles, birthday=birthday, grade=grade, Female_Name_Lastame=Female_Name_Lastame, living_place=living_place, offer_accepted=offer_accepted ) # if normalize_phone(user.phone) and await users_collection.find_one({"phone": normalize_phone(user.phone)}): # raise HTTPException(400, "Телефон уже зарегестрирован") if user.email and await users_collection.find_one({"email": user.email}): raise HTTPException(400, "Email уже зарегестрирован") hashed_password = get_password_hash(user.password) allowed_roles = [r for r in user.roles if r not in BLOCK_ROLES] if any(r in REQUIRES_OFFER for r in user.roles): if not user.offer_accepted: raise HTTPException(status_code=400, detail="Оферта должна быть принята") if not user.offer_accepted_at: user.offer_accepted_at = datetime.utcnow() if img: img_id = await save_to_gridfs(img, 'users') else: img_id = '/static/painter.svg' doc = { "username": user.username, "phone": normalize_phone(user.phone) if user.phone else None, "email": user.email, "img": img_id, "hashed_password": hashed_password, "roles": allowed_roles, "birthday": user.birthday.isoformat() if user.birthday else None, "grade": user.grade, "female_name_lastname": user.female_name_lastname, "living_place": user.living_place, "client_id": user.client_id, "extra_scopes": user.extra_scopes, "offer_accepted": bool(user.offer_accepted), "offer_accepted_at": user.offer_accepted_at.isoformat() if user.offer_accepted_at else None, } result = await users_collection.insert_one(doc) user_id_str = str(result.inserted_id) doc_out = { "_id": user_id_str, "id": user_id_str, **{k: v for k, v in doc.items()} } background_tasks.add_task(email_new_user, bson_clear(doc_out)) background_tasks.add_task(logo_user_task, bson_clear(doc_out)) scopes = get_all_user_scopes(allowed_roles, []) client_id = user.client_id client_ip = request.headers.get('X-Real-IP') or request.client.host access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) access_token = create_jwt_token( {"sub": user_id_str, "scopes": scopes, "client_id": client_id, "ip": client_ip}, access_exp_delta ) refresh_token = create_jwt_token( {"sub": user_id_str, "type": "refresh", "client_id": client_id, "ip": client_ip}, refresh_exp_delta ) await redis_client.setex(f"access:{access_token}", int(access_exp_delta.total_seconds()), user_id_str) await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp_delta.total_seconds()), user_id_str) session_key = f"user_sessions:{user_id_str}" 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_delta.total_seconds()) ) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) return { "ok": True, "access_token": access_token, "refresh_token": refresh_token, "user_id": user_id_str, "token_type": "bearer" } @app.post("/api/admin/login") async def login_admin( request: Request, response: Response, login_data: AdminLoginRequest = Body(...), current_user: Dict = Security(get_current_user, scopes=["admin"]) ): user = await users_collection.find_one({"_id": ObjectId(login_data.user_id)}) user_id_str = str(user["_id"]) scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", [])) client_ip = request.headers.get('X-Real-IP') or request.headers.get('X-Real-IP') or request.client.host access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) access_token = create_jwt_token( {"sub": user_id_str, "scopes": scopes, "client_id": 'admin', "ip": client_ip}, access_exp_delta ) refresh_token = create_jwt_token( {"sub": user_id_str, "type": "refresh", "client_id": 'admin', "ip": client_ip}, refresh_exp_delta ) await redis_client.setex(f"access:{access_token}", int(access_exp_delta.total_seconds()), user_id_str) await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp_delta.total_seconds()), user_id_str) session_key = f"user_sessions:{user_id_str}" await redis_client.sadd(session_key, f"access:{access_token}") await redis_client.sadd(session_key, f"refresh:{hash_token(refresh_token)}") admin_access_token = request.cookies.get("access_token") admin_refresh_token = request.cookies.get("refresh_token") response.set_cookie( key="admin_access_token", value=admin_access_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(access_exp_delta.total_seconds()) ) response.set_cookie( key="admin_refresh_token", value=admin_refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) response.set_cookie( key="access_token", value=access_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(access_exp_delta.total_seconds()) ) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) return { "access_token": access_token, "refresh_token": refresh_token, "user_id": user_id_str, "token_type": "bearer" } @app.post("/api/admin/back") async def login_admin_back( request: Request, response: Response, ): access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) admin_access_token = request.cookies.get("admin_access_token") admin_refresh_token = request.cookies.get("refreadmin_refresh_tokensh_token") response.set_cookie( key="access_token", value=admin_access_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(access_exp_delta.total_seconds()) ) response.set_cookie( key="refresh_token", value=admin_refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) return {'ok': True} @app.get("/login/{token}") async def login_token(token: str, request: Request, response: Response): token_hash = hash_token(token) log = await users_token_collection.find_one( {'token_hash': token_hash} ) if not log: return RedirectResponse("/login?" + urlencode({"next": '/profile'})) next_url = log.get('url', '/') created = log.get("created_at") if not created: try: created = isoparse(ObjectId(log.get("_id")).generation_time.replace(tzinfo=timezone.utc)) except Exception: return RedirectResponse("/login?" + urlencode({"next": next_url})) created = created.replace(tzinfo=timezone.utc) now = datetime.now(timezone.utc) if created < now - timedelta(days=TOKEN_TTL_DAYS): await users_token_collection.delete_one({'token_hash': token_hash}) return RedirectResponse("/login?" + urlencode({"next": log.get('url', next_url)})) user_id = log.get("user_id") if not user_id: return RedirectResponse("/login?" + urlencode({"next": log.get('url', next_url)})) user = await users_collection.find_one({"_id": ObjectId(user_id)}) if not user: return RedirectResponse("/login?" + urlencode({"next": log.get('url', next_url)})) user_id_str = str(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 access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) access_token = create_jwt_token( {"sub": user_id_str, "scopes": scopes, "client_id": 'token', "ip": client_ip}, access_exp_delta ) refresh_token = create_jwt_token( {"sub": user_id_str, "type": "refresh", "client_id": 'token', "ip": client_ip}, refresh_exp_delta ) await redis_client.setex(f"access:{access_token}", int(access_exp_delta.total_seconds()), user_id_str) await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp_delta.total_seconds()), user_id_str) session_key = f"user_sessions:{user_id_str}" 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_delta.total_seconds()) ) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) response.set_cookie( key="access_token", value=access_token, httponly=True, secure=PROD, domain='.kultmost.ru', samesite="Strict", max_age=int(access_exp_delta.total_seconds()) ) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=PROD, domain='.kultmost.ru', samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) # Validate and sanitize redirect if not is_safe_redirect(next_url): next_url = '/' return RedirectResponse(next_url,headers=response.headers) @app.post("/api/login") async def login( request: Request, response: Response, login_data: LoginRequest = Body(...), ): identifier = safe_strip(login_data.identifier) await check_bruteforce(identifier) 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: await increment_failed_attempt(identifier) raise HTTPException(401, "Incorrect credentials") # Track if a valid password was found valid_user_found = False for user in users: stored = user.get("hashed_password") if isinstance(stored, bytes): stored = stored.decode() # Check if the password is hashed with Argon2 if stored.startswith("$argon2"): ok = verify_password(login_data.password, stored) if ok: valid_user_found = True break # Password is valid, exit the loop # Check if the password is hashed with bcrypt elif stored.startswith("$2y$"): stored = stored.replace("$2y$", "$2b$", 1) if bcrypt.checkpw(login_data.password.encode(), stored.encode()): valid_user_found = True new_hash = get_password_hash(login_data.password) break # Password is valid, exit the loop # If no valid user was found, increment attempts and raise error if not valid_user_found: await increment_failed_attempt(identifier) raise HTTPException(401, "Incorrect credentials") # If we reach this point, authentication succeeded # Proceed with further logic such as generating tokens or returning user details # await users_collection.update_one({"_id": user["_id"]}, {"$set": {"hashed_password": new_hash}}) await reset_failed_attempts(identifier) user_id_str = str(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 access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) access_token = create_jwt_token( {"sub": user_id_str, "scopes": scopes, "client_id": login_data.client_id, "ip": client_ip}, access_exp_delta ) refresh_token = create_jwt_token( {"sub": user_id_str, "type": "refresh", "client_id": login_data.client_id, "ip": client_ip}, refresh_exp_delta ) await redis_client.setex(f"access:{access_token}", int(access_exp_delta.total_seconds()), user_id_str) await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp_delta.total_seconds()), user_id_str) session_key = f"user_sessions:{user_id_str}" 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_delta.total_seconds()) ) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) return { "access_token": access_token, "refresh_token": refresh_token, "user_id": user_id_str, "token_type": "bearer" } @app.post("/api/old/login") async def old_login( request: Request, response: Response, ): token_old = request.cookies.get('token') or request.cookies.get('v2token') access = await get_access_token(request, response) if token_old and not access: try: headers = {'Authorization': f"Bearer {token_old}"} async with httpx.AsyncClient(http2=True, verify=ctx) as client: res = await client.get('https://avtopost.kultmost.ru/api/v1/auth/me', headers=headers) email = res.json()['data']['email'] print(email) user = await users_collection.find_one({'email': email}) except: return {'ok': False} user_id_str = str(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 access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) access_token = create_jwt_token( {"sub": user_id_str, "scopes": scopes, "client_id": 'old_token', "ip": client_ip}, access_exp_delta ) refresh_token = create_jwt_token( {"sub": user_id_str, "type": "refresh", "client_id": 'old_token', "ip": client_ip}, refresh_exp_delta ) await redis_client.setex(f"access:{access_token}", int(access_exp_delta.total_seconds()), user_id_str) await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp_delta.total_seconds()), user_id_str) session_key = f"user_sessions:{user_id_str}" 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_delta.total_seconds()) ) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) return {'ok': True} @app.post("/refresh", response_model=TokenResponse) async def refresh_token( request: Request, response: Response, req: RefreshRequest ): refresh_token = ( request.cookies.get("refresh_token") or get_bearer_token(request) or (req.refresh_token if req else None) ) if not refresh_token: raise HTTPException(401, "Refresh token required") client_id = None if req: client_id = req.client_id refresh_hash = hash_token(refresh_token) user_id_str = await redis_client.get(f"refresh:{refresh_hash}") if not user_id_str: raise HTTPException(status_code=401, detail="Invalid or expired refresh token") try: payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM]) if payload.get("type") != "refresh": raise HTTPException(status_code=401, detail="Not a refresh token") if payload.get("sub") != user_id_str: raise HTTPException(status_code=401, detail="Token mismatch") except jwt.InvalidTokenError as e: raise HTTPException(status_code=401, detail=f"Invalid refresh token: {str(e)}") await redis_client.delete(f"refresh:{refresh_hash}") user = await users_collection.find_one({"_id": ObjectId(user_id_str)}) scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", [])) client_ip = request.headers.get('X-Real-IP') or request.client.host access_exp = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) new_access = create_jwt_token( {"sub": user_id_str, "scopes": scopes, "client_id": client_id, "ip": client_ip}, access_exp ) new_refresh = create_jwt_token( {"sub": user_id_str, "type": "refresh", "client_id": client_id, "ip": client_ip}, refresh_exp ) new_refresh_hash = hash_token(new_refresh) await redis_client.setex(f"access:{new_access}", int(access_exp.total_seconds()), user_id_str) await redis_client.setex(f"refresh:{new_refresh_hash}", int(refresh_exp.total_seconds()), user_id_str) session_key = f"user_sessions:{user_id_str}" await redis_client.sadd(session_key, f"access:{new_access}") await redis_client.sadd(session_key, f"refresh:{new_refresh_hash}") response.set_cookie( key="access_token", value=new_access, httponly=True, secure=PROD, samesite="strict", max_age=int(access_exp.total_seconds()) ) response.set_cookie( key="refresh_token", value=new_refresh, httponly=True, secure=PROD, samesite="strict", max_age=int(refresh_exp.total_seconds()) ) return { "access_token": new_access, "token_type": "bearer", "refresh_token": new_refresh } @app.post("/logout") async def logout( request: Request, response: Response, current_user: Dict = Depends(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, ) return {"message": "Logged out from all sessions"} @app.get("/api/me", response_model=dict) async def get_me(response: Response, current_user: Dict = Security(get_current_user, scopes=["read:self"])): user = display_user(current_user) return bson_clear(user) @app.get("/healthz") async def healthz(): try: await mongo_client.admin.command('ping') except Exception as e: logging.error(f"MongoDB is unhealthy: {str(e)}") raise HTTPException(status_code=500, detail=f"MongoDB is unhealthy: {str(e)}") try: await redis_client.ping() except Exception as e: logging.error(f"Redis is unhealthy: {str(e)}") raise HTTPException(status_code=500, detail=f"Redis is unhealthy: {str(e)}") return {} @app.get("/registration", response_class=HTMLResponse) async def index( request: Request, next: str | None = None, ): return templates.TemplateResponse( request=request, name="registration.html", context={'next': next} ) async def get_unique_users(competition_request_col: AsyncIOMotorCollection, competition_id: str, finish: bool = False) -> List[Dict[str, Any]]: # Determine match criteria based on finish parameter status_match = {"$match": {"posts.status": "Проходит"}} if finish else {"$match": {}} pipeline = [ {"$match": {"competition_id": competition_id}}, {"$unwind": "$user"}, {"$project": { "user": 1, "posts": 1, "status": 1 }}, {"$unwind": "$posts"}, status_match, # Use the conditional match here {"$group": { "_id": "$user._id", "user": {"$first": "$user"}, "unique_posts": {"$addToSet": "$posts._id"}, "status": {"$first": "$status"} }}, {"$project": { "user": 1, "post_count": {"$size": "$unique_posts"}, "status": { "$cond": {"if": {"$ne": ["$status", None]}, "then": "$status", "else": False } } }} ] cursor = competition_request_col.aggregate(pipeline) results = [] async for doc in cursor: results.append(doc) return results from typing import Any, Dict, List from bson import ObjectId from motor.motor_asyncio import AsyncIOMotorCollection async def get_unique_posts_by_user( competition_request_col: AsyncIOMotorCollection, competition_id: str, user_id: str, ) -> list[dict[str, Any]]: pipeline = [ {"$match": {"competition_id": competition_id, "user._id": user_id}}, {"$unwind": "$posts"}, { "$addFields": { "status_priority": { "$switch": { "branches": [ {"case": {"$eq": ["$posts.status", "Проходит"]}, "then": 1}, {"case": {"$eq": ["$posts.status", "На рассмотрении"]}, "then": 2}, ], "default": 3, } } } }, {"$sort": {"status_priority": 1, "posts._id": 1}}, # tie-breaker if needed {"$group": {"_id": "$posts._id", "post": {"$first": "$posts"}}}, {"$replaceRoot": {"newRoot": "$post"}}, ] cursor = competition_request_col.aggregate(pipeline) return await cursor.to_list(length=None) async def get_unique_posts_с(competition_request_col: AsyncIOMotorCollection, competition_id: str, finish: bool = False) -> List[Any]: match_criteria = { "competition_id": competition_id, } if finish: match_criteria['posts.status'] = 'Проходит' pipeline = [ {"$match": match_criteria}, {"$unwind": "$posts"}, {"$match": {"posts.status": "Проходит"} if finish else {}}, { "$group": { "_id": "$posts._id", # Group by unique post ID "post": {"$first": "$posts"} # Retrieve the first occurrence } }, {"$project": {"_id": 0, "post": 1}} # Include the post details ] cursor = competition_request_col.aggregate(pipeline) unique_posts = await cursor.to_list(length=None) # Fetch all unique posts return [unique_post["post"] for unique_post in unique_posts] if unique_posts else [] @app.get("/exhibition/{exhibition_id}", response_class=HTMLResponse) async def exhibition_members( request: Request, exhibition_id: str, user: Dict = Security(get_user) ): exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)}) if exhibition: match_ = { "$or": [ {"sender._id": user.get('_id')}, {"user._id": user.get('_id')}, {"receiver._id": user.get('_id')} ], 'prod': PROD, } if data.get('competition'): match_.update({'competition_id': data.get('competition').get('_id')}) if data.get('exhibition_id'): match_.update({'exhibition_id': data.get('exhibition_id')}) cursor = delivery_collection.aggregate([ {"$match": match_}, {"$sort": {"_id": -1}}, {"$group": { "_id": "$post_id", "doc": {"$first": "$$ROOT"} }}, {"$replaceRoot": {"newRoot": "$doc"}}, {"$sort": {"_id": -1}}, ]) posts = [doc async for doc in cursor if doc.get('status') == 'На выставке'] return templates.TemplateResponse( request=request, name="exhibition/index.html", context={"id": 1, "user": user, "exhibition": exhibition, "posts": posts} ) else: raise HTTPException(status_code=307, headers={"Location": '/competitions'}, detail="Конкурс не найден") @app.get("/competition/{competition_id}", response_class=HTMLResponse) async def competition_members( request: Request, competition_id: str, user: Dict = Security(get_user) ): competition = await competitions_col.find_one({'_id': ObjectId(competition_id)}) if competition: members = await get_unique_users(competition_request_col, competition_id, competition['status']==2) posts = await get_unique_posts_с(competition_request_col, competition_id, competition['status']==2) return templates.TemplateResponse( request=request, name="competition/index.html", context={"id": 1, "user": user, "competition": competition, "members": members, "posts": posts} ) else: raise HTTPException(status_code=307, headers={"Location": '/competitions'}, detail="Конкурс не найден") @app.get("/competition/{competition_id}/{member_id}", response_class=HTMLResponse) async def competition_member( request: Request, competition_id: str, member_id: str, user: Dict = Security(get_user) ): competition = await competitions_col.find_one({'_id': ObjectId(competition_id)}) if competition: member = await users_collection.find_one({'_id':ObjectId(member_id)}) req = await competition_request_col.find_one({'competition_id': competition_id,'user._id': member_id}) member['status'] = req.get('status', False) if member: posts = await get_unique_posts_by_user(competition_request_col, competition_id, member_id) return templates.TemplateResponse( request=request, name="competition/member/index.html", context={"id": 1, "user": user, "competition": competition, 'member':member, 'posts': posts} ) else: raise HTTPException(status_code=307, headers={"Location": f'/competition/{competition._id}'}, detail="Участник не найден") else: raise HTTPException(status_code=307, headers={"Location": '/competitions'}, detail="Конкурс не найден") async def _serialize(doc): doc = doc.copy() if "_id" in doc: doc["_id"] = str(doc["_id"]) if "user" in doc: doc["user"] = {'img': doc["user"].get('img'), 'username': doc["user"].get('username')} # if "_id" in doc["user"]: # doc["user"]["_id"] = str(doc["user"]["_id"]) # doc["user"]["notify"] = [] return doc @app.get("/api/competitions", response_model=List) async def get_competition(): competitions = [] current_time = datetime.now(timezone.utc) + timedelta(hours=3) async for doc in competitions_col.find({ 'datetime_start': {'$lt': current_time} },{'fb':0,'inst':0,'max':0,'tg':0,'vk':0}).sort('_id',-1).limit(32): competitions.append(bson_clear(doc)) return competitions @app.get("/exhibitions", response_class=HTMLResponse) async def exhibitions_all( request: Request, user: Dict = Security(get_user) ): current_time = datetime.now(timezone.utc) + timedelta(hours=3) find_list = [ {'datetime_start': {'$lt': current_time}}, {'user._id': user.get('id','') if user else ''} ] if user and 'admin' in user.get('roles',[]): find_list.append({'datetime_start': {'$gte': current_time}}) exhibitions = await exhibition_collection.find({ '$or': find_list, 'prod': PROD }).sort({'_id':-1}).limit(1000).to_list() return templates.TemplateResponse( request=request, name="exhibitions.html", context={"id": 1, "user": user, "exhibitions": exhibitions} ) @app.get("/competitions", response_class=HTMLResponse) async def competition( request: Request, user: Dict = Security(get_user) ): competitions = [] current_time = datetime.now(timezone.utc) + timedelta(hours=3) find_list = [ {'datetime_start': {'$lt': current_time}}, {'user._id': user.get('id','') if user else ''} ] if user and 'admin' in user.get('roles',[]): find_list.append({'datetime_start': {'$gte': current_time}}) async for doc in competitions_col.find({ '$or': find_list } if PROD else {}).sort({'_id':-1}).limit(1000): competitions.append(doc) return templates.TemplateResponse( request=request, name="competitions.html", context={"id": 1, "user": user, "competitions": competitions} ) router = APIRouter() @router.post("/api/restore") async def restore_api( request: Request, restore_data: RestoreRequest = Body(...), ): # 1. Валидация и очистка входных данных identifier = restore_data.identifier.strip() if not identifier or not is_valid_email(identifier): raise HTTPException(status_code=400, detail="Некорректный формат email") # 2. Проверка анти-спама / ограничения частоты запросов client_ip = request.client.host rate_limit_key = f"rate_limit:restore:{identifier}:{client_ip}" # Проверяем, запрашивал ли пользователь сброс недавно (например, за последние 60 секунд) existing_request = await redis_client.get(rate_limit_key) if existing_request: # Опционально: получаем TTL, чтобы сообщить пользователю, когда можно повторить попытку ttl = await redis_client.ttl(rate_limit_key) raise HTTPException( status_code=429, detail=f"Слишком много запросов. Попробуйте через {ttl} секунд." ) # Устанавливаем флаг ограничения частоты (истекает через 60 секунд) await redis_client.set(rate_limit_key, "1", ex=60) # 3. Поиск пользователя user = await users_collection.find_one({'$or': [ {'email': identifier}, {'email': identifier.lower()} ]}) if not user: return {'ok': True} # 4. Генерация токена token = secrets.token_urlsafe(32) expires_at = datetime.now(timezone.utc) + timedelta(hours=1) token_data = { "user_id": str(user["_id"]), "email": user["email"], "next": restore_data.next, "expires_at": expires_at.isoformat() } # 5. Сохранение токена сброса в Redis reset_token_key = f"reset_token:{token}" await redis_client.set(reset_token_key, json.dumps(token_data), ex=3600) # 6. Генерация ссылки для сброса base_url = str(request.base_url).rstrip('/') reset_link = f"{base_url}/restore/{token}" # 7. Асинхронная отправка email try: email_task.notify.delay( to_email=user['email'], data={ 'link': reset_link, 'email': user['email'] }, subject='Восстановление пароля', template='restore.html' ) except Exception as e: # Логирование ошибки print(f"Не удалось отправить письмо: {e}") # В зависимости от политики, вы можете захотеть удалить токен, если письмо не отправлено await redis_client.delete(reset_token_key) raise HTTPException(status_code=500, detail="Ошибка отправки письма") return {'ok': True} @router.get("/restore/{token}", response_class=HTMLResponse) async def restore_page(request: Request, token: str): token_key = f"reset_token:{token}" token_data = await redis_client.get(token_key) if not token_data: return HTMLResponse( "
Вы больше не будете получать письма от нас.
""" @app.get("/profile/admin/users", response_class=HTMLResponse) async def profile_admin_statistics( request: Request, user: Dict = Security(get_current_user, scopes=["admin"]) ): users = [doc async for doc in users_collection.find().sort({'_id':-1})] return templates.TemplateResponse( request=request, name="profile/admin/users.html", context={"user": user, 'users': users } ) from collections import defaultdict @app.get("/profile/admin/statistics", response_class=HTMLResponse) async def profile_admin_statistics( request: Request, user: Dict = Security(get_current_user, scopes=["admin"]) ): pipeline1 = [ {"$match": {"roles": "user"}}, { "$group": { "_id": "$living_place", "user_count": {"$sum": 1}, "user_ids": {"$push": "$_id"} } }, {"$sort": {"user_count": -1}} ] living_stats = await users_collection.aggregate(pipeline1).to_list(length=None) for stat in living_stats: if stat["_id"]: user_ids = stat["user_ids"] # list of user _id values (ObjectId or str) # If posts store user._id as string, convert ObjectId -> str user_id_strings = [str(u) for u in user_ids] # post_count (use count_documents as you had) stat["post_count"] = await posts_collection.count_documents( {"user._id": {"$in": user_id_strings}} ) # usernames cursor = users_collection.find( {"_id": {"$in": user_ids}}, {"username": 1, "_id": 1} ).limit(1000) stat["usernames"] = await cursor.to_list(length=None) # fetch posts for these users (limit/fields optional) posts_cursor = posts_collection.find( {"user._id": {"$in": user_id_strings}}, {"_id": 1, "title": 1, "created_at": 1, "user._id": 1} ).sort("created_at", -1) stat["posts"] = await posts_cursor.to_list(length=None) pipeline2 = [ {"$match": {"roles": "user"}}, { "$project": { "year_month": { "$dateToString": { "format": "%Y-%m", "date": {"$toDate": "$_id"} } } } }, { "$group": { "_id": "$year_month", "user_count": {"$sum": 1}, "user_ids": {"$push": "$_id"} } }, {"$sort": {"_id": -1}} ] reg_stats = await users_collection.aggregate(pipeline2).to_list(length=None) for stat in reg_stats: user_ids = stat["user_ids"] # list of user _id values (ObjectId or str) user_id_strings = [str(u) for u in user_ids] # use this if posts.user._id is a str stat["post_count"] = await posts_collection.count_documents( {"user._id": {"$in": user_id_strings}} ) cursor = users_collection.find( {"_id": {"$in": user_ids}}, {"username": 1, "_id": 0} ).limit(1000) stat["usernames"] = await cursor.to_list(length=None) # fetch posts (flat list) posts_cursor = posts_collection.find( {"user._id": {"$in": user_id_strings}}, {"_id": 1, "title": 1, "created_at": 1, "user._id": 1} ).sort("created_at", -1) stat["posts"] = await posts_cursor.to_list(length=None) # OR: group posts per user posts = stat["posts"] grouped = defaultdict(list) for p in posts: grouped[p["user"]["_id"]].append(p) stat["posts_by_user"] = dict(grouped) return templates.TemplateResponse( request=request, name="profile/admin/statistics.html", context={"user": user, "living_stats": living_stats, "reg_stats": reg_stats, "all_users": await users_collection.count_documents({ 'roles': "user" }), "all_city": len(await users_collection.distinct("living_place", filter={ 'roles': "user" })) } ) @app.post("/tg") async def tg_result_save(data:dict, token: str = Security(get_token)): item = data.get('item') result = data.get('result', {}) if item: update_spec = {"$push": {"tg": result}} await mongo_client[item.get('db')][item.get('col')].update_one({'_id': ObjectId(item.get('_id'))}, update_spec) @app.get("/profile/social", response_class=HTMLResponse) async def profile_social( request: Request, user: Dict = Security(get_current_user) ): socials = [doc async for doc in social_collection.find({'user_id': str(user.get('_id'))})] return templates.TemplateResponse( request=request, name="profile/social.html", context={"user": user, 'socials': socials} ) @app.get("/profile/invitation/{invitation_id}", response_class=HTMLResponse) async def profile_invitation( request: Request, invitation_id: str, user: Dict = Security(get_current_user) ): data = await invitations_collection.find_one({'_id': ObjectId(invitation_id)}) match_ = { "$or": [ {"sender._id": user.get('_id')}, {"user._id": user.get('_id')}, {"receiver._id": user.get('_id')} ], 'prod': PROD, } if data.get('competition'): match_.update({'competition_id': data.get('competition').get('_id')}) if data.get('exhibition_id'): match_.update({'exhibition_id': data.get('exhibition_id')}) cursor = delivery_collection.aggregate([ {"$match": match_}, {"$sort": {"_id": -1}}, {"$group": { "_id": "$post_id", "doc": {"$first": "$$ROOT"} }}, {"$replaceRoot": {"newRoot": "$doc"}}, {"$sort": {"_id": -1}}, ]) posts = [doc async for doc in cursor] return templates.TemplateResponse( request=request, name="profile/invitation.html", context={ "user": user, "doc": data, "data": posts} ) @app.get("/profile/invitation", response_class=HTMLResponse) async def profile_invitation( request: Request, user: Dict = Security(get_current_user) ): data = await invitations_collection.find({'prod': PROD,'user_id': user.get('_id')}).sort('_id', -1).to_list() await notify_collection.update_many({'user_id':user.get('_id'),'type':'invitation'},{"$set":{'read':False}}) return templates.TemplateResponse( request=request, name="profile/invitations.html", context={ "user": user, "data": data, "now": datetime.now()} ) @app.post('/api/social/settings', response_model=dict) async def social_setting( id: Optional[str] = Body(None, embed=True), key: Optional[str] = Body(None, embed=True), check: Optional[bool] = Body(None, embed=True), user: Dict = Security(get_current_user) ): await social_collection.update_one({'_id': ObjectId(id), 'user_id': user.get('_id')},{'$set': {key:check}}) return {'ok': True} async def get_delivery_post(post_id): cursor = delivery_collection.aggregate([ {"$match": { 'post_id': post_id, 'prod': PROD, }}, {"$sort": {"_id": -1}}, {"$group": { "_id": "$post_id", "doc": {"$first": "$$ROOT"} }}, {"$replaceRoot": {"newRoot": "$doc"}}, {"$sort": {"_id": -1}}, ]) data = [doc async for doc in cursor] return data[0] if data else False @app.post('/api/v1/views') async def get_views_count(request: Request): url = (await request.json())['url'] if 'artist' in url: user_id = url.split('/')[-1] total_key = f"total_w_c_artists:{user_id}" today_key = f"today_w_c_artists:{user_id}" total_watch_count = await redis_client.incr(total_key) today_date = datetime.now().strftime("%Y-%m-%d") daily_key = f"{today_key}:{today_date}" today_watch_count = await redis_client.incr(daily_key) return {"totalViews":total_watch_count,"todayViews":today_watch_count} if 'post' in url: paint_id = url.split('/')[-1] total_key = f"total_watch_count_paint:{paint_id}" today_key = f"today_watch_count_paint:{paint_id}" total_watch_count = await redis_client.incr(total_key) today_date = datetime.now().strftime("%Y-%m-%d") daily_key = f"{today_key}:{today_date}" today_watch_count = await redis_client.incr(daily_key) return {"totalViews":total_watch_count,"todayViews":today_watch_count} if 'news' in url: news_id = url.split('/')[-1] total_key = f"total_watch_count_news:{news_id}" today_key = f"today_watch_count_news:{news_id}" total_watch_count = await redis_client.incr(total_key) today_date = datetime.now().strftime("%Y-%m-%d") daily_key = f"{today_key}:{today_date}" today_watch_count = await redis_client.incr(daily_key) return {"totalViews":total_watch_count,"todayViews":today_watch_count} return {"totalViews":0,"todayViews":0} @app.get('/api/v1/generic/post', response_model=dict) async def get_posts( per_page: Optional[int] = Query(4), page: Optional[int] = Query(1), user_id: Optional[str] = Query(None), filter: Optional[str] = Query(None, description="JSON filter string or omitted") ): posts = [] if filter: try: filt = json.loads(filter) mql = convert_to_mql(filt) def mql_to_mongo(m): if "and" in m: return {"$and": [mql_to_mongo(x) for x in m["and"]]} if "or" in m: return {"$or": [mql_to_mongo(x) for x in m["or"]]} field, op, value = m["field"], m["op"], m["value"] if field == 'author_name': field = 'author' if field == 'user.gallery_name': field = 'user.username' if field == 'name': field = 'title' if op == "like": return {field: {"$regex": safe_strip(value), "$options": "i"}} return {field: value} mongo_query = mql_to_mongo(mql) for post in [doc async for doc in posts_collection.find(mongo_query).sort({"_id":-1}).limit(per_page * page + 8)][per_page * page - per_page:per_page * page]: post['delivery'] = await get_delivery_post(str(post.get('_id'))) post['user']['avatar_url'] = f"/a/{post['user'].get('_id')}" post['user']['name'] = post['user']['username'] photos = [{'url':f"/b/posts/{i}", 'type':'photo', 'sort_order':x+1} for x,i in enumerate(post['medias'])] post['id'] = post.get('_id') post['photos'] = photos post['name'] = post['title'] post['author_name'] = post['author'] post['media'] = photos posts.append(bson_clear(post)) post_count = await posts_collection.count_documents(mongo_query) return { 'current_page': page, 'data':posts, 'total': post_count, 'to': 16, 'last_page': post_count / per_page, 'per_page': per_page } except json.JSONDecodeError: pass q = {} if user_id: try: if len(str(user_id)) < 6: user_id = int(user_id) user = await users_collection.find_one({'id':user_id}) q['user._id'] = str(user["_id"]) else: q['user._id'] = user_id except: q['user._id'] = user_id for post in [doc async for doc in posts_collection.find(q).sort({"_id":-1}).limit(per_page * page + 8)][per_page * page - per_page:per_page * page]: post['delivery'] = await get_delivery_post(str(post.get('_id'))) post['user']['avatar_url'] = f"/a/{post['user'].get('_id')}" post['user']['name'] = post['user']['username'] post['author_name'] = post['author'] photos = [{'url':f"/b/posts/{i}", 'type':'photo', 'sort_order':x+1} for x,i in enumerate(post['medias'])] post['id'] = post.get('_id') post['photos'] = photos post['media'] = photos post['name'] = post['title'] posts.append(bson_clear(post)) post_count = await posts_collection.count_documents(q) return { 'current_page': page, 'data':posts, 'total': post_count, 'to': 16, 'last_page': post_count / per_page, 'per_page': per_page } @app.get('/api/v2/statistics', response_model=dict) async def statistics_v2( user: Optional[str] = Query(None), post: Optional[str] = Query(None), ): if user: return {"statistics":await users_collection.count_documents({ 'roles': "user" }),"additional_stats":len(await users_collection.distinct("living_place", filter={ 'roles': "user" }))} else: return {"statistics":await posts_collection.count_documents({}),"additional_stats":await users_collection.count_documents({ 'roles': "user" })} @app.get('/api/v2/user/{user_id}', response_model=dict) async def get_user_by_id( user_id: Optional[str] ): try: user = await users_collection.find_one({'id': int(user_id)}) except: user = await users_collection.find_one({'_id': ObjectId(user_id)}) timestamp = user.get('_id').generation_time user['posts_count'] = await posts_collection.count_documents({'user._id':str(user.get('_id'))}) timestamp_str = timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" user['created_at'] = timestamp_str user['avatar_url'] = f"https://avtopost.kultmost.ru/a/{user.get('_id')}" user['name'] = user['username'] user.pop('hashed_password') user.pop('phone') user.pop('email') return {'data':bson_clear(user)} @app.get('/api/v1/generic/post/{post_id}', response_model=dict) async def get_post_by_id( post_id: Optional[str] ): try: post = await posts_collection.find_one({'id': int(post_id)}) except: post = await posts_collection.find_one({'_id': ObjectId(post_id)}) post['user']['avatar_url'] = f"/a/{post['user'].get('_id')}" post['user']['name'] = post['user']['username'] photos = [{'url':f"/b/posts/{i}", 'type':'photo', 'sort_order':x+1} for x,i in enumerate(post['medias_original'])] post['photos'] = photos post['id'] = post.get('_id') post['name'] = post['title'] post['media'] = photos return bson_clear(post) ## Work @app.get("/profile/invitation/competition/{competition_id}", response_class=HTMLResponse) async def profile_invitation( request: Request, competition_id: str, user: Dict = Security(get_current_user) ): competition = await competitions_col.find_one({'_id': ObjectId(competition_id)}) req = await competition_request_col.find_one({'competition_id': competition_id, 'user._id': user.get('_id')}) return templates.TemplateResponse( request=request, name="profile/invitation/competition.html", context={ "user": user, "competition": competition, "req": req} ) @app.post('/api/invitation/accept/{invitations_id}') async def invitation_accept(invitations_id: str, request: Request): payload = await request.json() posts_ids: List[str] = payload.get("posts", []) # optional: валидация if not isinstance(posts_ids, list): posts_ids = [] await accept_invitations(invitations_id, posts_ids) return {'ok': True} @app.get('/profile/delivery/{post_id}') async def profile_delivery_history_post(request: Request, post_id:str, user: Dict = Security(get_current_user)): data = await delivery_collection.find({'post_id': post_id, "$or": [ {"sender._id": user.get('_id')}, {"user._id": user.get('_id')}, {"receiver._id": user.get('_id')} ]}).sort('_id', -1).to_list() return templates.TemplateResponse( request=request, name="profile/delivery_history.html", context={ "user": user, "data": data, } ) # @app.get('/profile/delivery/{type_d}/{item_id}') # async def profile_delivery_by_old(request: Request, type_d:str, item_id:str, user: Dict = Security(get_current_user)): # cursor = delivery_collection.aggregate([ # {"$match": { # "$or": [ # {"sender._id": user.get('_id')}, # {"user._id": user.get('_id')}, # {"receiver._id": user.get('_id')} # ], # f'{type_d}_id': ObjectId(item_id), # 'prod': PROD, # }}, # {"$sort": {"_id": -1}}, # {"$group": { # "_id": "$post_id", # "doc": {"$first": "$$ROOT"} # }}, # {"$replaceRoot": {"newRoot": "$doc"}}, # {"$sort": {"_id": -1}}, # ]) # data = [doc async for doc in cursor] # data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True) # return templates.TemplateResponse( # request=request, # name="profile/delivery_old.html", # context={ # "user": user, # "data": data, # } # ) @app.get('/profile/delivery/competition/{competition_id}') async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)): cursor = delivery_collection.aggregate([ {"$match": { "$or": [ {"sender._id": user.get('_id')}, {"user._id": user.get('_id')}, {"receiver._id": user.get('_id')} ], 'prod': PROD, }}, {"$sort": {"_id": -1}}, {"$group": { "_id": "$post_id", "doc": {"$first": "$$ROOT"} }}, {"$replaceRoot": {"newRoot": "$doc"}}, {"$sort": {"_id": -1}}, ]) data = [doc async for doc in cursor] data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True) return templates.TemplateResponse( request=request, name="profile/delivery_old.html", context={ "user": user, "data": data, } ) @app.get('/profile/delivery/exhibition/{exhibition_id}') async def profile_delivery_by_exhibition(request: Request, exhibition_id:str, user: Dict = Security(get_current_user)): cursor = delivery_collection.aggregate([ {"$match": { "$or": [ {"sender._id": user.get('_id')}, {"user._id": user.get('_id')}, {"receiver._id": user.get('_id')} ], 'exhibition_id': exhibition_id, 'prod': PROD, }}, {"$sort": {"_id": -1}}, {"$group": { "_id": "$post_id", "doc": {"$first": "$$ROOT"} }}, {"$replaceRoot": {"newRoot": "$doc"}}, {"$sort": {"_id": -1}}, ]) data = [doc async for doc in cursor] data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True) return templates.TemplateResponse( request=request, name="profile/delivery_old.html", context={ "user": user, "data": data, } ) @app.get( "/profile/delivery", response_class=HTMLResponse, summary="Личный кабинет → Доставка Картин", description=""" Отображает страницу с историей доставок пользователя. Показывает **все** доставки, где пользователь является либо отправителем (`sender`), либо получателем (`receiver`). """, tags=["Profile", "Delivery"], ) async def profile_delivery( request: Request, user: Dict = Security(get_current_user) ): """ Рендерит HTML-страницу с доставками текущего пользователя. Запрашивает все документы из коллекции `delivery`, где пользователь участвует как отправитель или получатель. Args: request: FastAPI Request (для шаблонизатора) user: Текущий авторизованный пользователь (из Security) Returns: HTMLResponse с шаблоном profile/delivery.html """ # Получаем все доставки пользователя (отправитель ИЛИ получатель) cursor = delivery_collection.aggregate([ {"$match": { "$or": [ {"sender._id": user.get('_id')}, {"user._id": user.get('_id')}, {"receiver._id": user.get('_id')} ], 'prod': PROD, }}, {"$sort": {"_id": -1}}, # why not work {"$group": { "_id": "$post_id", "doc": {"$first": "$$ROOT"} }}, {"$replaceRoot": {"newRoot": "$doc"}}, {"$sort": {"_id": -1}}, ]) data = [doc async for doc in cursor] exhibitions = await exhibition_collection.find({'_id': {'$in': [ObjectId(i.get('exhibition_id')) for i in data if i.get('exhibition_id')]}}).to_list() competitions = await competitions_col.find({'_id': {'$in': [i.get('competition_id') for i in data if i.get('competition_id')]}}).to_list() return templates.TemplateResponse( request=request, name="profile/delivery.html", context={ "user": user, "exhibitions": exhibitions, "competitions": competitions, "data": data, "now": datetime.now(), "page_title": "Доставка", "active_section": "delivery" } ) @app.post("/api/exhibition/send/invitations") async def api_exhibition_send_invitations( background_tasks: BackgroundTasks, exhibition_id: Optional[str] = Form(...), user: Dict = Security(get_current_user) ): background_tasks.add_task(exhibition_send_invitations, exhibition_id) logging.info(f"send exh inv {exhibition_id}") return {"ok": True} @app.post("/api/exhibition/post/select") async def api_exhibition_post_select( post_id: Optional[str] = Form(...), exhibition_id: Optional[str] = Form(...), user: Dict = Security(get_current_user) ): await exhibition_collection.update_one( { "_id": ObjectId(exhibition_id), "user._id": user.get("_id") }, [ { "$set": { "posts": { "$cond": [ {"$in": [post_id, {"$ifNull": ["$posts", []]}]}, {"$setDifference": ["$posts", [post_id]]}, {"$setUnion": ["$posts", [post_id]]} ] } } } ] ) return {"ok": True} @app.get("/profile/partner/exhibition/{exhibition_id}/catalog", response_class=HTMLResponse) async def profile_partner_exhibitions_catalog( request: Request, exhibition_id: str, user: Dict = Security(get_current_user) ): exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)}) cursor = posts_collection.aggregate([ { "$match": { "city": {"$nin": [None, ""]} } }, { '$group': { '_id': "$city", 'count': { '$sum': 1 } } }, { '$sort': { '_id': 1 } } ]) living_places = [doc async for doc in cursor] cursor = posts_collection.aggregate([ { "$match": { "genre": {"$nin": [None, ""]} } }, { "$project": { "genres": {"$split": ["$genre", ","]} } }, {"$unwind": "$genres"}, { "$match": { "genres": {"$nin": ["", None]} } }, { "$group": { "_id": "$genres", "count": {"$sum": 1} } }, {"$sort": {"_id": 1}} ]) genres = [{'_id': doc["_id"].strip(), 'count': doc["count"]} async for doc in cursor] users = await users_collection.find( {"roles": {"$in": ["partner", "galerist"]}}, {"username": 1, "_id": 0} ).to_list() usernames = [u["username"] for u in users] pipeline = [ {"$match": {"user.roles": {"$in": ["partner", "galerist"]}}}, {"$group": {"_id": "$user.username", "count": {"$sum": 1}}}, ] cursor = posts_collection.aggregate(pipeline) post_counts = await cursor.to_list(length=None) counts_by_username = {d["_id"]: d["count"] for d in post_counts} galerists = [{"_id": uname, "count": counts_by_username.get(uname, '')} for uname in usernames] posts = [doc async for doc in posts_collection.find().sort('_id', -1).limit(32)] d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list() for post in posts: for doc in d_posts: if str(post.get('_id')) == doc.get('post_id') and post.get('status') != 'Продано' and doc.get('receiver') and doc.get('receiver').get('_id') == str(user.get('_id')): post.update({'status':doc.get('status'), 'receiver': doc.get('receiver'), 'd_id':doc.get('_id')}) return templates.TemplateResponse( request=request, name="profile/partner/exhibition catalog.html", context={"now": datetime.now(), "user": user, "galerists": galerists, "genres": genres, "living_places": living_places, "exhibition": exhibition, "posts": posts} ) @app.get("/profile/partner/exhibition/{exhibition_id}", response_class=HTMLResponse) async def profile_partner_exhibitions( request: Request, exhibition_id: str, user: Dict = Security(get_current_user) ): exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)}) obj_ids = [ObjectId(x) for x in exhibition.get('posts', [])] posts = await posts_collection.find({"_id": {"$in": obj_ids}}).to_list() if obj_ids else [] d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list() for post in posts: for doc in d_posts: if str(post.get('_id')) == doc.get('post_id') and post.get('status') != 'Продано' and doc.get('receiver') and doc.get('receiver').get('_id') == str(user.get('_id')): post.update({'status':doc.get('status'), 'receiver': doc.get('receiver'), 'd_id':doc.get('_id')}) return templates.TemplateResponse( request=request, name="profile/partner/exhibition.html", context={"now": datetime.now(), "user": user, "exhibition": exhibition, "posts": posts} ) @app.get("/profile/partner/exhibitions", response_class=HTMLResponse) async def profile_exhibitions( request: Request, user: Dict = Security(get_current_user) ): data = await exhibition_collection.find({'user._id': user.get('_id')}).sort('_id', -1).to_list() return templates.TemplateResponse( request=request, name="profile/partner/exhibitions.html", context={"now": datetime.now(), "user": user, "data": data} )