Refactored: #1

Merged
FDKost merged 1 commits from feature/projectUpped into master 2025-12-18 14:33:09 +03:00
10 changed files with 103 additions and 33 deletions
Showing only changes of commit e04933b9c1 - Show all commits

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_22" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View File

@@ -1,7 +1,28 @@
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=newplanet
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/newplanet DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/newplanet
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_URL=redis://localhost:6379 REDIS_URL=redis://localhost:6379
SECRET_KEY=your-secret-key-here
GIGACHAT_API_KEY=your-gigachat-api-key # Security
MINIO_ENDPOINT=localhost:9000 SECRET_KEY=jwt-secret-key
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin # Storage (MinIO/S3)
STORAGE_ENDPOINT=localhost:9000
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin
STORAGE_BUCKET=new-planet-images
STORAGE_USE_SSL=false
STORAGE_REGION=us-east-1
# GigaChat API
GIGACHAT_CLIENT_ID=gigachat-client-id
GIGACHAT_CLIENT_SECRET="gigachat-token-here"

View File

@@ -31,12 +31,17 @@ pip install -r requirements.txt
3. Настройте `.env`: 3. Настройте `.env`:
```bash ```bash
cp .env.example .env cp .env.example .env
#В целом вам нужно поменять GIGACHAT API секцию, JWT Secret key сгенерить, просто в поисковике генератор на 256 байт сделаете JWT
#Для гигачата логинетесь, дергаете от туда CLIENT_ID и SECRET KEY
# Отредактируйте .env с вашими настройками # Отредактируйте .env с вашими настройками
``` ```
4. Запустите инфраструктуру (Docker): 4. Запустите инфраструктуру (Docker):
```bash ```bash
docker-compose -f docker/docker-compose.yml up -d docker-compose -f docker/docker-compose.yml up -d
# или используйте вариант ниже,но лучше вариант выше для избежания непредвиденного
#также напоминаю что вам необходим сам запущенный докер чтобы тестировать локально
docker-compose up
``` ```
5. Примените миграции: 5. Примените миграции:
@@ -47,6 +52,7 @@ alembic upgrade head
6. Запустите сервер: 6. Запустите сервер:
```bash ```bash
uvicorn app.main:app --reload uvicorn app.main:app --reload
# если не запустилось, проверяйте есть ли .venv(установлено ли окружение для питона), также попробуйте в венве uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
``` ```
API доступен на `http://localhost:8000` API доступен на `http://localhost:8000`
@@ -60,6 +66,9 @@ Swagger UI: `http://localhost:8000/docs`
- `POST /api/v1/auth/refresh` - Обновление токена - `POST /api/v1/auth/refresh` - Обновление токена
- `GET /api/v1/auth/me` - Текущий пользователь - `GET /api/v1/auth/me` - Текущий пользователь
### Для написания всех запросов ниже:
Не забывайте авторизоваться в сваггере, сверху кнопка Authorize
### Schedules ### Schedules
- `GET /api/v1/schedules` - Список расписаний - `GET /api/v1/schedules` - Список расписаний
- `POST /api/v1/schedules` - Создать расписание - `POST /api/v1/schedules` - Создать расписание
@@ -101,6 +110,6 @@ Swagger UI: `http://localhost:8000/docs`
## 🔗 Связанные репозитории ## 🔗 Связанные репозитории
- **Frontend (Android)** — [new-planet-android](https://github.com/your-org/new-planet-android) - **Frontend (Android)** — [new-planet-android](https://git.bro-js.ru/Glevel/New-planet-app.git)
- **AI Agents** — [new-planet-ai-agents](https://github.com/your-org/new-planet-ai-agents) - **AI Agents** — [new-planet-ai-agents](https://git.bro-js.ru/Glevel/New-planet-ai-agent.git)

View File

@@ -1,9 +1,8 @@
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool from sqlalchemy import pool
from alembic import context from alembic import context
import asyncio import asyncio
from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.ext.asyncio import create_async_engine
# this is the Alembic Config object # this is the Alembic Config object
config = context.config config = context.config
@@ -22,12 +21,12 @@ target_metadata = Base.metadata
def get_url(): def get_url():
"""Получить URL БД""" """Получить URL БД"""
return settings.database_url.replace("+asyncpg", "") return settings.database_url
def run_migrations_offline() -> None: def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.""" """Run migrations in 'offline' mode."""
url = get_url() url = get_url().replace("+asyncpg", "")
context.configure( context.configure(
url=url, url=url,
target_metadata=target_metadata, target_metadata=target_metadata,
@@ -48,15 +47,9 @@ def do_run_migrations(connection):
async def run_migrations_online() -> None: async def run_migrations_online() -> None:
"""Run migrations in 'online' mode.""" """Run migrations in 'online' mode."""
configuration = config.get_section(config.config_ini_section) connectable = create_async_engine(
configuration["sqlalchemy.url"] = get_url() get_url(),
connectable = AsyncEngine( poolclass=pool.NullPool,
engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True,
)
) )
async with connectable.connect() as connection: async with connectable.connect() as connection:

View File

@@ -1,20 +1,54 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from jose import JWTError, jwt from jose import JWTError, jwt
from passlib.context import CryptContext import bcrypt
from app.core.config import settings from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool: def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Проверка пароля""" """Проверка пароля"""
return pwd_context.verify(plain_password, hashed_password) # Используем bcrypt напрямую для проверки
try:
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
except (ValueError, TypeError, AttributeError):
return False
def _truncate_password_to_72_bytes(password: str) -> str:
"""Обрезает пароль до 72 байт, корректно обрабатывая UTF-8"""
password_bytes = password.encode('utf-8')
if len(password_bytes) <= 72:
return password
# Обрезаем до 72 байт
password_bytes = password_bytes[:72]
# Удаляем неполные UTF-8 последовательности в конце
# (байты, которые начинаются с 10xxxxxx, но не являются началом символа)
while password_bytes and (password_bytes[-1] & 0xC0) == 0x80:
password_bytes = password_bytes[:-1]
return password_bytes.decode('utf-8', errors='replace')
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
"""Хеширование пароля""" """Хеширование пароля"""
return pwd_context.hash(password) # bcrypt имеет ограничение в 72 байта
# Обрезаем пароль до 72 байт перед хешированием
password_bytes = password.encode('utf-8')
if len(password_bytes) > 72:
# Обрезаем до 72 байт
password_bytes = password_bytes[:72]
# Удаляем неполные UTF-8 последовательности в конце
while password_bytes and (password_bytes[-1] & 0xC0) == 0x80:
password_bytes = password_bytes[:-1]
password = password_bytes.decode('utf-8', errors='replace')
password_bytes = password.encode('utf-8')
# Используем bcrypt напрямую, чтобы избежать проблем с инициализацией passlib
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:

View File

@@ -1,6 +1,5 @@
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, DateTime, func from sqlalchemy import Column, DateTime, func, String
from sqlalchemy.dialects.postgresql import UUID
import uuid import uuid
Base = declarative_base() Base = declarative_base()
@@ -11,7 +10,7 @@ class BaseModel(Base):
__abstract__ = True __abstract__ = True
id = Column( id = Column(
UUID(as_uuid=False), String,
primary_key=True, primary_key=True,
default=lambda: str(uuid.uuid4()), default=lambda: str(uuid.uuid4()),
nullable=False nullable=False

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, String, Integer, ForeignKey, Text from sqlalchemy import Column, String, Integer, ForeignKey, Text, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.db.base import BaseModel from app.db.base import BaseModel

View File

@@ -11,7 +11,7 @@ class UserBase(BaseModel):
class UserCreate(UserBase): class UserCreate(UserBase):
password: str = Field(..., min_length=8) password: str = Field(..., min_length=8, max_length=72, description="Password must be between 8 and 72 characters")
class UserUpdate(BaseModel): class UserUpdate(BaseModel):

View File

@@ -1,4 +1,5 @@
import aiohttp import aiohttp
import ssl
import base64 import base64
import uuid import uuid
import time import time
@@ -30,7 +31,13 @@ class GigaChatService:
data = {"scope": "GIGACHAT_API_PERS"} data = {"scope": "GIGACHAT_API_PERS"}
async with aiohttp.ClientSession() as session: # Создаем SSL контекст без проверки сертификата (только для разработки!)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
connector = aiohttp.TCPConnector(ssl=ssl_context)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post( async with session.post(
settings.GIGACHAT_AUTH_URL, settings.GIGACHAT_AUTH_URL,
headers=headers, headers=headers,
@@ -75,7 +82,13 @@ class GigaChatService:
"max_tokens": 2000 "max_tokens": 2000
} }
async with aiohttp.ClientSession() as session: # Создаем SSL контекст без проверки сертификата (только для разработки!)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
connector = aiohttp.TCPConnector(ssl=ssl_context)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post( async with session.post(
f"{settings.GIGACHAT_BASE_URL}/chat/completions", f"{settings.GIGACHAT_BASE_URL}/chat/completions",
headers=headers, headers=headers,

View File

@@ -3,6 +3,7 @@ uvicorn[standard]
sqlalchemy>=2.0 sqlalchemy>=2.0
alembic alembic
asyncpg asyncpg
psycopg2-binary
redis redis
pydantic pydantic
pydantic-settings pydantic-settings