Added recruitment task

This commit is contained in:
pptx704
2025-07-03 10:01:56 +03:00
parent bcb64faf74
commit 60c5ba5e70
15 changed files with 705 additions and 75 deletions

View File

@ -2,9 +2,9 @@ from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .const import SQLALCHEMY_DATABASE_URL
from app.settings import settings
engine = create_engine(SQLALCHEMY_DATABASE_URL)
engine = create_engine(settings.SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@ -5,13 +5,87 @@ from sqlalchemy import (
Column,
DateTime,
ForeignKey,
Integer,
PrimaryKeyConstraint,
String,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from sqlalchemy.schema import Index
from sqlalchemy.sql import func
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(
UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
)
name = Column(String, nullable=False)
email = Column(String, unique=True, index=True, nullable=False)
password_hash = Column(String(60), nullable=False)
is_verified = Column(Boolean, default=False)
posts = relationship("Post", backref="user")
likes = relationship("Like", backref="user")
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}, name={self.name}, email={self.email}, is_active={self.is_verified}>"
class Post(Base):
__tablename__ = "posts"
id = Column(
UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
)
user_id = Column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
content = Column(String(512), nullable=False)
likes = relationship("Like", backref="post")
created_at = Column(
DateTime(timezone=True), server_default=func.now(), index=True
)
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@property
def time(self):
if self.updated_at:
return self.updated_at
return self.created_at
def __repr__(self):
return f"<Post(id={self.id}, content={self.content[:10]}>"
class Like(Base):
__tablename__ = "post_likes"
user_id = Column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
post_id = Column(
UUID(as_uuid=True),
ForeignKey("posts.id", ondelete="CASCADE"),
nullable=False,
)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
__table_args__ = (PrimaryKeyConstraint("user_id", "post_id"),)
def __repr__(self):
return f"<Like(user_id={self.user_id}, post_id={self.post_id}>"

View File

@ -1,6 +1,52 @@
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from .. import schemas
from ..models import User
from ..security import create_jwt_token, get_password_hash, verify_password
from app import schemas
from app.models import User
from app.security import create_jwt_token, get_password_hash, verify_password
def register(
request: schemas.RegistrationRequest, db: Session
) -> schemas.BaseResponse:
if db.query(User).filter(User.email == request.email).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
if request.password != request.confirm_password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Passwords do not match",
)
user = User(
name=request.name,
email=request.email,
password_hash=get_password_hash(request.password),
)
db.add(user)
db.commit()
return schemas.BaseResponse(message="Registration successful")
def login(request: schemas.LoginRequest, db: Session):
user = db.query(User).filter(User.email == request.email).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
if not verify_password(request.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect password",
)
token = create_jwt_token({"user_id": str(user.id)})
return schemas.LoginResponse(token=token, verified=user.is_verified)

128
app/repositories/post.py Normal file
View File

@ -0,0 +1,128 @@
import uuid
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app import schemas
from app.models import Like, Post, User
from app.security import create_jwt_token
def get_all_posts(user: schemas.User, db: Session) -> list[schemas.PostView]:
_posts = db.query(Post).order_by(Post.created_at.desc()).all()
posts = []
for post in _posts:
author = db.query(User).filter_by(id=post.user_id).first()
likes = db.query(Like).filter_by(post_id=post.id).count()
liked = (
db.query(Like).filter_by(post_id=post.id, user_id=user.id).first()
is not None
)
_post = schemas.PostView(
id=post.id,
content=post.content,
author=schemas.User.model_validate(author),
likes=likes,
liked=liked,
time=post.time,
)
posts.append(_post)
return posts
def create_post(
user: schemas.User, content: schemas.PostCreate, db: Session
) -> schemas.PostView:
new_post = Post(user_id=user.id, content=content.content)
db.add(new_post)
db.commit()
db.refresh(new_post)
post = schemas.PostView(
id=new_post.id,
content=new_post.content,
author=schemas.User.model_validate(user),
likes=0,
liked=False,
time=new_post.time,
)
return post
def edit_post(
user: schemas.User, info: schemas.PostEdit, db: Session
) -> schemas.PostView:
post = db.query(Post).filter_by(id=info.id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Post not found"
)
if post.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized"
)
post.content = info.content
db.commit()
db.refresh(post)
return schemas.PostView(
id=post.id,
content=post.content,
author=schemas.User.mode(user),
likes=db.query(Like).filter_by(post_id=post.id).count(),
liked=False,
time=post.time,
)
def delete_post(
user: schemas.User, info: schemas.PostAction, db: Session
) -> schemas.BaseResponse:
post = db.query(Post).filter_by(id=info.id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Post not found"
)
if post.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized"
)
db.delete(post)
db.commit()
return schemas.BaseResponse(message="Post deleted")
def toggle_like(
user: schemas.User, info: schemas.PostAction, db: Session
) -> schemas.BaseResponse:
post = db.query(Post).filter_by(id=info.id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Post not found"
)
if post.user_id == user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Can't like own post",
)
like = db.query(Like).filter_by(post_id=post.id, user_id=user.id).first()
if like:
db.delete(like)
db.commit()
return schemas.BaseResponse(message="Like removed")
like = Like(post_id=post.id, user_id=user.id)
db.add(like)
db.commit()
return schemas.BaseResponse(message="Liked")

View File

@ -2,10 +2,30 @@ from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app import database, schemas
from .. import schemas
from ..database import get_db
from ..repositories import auth
from ..security import get_user
from app.repositories import auth
from app.security import get_user
router = APIRouter(prefix="", tags=["auth"])
get_db = database.get_db
@router.post(
"/register",
response_model=schemas.BaseResponse,
status_code=status.HTTP_201_CREATED,
)
def register(
request: schemas.RegistrationRequest, db: Session = Depends(get_db)
):
return auth.register(request, db)
@router.post("/login", response_model=schemas.LoginResponse)
def login(request: schemas.LoginRequest, db: Session = Depends(get_db)):
return auth.login(request, db)
@router.get("/me", response_model=schemas.User)
def current_user(user: schemas.User = Depends(get_user)):
return user

61
app/routers/post.py Normal file
View File

@ -0,0 +1,61 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app import database, schemas
from app.repositories import auth, post
from app.security import get_user, get_user_strict
router = APIRouter(prefix="/post", tags=["posts"])
get_db = database.get_db
@router.get("/all", response_model=list[schemas.PostView])
def get_all_posts(
user: schemas.User = Depends(get_user), db: Session = Depends(get_db)
):
return post.get_all_posts(user, db)
@router.post(
"/create",
response_model=schemas.PostView,
status_code=status.HTTP_201_CREATED,
)
def create_post(
content: schemas.PostCreate,
user: schemas.User = Depends(get_user_strict),
db: Session = Depends(get_db),
):
return post.create_post(user, content, db)
@router.post("/edit", response_model=schemas.PostView)
def edit_post(
info: schemas.PostEdit,
user: schemas.User = Depends(get_user_strict),
db: Session = Depends(get_db),
):
return post.edit_post(user, info, db)
@router.post("/delete", response_model=schemas.BaseResponse)
def delete_post(
info: schemas.PostAction,
user: schemas.User = Depends(get_user_strict),
db: Session = Depends(get_db),
):
return post.delete_post(user, info, db)
@router.post(
"/like", response_model=schemas.BaseResponse, name="Like or unlike post"
)
def like_post(
info: schemas.PostAction,
user: schemas.User = Depends(get_user_strict),
db: Session = Depends(get_db),
):
return post.toggle_like(user, info, db)

View File

@ -1,17 +1,63 @@
import datetime
import enum
import uuid
from typing import List, Optional
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel, ConfigDict, EmailStr
class BaseResponse(BaseModel):
message: str
# Auth schemas
class RegistrationRequest(BaseModel):
name: str
email: EmailStr
password: str
confirm_password: str
class LoginRequest(BaseModel):
email: EmailStr
password: str
class LoginResponse(BaseModel):
token: str
verified: bool
class UserUnverified(BaseModel):
id: uuid.UUID
name: str
class User(BaseModel):
id: uuid.UUID
name: str
email: EmailStr
is_verified: bool
class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
# Post schemas
class PostView(BaseModel):
id: uuid.UUID
content: str
author: User
likes: int
liked: bool
time: datetime.datetime
class PostCreate(BaseModel):
content: str
class PostEdit(BaseModel):
id: uuid.UUID
content: str
class PostAction(BaseModel):
id: uuid.UUID

View File

@ -1,9 +1,8 @@
import os
from datetime import UTC, datetime, timedelta
import jwt
from bcrypt import checkpw, gensalt, hashpw
from fastapi import HTTPException, Security
from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app import schemas
@ -25,7 +24,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
def create_jwt_token(data: dict) -> str:
_ed = timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
iat = datetime.now(UTC)
exp = datetime.now(UTC) + _ed
exp = iat + _ed
token_payload = data
token_payload.update({"iat": iat, "exp": exp})
@ -50,8 +49,18 @@ def get_user_from_token(token: str) -> schemas.User:
user_id = payload.get("user_id")
# Return user from database
...
if user_id is None:
raise HTTPException(
status_code=401, detail="Invalid authentication credentials"
)
with SessionLocal() as db:
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return schemas.User.model_validate(user)
def get_user(
@ -64,3 +73,10 @@ def get_user(
token = authorization.credentials
return get_user_from_token(token)
def get_user_strict(user: schemas.User = Depends(get_user)) -> schemas.User:
if not user.is_verified:
raise HTTPException(status_code=401, detail="User not verified")
return user