diff --git a/main.py b/main.py index d0ce263..99bdf23 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ from datetime import datetime import os import json import secrets +import random from typing import List, Optional import uvicorn from hashlib import sha256 @@ -58,7 +59,6 @@ class AdminUser(Base): password_hash = Column(String(255)) created_at = Column(DateTime, default=datetime.utcnow) -# НОВАЯ МОДЕЛЬ: Настройки дашборда class DashboardSettings(Base): __tablename__ = "dashboard_settings" @@ -67,6 +67,16 @@ class DashboardSettings(Base): image_filename = Column(String(255), default="dashboard_default.jpg") updated_at = Column(DateTime, default=datetime.utcnow) +class MainImage(Base): + __tablename__ = "main_images" + + id = Column(Integer, primary_key=True, index=True) + filename = Column(String(255)) + description = Column(Text) + is_active = Column(Boolean, default=False) + uploaded_at = Column(DateTime, default=datetime.utcnow) + uploaded_by = Column(String(100)) + # Создаем таблицы Base.metadata.create_all(bind=engine) @@ -76,7 +86,8 @@ app = FastAPI(title="OldMarket Server") os.makedirs("static/icons", exist_ok=True) os.makedirs("static/apks", exist_ok=True) os.makedirs("static/admin", exist_ok=True) -os.makedirs("static/dashboard", exist_ok=True) # Новая папка для изображений дашборда +os.makedirs("static/dashboard", exist_ok=True) +os.makedirs("static/main_images", exist_ok=True) # Монтируем статические файлы app.mount("/static", StaticFiles(directory="static"), name="static") @@ -120,7 +131,18 @@ def create_default_dashboard_settings(db: Session): ) db.add(default_settings) db.commit() - print("Созданы дефолтные настройки дашборда") + +# Создаем дефолтное главное изображение +def create_default_main_image(db: Session): + if db.query(MainImage).count() == 0: + default_image = MainImage( + filename="default_main.jpg", + description="Дефолтное главное изображение", + is_active=True, + uploaded_by="system" + ) + db.add(default_image) + db.commit() # Сессии для аутентификации sessions = {} @@ -151,7 +173,38 @@ def require_auth(request: Request, db: Session = Depends(get_db)): ) return user -# API endpoints для клиентов (старая совместимость) +# Корневой endpoint - отдает изображение +@app.get("/") +def get_main_image(db: Session = Depends(get_db)): + active_image = db.query(MainImage).filter(MainImage.is_active == True).first() + + if active_image: + image_path = f"static/main_images/{active_image.filename}" + if os.path.exists(image_path): + return FileResponse( + image_path, + media_type="image/jpeg", + filename=active_image.filename + ) + + all_images = db.query(MainImage).all() + if all_images: + random_image = random.choice(all_images) + image_path = f"static/main_images/{random_image.filename}" + if os.path.exists(image_path): + return FileResponse( + image_path, + media_type="image/jpeg", + filename=random_image.filename + ) + + default_path = "static/main_images/default_main.jpg" + if os.path.exists(default_path): + return FileResponse(default_path, media_type="image/jpeg") + else: + raise HTTPException(status_code=404, detail="No images available") + +# API endpoints для клиентов @app.get("/api/apps") def get_apps(db: Session = Depends(get_db)): apps = db.query(App).all() @@ -184,14 +237,12 @@ def get_reviews(app_id: int, db: Session = Depends(get_db)): for review in reviews ] -# Скачивание основной версии приложения @app.get("/api/download/{app_id}") def download_app(app_id: int, db: Session = Depends(get_db)): app = db.query(App).filter(App.id == app_id).first() if not app: raise HTTPException(status_code=404, detail="App not found") - # Увеличиваем счетчик загрузок app.downloads = (app.downloads or 0) + 1 db.commit() @@ -205,19 +256,16 @@ def download_app(app_id: int, db: Session = Depends(get_db)): media_type='application/vnd.android.package-archive' ) -# Скачивание конкретной версии приложения @app.get("/api/download/{app_id}/{version}") def download_app_version(app_id: int, version: str, db: Session = Depends(get_db)): app = db.query(App).filter(App.id == app_id).first() if not app: raise HTTPException(status_code=404, detail="App not found") - # Ищем нужную версию в списке версий versions = json.loads(app.versions) if app.versions else [] target_version = None for ver in versions: - # Сравниваем версию, игнорируя регистр и пробелы if ver.get("version", "").replace(" ", "").lower() == version.replace(" ", "").lower(): target_version = ver break @@ -225,7 +273,6 @@ def download_app_version(app_id: int, version: str, db: Session = Depends(get_db if not target_version: raise HTTPException(status_code=404, detail=f"Version '{version}' not found for this app") - # Увеличиваем счетчик загрузок основной версии app.downloads = (app.downloads or 0) + 1 db.commit() @@ -247,6 +294,8 @@ def download_app_version(app_id: int, version: str, db: Session = Depends(get_db @app.get("/admin/login", response_class=HTMLResponse) def admin_login(request: Request, db: Session = Depends(get_db)): create_default_admin(db) + create_default_dashboard_settings(db) + create_default_main_image(db) return templates.TemplateResponse("login.html", {"request": request}) @app.post("/admin/login") @@ -277,16 +326,12 @@ def admin_logout(): # Защищенные роуты админ-панели @app.get("/admin", response_class=HTMLResponse) def admin_panel(request: Request, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)): - create_default_dashboard_settings(db) - apps_count = db.query(App).count() reviews_count = db.query(Review).count() - # ИСПРАВЛЕННАЯ СТРОКА: используем func.sum для получения суммы загрузок total_downloads_result = db.query(func.sum(App.downloads)).scalar() total_downloads = total_downloads_result if total_downloads_result is not None else 0 - # Получаем настройки дашборда dashboard_settings = db.query(DashboardSettings).first() return templates.TemplateResponse("admin.html", { @@ -322,7 +367,15 @@ def admin_reviews(request: Request, user: AdminUser = Depends(require_auth), db: "username": user.username }) -# НОВЫЕ ENDPOINTS: Управление настройками дашборда +@app.get("/admin/main-images", response_class=HTMLResponse) +def admin_main_images(request: Request, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)): + images = db.query(MainImage).all() + return templates.TemplateResponse("main_images.html", { + "request": request, + "images": images, + "username": user.username + }) + @app.get("/admin/dashboard-settings", response_class=HTMLResponse) def dashboard_settings(request: Request, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)): dashboard_settings = db.query(DashboardSettings).first() @@ -333,61 +386,7 @@ def dashboard_settings(request: Request, user: AdminUser = Depends(require_auth) "dashboard_image": dashboard_settings.image_filename if dashboard_settings else "dashboard_default.jpg" }) -@app.post("/api/admin/dashboard/motd") -def update_motd( - user: AdminUser = Depends(require_auth), - motd: str = Form(...), - db: Session = Depends(get_db) -): - dashboard_settings = db.query(DashboardSettings).first() - if not dashboard_settings: - dashboard_settings = DashboardSettings() - db.add(dashboard_settings) - - dashboard_settings.motd = motd - dashboard_settings.updated_at = datetime.utcnow() - db.commit() - - return {"message": "MOTD обновлен"} - -@app.post("/api/admin/dashboard/image") -def update_dashboard_image( - user: AdminUser = Depends(require_auth), - image: UploadFile = File(...), - db: Session = Depends(get_db) -): - # Проверяем, что файл является изображением - if not image.content_type.startswith('image/'): - raise HTTPException(status_code=400, detail="Файл должен быть изображением") - - # Генерируем уникальное имя файла - file_extension = os.path.splitext(image.filename)[1] - image_filename = f"dashboard_{int(datetime.now().timestamp())}{file_extension}" - image_path = f"static/dashboard/{image_filename}" - - # Сохраняем изображение - contents = image.file.read() - with open(image_path, "wb") as f: - f.write(contents) - - # Обновляем настройки дашборда - dashboard_settings = db.query(DashboardSettings).first() - if not dashboard_settings: - dashboard_settings = DashboardSettings() - db.add(dashboard_settings) - - # Удаляем старое изображение если оно не дефолтное - if (dashboard_settings.image_filename != "dashboard_default.jpg" and - os.path.exists(f"static/dashboard/{dashboard_settings.image_filename}")): - os.remove(f"static/dashboard/{dashboard_settings.image_filename}") - - dashboard_settings.image_filename = image_filename - dashboard_settings.updated_at = datetime.utcnow() - db.commit() - - return {"message": "Изображение дашборда обновлено"} - -# API для админ-панели (защищенные) +# API для админ-панели @app.get("/api/admin/apps") def get_admin_apps(user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)): apps = db.query(App).all() @@ -409,30 +408,27 @@ def create_app( api: str = Form(...), description: str = Form(...), is_game: bool = Form(False), + rating: float = Form(0.0), apk_file: UploadFile = File(...), icon: UploadFile = File(...), db: Session = Depends(get_db) ): - # Сохраняем APK apk_filename = f"{package}_{version.replace(' ', '_')}.apk" apk_path = f"static/apks/{apk_filename}" with open(apk_path, "wb") as f: f.write(apk_file.file.read()) - # Сохраняем иконку icon_filename = f"{package}_icon.png" icon_path = f"static/icons/{icon_filename}" with open(icon_path, "wb") as f: f.write(icon.file.read()) - # Создаем базовую версию base_version = { "api": api, "apk_file": apk_filename, "version": version } - # Создаем запись в БД db_app = App( name=name, package=package, @@ -440,6 +436,7 @@ def create_app( api=api, description=description, is_game=is_game, + rating=rating, apk_file=apk_filename, icon=icon_filename, screenshots="[]", @@ -451,7 +448,6 @@ def create_app( return {"message": "Приложение создано", "app_id": db_app.id} -# НОВЫЙ ENDPOINT: Обновление приложения @app.put("/api/admin/apps/{app_id}") def update_app( app_id: int, @@ -470,7 +466,6 @@ def update_app( if not app: raise HTTPException(status_code=404, detail="App not found") - # Обновляем поля если они переданы if name is not None: app.name = name if package is not None: @@ -486,7 +481,6 @@ def update_app( if rating is not None: app.rating = rating - # Обновляем иконку если передана if icon and icon.filename: icon_filename = f"{app.package}_icon.png" icon_path = f"static/icons/{icon_filename}" @@ -510,13 +504,11 @@ def add_app_version( if not app: raise HTTPException(status_code=404, detail="App not found") - # Сохраняем APK apk_filename = f"{app.package}_{version.replace(' ', '_')}.apk" apk_path = f"static/apks/{apk_filename}" with open(apk_path, "wb") as f: f.write(apk_file.file.read()) - # Добавляем версию versions = json.loads(app.versions) if app.versions else [] new_version = { "api": api, @@ -529,29 +521,6 @@ def add_app_version( db.commit() return {"message": "Версия добавлена"} -@app.delete("/api/admin/apps/{app_id}") -def delete_app(app_id: int, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)): - app = db.query(App).filter(App.id == app_id).first() - if app: - # Удаляем все APK файлы версий - versions = json.loads(app.versions) if app.versions else [] - for version in versions: - apk_file = version.get("apk_file") - if apk_file and os.path.exists(f"static/apks/{apk_file}"): - os.remove(f"static/apks/{apk_file}") - - # Удаляем иконку - if os.path.exists(f"static/icons/{app.icon}"): - os.remove(f"static/icons/{app.icon}") - - # Удаляем связанные отзывы - db.query(Review).filter(Review.app_id == app_id).delete() - - db.delete(app) - db.commit() - return {"message": "Приложение удалено"} - -# НОВЫЙ ENDPOINT: Установка основной версии из списка версий @app.post("/api/admin/apps/{app_id}/set-version") def set_main_version( app_id: int, @@ -563,7 +532,6 @@ def set_main_version( if not app: raise HTTPException(status_code=404, detail="App not found") - # Ищем версию в списке версий versions = json.loads(app.versions) if app.versions else [] target_version = None @@ -575,7 +543,6 @@ def set_main_version( if not target_version: raise HTTPException(status_code=404, detail="Version not found in app versions") - # Обновляем основную версию и APK файл app.version = version app.apk_file = target_version.get("apk_file") app.api = target_version.get("api") @@ -583,7 +550,25 @@ def set_main_version( db.commit() return {"message": f"Основная версия установлена на {version}"} -# ИСПРАВЛЕННЫЙ МЕТОД: Добавление отзывов с правильным подсчетом +@app.delete("/api/admin/apps/{app_id}") +def delete_app(app_id: int, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)): + app = db.query(App).filter(App.id == app_id).first() + if app: + versions = json.loads(app.versions) if app.versions else [] + for version in versions: + apk_file = version.get("apk_file") + if apk_file and os.path.exists(f"static/apks/{apk_file}"): + os.remove(f"static/apks/{apk_file}") + + if os.path.exists(f"static/icons/{app.icon}"): + os.remove(f"static/icons/{app.icon}") + + db.query(Review).filter(Review.app_id == app_id).delete() + + db.delete(app) + db.commit() + return {"message": "Приложение удалено"} + @app.post("/api/admin/reviews") def create_review( user: AdminUser = Depends(require_auth), @@ -597,23 +582,18 @@ def create_review( try: avatar_filename = "default_avatar.png" - # Обрабатываем аватар только если он действительно загружен if avatar and avatar.filename and avatar.filename.strip() != "": - # Проверяем, что это изображение if not avatar.content_type or not avatar.content_type.startswith('image/'): raise HTTPException(status_code=400, detail="Файл должен быть изображением") - # Генерируем уникальное имя файла file_extension = os.path.splitext(avatar.filename)[1] avatar_filename = f"avatar_{int(datetime.now().timestamp())}{file_extension}" avatar_path = f"static/icons/{avatar_filename}" - # Сохраняем аватар contents = avatar.file.read() with open(avatar_path, "wb") as f: f.write(contents) - # Создаем отзыв db_review = Review( app_id=app_id, username=username, @@ -626,10 +606,8 @@ def create_review( db.add(db_review) db.commit() - # ИСПРАВЛЕНИЕ: Правильно подсчитываем количество отзывов для приложения review_count = db.query(Review).filter(Review.app_id == app_id).count() - # Обновляем счетчик отзывов у приложения app = db.query(App).filter(App.id == app_id).first() if app: app.review_count = review_count @@ -651,10 +629,8 @@ def delete_review(review_id: int, user: AdminUser = Depends(require_auth), db: S db.delete(review) db.commit() - # ИСПРАВЛЕНИЕ: Правильно подсчитываем количество отзывов для приложения после удаления review_count = db.query(Review).filter(Review.app_id == app_id).count() - # Обновляем счетчик отзывов у приложения app = db.query(App).filter(App.id == app_id).first() if app: app.review_count = review_count @@ -662,5 +638,132 @@ def delete_review(review_id: int, user: AdminUser = Depends(require_auth), db: S return {"message": "Отзыв удален"} +# API для главных изображений +@app.get("/api/admin/main-images") +def get_main_images(user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)): + images = db.query(MainImage).all() + return images + +@app.post("/api/admin/main-images") +def upload_main_image( + user: AdminUser = Depends(require_auth), + description: str = Form(""), + image: UploadFile = File(...), + db: Session = Depends(get_db) +): + if not image.content_type.startswith('image/'): + raise HTTPException(status_code=400, detail="Файл должен быть изображением") + + file_extension = os.path.splitext(image.filename)[1] + filename = f"main_{int(datetime.now().timestamp())}{file_extension}" + image_path = f"static/main_images/{filename}" + + contents = image.file.read() + with open(image_path, "wb") as f: + f.write(contents) + + db_image = MainImage( + filename=filename, + description=description, + uploaded_by=user.username + ) + db.add(db_image) + db.commit() + db.refresh(db_image) + + return {"message": "Изображение загружено", "image_id": db_image.id} + +@app.post("/api/admin/main-images/{image_id}/activate") +def activate_main_image( + image_id: int, + user: AdminUser = Depends(require_auth), + db: Session = Depends(get_db) +): + db.query(MainImage).update({MainImage.is_active: False}) + + image = db.query(MainImage).filter(MainImage.id == image_id).first() + if image: + image.is_active = True + db.commit() + return {"message": f"Изображение '{image.filename}' активировано"} + else: + raise HTTPException(status_code=404, detail="Изображение не найдено") + +@app.delete("/api/admin/main-images/{image_id}") +def delete_main_image( + image_id: int, + user: AdminUser = Depends(require_auth), + db: Session = Depends(get_db) +): + image = db.query(MainImage).filter(MainImage.id == image_id).first() + if image: + if os.path.exists(f"static/main_images/{image.filename}"): + os.remove(f"static/main_images/{image.filename}") + + if image.is_active: + other_image = db.query(MainImage).filter(MainImage.id != image_id).first() + if other_image: + other_image.is_active = True + + db.delete(image) + db.commit() + return {"message": "Изображение удалено"} + else: + raise HTTPException(status_code=404, detail="Изображение не найдено") + +# API для настроек дашборда +@app.post("/api/admin/dashboard/motd") +def update_motd( + user: AdminUser = Depends(require_auth), + motd: str = Form(...), + db: Session = Depends(get_db) +): + dashboard_settings = db.query(DashboardSettings).first() + if not dashboard_settings: + dashboard_settings = DashboardSettings() + db.add(dashboard_settings) + + dashboard_settings.motd = motd + dashboard_settings.updated_at = datetime.utcnow() + db.commit() + + return {"message": "MOTD обновлен"} + +@app.post("/api/admin/dashboard/image") +def update_dashboard_image( + user: AdminUser = Depends(require_auth), + image: UploadFile = File(...), + db: Session = Depends(get_db) +): + if not image.content_type.startswith('image/'): + raise HTTPException(status_code=400, detail="Файл должен быть изображением") + + file_extension = os.path.splitext(image.filename)[1] + image_filename = f"dashboard_{int(datetime.now().timestamp())}{file_extension}" + image_path = f"static/dashboard/{image_filename}" + + contents = image.file.read() + with open(image_path, "wb") as f: + f.write(contents) + + dashboard_settings = db.query(DashboardSettings).first() + if not dashboard_settings: + dashboard_settings = DashboardSettings() + db.add(dashboard_settings) + + if (dashboard_settings.image_filename != "dashboard_default.jpg" and + os.path.exists(f"static/dashboard/{dashboard_settings.image_filename}")): + os.remove(f"static/dashboard/{dashboard_settings.image_filename}") + + dashboard_settings.image_filename = image_filename + dashboard_settings.updated_at = datetime.utcnow() + db.commit() + + return {"message": "Изображение дашборда обновлено"} + if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=5000) \ No newline at end of file + if not os.path.exists("static/main_images/default_main.jpg"): + os.makedirs("static/main_images", exist_ok=True) + open("static/main_images/default_main.jpg", "wb").close() + + uvicorn.run(app, host="0.0.0.0", port=5001) \ No newline at end of file diff --git a/oldmarket.db b/oldmarket.db index c9383c8..a9833d0 100644 Binary files a/oldmarket.db and b/oldmarket.db differ diff --git a/static/main_images/main_1760780294.jpg b/static/main_images/main_1760780294.jpg new file mode 100644 index 0000000..82bcf63 Binary files /dev/null and b/static/main_images/main_1760780294.jpg differ diff --git a/static/main_images/main_1760780334.jpg b/static/main_images/main_1760780334.jpg new file mode 100644 index 0000000..49bd03e Binary files /dev/null and b/static/main_images/main_1760780334.jpg differ diff --git a/static/main_images/main_1760780342.jpg b/static/main_images/main_1760780342.jpg new file mode 100644 index 0000000..6f3fb6f Binary files /dev/null and b/static/main_images/main_1760780342.jpg differ diff --git a/templates/admin.html b/templates/admin.html index 4e6c2a3..1dcff84 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -164,20 +164,21 @@ - +

Дашборд админ-панели

diff --git a/templates/main_images.html b/templates/main_images.html new file mode 100644 index 0000000..20f5da2 --- /dev/null +++ b/templates/main_images.html @@ -0,0 +1,315 @@ + + + + + + Управление главными изображениями - OldMarket Admin + + + + + +
+
+

Управление главными изображениями

+ +
+ +

Эти изображения показываются на корневом пути / сервера

+ +
+ {% for image in images %} +
+ {{ image.description }} +
+

{{ image.description or 'Без описания' }}

+

Файл: {{ image.filename }}

+

Загружено: {{ image.uploaded_at.strftime('%Y-%m-%d %H:%M') }}

+

Кем: {{ image.uploaded_by }}

+ +
+ {% if image.is_active %} + АКТИВНО + {% else %} + + {% endif %} + +
+
+
+ {% endfor %} +
+
+ + + + + + + \ No newline at end of file