Files
oldmarketcustomserver/main.py
2025-10-18 12:39:53 +03:00

769 lines
27 KiB
Python

from fastapi import FastAPI, HTTPException, Depends, UploadFile, File, Form, Request, status
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import create_engine, Column, Integer, String, Float, Boolean, Text, DateTime, func
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from datetime import datetime
import os
import json
import secrets
import random
from typing import List, Optional
import uvicorn
from hashlib import sha256
# Настройка БД
SQLALCHEMY_DATABASE_URL = "sqlite:///./oldmarket.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Модели БД
class App(Base):
__tablename__ = "apps"
id = Column(Integer, primary_key=True, index=True)
api = Column(String(10))
apk_file = Column(String(255))
description = Column(Text)
downloads = Column(Integer, default=0)
icon = Column(String(255))
is_game = Column(Boolean, default=False)
name = Column(String(255))
package = Column(String(255))
rating = Column(Float, default=0.0)
review_count = Column(Integer, default=0)
screenshots = Column(Text)
version = Column(String(50))
versions = Column(Text)
class Review(Base):
__tablename__ = "reviews"
id = Column(Integer, primary_key=True, index=True)
app_id = Column(Integer)
avatar = Column(String(255))
comment = Column(Text)
created_at = Column(String(50))
rating = Column(Integer)
user_id = Column(Integer)
username = Column(String(255))
class AdminUser(Base):
__tablename__ = "admin_users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True)
password_hash = Column(String(255))
created_at = Column(DateTime, default=datetime.utcnow)
class DashboardSettings(Base):
__tablename__ = "dashboard_settings"
id = Column(Integer, primary_key=True, index=True)
motd = Column(Text, default="Добро пожаловать в панель управления OldMarket!")
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)
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/main_images", exist_ok=True)
# Монтируем статические файлы
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/html/apps", StaticFiles(directory="static/icons"), name="icons")
# Настройка шаблонов
templates = Jinja2Templates(directory="templates")
# Зависимость для БД
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Аутентификация
def hash_password(password: str) -> str:
return sha256(password.encode()).hexdigest()
def verify_password(plain_password: str, hashed_password: str) -> bool:
return hash_password(plain_password) == hashed_password
# Создаем дефолтного админа если нет пользователей
def create_default_admin(db: Session):
if db.query(AdminUser).count() == 0:
default_admin = AdminUser(
username="admin",
password_hash=hash_password("admin123")
)
db.add(default_admin)
db.commit()
print("Создан дефолтный администратор: admin / admin123")
# Создаем дефолтные настройки дашборда
def create_default_dashboard_settings(db: Session):
if db.query(DashboardSettings).count() == 0:
default_settings = DashboardSettings(
motd="Добро пожаловать в панель управления OldMarket!",
image_filename="dashboard_default.jpg"
)
db.add(default_settings)
db.commit()
# Создаем дефолтное главное изображение
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 = {}
def create_session(user_id: int) -> str:
session_id = secrets.token_urlsafe(32)
sessions[session_id] = {
"user_id": user_id,
"created_at": datetime.utcnow()
}
return session_id
def get_current_user(request: Request, db: Session = Depends(get_db)):
session_id = request.cookies.get("session_id")
if not session_id or session_id not in sessions:
return None
user_data = sessions[session_id]
user = db.query(AdminUser).filter(AdminUser.id == user_data["user_id"]).first()
return user
def require_auth(request: Request, db: Session = Depends(get_db)):
user = get_current_user(request, db)
if not user:
raise HTTPException(
status_code=status.HTTP_303_SEE_OTHER,
headers={"Location": "/admin/login"}
)
return user
# Корневой 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()
return [
{
**{k: v for k, v in app.__dict__.items() if k != '_sa_instance_state'},
"screenshots": json.loads(app.screenshots) if app.screenshots else [],
"versions": json.loads(app.versions) if app.versions else []
}
for app in apps
]
@app.get("/api/app/{app_id}")
def get_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")
return {
**{k: v for k, v in app.__dict__.items() if k != '_sa_instance_state'},
"screenshots": json.loads(app.screenshots) if app.screenshots else [],
"versions": json.loads(app.versions) if app.versions else []
}
@app.get("/api/app/{app_id}/reviews")
def get_reviews(app_id: int, db: Session = Depends(get_db)):
reviews = db.query(Review).filter(Review.app_id == app_id).all()
return [
{k: v for k, v in review.__dict__.items() if k != '_sa_instance_state'}
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()
apk_path = f"static/apks/{app.apk_file}"
if not os.path.exists(apk_path):
raise HTTPException(status_code=404, detail="APK file not found")
return FileResponse(
apk_path,
filename=app.apk_file,
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
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()
apk_filename = target_version.get("apk_file")
if not apk_filename:
raise HTTPException(status_code=404, detail="APK filename not found for this version")
apk_path = f"static/apks/{apk_filename}"
if not os.path.exists(apk_path):
raise HTTPException(status_code=404, detail="APK file not found")
return FileResponse(
apk_path,
filename=apk_filename,
media_type='application/vnd.android.package-archive'
)
# Аутентификация админ-панели
@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")
def admin_login_post(
request: Request,
username: str = Form(...),
password: str = Form(...),
db: Session = Depends(get_db)
):
user = db.query(AdminUser).filter(AdminUser.username == username).first()
if not user or not verify_password(password, user.password_hash):
return templates.TemplateResponse("login.html", {
"request": request,
"error": "Неверное имя пользователя или пароль"
})
session_id = create_session(user.id)
response = RedirectResponse(url="/admin", status_code=302)
response.set_cookie(key="session_id", value=session_id, httponly=True)
return response
@app.get("/admin/logout")
def admin_logout():
response = RedirectResponse(url="/admin/login", status_code=302)
response.delete_cookie("session_id")
return response
# Защищенные роуты админ-панели
@app.get("/admin", response_class=HTMLResponse)
def admin_panel(request: Request, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)):
apps_count = db.query(App).count()
reviews_count = db.query(Review).count()
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", {
"request": request,
"apps_count": apps_count,
"reviews_count": reviews_count,
"total_downloads": total_downloads,
"username": user.username,
"motd": dashboard_settings.motd if dashboard_settings else "Добро пожаловать!",
"dashboard_image": dashboard_settings.image_filename if dashboard_settings else "dashboard_default.jpg"
})
@app.get("/admin/apps", response_class=HTMLResponse)
def admin_apps(request: Request, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)):
apps = db.query(App).all()
return templates.TemplateResponse("apps.html", {
"request": request,
"apps": apps,
"username": user.username
})
@app.get("/admin/reviews", response_class=HTMLResponse)
def admin_reviews(request: Request, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)):
reviews = db.query(Review).all()
apps = db.query(App).all()
app_names = {app.id: app.name for app in apps}
return templates.TemplateResponse("reviews.html", {
"request": request,
"reviews": reviews,
"app_names": app_names,
"apps": apps,
"username": user.username
})
@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()
return templates.TemplateResponse("dashboard_settings.html", {
"request": request,
"username": user.username,
"motd": dashboard_settings.motd if dashboard_settings else "",
"dashboard_image": dashboard_settings.image_filename if dashboard_settings else "dashboard_default.jpg"
})
# 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()
return apps
@app.get("/api/admin/apps/{app_id}")
def get_admin_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 not app:
raise HTTPException(status_code=404, detail="App not found")
return app
@app.post("/api/admin/apps")
def create_app(
user: AdminUser = Depends(require_auth),
name: str = Form(...),
package: str = Form(...),
version: str = Form(...),
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_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,
version=version,
api=api,
description=description,
is_game=is_game,
rating=rating,
apk_file=apk_filename,
icon=icon_filename,
screenshots="[]",
versions=json.dumps([base_version])
)
db.add(db_app)
db.commit()
db.refresh(db_app)
return {"message": "Приложение создано", "app_id": db_app.id}
@app.put("/api/admin/apps/{app_id}")
def update_app(
app_id: int,
user: AdminUser = Depends(require_auth),
name: str = Form(None),
package: str = Form(None),
version: str = Form(None),
api: str = Form(None),
description: str = Form(None),
is_game: bool = Form(None),
rating: float = Form(None),
icon: UploadFile = File(None),
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")
if name is not None:
app.name = name
if package is not None:
app.package = package
if version is not None:
app.version = version
if api is not None:
app.api = api
if description is not None:
app.description = description
if is_game is not None:
app.is_game = is_game
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}"
with open(icon_path, "wb") as f:
f.write(icon.file.read())
app.icon = icon_filename
db.commit()
return {"message": "Приложение обновлено"}
@app.post("/api/admin/apps/{app_id}/versions")
def add_app_version(
app_id: int,
user: AdminUser = Depends(require_auth),
version: str = Form(...),
api: str = Form(...),
apk_file: UploadFile = File(...),
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")
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,
"apk_file": apk_filename,
"version": version
}
versions.append(new_version)
app.versions = json.dumps(versions)
db.commit()
return {"message": "Версия добавлена"}
@app.post("/api/admin/apps/{app_id}/set-version")
def set_main_version(
app_id: int,
user: AdminUser = Depends(require_auth),
version: str = Form(...),
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") == version:
target_version = ver
break
if not target_version:
raise HTTPException(status_code=404, detail="Version not found in app versions")
app.version = version
app.apk_file = target_version.get("apk_file")
app.api = target_version.get("api")
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),
app_id: int = Form(...),
username: str = Form(...),
rating: int = Form(...),
comment: str = Form(...),
avatar: UploadFile = File(None),
db: Session = Depends(get_db)
):
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,
rating=rating,
comment=comment,
avatar=avatar_filename,
created_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
user_id=1
)
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
db.commit()
db.refresh(db_review)
return {"message": "Отзыв создан", "review_id": db_review.id}
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"Ошибка при создании отзыва: {str(e)}")
@app.delete("/api/admin/reviews/{review_id}")
def delete_review(review_id: int, user: AdminUser = Depends(require_auth), db: Session = Depends(get_db)):
review = db.query(Review).filter(Review.id == review_id).first()
if review:
app_id = review.app_id
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
db.commit()
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__":
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)