Initial commit

This commit is contained in:
2026-01-18 21:01:43 +03:00
commit 455b6df2ba
3329 changed files with 1143638 additions and 0 deletions
View File
+27
View File
@@ -0,0 +1,27 @@
from typing import Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ....database.database import get_db
from ....auth.telegram_auth import authenticate_user_telegram
from ....schemas.token import Token
router = APIRouter()
@router.post("/telegram-login", response_model=Token)
async def telegram_login(auth_data: Dict[str, Any], db: Session = Depends(get_db)):
"""
Authenticate user via Telegram Login Widget
Expects data received from Telegram Login Widget
"""
result = authenticate_user_telegram(db, auth_data)
if not result:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Telegram login data",
headers={"WWW-Authenticate": "Bearer"},
)
return result
+101
View File
@@ -0,0 +1,101 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ....database.database import get_db
from ....auth.jwt import get_current_user
from ....models.user import User
from ....schemas.post import Post, PostCreate, PostUpdate
from ....services.post_service import PostService
router = APIRouter(prefix="/posts", tags=["posts"])
@router.get("/", response_model=List[Post])
def get_posts(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Получить список постов пользователя
"""
post_service = PostService(db)
posts = post_service.get_posts(owner_id=current_user.id, skip=skip, limit=limit)
return posts
@router.post("/", response_model=Post)
def create_post(
post: PostCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Создать новый пост
"""
post_service = PostService(db)
db_post = post_service.create_post(post_data=post, owner_id=current_user.id)
return db_post
@router.get("/{post_id}", response_model=Post)
def get_post(
post_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Получить конкретный пост
"""
post_service = PostService(db)
db_post = post_service.get_post(post_id=post_id, owner_id=current_user.id)
if not db_post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
return db_post
@router.put("/{post_id}", response_model=Post)
def update_post(
post_id: int,
post_update: PostUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Обновить пост
"""
post_service = PostService(db)
db_post = post_service.update_post(
post_id=post_id,
post_data=post_update,
owner_id=current_user.id
)
if not db_post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
return db_post
@router.delete("/{post_id}")
def delete_post(
post_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
Удалить пост
"""
post_service = PostService(db)
success = post_service.delete_post(post_id=post_id, owner_id=current_user.id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
return {"message": "Post deleted successfully"}
+11
View File
@@ -0,0 +1,11 @@
from fastapi import APIRouter
from .endpoints import auth
from .endpoints import posts
api_router = APIRouter()
# Include authentication routes
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
# Include posts routes
api_router.include_router(posts.router)
View File
+61
View File
@@ -0,0 +1,61 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from fastapi import HTTPException, status, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from ..database.config import settings
from ..models.user import User
from ..schemas.token import TokenData
from ..database.database import get_db
security = HTTPBearer()
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_token(token: str, credentials_exception):
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
return token_data
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
token_data = verify_token(credentials.credentials, credentials_exception)
user = db.query(User).filter(User.username == token_data.username).first()
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
+117
View File
@@ -0,0 +1,117 @@
import hashlib
import hmac
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from ..database.config import settings
from ..models.user import User
from ..schemas.user import UserCreate, UserInDB
from .jwt import create_access_token
def verify_telegram_login_data(auth_data: Dict[str, Any]) -> bool:
"""
Verify the authenticity of Telegram login data
"""
if not settings.TELEGRAM_BOT_TOKEN:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Telegram bot token not configured"
)
# Extract the hash
received_hash = auth_data.pop('hash', None)
if not received_hash:
return False
# Get the auth date
auth_date = auth_data.pop('auth_date', None)
if auth_date:
# Check if the data is not too old (more than 1 day)
auth_time = datetime.fromtimestamp(int(auth_date))
if (datetime.now() - auth_time).days > 0:
return False
# Sort the remaining parameters alphabetically by key
data_check_arr = []
for key, value in sorted(auth_data.items()):
data_check_arr.append(f"{key}={value}")
data_check_string = '\n'.join(data_check_arr)
# Create secret key using SHA256 of bot token
secret_key = hashlib.sha256(settings.TELEGRAM_BOT_TOKEN.encode()).digest()
# Calculate HMAC-SHA256 signature
calculated_hash = hmac.new(
secret_key,
data_check_string.encode(),
hashlib.sha256
).hexdigest()
# Compare the calculated hash with the received hash
return calculated_hash == received_hash
def authenticate_user_telegram(db: Session, auth_data: Dict[str, Any]):
"""
Authenticate user via Telegram login data and return JWT token
"""
# Verify the authenticity of the data
if not verify_telegram_login_data(auth_data.copy()): # Copy to avoid modifying original
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Telegram login data"
)
# Extract user data
telegram_id = str(auth_data.get('id'))
username = auth_data.get('username')
first_name = auth_data.get('first_name')
last_name = auth_data.get('last_name')
photo_url = auth_data.get('photo_url')
# Try to find existing user by telegram_id
user = db.query(User).filter(User.telegram_id == telegram_id).first()
if user:
# Update user data if it has changed
user.username = username
user.first_name = first_name
user.last_name = last_name
user.avatar_url = photo_url
user.is_verified = True
db.commit()
db.refresh(user)
else:
# Create new user
user = User(
telegram_id=telegram_id,
username=username,
first_name=first_name,
last_name=last_name,
avatar_url=photo_url,
is_verified=True,
is_active=True
)
db.add(user)
db.commit()
db.refresh(user)
# Create JWT token
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username or user.telegram_id},
expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"user": UserInDB.from_orm(user) if hasattr(UserInDB, 'from_orm') else UserInDB.model_validate(user)
}
View File
+21
View File
@@ -0,0 +1,21 @@
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql://user:password@localhost/telepost"
REDIS_URL: str = "redis://localhost:6379"
SECRET_KEY: str = "your-secret-key-here"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Telegram Bot API settings
TELEGRAM_BOT_TOKEN: Optional[str] = None
TELEGRAM_BOT_NAME: Optional[str] = None
class Config:
env_file = ".env"
settings = Settings()
+31
View File
@@ -0,0 +1,31 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .config import settings
# Create the database engine
engine = create_engine(
settings.DATABASE_URL,
pool_pre_ping=True, # Verify connections before use
pool_recycle=300, # Recycle connections after 5 minutes
)
# Create a configured "SessionLocal" class
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine
)
# Create a Base class for declarative models
Base = declarative_base()
def get_db():
"""
Dependency to get database session
"""
db = SessionLocal()
try:
yield db
finally:
db.close()
View File
+19
View File
@@ -0,0 +1,19 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from typing import Generator
Base = declarative_base()
def get_db() -> Generator:
"""
Dependency to get database session
"""
db = SessionLocal()
try:
yield db
finally:
db.close()
# Engine and session will be configured in database/database.py
SessionLocal = sessionmaker(autocommit=False, autoflush=False)
+26
View File
@@ -0,0 +1,26 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database.database import Base
from datetime import datetime
import uuid
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
content = Column(Text, nullable=False)
scheduled_time = Column(DateTime, nullable=True)
status = Column(String(50), default="draft") # draft, scheduled, published, cancelled
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
published_at = Column(DateTime(timezone=True), nullable=True)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Relationship
owner = relationship("User", back_populates="posts")
def __repr__(self):
return f"<Post(id={self.id}, title='{self.title}', status='{self.status}')>"
+28
View File
@@ -0,0 +1,28 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from typing import TYPE_CHECKING
from .base import Base
if TYPE_CHECKING:
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
telegram_id = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=True)
first_name = Column(String, nullable=True)
last_name = Column(String, nullable=True)
email = Column(String, unique=True, index=True, nullable=True)
avatar_url = Column(String, nullable=True)
is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
def __repr__(self):
return f"<User(id={self.id}, telegram_id={self.telegram_id}, username={self.username})>"
View File
+44
View File
@@ -0,0 +1,44 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
from enum import Enum
class PostStatus(str, Enum):
draft = "draft"
scheduled = "scheduled"
published = "published"
cancelled = "cancelled"
class PostBase(BaseModel):
title: str
content: str
scheduled_time: Optional[datetime] = None
status: PostStatus = PostStatus.draft
class PostCreate(PostBase):
pass
class PostUpdate(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
scheduled_time: Optional[datetime] = None
status: Optional[PostStatus] = None
class PostInDB(PostBase):
id: int
owner_id: int
created_at: datetime
updated_at: Optional[datetime] = None
published_at: Optional[datetime] = None
class Config:
from_attributes = True
class Post(PostInDB):
pass
+12
View File
@@ -0,0 +1,12 @@
from pydantic import BaseModel
from typing import Optional
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
user_id: Optional[int] = None
+48
View File
@@ -0,0 +1,48 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
telegram_id: str
username: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
email: Optional[str] = None
avatar_url: Optional[str] = None
is_active: Optional[bool] = True
is_verified: Optional[bool] = False
class UserCreate(UserBase):
pass
class UserUpdate(BaseModel):
username: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
email: Optional[str] = None
avatar_url: Optional[str] = None
class UserInDB(UserBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class UserPublic(BaseModel):
id: int
telegram_id: str
username: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
avatar_url: Optional[str] = None
is_verified: bool
class Config:
from_attributes = True
View File
+88
View File
@@ -0,0 +1,88 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from ..models.post import Post
from ..schemas.post import PostCreate, PostUpdate
from datetime import datetime
class PostService:
def __init__(self, db_session: Session):
self.db = db_session
def get_post(self, post_id: int, owner_id: int) -> Optional[Post]:
"""Получить пост по ID и владельцу"""
return self.db.query(Post).filter(
and_(Post.id == post_id, Post.owner_id == owner_id)
).first()
def get_posts(self, owner_id: int, skip: int = 0, limit: int = 100) -> List[Post]:
"""Получить список постов пользователя"""
return self.db.query(Post).filter(Post.owner_id == owner_id)\
.order_by(Post.created_at.desc())\
.offset(skip)\
.limit(limit)\
.all()
def create_post(self, post_data: PostCreate, owner_id: int) -> Post:
"""Создать новый пост"""
db_post = Post(
title=post_data.title,
content=post_data.content,
scheduled_time=post_data.scheduled_time,
status=post_data.status,
owner_id=owner_id
)
self.db.add(db_post)
self.db.commit()
self.db.refresh(db_post)
return db_post
def update_post(self, post_id: int, post_data: PostUpdate, owner_id: int) -> Optional[Post]:
"""Обновить пост"""
db_post = self.get_post(post_id, owner_id)
if not db_post:
return None
# Обновляем только те поля, которые предоставлены
update_data = post_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_post, field, value)
# Обновляем время последнего изменения
db_post.updated_at = datetime.utcnow()
# Если статус меняется на published и published_at еще не установлен, устанавливаем его
if post_data.status == "published" and not db_post.published_at:
db_post.published_at = datetime.utcnow()
self.db.commit()
self.db.refresh(db_post)
return db_post
def delete_post(self, post_id: int, owner_id: int) -> bool:
"""Удалить пост"""
db_post = self.get_post(post_id, owner_id)
if not db_post:
return False
self.db.delete(db_post)
self.db.commit()
return True
def get_scheduled_posts(self) -> List[Post]:
"""Получить посты, готовые к публикации (статус scheduled и время наступило)"""
current_time = datetime.utcnow()
return self.db.query(Post).filter(
and_(
Post.status == "scheduled",
Post.scheduled_time <= current_time
)
).all()
def get_posts_by_status(self, owner_id: int, status: str) -> List[Post]:
"""Получить посты по статусу"""
return self.db.query(Post).filter(
and_(Post.owner_id == owner_id, Post.status == status)
).all()
View File
+48
View File
@@ -0,0 +1,48 @@
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from app.api.v1.routers import api_router
from app.database.database import engine, Base, get_db
from app.database.config import settings
# Create database tables
Base.metadata.create_all(bind=engine)
# Initialize FastAPI app
app = FastAPI(
title="TelePost Clone API",
description="API for TelePost Clone application",
version="0.1.0"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, replace with specific origins
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API routes
app.include_router(api_router, prefix="/api/v1", tags=["api"])
@app.get("/")
def read_root():
return {"message": "Welcome to TelePost Clone API"}
@app.get("/health")
def health_check():
return {"status": "healthy", "service": "TelePost Clone API"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True # Only for development
)
View File
+13
View File
@@ -0,0 +1,13 @@
fastapi==0.104.1
uvicorn==0.24.0
sqlalchemy==2.0.23
alembic==1.13.1
psycopg2-binary==2.9.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
requests==2.31.0
pydantic==2.5.0
pydantic-settings==2.1.0
redis==5.0.1
cachetools==5.3.2
View File