abdullah090809 commited on
Commit
55fd541
·
1 Parent(s): 752a4b2

initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ venv/
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ .env.*
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
alembic.ini ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts.
5
+ # this is typically a path given in POSIX (e.g. forward slashes)
6
+ # format, relative to the token %(here)s which refers to the location of this
7
+ # ini file
8
+ script_location = %(here)s/alembic
9
+
10
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
11
+ # Uncomment the line below if you want the files to be prepended with date and time
12
+ # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
13
+ # for all available tokens
14
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
15
+ # Or organize into date-based subdirectories (requires recursive_version_locations = true)
16
+ # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
17
+
18
+ # sys.path path, will be prepended to sys.path if present.
19
+ # defaults to the current working directory. for multiple paths, the path separator
20
+ # is defined by "path_separator" below.
21
+ prepend_sys_path = .
22
+
23
+
24
+ # timezone to use when rendering the date within the migration file
25
+ # as well as the filename.
26
+ # If specified, requires the tzdata library which can be installed by adding
27
+ # `alembic[tz]` to the pip requirements.
28
+ # string value is passed to ZoneInfo()
29
+ # leave blank for localtime
30
+ # timezone =
31
+
32
+ # max length of characters to apply to the "slug" field
33
+ # truncate_slug_length = 40
34
+
35
+ # set to 'true' to run the environment during
36
+ # the 'revision' command, regardless of autogenerate
37
+ # revision_environment = false
38
+
39
+ # set to 'true' to allow .pyc and .pyo files without
40
+ # a source .py file to be detected as revisions in the
41
+ # versions/ directory
42
+ # sourceless = false
43
+
44
+ # version location specification; This defaults
45
+ # to <script_location>/versions. When using multiple version
46
+ # directories, initial revisions must be specified with --version-path.
47
+ # The path separator used here should be the separator specified by "path_separator"
48
+ # below.
49
+ # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
50
+
51
+ # path_separator; This indicates what character is used to split lists of file
52
+ # paths, including version_locations and prepend_sys_path within configparser
53
+ # files such as alembic.ini.
54
+ # The default rendered in new alembic.ini files is "os", which uses os.pathsep
55
+ # to provide os-dependent path splitting.
56
+ #
57
+ # Note that in order to support legacy alembic.ini files, this default does NOT
58
+ # take place if path_separator is not present in alembic.ini. If this
59
+ # option is omitted entirely, fallback logic is as follows:
60
+ #
61
+ # 1. Parsing of the version_locations option falls back to using the legacy
62
+ # "version_path_separator" key, which if absent then falls back to the legacy
63
+ # behavior of splitting on spaces and/or commas.
64
+ # 2. Parsing of the prepend_sys_path option falls back to the legacy
65
+ # behavior of splitting on spaces, commas, or colons.
66
+ #
67
+ # Valid values for path_separator are:
68
+ #
69
+ # path_separator = :
70
+ # path_separator = ;
71
+ # path_separator = space
72
+ # path_separator = newline
73
+ #
74
+ # Use os.pathsep. Default configuration used for new projects.
75
+ path_separator = os
76
+
77
+ # set to 'true' to search source files recursively
78
+ # in each "version_locations" directory
79
+ # new in Alembic version 1.10
80
+ # recursive_version_locations = false
81
+
82
+ # the output encoding used when revision files
83
+ # are written from script.py.mako
84
+ # output_encoding = utf-8
85
+
86
+ # database URL. This is consumed by the user-maintained env.py script only.
87
+ # other means of configuring database URLs may be customized within the env.py
88
+ # file.
89
+ sqlalchemy.url = driver://user:pass@localhost/dbname
90
+
91
+
92
+ [post_write_hooks]
93
+ # post_write_hooks defines scripts or Python functions that are run
94
+ # on newly generated revision scripts. See the documentation for further
95
+ # detail and examples
96
+
97
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
98
+ # hooks = black
99
+ # black.type = console_scripts
100
+ # black.entrypoint = black
101
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
102
+
103
+ # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
104
+ # hooks = ruff
105
+ # ruff.type = module
106
+ # ruff.module = ruff
107
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
108
+
109
+ # Alternatively, use the exec runner to execute a binary found on your PATH
110
+ # hooks = ruff
111
+ # ruff.type = exec
112
+ # ruff.executable = ruff
113
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
114
+
115
+ # Logging configuration. This is also consumed by the user-maintained
116
+ # env.py script only.
117
+ [loggers]
118
+ keys = root,sqlalchemy,alembic
119
+
120
+ [handlers]
121
+ keys = console
122
+
123
+ [formatters]
124
+ keys = generic
125
+
126
+ [logger_root]
127
+ level = WARNING
128
+ handlers = console
129
+ qualname =
130
+
131
+ [logger_sqlalchemy]
132
+ level = WARNING
133
+ handlers =
134
+ qualname = sqlalchemy.engine
135
+
136
+ [logger_alembic]
137
+ level = INFO
138
+ handlers =
139
+ qualname = alembic
140
+
141
+ [handler_console]
142
+ class = StreamHandler
143
+ args = (sys.stderr,)
144
+ level = NOTSET
145
+ formatter = generic
146
+
147
+ [formatter_generic]
148
+ format = %(levelname)-5.5s [%(name)s] %(message)s
149
+ datefmt = %H:%M:%S
alembic/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
alembic/env.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+
3
+ from sqlalchemy import engine_from_config
4
+ from sqlalchemy import pool
5
+
6
+ from alembic import context
7
+ from app.models import Base
8
+ from app.config import setting
9
+
10
+ # this is the Alembic Config object, which provides
11
+ # access to the values within the .ini file in use.
12
+ config = context.config
13
+ config.set_main_option("sqlalchemy.url",f"postgresql://{setting.database_username}:{setting.database_password}@{setting.database_hostname}:{setting.database_port}/{setting.database_name}")
14
+
15
+ # Interpret the config file for Python logging.
16
+ # This line sets up loggers basically.
17
+ if config.config_file_name is not None:
18
+ fileConfig(config.config_file_name)
19
+
20
+ # add your model's MetaData object here
21
+ # for 'autogenerate' support
22
+ # from myapp import mymodel
23
+ # target_metadata = mymodel.Base.metadata
24
+ target_metadata = Base.metadata
25
+
26
+ # other values from the config, defined by the needs of env.py,
27
+ # can be acquired:
28
+ # my_important_option = config.get_main_option("my_important_option")
29
+ # ... etc.
30
+
31
+
32
+ def run_migrations_offline() -> None:
33
+ """Run migrations in 'offline' mode.
34
+
35
+ This configures the context with just a URL
36
+ and not an Engine, though an Engine is acceptable
37
+ here as well. By skipping the Engine creation
38
+ we don't even need a DBAPI to be available.
39
+
40
+ Calls to context.execute() here emit the given string to the
41
+ script output.
42
+
43
+ """
44
+ url = config.get_main_option("sqlalchemy.url")
45
+ context.configure(
46
+ url=url,
47
+ target_metadata=target_metadata,
48
+ literal_binds=True,
49
+ dialect_opts={"paramstyle": "named"},
50
+ )
51
+
52
+ with context.begin_transaction():
53
+ context.run_migrations()
54
+
55
+
56
+ def run_migrations_online() -> None:
57
+ """Run migrations in 'online' mode.
58
+
59
+ In this scenario we need to create an Engine
60
+ and associate a connection with the context.
61
+
62
+ """
63
+ connectable = engine_from_config(
64
+ config.get_section(config.config_ini_section, {}),
65
+ prefix="sqlalchemy.",
66
+ poolclass=pool.NullPool,
67
+ )
68
+
69
+ with connectable.connect() as connection:
70
+ context.configure(
71
+ connection=connection, target_metadata=target_metadata
72
+ )
73
+
74
+ with context.begin_transaction():
75
+ context.run_migrations()
76
+
77
+
78
+ if context.is_offline_mode():
79
+ run_migrations_offline()
80
+ else:
81
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}
alembic/versions/adcdae776345_initial.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """initial
2
+
3
+ Revision ID: adcdae776345
4
+ Revises:
5
+ Create Date: 2026-06-05 11:40:46.228344
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = 'adcdae776345'
16
+ down_revision: Union[str, Sequence[str], None] = None
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table('users',
25
+ sa.Column('id', sa.Integer(), nullable=False),
26
+ sa.Column('email', sa.String(), nullable=False),
27
+ sa.Column('password', sa.String(), nullable=False),
28
+ sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
29
+ sa.PrimaryKeyConstraint('id'),
30
+ sa.UniqueConstraint('email')
31
+ )
32
+ op.create_table('posts',
33
+ sa.Column('id', sa.Integer(), nullable=False),
34
+ sa.Column('title', sa.String(), nullable=False),
35
+ sa.Column('content', sa.String(), nullable=False),
36
+ sa.Column('published', sa.Boolean(), server_default='TRUE', nullable=False),
37
+ sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
38
+ sa.Column('owner_id', sa.Integer(), nullable=False),
39
+ sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ondelete='CASCADE'),
40
+ sa.PrimaryKeyConstraint('id')
41
+ )
42
+ op.create_table('votes',
43
+ sa.Column('user_id', sa.Integer(), nullable=False),
44
+ sa.Column('post_id', sa.Integer(), nullable=False),
45
+ sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'),
46
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
47
+ sa.PrimaryKeyConstraint('user_id', 'post_id')
48
+ )
49
+ # ### end Alembic commands ###
50
+
51
+
52
+ def downgrade() -> None:
53
+ """Downgrade schema."""
54
+ # ### commands auto generated by Alembic - please adjust! ###
55
+ op.drop_table('votes')
56
+ op.drop_table('posts')
57
+ op.drop_table('users')
58
+ # ### end Alembic commands ###
app/__init__.py ADDED
File without changes
app/config.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+
3
+ class Settings(BaseSettings):
4
+ database_hostname: str
5
+ database_port: str
6
+ database_password: str
7
+ database_name: str
8
+ database_username: str
9
+ secret_key: str
10
+ algorithm: str
11
+ access_token_expire_minutes: int
12
+
13
+ class Config:
14
+ env_file = ".env"
15
+
16
+ setting = Settings()
app/database.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+ from app.config import setting
5
+
6
+ SQLALCHEMY_DATABASE_URL=f"postgresql://{setting.database_username}:{setting.database_password}@{setting.database_hostname}:{setting.database_port}/{setting.database_name}"
7
+ engine = create_engine(SQLALCHEMY_DATABASE_URL)
8
+ SessionMaker = sessionmaker(autoflush=False,autocommit=False,bind=engine)
9
+
10
+ Base = declarative_base()
11
+
12
+ def get_db():
13
+ db = SessionMaker()
14
+ try:
15
+ yield db
16
+ finally:
17
+ db.close()
app/main.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from app.database import Base, engine
3
+ from app.routers import post, user, auth, vote
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+
6
+ app = FastAPI()
7
+
8
+ origins=["*"]
9
+
10
+ app.add_middleware(
11
+ CORSMiddleware,
12
+ allow_origins=origins,
13
+ allow_credentials=True,
14
+ allow_methods=["*"],
15
+ allow_headers=["*"],
16
+ )
17
+
18
+
19
+ app.include_router(post.router)
20
+ app.include_router(user.router)
21
+ app.include_router(auth.router)
22
+ app.include_router(vote.router)
23
+
24
+ @app.get("/")
25
+ def home():
26
+ return "FastAPI Project"
app/models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from app.models.posts import Post
2
+ from app.models.users import User
3
+ from app.models.votes import Votes
app/models/posts.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, TIMESTAMP, text
2
+ from sqlalchemy.orm import relationship
3
+ from app.database import Base
4
+
5
+ class Post(Base):
6
+ __tablename__ = "posts"
7
+ id = Column(Integer, primary_key=True, nullable=False)
8
+ title = Column(String, nullable=False)
9
+ content = Column(String, nullable=False)
10
+ published = Column(Boolean, server_default="TRUE", nullable=False)
11
+ created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text("now()"))
12
+ owner_id = Column(Integer, ForeignKey("users.id",ondelete="CASCADE"),nullable=False)
13
+ owner = relationship("User")
14
+
app/models/users.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, TIMESTAMP, text
2
+ from app.database import Base
3
+
4
+ class User(Base):
5
+ __tablename__ = "users"
6
+ id = Column(Integer, primary_key=True, nullable=False)
7
+ email = Column(String, nullable=False, unique=True)
8
+ password= Column(String, nullable=False)
9
+ created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text("now()"))
app/models/votes.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, ForeignKey, Integer
2
+ from app.database import Base
3
+
4
+ class Votes(Base):
5
+ __tablename__= "votes"
6
+ user_id= Column(Integer,ForeignKey("users.id",ondelete="CASCADE"),primary_key=True)
7
+ post_id= Column(Integer,ForeignKey("posts.id",ondelete="CASCADE"),primary_key=True)
app/oauth2.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from fastapi import Depends, HTTPException, status
3
+ from fastapi.security import OAuth2PasswordBearer
4
+ from jose import JWTError, jwt
5
+ from app.database import get_db
6
+ from app.models.users import User
7
+ from app.schemas.token import TokenData
8
+ from sqlalchemy.orm import Session
9
+ from app.config import setting
10
+
11
+ oauth2_scheme=OAuth2PasswordBearer(tokenUrl="login")
12
+ SECRET_KEY=setting.secret_key
13
+ ALGORITHM=setting.algorithm
14
+ ACCESS_TOKEN_EXPIRE_MINUTES=setting.access_token_expire_minutes
15
+
16
+ def Create_Access_Token(data: dict):
17
+ payload=data.copy()
18
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
19
+ payload.update({"exp": expire})
20
+
21
+ encoded_jwt=jwt.encode(payload,SECRET_KEY,algorithm=ALGORITHM)
22
+ return encoded_jwt
23
+
24
+ def Verify_Access_Token(token: str, credentials_exception):
25
+ try:
26
+ payload=jwt.decode(token,SECRET_KEY,algorithms=[ALGORITHM])
27
+ id=payload.get("id")
28
+ if id is None:
29
+ raise credentials_exception
30
+ token_data=TokenData(**payload)
31
+ return token_data
32
+ except JWTError:
33
+ raise credentials_exception
34
+
35
+ def Get_Current_User(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
36
+ credentials_exception=HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
37
+ detail="Could not validate Credentials",headers={"WWW-Authenticate": "Bearer"})
38
+
39
+ token = Verify_Access_Token(token,credentials_exception)
40
+ user=db.query(User).filter(User.id==token.id).first()
41
+ return user
42
+
app/routers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from . import post, user, auth
app/routers/auth.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.security.oauth2 import OAuth2PasswordRequestForm
2
+ from fastapi import APIRouter, Depends, HTTPException, status
3
+ from sqlalchemy.orm import Session
4
+ from app.schemas import Token
5
+ from app import utils
6
+ from app.database import get_db
7
+ from app.models import User
8
+ from app.oauth2 import Create_Access_Token
9
+
10
+
11
+ router = APIRouter(tags=["Authentication"])
12
+
13
+ @router.post('/login',response_model=Token)
14
+ def login(user_credentials: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
15
+ user=db.query(User).filter(User.email == user_credentials.username).first()
16
+
17
+ if not user:
18
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail="Invalid Credentials")
19
+
20
+ if not utils.verify(user_credentials.password,user.password):
21
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail="Invalid Credentials")
22
+
23
+ access_token=Create_Access_Token(data={"id": user.id})
24
+ return {
25
+ "access_token": access_token,
26
+ "token_type": "bearer"
27
+ }
app/routers/post.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from fastapi import APIRouter, HTTPException, Depends, status, Response
3
+ from sqlalchemy import Result, func
4
+ from sqlalchemy.orm import Session
5
+ from app.database import get_db
6
+ from app.models import Post, User, Votes
7
+ from app.oauth2 import Get_Current_User
8
+ from app.schemas import PostCreate, PostUpdate, PostResponse, PostVote
9
+
10
+ router = APIRouter(prefix="/posts", tags=["Posts"])
11
+
12
+ @router.get("/", response_model=list[PostVote])
13
+ def get_posts(db: Session = Depends(get_db),current_user: User= Depends(Get_Current_User),limit: int = 10, skip: int = 0, search: Optional[str] = ""):
14
+ results = (
15
+ db.query(Post, func.count(Votes.post_id).label("votes"))
16
+ .join(Votes, Votes.post_id == Post.id, isouter=True)
17
+ .group_by(Post.id)
18
+ .filter(Post.title.contains(search))
19
+ .limit(limit)
20
+ .offset(skip)
21
+ .all()
22
+ )
23
+ posts = []
24
+ for post, vote_count in results:
25
+ post.votes = vote_count
26
+ posts.append(post)
27
+ return posts
28
+ @router.post("/", response_model=PostResponse)
29
+ def create_post(post: PostCreate, db: Session = Depends(get_db), current_user: User= Depends(Get_Current_User)):
30
+ new_post = Post(owner_id=current_user.id,**post.model_dump())
31
+ db.add(new_post)
32
+ db.commit()
33
+ db.refresh(new_post)
34
+ return new_post
35
+
36
+ @router.get("/{id}", response_model=PostVote)
37
+ def get_post(id: int, db: Session = Depends(get_db),current_user: User= Depends(Get_Current_User)):
38
+ result = (
39
+ db.query(Post, func.count(Votes.post_id).label("votes"))
40
+ .join(Votes, Votes.post_id == Post.id, isouter=True)
41
+ .group_by(Post.id)
42
+ .filter(Post.id == id)
43
+ .first())
44
+ if not result:
45
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Post with id {id} does not exist")
46
+ post,vote_count=result
47
+ post.votes=vote_count
48
+ return post
49
+
50
+ @router.delete("/{id}")
51
+ def delete_post(id: int, db: Session = Depends(get_db), current_user: User = Depends(Get_Current_User)):
52
+ post_query = db.query(Post).filter(Post.id == id)
53
+ post = post_query.first()
54
+ if not post:
55
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Post with id {id} does not exist")
56
+ if post.owner_id != current_user.id:
57
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not Authorized to Perform this Action")
58
+ post_query.delete(synchronize_session=False)
59
+ db.commit()
60
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
61
+
62
+ @router.put("/{id}", response_model=PostResponse)
63
+ def update_post(id: int, updated_post: PostCreate, db: Session = Depends(get_db), current_user: User = Depends(Get_Current_User)):
64
+ post_query = db.query(Post).filter(Post.id == id)
65
+ post = post_query.first()
66
+ if not post:
67
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Post with id {id} does not exist")
68
+ if post.owner_id != current_user.id:
69
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not Authorized to Perform this Action")
70
+ post_query.update(updated_post.model_dump(), synchronize_session=False)
71
+ db.commit()
72
+ return post_query.first()
73
+
74
+ @router.patch("/{id}", response_model=PostResponse)
75
+ def update_post_patch(id: int, updated_post: PostUpdate, db: Session = Depends(get_db), current_user: User = Depends(Get_Current_User)):
76
+ post_query = db.query(Post).filter(Post.id == id)
77
+ post = post_query.first()
78
+ if not post:
79
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Post with id {id} does not exist")
80
+ if post.owner_id != current_user.id:
81
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not Authorized to Perform this Action")
82
+ post_query.update(updated_post.model_dump(exclude_unset=True), synchronize_session=False)
83
+ db.commit()
84
+ return post_query.first()
app/routers/user.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends, status, Response
2
+ from sqlalchemy.orm import Session
3
+ from websockets import route
4
+ from app.database import get_db
5
+ from app.schemas import UserCreate, UserResponse
6
+ from app.models import User
7
+ from app.utils import Hash
8
+
9
+ router = APIRouter(prefix="/users", tags=["Users"])
10
+
11
+ @router.post("/",status_code=status.HTTP_201_CREATED,response_model=UserResponse)
12
+ def create_user(user : UserCreate, db: Session = Depends(get_db)):
13
+ user.password=Hash(user.password)
14
+ new_user=User(**user.model_dump())
15
+ db.add(new_user)
16
+ db.commit()
17
+ db.refresh(new_user)
18
+ return new_user
19
+
20
+ @router.get("/",response_model=list[UserResponse])
21
+ def get_all_user(db: Session= Depends(get_db)):
22
+ return db.query(User).all()
23
+
24
+ @router.get("/{id}",response_model=UserResponse)
25
+ def get_user(id: int,db: Session = Depends(get_db)):
26
+ user=db.query(User).filter(User.id == id).first()
27
+ if not user:
28
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User with id {id} does not Exit")
29
+ return user
app/routers/vote.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends, status, Response
2
+ from sqlalchemy.orm import Session
3
+ from app.database import get_db
4
+ from app.models import User, Votes, Post
5
+ from app.oauth2 import Get_Current_User
6
+ from app.schemas import VoteBase
7
+
8
+ router = APIRouter(
9
+ prefix="/vote",
10
+ tags=["Votes"]
11
+ )
12
+
13
+ @router.post("/",status_code=status.HTTP_201_CREATED)
14
+ def vote(vote: VoteBase, db: Session = Depends(get_db), current_user: User = Depends(Get_Current_User)):
15
+ post = db.query(Post).filter(Post.id==vote.post_id).first()
16
+ if not post:
17
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail=f"Post with {vote.post_id} does not Exit")
18
+ else:
19
+ vote_query=db.query(Votes).filter(Votes.post_id==vote.post_id,Votes.user_id==current_user.id)
20
+ found_vote=vote_query.first()
21
+ if vote.dir==1:
22
+ if found_vote:
23
+ raise HTTPException(status_code=status.HTTP_409_CONFLICT,detail=f"User with id {current_user.id} has already voted on this Post")
24
+ new_vote=Votes(post_id=vote.post_id,user_id=current_user.id)
25
+ db.add(new_vote)
26
+ db.commit()
27
+ return{
28
+ "message": "Successfully Voted"
29
+ }
30
+ else:
31
+ if not found_vote:
32
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="Vote does not Exit")
33
+ vote_query.delete(synchronize_session=False)
34
+ db.commit()
35
+ return{
36
+ "message": "Vote Successfully Deleted"
37
+ }
38
+
app/schemas/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .post import *
2
+ from .user import *
3
+ from .token import *
4
+ from .vote import *
app/schemas/post.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from pydantic import BaseModel
4
+ from app.schemas.user import UserResponse
5
+
6
+ class PostBase(BaseModel):
7
+ title: str
8
+ content: str
9
+ published: bool = True
10
+
11
+ class PostCreate(PostBase):
12
+ pass
13
+
14
+ class PostUpdate(BaseModel):
15
+ title: Optional[str] = None
16
+ content: Optional[str] = None
17
+ published: Optional[bool] = None
18
+
19
+ class PostResponse(PostBase):
20
+ id: int
21
+ created_at: datetime
22
+ owner_id: int
23
+ owner: UserResponse
24
+ class Config:
25
+ from_attributes = True
26
+
27
+ class PostVote(PostResponse):
28
+ votes: int
app/schemas/token.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class Token(BaseModel):
6
+ access_token: str
7
+ token_type: str
8
+
9
+ class TokenData(BaseModel):
10
+ id: Optional[int] = None
app/schemas/user.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from pydantic import BaseModel, EmailStr
3
+
4
+ class UserBase(BaseModel):
5
+ email: EmailStr
6
+ password: str
7
+
8
+ class UserCreate(UserBase):
9
+ pass
10
+
11
+ class UserResponse(BaseModel):
12
+ id: int
13
+ email: EmailStr
14
+ created_at: datetime
15
+ class Config:
16
+ from_attributes = True
17
+
app/schemas/vote.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from typing import Literal
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class VoteBase(BaseModel):
6
+ post_id: int
7
+ dir: Literal[0, 1]
app/utils.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from passlib.context import CryptContext # type: ignore
2
+
3
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
4
+
5
+ def Hash(password: str):
6
+ return pwd_context.hash(password)
7
+
8
+ def verify(plain_password,hashed_password):
9
+ return pwd_context.verify(plain_password,hashed_password)
requirements.txt ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ alembic==1.18.4
2
+ annotated-doc==0.0.4
3
+ annotated-types==0.7.0
4
+ anyio==4.13.0
5
+ certifi==2026.2.25
6
+ cffi==2.0.0
7
+ click==8.3.2
8
+ colorama==0.4.6
9
+ cryptography==47.0.0
10
+ dnspython==2.8.0
11
+ ecdsa==0.19.2
12
+ email-validator==2.3.0
13
+ fastapi==0.136.0
14
+ fastapi-cli==0.0.24
15
+ fastapi-cloud-cli==0.17.0
16
+ fastar==0.11.0
17
+ greenlet==3.4.0
18
+ h11==0.16.0
19
+ httpcore==1.0.9
20
+ httptools==0.7.1
21
+ httpx==0.28.1
22
+ idna==3.11
23
+ itsdangerous==2.2.0
24
+ Jinja2==3.1.6
25
+ Mako==1.3.12
26
+ markdown-it-py==4.0.0
27
+ MarkupSafe==3.0.3
28
+ mdurl==0.1.2
29
+ psycopg2==2.9.11
30
+ pyasn1==0.6.3
31
+ pycparser==3.0
32
+ pydantic==2.13.2
33
+ pydantic-extra-types==2.11.1
34
+ pydantic-settings==2.13.1
35
+ pydantic_core==2.46.2
36
+ Pygments==2.20.0
37
+ python-dotenv==1.2.2
38
+ python-jose==3.5.0
39
+ python-multipart==0.0.26
40
+ PyYAML==6.0.3
41
+ rich==15.0.0
42
+ rich-toolkit==0.19.7
43
+ rignore==0.7.6
44
+ rsa==4.9.1
45
+ sentry-sdk==2.58.0
46
+ shellingham==1.5.4
47
+ six==1.17.0
48
+ SQLAlchemy==2.0.49
49
+ starlette==1.0.0
50
+ typer==0.24.1
51
+ typing-inspection==0.4.2
52
+ typing_extensions==4.15.0
53
+ urllib3==2.6.3
54
+ uvicorn==0.44.0
55
+ watchfiles==1.1.1
56
+ websockets==16.0
57
+ passlib==1.7.4
58
+ bcrypt==4.0.1