diff --git a/.env b/.env index a98864b..ba5e6ad 100644 --- a/.env +++ b/.env @@ -15,8 +15,8 @@ VERIFICATION_SECRET=my-email-verification-secret EMAIL_HOST=smtp.mailtrap.io EMAIL_PORT=587 -EMAIL_USERNAME=4aeca0c9318dd2 -EMAIL_PASSWORD=a987a0e0eac00d +EMAIL_USERNAME=90cf952fb44469 +EMAIL_PASSWORD=0524531956c552 EMAIL_FROM=admin@admin.com JWT_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT2dJQkFBSkJBSSs3QnZUS0FWdHVQYzEzbEFkVk94TlVmcWxzMm1SVmlQWlJyVFpjd3l4RVhVRGpNaFZuCi9KVHRsd3h2a281T0pBQ1k3dVE0T09wODdiM3NOU3ZNd2xNQ0F3RUFBUUpBYm5LaENOQ0dOSFZGaHJPQ0RCU0IKdmZ2ckRWUzVpZXAwd2h2SGlBUEdjeWV6bjd0U2RweUZ0NEU0QTNXT3VQOXhqenNjTFZyb1pzRmVMUWlqT1JhUwp3UUloQU84MWl2b21iVGhjRkltTFZPbU16Vk52TGxWTW02WE5iS3B4bGh4TlpUTmhBaUVBbWRISlpGM3haWFE0Cm15QnNCeEhLQ3JqOTF6bVFxU0E4bHUvT1ZNTDNSak1DSVFEbDJxOUdtN0lMbS85b0EyaCtXdnZabGxZUlJPR3oKT21lV2lEclR5MUxaUVFJZ2ZGYUlaUWxMU0tkWjJvdXF4MHdwOWVEejBEWklLVzVWaSt6czdMZHRDdUVDSUVGYwo3d21VZ3pPblpzbnU1clBsTDJjZldLTGhFbWwrUVFzOCtkMFBGdXlnCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t diff --git a/alembic/versions/15770e820938_created_users_table.py b/alembic/versions/15770e820938_created_users_table.py deleted file mode 100644 index 45ccf97..0000000 --- a/alembic/versions/15770e820938_created_users_table.py +++ /dev/null @@ -1,40 +0,0 @@ -"""created_users_table - -Revision ID: 15770e820938 -Revises: -Create Date: 2022-07-06 15:11:26.439123 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = '15770e820938' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('users', - sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('photo', sa.String(), nullable=True), - sa.Column('verified', sa.Boolean(), server_default='False', nullable=False), - sa.Column('role', sa.String(), server_default='user', nullable=False), - sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('users') - # ### end Alembic commands ### diff --git a/alembic/versions/1c7984990e1d_created_posts_table.py b/alembic/versions/1c7984990e1d_created_posts_table.py deleted file mode 100644 index 76882a0..0000000 --- a/alembic/versions/1c7984990e1d_created_posts_table.py +++ /dev/null @@ -1,39 +0,0 @@ -"""created_posts_table - -Revision ID: 1c7984990e1d -Revises: 15770e820938 -Create Date: 2022-07-06 23:15:42.761079 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = '1c7984990e1d' -down_revision = '15770e820938' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('posts', - sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('user_id', postgresql.UUID(), nullable=False), - sa.Column('title', sa.String(), nullable=False), - sa.Column('content', sa.String(), nullable=False), - sa.Column('category', sa.String(), nullable=False), - sa.Column('image', sa.String(), nullable=False), - sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('posts') - # ### end Alembic commands ### diff --git a/alembic/versions/39256113e8e5_added_verification_code.py b/alembic/versions/39256113e8e5_added_verification_code.py deleted file mode 100644 index 9eca0eb..0000000 --- a/alembic/versions/39256113e8e5_added_verification_code.py +++ /dev/null @@ -1,43 +0,0 @@ -"""added verification code - -Revision ID: 39256113e8e5 -Revises: 1c7984990e1d -Create Date: 2022-07-14 08:03:57.507140 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = '39256113e8e5' -down_revision = '1c7984990e1d' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('posts') - op.add_column('users', sa.Column('verification_code', sa.String(), nullable=True)) - op.create_unique_constraint(None, 'users', ['verification_code']) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'users', type_='unique') - op.drop_column('users', 'verification_code') - op.create_table('posts', - sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False), - sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=False), - sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('content', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('category', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('image', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='posts_user_id_fkey', ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name='posts_pkey') - ) - # ### end Alembic commands ### diff --git a/alembic/versions/4917da928a79_added_post_table.py b/alembic/versions/61055af0da89_added_crud_entities.py similarity index 53% rename from alembic/versions/4917da928a79_added_post_table.py rename to alembic/versions/61055af0da89_added_crud_entities.py index c32206b..18069d5 100644 --- a/alembic/versions/4917da928a79_added_post_table.py +++ b/alembic/versions/61055af0da89_added_crud_entities.py @@ -1,8 +1,8 @@ -"""added post table +"""added-crud-entities -Revision ID: 4917da928a79 -Revises: 39256113e8e5 -Create Date: 2022-07-14 09:05:17.444518 +Revision ID: 61055af0da89 +Revises: +Create Date: 2022-08-26 20:52:17.830383 """ from alembic import op @@ -10,14 +10,29 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = '4917da928a79' -down_revision = '39256113e8e5' +revision = '61055af0da89' +down_revision = None branch_labels = None depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('password', sa.String(), nullable=False), + sa.Column('photo', sa.String(), nullable=True), + sa.Column('verified', sa.Boolean(), server_default='False', nullable=False), + sa.Column('verification_code', sa.String(), nullable=True), + sa.Column('role', sa.String(), server_default='user', nullable=False), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('verification_code') + ) op.create_table('posts', sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), @@ -36,4 +51,5 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_table('posts') + op.drop_table('users') # ### end Alembic commands ### diff --git a/app/email.py b/app/email.py index adbeb6c..0bdcf49 100644 --- a/app/email.py +++ b/app/email.py @@ -32,8 +32,8 @@ async def sendMail(self, subject, template): MAIL_FROM=settings.EMAIL_FROM, MAIL_PORT=settings.EMAIL_PORT, MAIL_SERVER=settings.EMAIL_HOST, - MAIL_TLS=True, - MAIL_SSL=False, + MAIL_STARTTLS=False, + MAIL_SSL_TLS=False, USE_CREDENTIALS=True, VALIDATE_CERTS=True ) diff --git a/app/routers/auth.py b/app/routers/auth.py index cf0e725..5d3612a 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -150,6 +150,9 @@ def verify_me(token: str, db: Session = Depends(get_db)): db.commit() user = user_query.first() if not user: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Invalid code or user doesn't exist") + if user.verified: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail='Email can only be verified once') user_query.update( diff --git a/app/routers/post.py b/app/routers/post.py index afe0951..1552ef5 100644 --- a/app/routers/post.py +++ b/app/routers/post.py @@ -38,7 +38,8 @@ def update_post(id: str, post: schemas.UpdatePostSchema, db: Session = Depends(g if updated_post.user_id != uuid.UUID(user_id): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='You are not allowed to perform this action') - post_query.update(post.dict(), synchronize_session=False) + post.user_id = user_id + post_query.update(post.dict(exclude_unset=True), synchronize_session=False) db.commit() return updated_post @@ -60,7 +61,7 @@ def delete_post(id: str, db: Session = Depends(get_db), user_id: str = Depends(r raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'No post with this id: {id} found') - if post.owner_id != user_id: + if str(post.user_id) != user_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='You are not allowed to perform this action') post_query.delete(synchronize_session=False) diff --git a/app/schemas.py b/app/schemas.py index b587b4b..8cf4ccd 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -58,11 +58,13 @@ class PostResponse(PostBaseSchema): class UpdatePostSchema(BaseModel): - title: str | None = None - content: str | None = None - category: str | None = None - image: str | None = None + title: str + content: str + category: str + image: str user_id: uuid.UUID | None = None + created_at: datetime | None = None + updated_at: datetime | None = None class Config: orm_mode = True diff --git a/readMe.md b/readMe.md index 44a5eca..a7a6c4f 100644 --- a/readMe.md +++ b/readMe.md @@ -1,9 +1,38 @@ -# RESTful API with Python, FastAPI, Pydantic, SQLAlchemy and Docker +# CRUD RESTful API Server with Python, SQLAlchemy, FastAPI, and PostgreSQL -### 1. RESTful API with Python & FastAPI: Access and Refresh Tokens +This article will teach you how to create a CRUD RESTful API with Python, FastAPI, SQLAlchemy ORM, Pydantic, Alembic, PostgreSQL, and Docker-compose to perform the basic Create/Read/Update/Delete operations against a database. -[RESTful API with Python & FastAPI: Access and Refresh Tokens](https://codevoweb.com/restful-api-with-python-fastapi-access-and-refresh-tokens) +![CRUD RESTful API Server with Python, SQLAlchemy, FastAPI, and PostgreSQL](https://codevoweb.com/wp-content/uploads/2022/07/CRUD-RESTful-API-Server-with-Python-FastAPI-and-PostgreSQL.webp) -### 2. RESTful API with Python & FastAPI: Send HTML Emails +## Topics Covered -[RESTful API with Python & FastAPI: Send HTML Emails](https://codevoweb.com/restful-api-with-python-fastapi-send-html-emails) +- Python, FastAPI, PostgreSQL, SQLAlchemy CRUD API Overview +- Setting up FastAPI and PostgreSQL + - Building the FastAPI Server + - Starting the FastAPI Server +- Setting up Environment Variables in FastAPI +- Connecting to the PostgreSQL Server +- Installing the UUID OSSP PostgreSQL Plugin +- How to Create Database Models with SQLAlchemy +- Creating Validation Schemas with Pydantic +- Creating the FastAPI Route Handlers + - Fetch All Posts Handler + - Create New Post Handler + - Update Post Handler + - Get a Single Post Handler + - Remove Post Handler +- Add the Routes to the FastAPI Middleware Stack + +Read the entire article here: [https://codevoweb.com/crud-restful-api-server-with-python-fastapi-and-postgresql](https://codevoweb.com/crud-restful-api-server-with-python-fastapi-and-postgresql) + +### 1. RESTful API with Python,SQLAlchemy, & FastAPI: Access and Refresh Tokens + +[RESTful API with Python, SQLAlchemy, & FastAPI: Access and Refresh Tokens](https://codevoweb.com/restful-api-with-python-fastapi-access-and-refresh-tokens) + +### 2. RESTful API with Python, SQLAlchemy, & FastAPI: Send HTML Emails + +[RESTful API with Python, SQLAlchemy & FastAPI: Send HTML Emails](https://codevoweb.com/restful-api-with-python-fastapi-send-html-emails) + +### 3. CRUD RESTful API Server with Python, FastAPI, SQLAlchemy, and PostgreSQL + +[CRUD RESTful API Server with Python, FastAPI, SQLAlchemy, and PostgreSQL](https://codevoweb.com/crud-restful-api-server-with-python-fastapi-and-postgresql) diff --git a/requirements.txt b/requirements.txt index 2c14003..095a868 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,46 +1,44 @@ -alembic==1.8.0 -anyio==3.6.1 -asgiref==3.5.2 -autopep8==1.6.0 -bcrypt==3.2.2 -certifi==2022.6.15 +aiosmtplib==1.1.7 +alembic==1.9.0 +anyio==3.6.2 +bcrypt==4.0.1 +blinker==1.5 +certifi==2022.12.7 cffi==1.15.1 -charset-normalizer==2.1.0 click==8.1.3 -colorama==0.4.5 +colorama==0.4.6 cryptography==3.4.8 dnspython==2.2.1 -email-validator==1.2.1 -fastapi==0.78.0 +email-validator==1.3.0 +fastapi==0.87.0 fastapi-jwt-auth==0.5.0 -greenlet==1.1.2 -h11==0.13.0 -httptools==0.4.0 -idna==3.3 +fastapi-mail==1.2.2 +greenlet==2.0.1 +h11==0.14.0 +httpcore==0.16.3 +httptools==0.5.0 +httpx==0.23.1 +idna==3.4 itsdangerous==2.1.2 Jinja2==3.1.2 -Mako==1.2.1 +Mako==1.2.4 MarkupSafe==2.1.1 -orjson==3.7.6 +orjson==3.8.3 passlib==1.7.4 -psycopg2==2.9.3 -pycodestyle==2.8.0 +psycopg2==2.9.5 pycparser==2.21 -pydantic==1.9.1 +pydantic==1.10.2 PyJWT==1.7.1 -python-dotenv==0.20.0 +python-dotenv==0.21.0 python-multipart==0.0.5 PyYAML==6.0 -requests==2.28.1 +rfc3986==1.5.0 six==1.16.0 -sniffio==1.2.0 -SQLAlchemy==1.4.39 -starlette==0.19.1 -toml==0.10.2 -typing_extensions==4.3.0 -tzdata==2022.1 -ujson==5.4.0 -urllib3==1.26.9 -uvicorn==0.17.6 -watchgod==0.8.2 -websockets==10.3 +sniffio==1.3.0 +SQLAlchemy==1.4.45 +starlette==0.21.0 +typing_extensions==4.4.0 +ujson==5.6.0 +uvicorn==0.20.0 +watchfiles==0.18.1 +websockets==10.4