generated from pptx704/fastapi-template
Added recruitment task
This commit is contained in:
@ -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)
|
||||
|
||||
|
||||
@ -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}>"
|
||||
|
||||
@ -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
128
app/repositories/post.py
Normal 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")
|
||||
@ -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
61
app/routers/post.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user