From ecbf757ddc05ba299ad923e64f9b420269d5c321 Mon Sep 17 00:00:00 2001 From: Roger Carhuatocto Date: Thu, 20 Jul 2023 15:08:23 +0100 Subject: [PATCH] chore: many updates --- .env => .env.sample | 12 +- .gitignore | 3 +- 01-step-by-step-guide-crud-python.md | 174 +++++++++++++ readMe.md => README.md | 19 +- alembic/env.py | 20 +- app/config.py | 12 +- app/database.py | 2 +- app/main.py | 4 + app/models.py | 22 +- app/oauth2.py | 18 +- app/routers/auth.py | 77 ++---- app/routers/post.py | 18 +- docker-compose.yml | 26 +- .../docker-entrypoint-initdb/extension.sql | 1 + postman_collection/969286ffb3ee641b3a83.json | 239 ++++++++++++++++++ 15 files changed, 515 insertions(+), 132 deletions(-) rename .env => .env.sample (89%) create mode 100644 01-step-by-step-guide-crud-python.md rename readMe.md => README.md (86%) create mode 100644 postgres_db/docker-entrypoint-initdb/extension.sql create mode 100644 postman_collection/969286ffb3ee641b3a83.json diff --git a/.env b/.env.sample similarity index 89% rename from .env rename to .env.sample index 373fdc7..9984398 100644 --- a/.env +++ b/.env.sample @@ -1,9 +1,9 @@ -DATABASE_PORT=6500 -POSTGRES_PASSWORD=password123 -POSTGRES_USER=postgres -POSTGRES_DB=fastapi +DB_PORT=6500 +DB_PWD=password123 +DB_USR=postgres +DB_NAME=fastapi POSTGRES_HOST=postgres -POSTGRES_HOSTNAME=127.0.0.1 +DB_HOST=127.0.0.1 ACCESS_TOKEN_EXPIRES_IN=15 REFRESH_TOKEN_EXPIRES_IN=60 @@ -13,7 +13,7 @@ CLIENT_ORIGIN=http://localhost:3000 VERIFICATION_SECRET=my-email-verification-secret -EMAIL_HOST=smtp.mailtrap.io +EMAIL_HOST=sandbox.smtp.mailtrap.io EMAIL_PORT=587 EMAIL_USERNAME=af7917ee09173f EMAIL_PASSWORD=dbd2433183aa32 diff --git a/.gitignore b/.gitignore index 0ef896b..2c9a08b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__ venv/ -# .env \ No newline at end of file +venv1/ +.env \ No newline at end of file diff --git a/01-step-by-step-guide-crud-python.md b/01-step-by-step-guide-crud-python.md new file mode 100644 index 0000000..f9a781e --- /dev/null +++ b/01-step-by-step-guide-crud-python.md @@ -0,0 +1,174 @@ +# 01. Step by step guide to run and use the CRUD RESTful API with Python + +## Prerequisites + +- Docker and Docker Compose to get PostgreSQL, pgAdmin and deploy the Python CRUD. +- Python > 3.6 + +## 1. Setup the environment + +```sh +docker-compose up -d +``` + +## 2. checks docker-compose.yml and vars + +```sh +docker compose config +``` + +## 3. checks if extensions.sql has been executed + +```sh +docker logs -f crud-postgres-1 + +... +CREATE DATABASE + + +/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/extension.sql +CREATE EXTENSION +... +``` + +And tailing the logs. +```sh +docker compose logs -f +``` + + +## 4. Get a shell from docker to check uuid-ossp + +```sh +docker exec -it postgres bash + +root@f41e7e519277:/# psql -U postgres fastapi +psql (15.3 (Debian 15.3-1.pgdg120+1)) +Type "help" for help. + +fastapi=# select * from pg_available_extensions; +... +uuid-ossp | 1.1 | 1.1 | generate universally unique identifiers (UUIDs) +... +``` + + +## 5. Create env, install deps + +```sh +$ python -V +Python 3.11.3 + +$ sudo pacman -Sy python-pip +$ pip -V +pip 23.1.2 from /usr/lib/python3.11/site-packages/pip (python 3.11) + +$ python -m venv venv1 +$ source venv1/bin/activate +``` + +Install Postgres libraries and Python module. +```sh +$ sudo pacman -S postgresql-libs +$ pip install psycopg2 +``` + +Update the `pip` if needed. +```sh +$ pip install --upgrade pip +``` + +## 6. Optionally, upgrade the dependencies + +You have 3 options, install one by one each python module, run `pip install -r requirements.txt --upgrade` or use `pip-upgrader` and `requirements.txt` to install all python modules in a consistent way. + +* Option 1, this is a slow process and requires to review the code to identify what modules are used. +The next command only install 3 main modules. There are more. + +```sh +$ pip install uvicorn fastapi SQLAlchemy +``` + +* Option 2, using `pip install -r requirements.txt --upgrade` is faster but generate conflicts. Use it if you are sure about the dependencies versions. + +```sh +$ pip install -r requirements.txt --upgrade +``` + +* Option 3, this uses `pip-upgrader` and `requirements.txt`. Helps to choose the right module version to avoid conflicts. +```sh +$ pip install pip-upgrader +$ pip-upgrade requirements.txt +``` + +Once completed this process, the next task is freeze and update the `requirements.txt` file optionally. +```sh +pip freeze > requirements.txt +``` + +## 7. Run the application + +```sh +$ uvicorn app.main:app --host localhost --port 8000 --reload +``` + +This process automatically will create the DB, serve the application using uvicorn as server. If the DBs changes, then Alembic will detect the changes and will apply them automatically. +Additionally, this will expose a set of REST API. You can see all API endpoints available here: + +* [http://localhost:8000/docs](http://localhost:8000/docs) + + +## 8. Using the applicaion + +### 8.1. Mailserver for testing + +Before using the application, create an account on [Mailtrap](https://mailtrap.io) to simulate the process account validation during the of user signup. Once created, update the `.env` file, specifically the next values: +``` +EMAIL_HOST=sandbox.smtp.mailtrap.io +EMAIL_PORT=587 +EMAIL_USERNAME=your-username +EMAIL_PASSWORD=your-password +EMAIL_FROM=info@your-domain.com +``` + +### 8.2. RESTful API testing + +Import this [Postman collection](https://www.getpostman.com/collections/969286ffb3ee641b3a83) in your favorite API Testing Tool (Postman or Insomnia). It is a set of API requests that we can use for testing our application. + +But, if you do want to use `cURL` instead of Postman or Insomnia, use these commands: + +#### 1. API Healthceck + +```sh + +``` + +#### 2. API User signup +```sh + +``` + +#### 3. API User login +```sh + +``` + +#### 4. API User info +```sh + +``` + +#### 5. API create Post +```sh + +``` + +#### 6. API list Posts +```sh + +``` + + +## References + +1. https://credibledev.com/python-flask-dev-environment-on-manjaro-linux/ diff --git a/readMe.md b/README.md similarity index 86% rename from readMe.md rename to README.md index 2b06358..a9c52ae 100644 --- a/readMe.md +++ b/README.md @@ -4,8 +4,6 @@ In this article, you'll learn how to secure a FastAPI app by implementing access and refresh token functionalities using JSON Web Tokens (JWTs). We'll use the FastAPI JWT Auth package to sign, encode and decode the access and refresh JWT tokens. -![RESTful API with Python,SQLAlchemy, & FastAPI: Access and Refresh Tokens](https://codevoweb.com/wp-content/uploads/2022/07/RESTful-API-with-Python-FastAPI-Access-and-Refresh-Tokens.webp) - ### Topics Covered - Python FastAPI JWT Authentication Overview @@ -36,8 +34,6 @@ Read the entire article here: [https://codevoweb.com/restful-api-with-python-fas In this article, you'll learn how to send HTML emails with Python, FastAPI, SQLAlchemy, PostgreSQL, Jinja2, and Docker-compose. Also, you'll learn how to dynamically generate HTML templates with the Jinja2 package. -![RESTful API with Python, SQLAlchemy, & FastAPI: Send HTML Emails](https://codevoweb.com/wp-content/uploads/2022/07/RESTful-API-with-Python-FastAPI-Send-HTML-Emails.webp) - ### Topics Covered - Send HTML Email with jinja2 and FastAPI Overview @@ -56,8 +52,6 @@ Read the entire article here: [https://codevoweb.com/restful-api-with-python-fas 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. -![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) - ### Topics Covered - Python, FastAPI, PostgreSQL, SQLAlchemy CRUD API Overview @@ -79,3 +73,16 @@ This article will teach you how to create a CRUD RESTful API with Python, FastAP 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) +### Step by step guide + +:arrow_right: I've created step by step guide to run and use this CRUD RESTful API. +:point_right: [01-step-by-step-guide-crud-python.md](01-step-by-step-guide-crud-python.md) + +Changes I did: +1. Postgres extension installation +2. Docker compose for pgAdmin +3. Adding the Postman collection in the repo +4. Examples using curl commands +5. Deploying the python application in Docker + + diff --git a/alembic/env.py b/alembic/env.py index d2da45f..d5fdb7e 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -10,8 +10,7 @@ # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -config.set_main_option( - "sqlalchemy.url", f"postgresql+psycopg2://{settings.POSTGRES_USER}:{settings.POSTGRES_PASSWORD}@{settings.POSTGRES_HOSTNAME}:{settings.DATABASE_PORT}/{settings.POSTGRES_DB}") +config.set_main_option("sqlalchemy.url", f"postgresql+psycopg2://{settings.DB_USR}:{settings.DB_PWD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}") # Interpret the config file for Python logging. # This line sets up loggers basically. @@ -43,12 +42,7 @@ def run_migrations_offline() -> None: """ url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) + context.configure(url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"},) with context.begin_transaction(): context.run_migrations() @@ -61,16 +55,10 @@ def run_migrations_online() -> None: and associate a connection with the context. """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) + connectable = engine_from_config(config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool,) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/config.py b/app/config.py index cd46047..3a36787 100644 --- a/app/config.py +++ b/app/config.py @@ -1,13 +1,12 @@ from pydantic import BaseSettings, EmailStr - class Settings(BaseSettings): - DATABASE_PORT: int - POSTGRES_PASSWORD: str - POSTGRES_USER: str - POSTGRES_DB: str + DB_PORT: int + DB_PWD: str + DB_USR: str + DB_NAME: str POSTGRES_HOST: str - POSTGRES_HOSTNAME: str + DB_HOST: str JWT_PUBLIC_KEY: str JWT_PRIVATE_KEY: str @@ -28,5 +27,4 @@ class Settings(BaseSettings): class Config: env_file = './.env' - settings = Settings() diff --git a/app/database.py b/app/database.py index cc358d7..70d097e 100644 --- a/app/database.py +++ b/app/database.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import sessionmaker from .config import settings -SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.POSTGRES_USER}:{settings.POSTGRES_PASSWORD}@{settings.POSTGRES_HOSTNAME}:{settings.DATABASE_PORT}/{settings.POSTGRES_DB}" +SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.DB_USR}:{settings.DB_PWD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}" engine = create_engine( SQLALCHEMY_DATABASE_URL diff --git a/app/main.py b/app/main.py index 5993a7b..9d71f4b 100644 --- a/app/main.py +++ b/app/main.py @@ -26,3 +26,7 @@ @app.get('/api/healthchecker') def root(): return {'message': 'Hello World'} + +## $ docker compose -f docker-compose.pgadmin.yaml config (test config file) +## $ docker compose -f docker-compose.pgadmin.yaml up (creates and starts containers) +## $ docker compose -f docker-compose.pgadmin.yaml up --build (build containers before running containers) \ No newline at end of file diff --git a/app/models.py b/app/models.py index 61f4d26..83bb1a3 100644 --- a/app/models.py +++ b/app/models.py @@ -4,11 +4,9 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship - class User(Base): __tablename__ = 'users' - id = Column(UUID(as_uuid=True), primary_key=True, nullable=False, - default=uuid.uuid4) + id = Column(UUID(as_uuid=True), primary_key=True, nullable=False, default=uuid.uuid4) name = Column(String, nullable=False) email = Column(String, unique=True, nullable=False) password = Column(String, nullable=False) @@ -16,24 +14,18 @@ class User(Base): verified = Column(Boolean, nullable=False, server_default='False') verification_code = Column(String, nullable=True, unique=True) role = Column(String, server_default='user', nullable=False) - created_at = Column(TIMESTAMP(timezone=True), - nullable=False, server_default=text("now()")) - updated_at = Column(TIMESTAMP(timezone=True), - nullable=False, server_default=text("now()")) + created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text("now()")) + updated_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text("now()")) class Post(Base): __tablename__ = 'posts' - id = Column(UUID(as_uuid=True), primary_key=True, nullable=False, - default=uuid.uuid4) - user_id = Column(UUID(as_uuid=True), ForeignKey( - 'users.id', ondelete='CASCADE'), nullable=False) + id = Column(UUID(as_uuid=True), primary_key=True, nullable=False, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), ForeignKey('users.id', ondelete='CASCADE'), nullable=False) title = Column(String, nullable=False) content = Column(String, nullable=False) category = Column(String, nullable=False) image = Column(String, nullable=False) - created_at = Column(TIMESTAMP(timezone=True), - nullable=False, server_default=text("now()")) - updated_at = Column(TIMESTAMP(timezone=True), - nullable=False, server_default=text("now()")) + created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text("now()")) + updated_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text("now()")) user = relationship('User') diff --git a/app/oauth2.py b/app/oauth2.py index 85edcde..14a60b3 100644 --- a/app/oauth2.py +++ b/app/oauth2.py @@ -17,10 +17,8 @@ class Settings(BaseModel): authjwt_access_cookie_key: str = 'access_token' authjwt_refresh_cookie_key: str = 'refresh_token' authjwt_cookie_csrf_protect: bool = False - authjwt_public_key: str = base64.b64decode( - settings.JWT_PUBLIC_KEY).decode('utf-8') - authjwt_private_key: str = base64.b64decode( - settings.JWT_PRIVATE_KEY).decode('utf-8') + authjwt_public_key: str = base64.b64decode(settings.JWT_PUBLIC_KEY).decode('utf-8') + authjwt_private_key: str = base64.b64decode(settings.JWT_PRIVATE_KEY).decode('utf-8') @AuthJWT.load_config @@ -52,14 +50,10 @@ def require_user(db: Session = Depends(get_db), Authorize: AuthJWT = Depends()): error = e.__class__.__name__ print(error) if error == 'MissingTokenError': - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail='You are not logged in') + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='You are not logged in') if error == 'UserNotFound': - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail='User no longer exist') + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='User no longer exist') if error == 'NotVerified': - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail='Please verify your account') - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail='Token is invalid or has expired') + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Please verify your account') + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Token is invalid or has expired') return user_id diff --git a/app/routers/auth.py b/app/routers/auth.py index cf0e725..264f35a 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -21,16 +21,13 @@ @router.post('/register', status_code=status.HTTP_201_CREATED) async def create_user(payload: schemas.CreateUserSchema, request: Request, db: Session = Depends(get_db)): # Check if user already exist - user_query = db.query(models.User).filter( - models.User.email == EmailStr(payload.email.lower())) + user_query = db.query(models.User).filter(models.User.email == EmailStr(payload.email.lower())) user = user_query.first() if user: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, - detail='Account already exist') + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail='Account already exist') # Compare password and passwordConfirm if payload.password != payload.passwordConfirm: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail='Passwords do not match') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Passwords do not match') # Hash the password payload.password = utils.hash_password(payload.password) del payload.passwordConfirm @@ -48,55 +45,43 @@ async def create_user(payload: schemas.CreateUserSchema, request: Request, db: S hashedCode = hashlib.sha256() hashedCode.update(token) verification_code = hashedCode.hexdigest() - user_query.update( - {'verification_code': verification_code}, synchronize_session=False) + user_query.update({'verification_code': verification_code}, synchronize_session=False) db.commit() url = f"{request.url.scheme}://{request.client.host}:{request.url.port}/api/auth/verifyemail/{token.hex()}" await Email(new_user, url, [payload.email]).sendVerificationCode() except Exception as error: print('Error', error) - user_query.update( - {'verification_code': None}, synchronize_session=False) + user_query.update({'verification_code': None}, synchronize_session=False) db.commit() - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='There was an error sending email') + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='There was an error sending email') return {'status': 'success', 'message': 'Verification token successfully sent to your email'} @router.post('/login') def login(payload: schemas.LoginUserSchema, response: Response, db: Session = Depends(get_db), Authorize: AuthJWT = Depends()): # Check if the user exist - user = db.query(models.User).filter( - models.User.email == EmailStr(payload.email.lower())).first() + user = db.query(models.User).filter(models.User.email == EmailStr(payload.email.lower())).first() if not user: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail='Incorrect Email or Password') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Incorrect Email or Password') # Check if user verified his email if not user.verified: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, - detail='Please verify your email address') + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Please verify your email address') # Check if the password is valid if not utils.verify_password(payload.password, user.password): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail='Incorrect Email or Password') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Incorrect Email or Password') # Create access token - access_token = Authorize.create_access_token( - subject=str(user.id), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN)) + access_token = Authorize.create_access_token(subject=str(user.id), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN)) # Create refresh token - refresh_token = Authorize.create_refresh_token( - subject=str(user.id), expires_time=timedelta(minutes=REFRESH_TOKEN_EXPIRES_IN)) + refresh_token = Authorize.create_refresh_token(subject=str(user.id), expires_time=timedelta(minutes=REFRESH_TOKEN_EXPIRES_IN)) # Store refresh and access tokens in cookie - response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60, - ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') - response.set_cookie('refresh_token', refresh_token, - REFRESH_TOKEN_EXPIRES_IN * 60, REFRESH_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') - response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60, - ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax') + response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60, ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') + response.set_cookie('refresh_token', refresh_token, REFRESH_TOKEN_EXPIRES_IN * 60, REFRESH_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') + response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60, ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax') # Send both access return {'status': 'success', 'access_token': access_token} @@ -109,26 +94,19 @@ def refresh_token(response: Response, request: Request, Authorize: AuthJWT = Dep user_id = Authorize.get_jwt_subject() if not user_id: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, - detail='Could not refresh access token') + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Could not refresh access token') user = db.query(models.User).filter(models.User.id == user_id).first() if not user: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, - detail='The user belonging to this token no logger exist') - access_token = Authorize.create_access_token( - subject=str(user.id), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN)) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='The user belonging to this token no logger exist') + access_token = Authorize.create_access_token(subject=str(user.id), expires_time=timedelta(minutes=ACCESS_TOKEN_EXPIRES_IN)) except Exception as e: error = e.__class__.__name__ if error == 'MissingTokenError': - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail='Please provide refresh token') - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=error) - - response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60, - ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') - response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60, - ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Please provide refresh token') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error) + + response.set_cookie('access_token', access_token, ACCESS_TOKEN_EXPIRES_IN * 60, ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, True, 'lax') + response.set_cookie('logged_in', 'True', ACCESS_TOKEN_EXPIRES_IN * 60, ACCESS_TOKEN_EXPIRES_IN * 60, '/', None, False, False, 'lax') return {'access_token': access_token} @@ -145,15 +123,12 @@ def verify_me(token: str, db: Session = Depends(get_db)): hashedCode = hashlib.sha256() hashedCode.update(bytes.fromhex(token)) verification_code = hashedCode.hexdigest() - user_query = db.query(models.User).filter( - models.User.verification_code == verification_code) + user_query = db.query(models.User).filter(models.User.verification_code == verification_code) db.commit() user = user_query.first() if not user: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail='Email can only be verified once') - user_query.update( - {'verified': True, 'verification_code': None}, synchronize_session=False) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Email can only be verified once') + user_query.update({'verified': True, 'verification_code': None}, synchronize_session=False) db.commit() return { "status": "success", diff --git a/app/routers/post.py b/app/routers/post.py index 1552ef5..3d2dc55 100644 --- a/app/routers/post.py +++ b/app/routers/post.py @@ -12,8 +12,7 @@ def get_posts(db: Session = Depends(get_db), limit: int = 10, page: int = 1, search: str = '', user_id: str = Depends(require_user)): skip = (page - 1) * limit - posts = db.query(models.Post).group_by(models.Post.id).filter( - models.Post.title.contains(search)).limit(limit).offset(skip).all() + posts = db.query(models.Post).group_by(models.Post.id).filter(models.Post.title.contains(search)).limit(limit).offset(skip).all() return {'status': 'success', 'results': len(posts), 'posts': posts} @@ -33,11 +32,9 @@ def update_post(id: str, post: schemas.UpdatePostSchema, db: Session = Depends(g updated_post = post_query.first() if not updated_post: - raise HTTPException(status_code=status.HTTP_200_OK, - detail=f'No post with this id: {id} found') + raise HTTPException(status_code=status.HTTP_200_OK, detail=f'No post with this id: {id} found') 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') + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='You are not allowed to perform this action') post.user_id = user_id post_query.update(post.dict(exclude_unset=True), synchronize_session=False) db.commit() @@ -48,8 +45,7 @@ def update_post(id: str, post: schemas.UpdatePostSchema, db: Session = Depends(g def get_post(id: str, db: Session = Depends(get_db), user_id: str = Depends(require_user)): post = db.query(models.Post).filter(models.Post.id == id).first() if not post: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, - detail=f"No post with this id: {id} found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No post with this id: {id} found") return post @@ -58,12 +54,10 @@ def delete_post(id: str, db: Session = Depends(get_db), user_id: str = Depends(r post_query = db.query(models.Post).filter(models.Post.id == id) post = post_query.first() if not post: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, - detail=f'No post with this id: {id} found') + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'No post with this id: {id} found') if str(post.user_id) != user_id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, - detail='You are not allowed to perform this action') + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='You are not allowed to perform this action') post_query.delete(synchronize_session=False) db.commit() return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/docker-compose.yml b/docker-compose.yml index 4ca5f29..d2de02a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,30 @@ version: '3' + +name: crud + services: postgres: image: postgres - container_name: postgres ports: - '6500:5432' restart: always - env_file: - - ./.env + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USR} + POSTGRES_PASSWORD: ${DB_PWD} volumes: - - postgres-db:/var/lib/postgresql/data + - postgres-vol:/var/lib/postgresql/data + - ./postgres_db/docker-entrypoint-initdb:/docker-entrypoint-initdb.d/ + + pgamin: + image: dpage/pgadmin4 + environment: + - PGADMIN_DEFAULT_EMAIL=dba@intix.info + - PGADMIN_DEFAULT_PASSWORD=dbapwd + ports: + - "5080:80" + depends_on: + - postgres + volumes: - postgres-db: + postgres-vol: diff --git a/postgres_db/docker-entrypoint-initdb/extension.sql b/postgres_db/docker-entrypoint-initdb/extension.sql new file mode 100644 index 0000000..682131d --- /dev/null +++ b/postgres_db/docker-entrypoint-initdb/extension.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; \ No newline at end of file diff --git a/postman_collection/969286ffb3ee641b3a83.json b/postman_collection/969286ffb3ee641b3a83.json new file mode 100644 index 0000000..ad40753 --- /dev/null +++ b/postman_collection/969286ffb3ee641b3a83.json @@ -0,0 +1,239 @@ +{ + "info": { + "_postman_id": "d18df448-8d92-464f-af14-b1b9f62c3a32", + "name": "Python Fastapi", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "Post", + "item": [ + { + "name": "Create Post", + "id": "cbd123da-3ef9-4107-97a8-f6a97d89f713", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"title\": \"How to code a Reactjs app\",\r\n \"category\": \"Reactjs\",\r\n \"content\": \"My content haha My content haha\",\r\n \"image\": \"default.png\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{host}}/api/posts" + }, + "response": [] + }, + { + "name": "Get Post", + "id": "5fb3d4d4-4c20-4742-9591-f6e310867a64", + "request": { + "method": "GET", + "header": [], + "url": "{{host}}/api/posts/6abb2e93-d6d8-48d0-b3af-d033ef4251ca" + }, + "response": [] + }, + { + "name": "Update Post", + "id": "d6739f04-0ee8-437b-937e-99405d39c215", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"title\": \"How to code a Node.js apps\",\r\n \"content\": \"My content haha My content haha\",\r\n \"category\": \"Node.js\",\r\n \"image\": \"default.png\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{host}}/api/posts/6abb2e93-d6d8-48d0-b3af-d033ef4251ca" + }, + "response": [] + }, + { + "name": "Delete Post", + "id": "3261213e-6524-4af4-9293-6ab629ea43cb", + "request": { + "method": "DELETE", + "header": [], + "url": "{{host}}/api/posts/524161ee-0ac0-4500-b6e6-6c6e5b5f48e0" + }, + "response": [] + }, + { + "name": "Get All Posts", + "id": "9cd38c91-cd9e-47e4-ab47-305c16121648", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/posts?page=1&limit=10", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "posts" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "10" + } + ] + } + }, + "response": [] + } + ], + "id": "0633c82d-c4e2-47ef-a50a-55475ae0f579" + }, + { + "name": "Users", + "item": [ + { + "name": "Get Me", + "id": "a8c640c5-86c8-4338-bb54-3650a868a133", + "request": { + "method": "GET", + "header": [], + "url": "{{host}}/api/users/me" + }, + "response": [] + }, + { + "name": "Update Me", + "id": "09c7eae4-75fb-4c9d-b3b3-bcd00ac26acd", + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{access_token}}" + } + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Price\",\r\n \"email\": \"prince@gmail.com\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{host}}/api/users/updateMe" + }, + "response": [] + } + ], + "id": "776fb8a1-e790-4012-b2c3-afd3ce52cbd5" + }, + { + "name": "Auth", + "item": [ + { + "name": "Signup", + "id": "3cabb31c-5498-4ec7-b117-6335d4b4c488", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"John Smith\",\r\n \"email\": \"johnsmith@gmail.com\",\r\n \"password\": \"password123\",\r\n \"passwordConfirm\": \"password123\",\r\n \"photo\": \"default.png\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{host}}/api/auth/register" + }, + "response": [] + }, + { + "name": "Login", + "event": [ + { + "listen": "test", + "script": { + "id": "aae50bf5-79ee-40e7-ba89-89238a6f8436", + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "id": "8883413b-4110-4cb1-8340-396e50d9bbf4", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"johndoe@gmail.com\",\r\n \"password\":\"password123\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{host}}/api/auth/login" + }, + "response": [] + }, + { + "name": "Verify Email Address", + "id": "7a779b28-7f7c-4803-9364-aaff665e00dd", + "request": { + "method": "GET", + "header": [], + "url": "{{host}}/api/auth/verifyemail/c606d17319ece722af2a" + }, + "response": [] + }, + { + "name": "Refresh Access Token", + "id": "82a9c587-610e-486a-b28a-1e40a8f69e82", + "request": { + "method": "GET", + "header": [], + "url": "{{host}}/api/auth/refresh" + }, + "response": [] + }, + { + "name": "Logout", + "id": "a6d82bac-062a-4128-8b09-ddb0bfb99905", + "request": { + "method": "GET", + "header": [], + "url": "{{host}}/api/auth/logout" + }, + "response": [] + } + ], + "id": "63e08da0-804a-4df6-8560-abd13378c166" + }, + { + "name": "Health Checker", + "id": "9f34a7e8-a573-49b3-945c-3ee16cf58b56", + "request": { + "method": "GET", + "header": [], + "url": "{{host}}/api/healthchecker" + }, + "response": [] + } + ] +} \ No newline at end of file