├── logs └── .gitkeep ├── tests ├── __init__.py ├── unit │ ├── __init__.py │ └── tests_users_unit.py ├── integration │ ├── __init__.py │ └── tests_users_integration.py └── postman │ ├── FlaskTemplate.postman_environment.json │ └── FlaskTemplate.postman_collection.json ├── app ├── routers │ ├── __init__.py │ ├── role_permission_router.py │ ├── user_role_router.py │ ├── role_router.py │ ├── permission_router.py │ └── user_router.py ├── schemas │ ├── __init__.py │ ├── permission_schema.py │ ├── role_schema.py │ ├── role_permission_schema.py │ └── user_schema.py ├── utils │ ├── __init__.py │ ├── decorators.py │ ├── logging.py │ └── auth.py ├── services │ ├── __init__.py │ ├── role_permission_service.py │ ├── user_role_service.py │ ├── permission_service.py │ ├── role_service.py │ └── user_service.py ├── db.py ├── extention.py ├── models │ ├── blocklist_model.py │ ├── user_role_model.py │ ├── __init__.py │ ├── role_permission_model.py │ ├── permission_model.py │ ├── role_model.py │ └── user_model.py ├── blueprint.py └── __init__.py ├── .gitattributes ├── .github └── workflows │ ├── ansible │ ├── hosts │ ├── roles │ │ └── deploy │ │ │ └── tasks │ │ │ └── main.yml │ └── deploy_applications.yml │ ├── cloudformations │ ├── README.md │ ├── server-parameters.json │ └── server.yml │ ├── development_pipeline.yml │ ├── README.md │ ├── production_pipeline.yml │ └── staging_pipeline.yml ├── assets ├── logo.png └── swagger.png ├── .env.db.local ├── .flake8 ├── .env.pro ├── .env ├── nginx └── default.conf ├── Dockerfile ├── .env.api.local ├── docker-compose.yml ├── entrypoint.sh ├── .pre-commit-config.yaml ├── gunicorn └── gunicorn_config.py ├── requirements.txt ├── .gitignore ├── config.py ├── .dockerignore ├── manage.py ├── README.md └── LICENSE /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/ansible/hosts: -------------------------------------------------------------------------------- 1 | [web] 2 | -------------------------------------------------------------------------------- /app/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectornguyen76/flask-rest-api-template/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectornguyen76/flask-rest-api-template/HEAD/assets/swagger.png -------------------------------------------------------------------------------- /.env.db.local: -------------------------------------------------------------------------------- 1 | # Database service configuration 2 | POSTGRES_USER=db_user 3 | POSTGRES_PASSWORD=db_password 4 | POSTGRES_DB=db_dev 5 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = F401 3 | max-line-length = 100 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /app/extention.py: -------------------------------------------------------------------------------- 1 | from flask_cors import CORS 2 | from flask_jwt_extended import JWTManager 3 | from flask_migrate import Migrate 4 | 5 | migrate = Migrate() 6 | jwt = JWTManager() 7 | cors = CORS() 8 | -------------------------------------------------------------------------------- /app/models/blocklist_model.py: -------------------------------------------------------------------------------- 1 | from app.db import db 2 | 3 | 4 | class BlocklistModel(db.Model): 5 | __tablename__ = "blocklist" 6 | 7 | jti_blocklist = db.Column(db.String(), primary_key=True) 8 | -------------------------------------------------------------------------------- /.github/workflows/cloudformations/README.md: -------------------------------------------------------------------------------- 1 | ## Create Stack 2 | 3 | aws cloudformation create-stack --stack-name server --template-body file://server.yml --parameters file://server-parameters.json --region us-east-1 4 | 5 | ## Delete Stack 6 | 7 | aws cloudformation delete-stack --stack-name server --region us-east-1 8 | -------------------------------------------------------------------------------- /app/models/user_role_model.py: -------------------------------------------------------------------------------- 1 | from app.db import db 2 | 3 | 4 | class UserRoleModel(db.Model): 5 | __tablename__ = "user_role" 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | user_id = db.Column(db.Integer, db.ForeignKey("user.id")) 9 | role_id = db.Column(db.Integer, db.ForeignKey("role.id")) 10 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from app.models.blocklist_model import BlocklistModel 2 | from app.models.permission_model import PermissionModel 3 | from app.models.role_model import RoleModel 4 | from app.models.role_permission_model import RolePermissionModel 5 | from app.models.user_model import UserModel 6 | from app.models.user_role_model import UserRoleModel 7 | -------------------------------------------------------------------------------- /app/models/role_permission_model.py: -------------------------------------------------------------------------------- 1 | from app.db import db 2 | 3 | 4 | class RolePermissionModel(db.Model): 5 | __tablename__ = "role_permission" 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | role_id = db.Column(db.Integer, db.ForeignKey("role.id")) 9 | permission_id = db.Column(db.Integer, db.ForeignKey("permission.id")) 10 | -------------------------------------------------------------------------------- /app/schemas/permission_schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class PlainPermissionSchema(Schema): 5 | id = fields.Int(dump_only=True) 6 | name = fields.Str(required=True) 7 | route = fields.Str(required=True) 8 | 9 | 10 | class UpdatePermissionRoleSchema(Schema): 11 | data_update = fields.Dict(required=True) 12 | -------------------------------------------------------------------------------- /app/schemas/role_schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class PlainRoleSchema(Schema): 5 | id = fields.Int(dump_only=True) 6 | name = fields.Str(required=True) 7 | description = fields.Str(required=True) 8 | 9 | 10 | class GetRolePermissionSchema(Schema): 11 | id = fields.Int(dump_only=True) 12 | role_id = fields.Int(dump_only=True) 13 | permission_id = fields.Int(dump_only=True) 14 | -------------------------------------------------------------------------------- /app/models/permission_model.py: -------------------------------------------------------------------------------- 1 | from app.db import db 2 | 3 | 4 | class PermissionModel(db.Model): 5 | __tablename__ = "permission" 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(), unique=True, nullable=False) 9 | description = db.Column(db.String()) 10 | roles = db.relationship( 11 | "RoleModel", back_populates="permissions", secondary="role_permission" 12 | ) 13 | -------------------------------------------------------------------------------- /.env.pro: -------------------------------------------------------------------------------- 1 | # APP configuration 2 | APP_NAME=Flask API Rest Template 3 | APP_ENV=production 4 | 5 | # Flask Configuration 6 | API_ENTRYPOINT=app:app 7 | APP_SETTINGS_MODULE=config.ProductionConfig 8 | 9 | # API service configuration 10 | API_HOST=0.0.0.0 11 | API_PORT=5000 12 | 13 | # Secret key 14 | SECRET_KEY= 15 | JWT_SECRET_KEY= 16 | 17 | # Database service configuration 18 | DATABASE_URL=sqlite:///production.db 19 | 20 | # Deployment platform 21 | PLATFORM_DEPLOY=AWS 22 | -------------------------------------------------------------------------------- /app/models/role_model.py: -------------------------------------------------------------------------------- 1 | from app.db import db 2 | 3 | 4 | class RoleModel(db.Model): 5 | __tablename__ = "role" 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(), unique=True, nullable=False) 9 | description = db.Column(db.String(), nullable=False) 10 | permissions = db.relationship( 11 | "PermissionModel", back_populates="roles", secondary="role_permission" 12 | ) 13 | users = db.relationship("UserModel", back_populates="roles", secondary="user_role") 14 | -------------------------------------------------------------------------------- /app/models/user_model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from app.db import db 4 | 5 | 6 | class UserModel(db.Model): 7 | __tablename__ = "user" 8 | 9 | id = db.Column(db.Integer, primary_key=True) 10 | username = db.Column(db.String(80), unique=True, nullable=False) 11 | password = db.Column(db.String(), nullable=False) 12 | block = db.Column(db.Boolean, default=False, nullable=False) 13 | time_created = db.Column(db.String(), default=datetime.now()) 14 | roles = db.relationship("RoleModel", back_populates="users", secondary="user_role") 15 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # APP configuration 2 | APP_NAME=Flask API Rest Template 3 | APP_ENV=develop 4 | 5 | # Flask Configuration 6 | FLASK_APP=app:app 7 | FLASK_DEBUG=true 8 | APP_SETTINGS_MODULE=config.DevelopConfig 9 | APP_TEST_SETTINGS_MODULE=config.TestingConfig 10 | FLASK_RUN_HOST=0.0.0.0 11 | FLASK_RUN_PORT=5000 12 | 13 | # Secret key 14 | SECRET_KEY= 15 | JWT_SECRET_KEY= 16 | 17 | # Database service configuration 18 | DATABASE_URL=postgresql://db_user:db_password@localhost/db_dev 19 | DATABASE_TEST_URL=postgresql://db_user:db_password@localhost/db_test 20 | -------------------------------------------------------------------------------- /nginx/default.conf: -------------------------------------------------------------------------------- 1 | upstream flask-api { 2 | server api_service:5000; 3 | } 4 | 5 | server { 6 | listen 80; 7 | 8 | location / { 9 | proxy_pass http://flask-api; 10 | proxy_redirect off; 11 | 12 | proxy_set_header Host $host; 13 | proxy_set_header X-Real-IP $remote_addr; 14 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 15 | proxy_set_header X-Forwarded-Proto $scheme; 16 | } 17 | 18 | # Log 19 | access_log /var/log/nginx/access.log; 20 | error_log /var/log/nginx/error.log; 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.9-slim 2 | 3 | WORKDIR /app 4 | 5 | # Update the package lists and install the PostgreSQL client 6 | RUN apt-get update && \ 7 | apt-get install -y postgresql-client && \ 8 | apt clean && \ 9 | rm -rf /var/cache/apt/* 10 | 11 | ENV PYTHONDONTWRITEBYTECODE=1 \ 12 | PYTHONUNBUFFERED=1 \ 13 | PYTHONIOENCODING=utf-8 14 | 15 | COPY requirements.txt /app 16 | 17 | RUN pip install --upgrade pip 18 | RUN pip install --no-cache-dir -r requirements.txt 19 | 20 | COPY . /app 21 | 22 | # Chmod to entrypoint.sh 23 | RUN chmod +x ./entrypoint.sh 24 | 25 | # Run entrypoint.sh 26 | ENTRYPOINT ["/app/entrypoint.sh"] 27 | -------------------------------------------------------------------------------- /.github/workflows/cloudformations/server-parameters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "EnvironmentName", 4 | "ParameterValue": "Flask-Template" 5 | }, 6 | { 7 | "ParameterKey": "VpcCIDR", 8 | "ParameterValue": "10.0.0.0/16" 9 | }, 10 | { 11 | "ParameterKey": "PublicSubnetCIDR", 12 | "ParameterValue": "10.0.1.0/24" 13 | }, 14 | { 15 | "ParameterKey": "InstanceType", 16 | "ParameterValue": "t3.micro" 17 | }, 18 | { 19 | "ParameterKey": "KeyPairName", 20 | "ParameterValue": "vectornguyen-keypair" 21 | }, 22 | { 23 | "ParameterKey": "AMItoUse", 24 | "ParameterValue": "ami-053b0d53c279acc90" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /app/blueprint.py: -------------------------------------------------------------------------------- 1 | from flask_smorest import Api 2 | 3 | from app.routers.permission_router import blp as PermissionBlueprint 4 | from app.routers.role_permission_router import blp as RolePermissionBlueprint 5 | from app.routers.role_router import blp as RoleBlueprint 6 | from app.routers.user_role_router import blp as UserRoleBlueprint 7 | from app.routers.user_router import blp as UserBlueprint 8 | 9 | 10 | # Register Blueprint 11 | def register_routing(app): 12 | api = Api(app) 13 | api.register_blueprint(UserBlueprint) 14 | api.register_blueprint(RoleBlueprint) 15 | api.register_blueprint(PermissionBlueprint) 16 | api.register_blueprint(UserRoleBlueprint) 17 | api.register_blueprint(RolePermissionBlueprint) 18 | -------------------------------------------------------------------------------- /tests/postman/FlaskTemplate.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "25c79762-c6a7-41e5-997c-ac2c02ed715a", 3 | "name": "FlaskTemplate", 4 | "values": [ 5 | { 6 | "key": "HOST", 7 | "value": "http://127.0.0.1:5000", 8 | "type": "default", 9 | "enabled": true 10 | }, 11 | { 12 | "key": "TOKEN", 13 | "value": "", 14 | "type": "default", 15 | "enabled": true 16 | }, 17 | { 18 | "key": "REFRESHTOKEN", 19 | "value": "", 20 | "type": "default", 21 | "enabled": true 22 | } 23 | ], 24 | "_postman_variable_scope": "environment", 25 | "_postman_exported_at": "2024-03-15T03:02:54.988Z", 26 | "_postman_exported_using": "Postman/10.24.3" 27 | } 28 | -------------------------------------------------------------------------------- /app/schemas/role_permission_schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields 2 | 3 | from app.schemas.permission_schema import PlainPermissionSchema 4 | from app.schemas.role_schema import PlainRoleSchema 5 | from app.schemas.user_schema import PlainUserSchema 6 | 7 | 8 | class PermissionSchema(PlainPermissionSchema): 9 | roles = fields.List(fields.Nested(PlainRoleSchema()), dump_only=True) 10 | 11 | 12 | class RoleSchema(PlainRoleSchema): 13 | permissions = fields.List(fields.Nested(PlainPermissionSchema()), dump_only=True) 14 | users = fields.List(fields.Nested(PlainUserSchema()), dump_only=True) 15 | 16 | 17 | class UpdateRolePermissionSchema(PlainRoleSchema): 18 | permissions = fields.List(cls_or_instance=fields.Int, required=True) 19 | -------------------------------------------------------------------------------- /.env.api.local: -------------------------------------------------------------------------------- 1 | # APP configuration 2 | APP_NAME=Flask API Rest Template 3 | APP_ENV=local 4 | 5 | # Flask Configuration 6 | API_ENTRYPOINT=app:app 7 | APP_SETTINGS_MODULE=config.LocalConfig 8 | APP_TEST_SETTINGS_MODULE=config.TestingConfig 9 | 10 | # API service configuration 11 | API_HOST=0.0.0.0 12 | API_PORT=5000 13 | 14 | # Database service configuration 15 | DATABASE=postgres 16 | BBDD_HOST=db_service 17 | BBDD_PORT=5432 18 | POSTGRES_DB=db_dev 19 | POSTGRES_USER=db_user 20 | PGPASSWORD=db_password 21 | 22 | # Secret key 23 | SECRET_KEY= 24 | JWT_SECRET_KEY= 25 | 26 | DATABASE_TEST_URL=postgresql+psycopg2://db_user:db_password@db_service:5432/db_test 27 | DATABASE_URL=postgresql+psycopg2://db_user:db_password@db_service:5432/db_dev 28 | -------------------------------------------------------------------------------- /app/routers/role_permission_router.py: -------------------------------------------------------------------------------- 1 | from flask.views import MethodView 2 | from flask_jwt_extended import jwt_required 3 | from flask_smorest import Blueprint 4 | 5 | from app.schemas.role_schema import GetRolePermissionSchema 6 | from app.services import role_permission_service 7 | from app.utils.decorators import permission_required 8 | 9 | blp = Blueprint("Role And Permission", __name__, description="Role And Permission API") 10 | 11 | 12 | @blp.route("/role-permission") 13 | class GetRolePermission(MethodView): 14 | @jwt_required() 15 | @permission_required(permission_name="read") 16 | @blp.response(200, GetRolePermissionSchema(many=True)) 17 | def get(self): 18 | result = role_permission_service.get_role_permission() 19 | return result 20 | -------------------------------------------------------------------------------- /.github/workflows/ansible/roles/deploy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: "Creates directory" 2 | file: 3 | path: "/home/ubuntu/server" 4 | state: "directory" 5 | mode: 0755 6 | 7 | - name: "Copy compressed app folder" 8 | copy: 9 | src: "artifact.zip" 10 | dest: "/home/ubuntu/server/artifact.zip" 11 | 12 | - name: "Extract app" 13 | ansible.builtin.unarchive: 14 | remote_src: yes 15 | src: "/home/ubuntu/server/artifact.zip" 16 | dest: "/home/ubuntu/server" 17 | 18 | - name: Pull flask template image 19 | community.docker.docker_image: 20 | name: "{{ flask_template_image }}" 21 | tag: "{{ flask_template_tag }}" 22 | source: pull 23 | 24 | - name: Run docker compose 25 | become: True 26 | shell: 27 | chdir: /home/ubuntu/server 28 | cmd: "docker compose up -d" 29 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | 5 | import manage 6 | from app.blueprint import register_routing 7 | from app.db import db 8 | from app.extention import cors, migrate 9 | from app.utils.auth import jwt 10 | from app.utils.logging import configure_logging 11 | 12 | 13 | def create_app(settings_module): 14 | app = Flask(__name__) 15 | app.config.from_object(settings_module) 16 | 17 | # Initialize the extensions 18 | db.init_app(app) 19 | migrate.init_app(app, db) 20 | jwt.init_app(app) 21 | cors.init_app(app, supports_credentials="true", resources={r"*": {"origins": "*"}}) 22 | manage.init_app(app) 23 | 24 | # Logging configuration 25 | configure_logging(app) 26 | 27 | # Register Blueprint 28 | register_routing(app) 29 | 30 | return app 31 | 32 | 33 | settings_module = os.getenv("APP_SETTINGS_MODULE") 34 | app = create_app(settings_module) 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | api_service: 4 | container_name: api_container 5 | image: vectornguyen76/flask_template_image 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | env_file: 10 | - .env.api.local 11 | ports: 12 | - 5000:5000 13 | volumes: 14 | - ./logs:/app/logs 15 | depends_on: 16 | - db_service 17 | 18 | db_service: 19 | container_name: db_container 20 | image: postgres:14.1 21 | env_file: 22 | - .env.db.local 23 | ports: 24 | - 5432:5432 25 | volumes: 26 | - postgresql_data:/var/lib/postgresql/data/ 27 | 28 | nginx_service: 29 | container_name: nginx_container 30 | image: nginx:1.25.1-alpine 31 | ports: 32 | - 80:80 33 | volumes: 34 | - ./nginx/default.conf:/etc/nginx/conf.d/default.conf 35 | - ./nginx/log:/var/log/nginx 36 | depends_on: 37 | - api_service 38 | volumes: 39 | postgresql_data: 40 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Start run entrypoint script..." 4 | 5 | echo "Database:" $DATABASE 6 | 7 | if [ "$DATABASE" = "postgres" ] 8 | then 9 | echo "Waiting for postgres..." 10 | 11 | while ! psql -h $BBDD_HOST -U $POSTGRES_USER -d $POSTGRES_DB 12 | do 13 | echo "Waiting for PostgreSQL..." 14 | sleep 0.5 15 | done 16 | 17 | echo "PostgreSQL started" 18 | fi 19 | 20 | echo "Enviroment:" $APP_ENV 21 | 22 | if [ "$APP_ENV" = "local" ]; then 23 | echo "Start create database" 24 | flask create-db 25 | echo "Done create database" 26 | 27 | echo "Start check user-admin" 28 | flask create-user-admin 29 | echo "Done init user-admin" 30 | 31 | echo "Run app with gunicorn server..." 32 | gunicorn -c ./gunicorn/gunicorn_config.py $API_ENTRYPOINT; 33 | fi 34 | 35 | if [ "$APP_ENV" = "production" ]; then 36 | echo "Run app with gunicorn server..." 37 | gunicorn --bind $API_HOST:$API_PORT $API_ENTRYPOINT --timeout 10 --workers 4; 38 | fi 39 | -------------------------------------------------------------------------------- /app/routers/user_role_router.py: -------------------------------------------------------------------------------- 1 | from flask.views import MethodView 2 | from flask_jwt_extended import jwt_required 3 | from flask_smorest import Blueprint 4 | 5 | from app.schemas.role_permission_schema import RoleSchema 6 | from app.schemas.user_schema import UserAndRoleSchema 7 | from app.services import user_role_service 8 | from app.utils.decorators import permission_required 9 | 10 | blp = Blueprint("User And Role", __name__, description="User And Role API") 11 | 12 | 13 | @blp.route("/user//role/") 14 | class LinkRolesToUser(MethodView): 15 | @jwt_required() 16 | @permission_required(permission_name="write") 17 | @blp.response(201, RoleSchema) 18 | def post(self, user_id, role_id): 19 | result = user_role_service.link_roles_to_user(user_id, role_id) 20 | return result 21 | 22 | @jwt_required() 23 | @permission_required(permission_name="delete") 24 | @blp.response(200, UserAndRoleSchema) 25 | def delete(self, user_id, role_id): 26 | result = user_role_service.delete_roles_to_user(user_id, role_id) 27 | return result 28 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: "^\ 2 | (third-party/.*)\ 3 | " 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.1.0 8 | hooks: 9 | - id: check-merge-conflict # checks for some markers such as "<<<<<<<", "=======", and ">>>>>>>". 10 | - id: detect-private-key # detects the presence of private keys. 11 | - id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline. 12 | - id: requirements-txt-fixer # sorts entries in requirements.txt. 13 | - id: trailing-whitespace # trims trailing whitespace at the end of lines. 14 | 15 | # Format YAML and other files 16 | - repo: https://github.com/pre-commit/mirrors-prettier 17 | rev: v2.5.1 18 | hooks: 19 | - id: prettier 20 | files: \.(js|ts|jsx|tsx|css|less|html|json|markdown|md|yaml|yml)$ 21 | 22 | # Sort the order of importing libs 23 | - repo: https://github.com/PyCQA/isort 24 | rev: 5.12.0 25 | hooks: 26 | - id: isort 27 | args: [--profile=black] 28 | 29 | # Format Python files 30 | - repo: https://github.com/psf/black 31 | rev: 23.7.0 32 | hooks: 33 | - id: black 34 | 35 | - repo: https://github.com/PyCQA/flake8 36 | rev: 6.1.0 37 | hooks: 38 | - id: flake8 39 | -------------------------------------------------------------------------------- /app/services/role_permission_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask_smorest import abort 4 | 5 | from app.db import db 6 | from app.models.permission_model import PermissionModel 7 | from app.models.role_model import RoleModel 8 | from app.models.role_permission_model import RolePermissionModel 9 | 10 | # Create logger for this module 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def get_role_permission(): 15 | results = RolePermissionModel.query.all() 16 | return results 17 | 18 | 19 | def update_roles_to_permission(permission_data): 20 | for permission_id in permission_data["data_update"]: 21 | permission = PermissionModel.query.filter_by(id=int(permission_id)).first() 22 | 23 | permission.roles = [] 24 | 25 | for role_id in permission_data["data_update"][permission_id]: 26 | role = RoleModel.query.filter_by(id=role_id).first() 27 | permission.roles.append(role) 28 | try: 29 | db.session.add(permission) 30 | db.session.commit() 31 | except Exception as ex: 32 | logger.error(f"An error occurred while inserting the roles. Error: {ex}") 33 | abort( 34 | 500, message=f"An error occurred while inserting the roles. Error: {ex}" 35 | ) 36 | return {"message": "Update successfully!"} 37 | -------------------------------------------------------------------------------- /app/utils/decorators.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from functools import wraps 4 | 5 | from flask_jwt_extended import get_jwt 6 | from flask_smorest import abort 7 | 8 | from app.models import UserModel 9 | 10 | # Create logger for this module 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def permission_required(permission_name): 15 | def decorator(func): 16 | def wrapper(*arg, **kwargs): 17 | jwt_data = get_jwt() 18 | user_id = jwt_data["sub"] 19 | user = UserModel.query.filter_by(id=user_id).first() 20 | for role in user.roles: 21 | for permission in role.permissions: 22 | if permission.name == permission_name: 23 | return func(*arg, **kwargs) 24 | logger.error("User does not have permission to access this API!") 25 | abort(403, message="User does not have permission to access this API!") 26 | 27 | return wrapper 28 | 29 | return decorator 30 | 31 | 32 | def time_profiling(func): 33 | def wrapper(*args, **kwargs): 34 | start_time = time.time() 35 | result = func(*args, **kwargs) 36 | end_time = time.time() 37 | logger.info(f"Function {func.__name__} took {end_time-start_time:.4f}s.") 38 | return result 39 | 40 | return wrapper 41 | -------------------------------------------------------------------------------- /gunicorn/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import os 3 | 4 | host = os.getenv("API_HOST", "0.0.0.0") 5 | port = os.getenv("API_PORT", "5000") 6 | bind_env = os.getenv("BIND", None) 7 | 8 | use_bind = bind_env if bind_env else f"{host}:{port}" 9 | 10 | workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") 11 | max_workers_str = os.getenv("MAX_WORKERS") 12 | web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) 13 | 14 | cores = multiprocessing.cpu_count() 15 | workers_per_core = int(workers_per_core_str) 16 | default_web_concurrency = workers_per_core * cores + 1 17 | 18 | if web_concurrency_str: 19 | web_concurrency = int(web_concurrency_str) 20 | assert web_concurrency > 0 21 | else: 22 | web_concurrency = max(int(default_web_concurrency), 2) 23 | if max_workers_str: 24 | use_max_workers = int(max_workers_str) 25 | web_concurrency = min(web_concurrency, use_max_workers) 26 | 27 | graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120") 28 | timeout_str = os.getenv("TIMEOUT", "120") 29 | keepalive_str = os.getenv("KEEP_ALIVE", "5") 30 | use_loglevel = os.getenv("LOG_LEVEL", "info") 31 | 32 | # Gunicorn config variables 33 | loglevel = use_loglevel 34 | workers = web_concurrency 35 | bind = use_bind 36 | worker_tmp_dir = "/dev/shm" 37 | graceful_timeout = int(graceful_timeout_str) 38 | timeout = int(timeout_str) 39 | keepalive = int(keepalive_str) 40 | -------------------------------------------------------------------------------- /app/services/user_role_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask_smorest import abort 4 | 5 | from app.db import db 6 | from app.models.role_model import RoleModel 7 | from app.models.user_model import UserModel 8 | 9 | # Create logger for this module 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def link_roles_to_user(user_id, role_id): 14 | user = UserModel.query.filter_by(id=user_id).first() 15 | role = RoleModel.query.filter_by(id=role_id).first() 16 | 17 | user.roles.append(role) 18 | 19 | try: 20 | db.session.add(user) 21 | db.session.commit() 22 | except Exception as ex: 23 | logger.error(f"An error occurred while inserting the role. Error: {ex}") 24 | abort(500, message=f"An error occurred while inserting the role. Error: {ex}") 25 | 26 | return role 27 | 28 | 29 | def delete_roles_to_user(user_id, role_id): 30 | user = UserModel.query.filter_by(id=user_id).first() 31 | role = RoleModel.query.filter_by(id=role_id).first() 32 | 33 | user.roles.remove(role) 34 | 35 | try: 36 | db.session.add(user) 37 | db.session.commit() 38 | except Exception as ex: 39 | logger.error(f"An error occurred while deleting the role. Error: {ex}") 40 | abort(500, message=f"An error occurred while deleting the role. Error: {ex}") 41 | 42 | return {"message": "User removed from role", "user": user, "role": role} 43 | -------------------------------------------------------------------------------- /app/utils/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | import pytz 5 | from flask import Flask 6 | 7 | 8 | def configure_logging(app: Flask): 9 | # Create a logger instance 10 | logger = logging.getLogger(app.name) 11 | 12 | # Set the logging level 13 | logger.setLevel(logging.DEBUG) 14 | 15 | # Set the timezone to Vietnam 16 | vietnam_timezone = pytz.timezone("Asia/Ho_Chi_Minh") 17 | 18 | # Configure logging with the Vietnam timezone 19 | logging.Formatter.converter = ( 20 | lambda *args: pytz.utc.localize(datetime.utcnow()) 21 | .astimezone(vietnam_timezone) 22 | .timetuple() 23 | ) 24 | 25 | # Define the log format 26 | console_log_format = "%(asctime)s - %(levelname)s - %(message)s" 27 | file_log_format = ( 28 | "%(asctime)s - %(levelname)s - %(message)s - (%(filename)s:%(lineno)d)" 29 | ) 30 | 31 | # Create a console handler 32 | console_handler = logging.StreamHandler() 33 | console_handler.setLevel(logging.DEBUG) 34 | console_handler.setFormatter( 35 | logging.Formatter(console_log_format, datefmt=app.config["DATE_FMT"]) 36 | ) 37 | logger.addHandler(console_handler) 38 | 39 | # Create a file handler 40 | file_handler = logging.FileHandler( 41 | filename=app.config["LOG_FILE_API"], encoding="utf-8" 42 | ) 43 | file_handler.setLevel(logging.DEBUG) 44 | file_handler.setFormatter( 45 | logging.Formatter(file_log_format, datefmt=app.config["DATE_FMT"]) 46 | ) 47 | logger.addHandler(file_handler) 48 | -------------------------------------------------------------------------------- /tests/unit/tests_users_unit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from passlib.hash import pbkdf2_sha256 5 | 6 | from app import create_app, db 7 | from app.models import UserModel 8 | 9 | 10 | class UsersUnitTests(unittest.TestCase): 11 | def setUp(self): 12 | """ 13 | This method runs once before any test in this class. 14 | It sets up the application context and creates the necessary database tables. 15 | """ 16 | self.app = create_app( 17 | settings_module=os.environ.get("APP_TEST_SETTINGS_MODULE") 18 | ) 19 | with self.app.app_context(): 20 | db.create_all() 21 | 22 | def tearDown(self): 23 | """ 24 | This method runs once after all tests in this class have been executed. 25 | It removes the database session and drops the database tables. 26 | """ 27 | with self.app.app_context(): 28 | db.session.remove() 29 | db.drop_all() 30 | 31 | def test_create_user_success(self): 32 | """ 33 | Test case to check if creating a user is successful. 34 | """ 35 | # Given 36 | username = "test_user" 37 | password = "123456" 38 | 39 | # When 40 | with self.app.app_context(): 41 | user = UserModel(username=username, password=pbkdf2_sha256.hash(password)) 42 | 43 | # Then 44 | # Assertions to check if the user object is created correctly 45 | self.assertEqual(username, user.username) 46 | self.assertTrue(pbkdf2_sha256.verify(password, user.password)) 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /.github/workflows/ansible/deploy_applications.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Deploy applications" 3 | hosts: web 4 | user: ubuntu 5 | become: true 6 | gather_facts: false 7 | vars: 8 | - ansible_python_interpreter: /usr/bin/python3 9 | - ansible_host_key_checking: false 10 | - ansible_stdout_callback: yaml 11 | 12 | - flask_template_image: vectornguyen76/flask_template_image 13 | - flask_template_tag: latest 14 | 15 | pre_tasks: 16 | - name: "wait 600 seconds for target connection to become reachable/usable." 17 | wait_for_connection: 18 | 19 | - name: Install aptitude 20 | apt: 21 | name: aptitude 22 | state: latest 23 | update_cache: true 24 | 25 | - name: Install required system packages 26 | apt: 27 | pkg: 28 | - apt-transport-https 29 | - ca-certificates 30 | - curl 31 | - software-properties-common 32 | - python3-pip 33 | - virtualenv 34 | - python3-setuptools 35 | - unzip 36 | state: latest 37 | update_cache: true 38 | 39 | - name: Add Docker GPG apt Key 40 | apt_key: 41 | url: https://download.docker.com/linux/ubuntu/gpg 42 | state: present 43 | 44 | - name: Add Docker Repository 45 | apt_repository: 46 | repo: deb https://download.docker.com/linux/ubuntu jammy stable 47 | state: present 48 | 49 | - name: Update apt and install docker-ce 50 | apt: 51 | name: docker-ce 52 | state: latest 53 | update_cache: true 54 | 55 | - name: Install Docker Module for Python 56 | pip: 57 | name: docker 58 | roles: 59 | - deploy 60 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.10.2 2 | apispec==6.3.0 3 | asttokens==2.2.1 4 | backcall==0.2.0 5 | black==23.9.1 6 | blinker==1.5 7 | certifi==2023.5.7 8 | click==8.1.3 9 | colorama==0.4.6 10 | comm==0.1.2 11 | coverage==7.2.7 12 | debugpy==1.6.4 13 | decorator==5.1.1 14 | distlib==0.3.6 15 | entrypoints==0.4 16 | executing==1.2.0 17 | filelock==3.12.2 18 | flake8==6.1.0 19 | Flask==2.2.5 20 | Flask-Cors==3.0.10 21 | Flask-JWT-Extended==4.4.4 22 | Flask-Migrate==4.0.4 23 | Flask-Script==2.0.5 24 | flask-smorest==0.40.0 25 | Flask-SQLAlchemy==3.0.3 26 | greenlet==2.0.2 27 | gunicorn==20.1.0 28 | importlib-metadata==6.7.0 29 | ipykernel==6.19.2 30 | ipython==8.10.0 31 | isort==5.12.0 32 | itsdangerous==2.1.2 33 | jedi==0.18.2 34 | Jinja2==3.1.2 35 | jupyter_client==7.4.8 36 | jupyter_core==5.1.0 37 | Mako==1.2.4 38 | MarkupSafe==2.1.2 39 | marshmallow==3.19.0 40 | matplotlib-inline==0.1.6 41 | mccabe==0.7.0 42 | mypy-extensions==1.0.0 43 | nest-asyncio==1.5.6 44 | packaging==22.0 45 | parso==0.8.3 46 | passlib==1.7.4 47 | pathspec==0.12.1 48 | pexpect==4.8.0 49 | pickleshare==0.7.5 50 | pipenv==2023.7.3 51 | platformdirs==3.8.0 52 | prompt-toolkit==3.0.36 53 | psutil==5.9.4 54 | psycopg2-binary==2.9.6 55 | ptyprocess==0.7.0 56 | pure-eval==0.2.2 57 | pycodestyle==2.11.1 58 | pyflakes==3.1.0 59 | Pygments==2.13.0 60 | PyJWT==2.6.0 61 | python-dateutil==2.8.2 62 | python-dotenv==1.0.0 63 | pytz==2023.3 64 | pyzmq==24.0.1 65 | six==1.16.0 66 | SQLAlchemy==2.0.7 67 | stack-data==0.6.2 68 | tomli==2.0.1 69 | tornado==6.3.2 70 | traitlets==5.7.1 71 | typing_extensions==4.5.0 72 | virtualenv==20.23.1 73 | virtualenv-clone==0.5.7 74 | wcwidth==0.2.5 75 | webargs==8.2.0 76 | Werkzeug==2.2.3 77 | wincertstore==0.2 78 | zipp==3.15.0 79 | -------------------------------------------------------------------------------- /app/routers/role_router.py: -------------------------------------------------------------------------------- 1 | from flask.views import MethodView 2 | from flask_jwt_extended import jwt_required 3 | from flask_smorest import Blueprint 4 | 5 | from app.schemas.role_permission_schema import RoleSchema, UpdateRolePermissionSchema 6 | from app.services import role_service 7 | from app.utils.decorators import permission_required 8 | 9 | blp = Blueprint("Role", __name__, description="Role API") 10 | 11 | 12 | @blp.route("/role") 13 | class RoleList(MethodView): 14 | @jwt_required() 15 | @permission_required(permission_name="read") 16 | @blp.response(200, RoleSchema(many=True)) 17 | def get(self): 18 | result = role_service.get_all_role() 19 | return result 20 | 21 | @jwt_required() 22 | @permission_required(permission_name="write") 23 | @blp.arguments(UpdateRolePermissionSchema) 24 | def post(self, qa_history_data): 25 | result = role_service.post_role(qa_history_data) 26 | return result 27 | 28 | 29 | @blp.route("/role/") 30 | class Role(MethodView): 31 | @jwt_required() 32 | @permission_required(permission_name="read") 33 | @blp.response(200, RoleSchema) 34 | def get(self, role_id): 35 | result = role_service.get_role(role_id) 36 | return result 37 | 38 | @jwt_required() 39 | @permission_required(permission_name="write") 40 | @blp.arguments(UpdateRolePermissionSchema) 41 | def put(self, role_data, role_id): 42 | result = role_service.update_role(role_data, role_id) 43 | return result 44 | 45 | @jwt_required() 46 | @permission_required(permission_name="delete") 47 | def delete(self, role_id): 48 | result = role_service.delete_role(role_id) 49 | return result 50 | -------------------------------------------------------------------------------- /tests/integration/tests_users_integration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from passlib.hash import pbkdf2_sha256 5 | 6 | from app import create_app, db 7 | from app.models import UserModel 8 | 9 | 10 | class UsersUnitTests(unittest.TestCase): 11 | def setUp(self): 12 | """ 13 | This method runs once before any test in this class. 14 | It sets up the application context and creates the necessary database tables. 15 | """ 16 | self.app = create_app( 17 | settings_module=os.environ.get("APP_TEST_SETTINGS_MODULE") 18 | ) 19 | with self.app.app_context(): 20 | db.create_all() 21 | 22 | def tearDown(self): 23 | """ 24 | This method runs once after all tests in this class have been executed. 25 | It removes the database session and drops the database tables. 26 | """ 27 | with self.app.app_context(): 28 | db.session.remove() 29 | db.drop_all() 30 | 31 | def test_create_user(self): 32 | """ 33 | Test case to check if creating a user is successful. 34 | """ 35 | 36 | with self.app.app_context(): 37 | username = "test_user" 38 | password = "123456" 39 | 40 | user = UserModel(username=username, password=pbkdf2_sha256.hash(password)) 41 | 42 | # Add to database 43 | db.session.add(user) 44 | db.session.commit() 45 | 46 | # Assertions to check if the user object is created correctly 47 | self.assertEqual(username, user.username) 48 | self.assertTrue(pbkdf2_sha256.verify(password, user.password)) 49 | 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /app/services/permission_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask_smorest import abort 4 | from sqlalchemy import asc 5 | 6 | from app.db import db 7 | from app.models.permission_model import PermissionModel 8 | 9 | # Create logger for this module 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def get_all_permission(): 14 | results = PermissionModel.query.order_by(asc(PermissionModel.id)).all() 15 | return results 16 | 17 | 18 | def post_permission(permission_data): 19 | name = permission_data["name"] 20 | description = permission_data["description"] 21 | 22 | try: 23 | new_row = PermissionModel(name=name, description=description) 24 | 25 | db.session.add(new_row) 26 | db.session.commit() 27 | except Exception as ex: 28 | db.session.rollback() 29 | logger.error(f"Can not add permission! Error: {ex}") 30 | abort(400, message=f"Can not add permission! Error: {ex}") 31 | 32 | return {"message": "Add successfully!"} 33 | 34 | 35 | def get_permission(permission_id): 36 | results = PermissionModel.query.filter_by(id=permission_id).first() 37 | return results 38 | 39 | 40 | def update_permission(permission_data, permission_id): 41 | permission = PermissionModel.query.filter_by(id=permission_id).first() 42 | 43 | if not permission: 44 | logger.error("permission doesn't exist, cannot update!") 45 | abort(400, message="permission doesn't exist, cannot update!") 46 | 47 | try: 48 | if permission_data["name"]: 49 | permission.name = permission_data["name"] 50 | 51 | if permission_data["description"]: 52 | permission.description = permission_data["description"] 53 | 54 | db.session.commit() 55 | except Exception as ex: 56 | db.session.rollback() 57 | logger.error(f"Can not update permission! Error: {ex}") 58 | abort(400, message=f"Can not update permission! Error: {ex}") 59 | 60 | return {"message": "Update successfully!"} 61 | -------------------------------------------------------------------------------- /.github/workflows/development_pipeline.yml: -------------------------------------------------------------------------------- 1 | name: development 2 | on: 3 | pull_request: 4 | branches: 5 | - develop 6 | 7 | push: 8 | branches: 9 | - develop 10 | jobs: 11 | build-test: 12 | runs-on: ubuntu-latest 13 | services: 14 | postgres: 15 | image: postgres:13.3 16 | env: 17 | POSTGRES_USER: db_user 18 | POSTGRES_PASSWORD: db_password 19 | POSTGRES_DB: db_test 20 | ports: 21 | - 5432:5432 22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 23 | steps: 24 | - name: Checkout Repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: "3.10" 31 | cache: "pip" 32 | 33 | - name: Install Python dependencies 34 | run: python -m pip install -r requirements.txt 35 | 36 | - name: Run isort 37 | run: isort --check-only --profile=black . 38 | 39 | - name: Run black 40 | run: black --check . 41 | 42 | - name: Run flake8 43 | run: flake8 --ignore=E501,W503,F401 . 44 | 45 | - name: Unit Tests and Integration Tests 46 | env: 47 | DATABASE_TEST_URL: postgresql://db_user:db_password@localhost/db_test 48 | run: python -m flask tests 49 | 50 | - name: Set up Docker Buildx 51 | uses: docker/setup-buildx-action@v2 52 | 53 | - name: Login to Docker Hub 54 | id: docker_hub_auth 55 | uses: docker/login-action@v2 56 | with: 57 | username: ${{ secrets.DOCKERHUB_USERNAME }} 58 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 59 | 60 | - name: Build and push 61 | uses: docker/build-push-action@v4 62 | with: 63 | context: . 64 | push: true 65 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/flask_template_image:latest 66 | cache-from: type=gha 67 | cache-to: type=gha,mode=max 68 | -------------------------------------------------------------------------------- /app/routers/permission_router.py: -------------------------------------------------------------------------------- 1 | from flask.views import MethodView 2 | from flask_jwt_extended import jwt_required 3 | from flask_smorest import Blueprint 4 | 5 | from app.schemas.permission_schema import UpdatePermissionRoleSchema 6 | from app.schemas.role_permission_schema import PermissionSchema 7 | from app.services import permission_service, role_permission_service 8 | from app.utils.decorators import permission_required 9 | 10 | blp = Blueprint("Permission", __name__, description="Permission API") 11 | 12 | 13 | @blp.route("/permission") 14 | class PermissionList(MethodView): 15 | @jwt_required() 16 | @permission_required(permission_name="read") 17 | @blp.response(200, PermissionSchema(many=True)) 18 | def get(self): 19 | result = permission_service.get_all_permission() 20 | return result 21 | 22 | @jwt_required() 23 | @permission_required(permission_name="write") 24 | @blp.arguments(PermissionSchema) 25 | def post(self, qa_history_data): 26 | result = permission_service.post_permission(qa_history_data) 27 | return result 28 | 29 | 30 | @blp.route("/permission/") 31 | class Permission(MethodView): 32 | @jwt_required() 33 | @permission_required(permission_name="read") 34 | @blp.response(200, PermissionSchema) 35 | def get(self, permission_id): 36 | result = permission_service.get_permission(permission_id) 37 | return result 38 | 39 | @jwt_required() 40 | @permission_required(permission_name="write") 41 | @blp.arguments(PermissionSchema) 42 | def put(self, permission_data, permission_id): 43 | result = permission_service.update_permission(permission_data, permission_id) 44 | return result 45 | 46 | 47 | @blp.route("/permission-role-update") 48 | class PermissionRole(MethodView): 49 | @jwt_required() 50 | @permission_required(permission_name="write") 51 | @blp.arguments(UpdatePermissionRoleSchema) 52 | def put(self, permission_data): 53 | result = role_permission_service.update_roles_to_permission(permission_data) 54 | return result 55 | -------------------------------------------------------------------------------- /app/utils/auth.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from app.extention import jwt 4 | from app.models import BlocklistModel, UserModel 5 | 6 | 7 | @jwt.token_verification_loader 8 | def custom_token_verification_callback(jwt_header, jwt_data): 9 | # Query in database 10 | user = UserModel.query.filter_by(id=jwt_data["sub"]).first() 11 | 12 | # If user was blocked, user will not access. 13 | if user.block is True: 14 | return False 15 | 16 | return True 17 | 18 | 19 | @jwt.token_in_blocklist_loader 20 | def check_if_token_in_blocklist(jwt_header, jwt_payload): 21 | return BlocklistModel.query.filter_by(jti_blocklist=jwt_payload["jti"]).first() 22 | 23 | 24 | @jwt.revoked_token_loader 25 | def revoked_token_callback(jwt_header, jwt_payload): 26 | return ( 27 | jsonify( 28 | {"description": "The token has been revoked", "error": "token_revoked"} 29 | ), 30 | 401, 31 | ) 32 | 33 | 34 | @jwt.needs_fresh_token_loader 35 | def token_not_fresh_callback(jwt_header, jwt_payload): 36 | return ( 37 | jsonify( 38 | {"description": "The token is not fresh", "error": "fresh_token_reqired"} 39 | ), 40 | 401, 41 | ) 42 | 43 | 44 | @jwt.additional_claims_loader 45 | def add_claims_to_jwt(identity): 46 | # Look in admin in database 47 | if identity == 1: 48 | return {"is_admin": True} 49 | return {"is_admin": False} 50 | 51 | 52 | @jwt.expired_token_loader 53 | def expired_token_callback(jwt_header, jwt_payload): 54 | return jsonify({"message": "Token has expired.", "error": "token_expired"}), 401 55 | 56 | 57 | @jwt.invalid_token_loader 58 | def invalid_token_callback(error): 59 | return ( 60 | jsonify( 61 | {"message": "Signature verification failed.", "error": "invalid_token"} 62 | ), 63 | 401, 64 | ) 65 | 66 | 67 | @jwt.unauthorized_loader 68 | def miss_token_callback(error): 69 | return ( 70 | jsonify( 71 | { 72 | "description": "Request does not contain an access token", 73 | "error": "authorization_required", 74 | } 75 | ), 76 | 401, 77 | ) 78 | -------------------------------------------------------------------------------- /app/schemas/user_schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | from app.schemas.permission_schema import PlainPermissionSchema 4 | from app.schemas.role_schema import PlainRoleSchema 5 | 6 | 7 | class PlainUserSchema(Schema): 8 | # Dump only: only read 9 | id = fields.Str(dump_only=True) 10 | username = fields.Str(required=True) 11 | password = fields.Str(required=True) 12 | time_created = fields.Str(dump_only=True) 13 | 14 | 15 | class UserUpdateSchema(Schema): 16 | username = fields.Str(allow_none=True, required=True) 17 | password = fields.Str(allow_none=True, required=True) 18 | roles = fields.List(cls_or_instance=fields.Int, required=True) 19 | 20 | 21 | class UserSchema(PlainUserSchema): 22 | block = fields.Bool(dump_only=True) 23 | roles = fields.List(fields.Nested(PlainRoleSchema()), dump_only=True) 24 | 25 | 26 | class UserExportSchema(Schema): 27 | role_id = fields.Int(allow_none=True, required=True) 28 | search_content = fields.Str(allow_none=True, required=True) 29 | 30 | 31 | class UserFilterSchema(UserExportSchema): 32 | page_size = fields.Int(allow_none=True, required=True) 33 | page = fields.Int(allow_none=True, required=True) 34 | 35 | 36 | class UserPageSchema(Schema): 37 | results = fields.List(fields.Nested(UserSchema())) 38 | total_page = fields.Int() 39 | total_user = fields.Int() 40 | 41 | 42 | class UserAndRoleSchema(Schema): 43 | message = fields.Str() 44 | user = fields.Nested(PlainUserSchema) 45 | role = fields.Nested(PlainRoleSchema) 46 | 47 | 48 | class RoleAndPermissionSchema(Schema): 49 | message = fields.Str() 50 | role = fields.Nested(PlainRoleSchema) 51 | permission = fields.Nested(PlainPermissionSchema) 52 | 53 | 54 | class UpdateUserRoleSchema(Schema): 55 | roles = fields.List(cls_or_instance=fields.Int, required=True) 56 | 57 | 58 | class UpdateBlockUserSchema(Schema): 59 | block = fields.Bool(required=True) 60 | 61 | 62 | class CheckUserExistsSchema(Schema): 63 | email = fields.Str(required=True) 64 | 65 | 66 | class UserLoginSchema(Schema): 67 | access_token = fields.Str() 68 | refresh_token = fields.Str() 69 | user = fields.Nested(UserSchema) 70 | -------------------------------------------------------------------------------- /app/routers/user_router.py: -------------------------------------------------------------------------------- 1 | from flask.views import MethodView 2 | from flask_jwt_extended import get_jwt, jwt_required 3 | from flask_smorest import Blueprint 4 | 5 | from app.schemas.user_schema import UpdateBlockUserSchema, UserSchema, UserUpdateSchema 6 | from app.services import user_service 7 | from app.utils.decorators import permission_required 8 | 9 | blp = Blueprint("User", __name__, description="User API") 10 | 11 | 12 | @blp.route("/user") 13 | class UserList(MethodView): 14 | @jwt_required() 15 | @permission_required(permission_name="read") 16 | @blp.response(200, UserSchema(many=True)) 17 | def get(self): 18 | result = user_service.get_all_user() 19 | return result 20 | 21 | 22 | @blp.route("/user/") 23 | class User(MethodView): 24 | @jwt_required() 25 | @permission_required(permission_name="read") 26 | @blp.response(200, UserSchema) 27 | def get(self, user_id): 28 | result = user_service.get_user(user_id) 29 | return result 30 | 31 | @jwt_required() 32 | @permission_required(permission_name="write") 33 | @blp.arguments(UserUpdateSchema) 34 | def put(self, user_data, user_id): 35 | result = user_service.update_user(user_data, user_id) 36 | return result 37 | 38 | 39 | @blp.route("/block-user/") 40 | class BlockUser(MethodView): 41 | @jwt_required() 42 | @permission_required(permission_name="delete") 43 | @blp.arguments(UpdateBlockUserSchema) 44 | def put(self, user_data, user_id): 45 | result = user_service.update_block_user(user_data, user_id) 46 | return result 47 | 48 | 49 | @blp.route("/login") 50 | class Login(MethodView): 51 | @blp.arguments(UserSchema) 52 | def post(self, user_data): 53 | result = user_service.login_user(user_data) 54 | return result 55 | 56 | 57 | @blp.route("/register") 58 | class Register(MethodView): 59 | @blp.arguments(UserSchema) 60 | def post(self, user_data): 61 | result = user_service.register_user(user_data) 62 | return result 63 | 64 | 65 | @blp.route("/logout") 66 | class Logout(MethodView): 67 | @jwt_required() 68 | def post(self): 69 | # Block access_token 70 | jti = get_jwt()["jti"] 71 | user_service.add_jti_blocklist(jti) 72 | 73 | return {"message": "Logout successfully!"} 74 | 75 | 76 | @blp.route("/refresh") 77 | class Refresh(MethodView): 78 | @jwt_required(refresh=True) 79 | def post(self): 80 | result = user_service.refresh_token() 81 | 82 | return result 83 | -------------------------------------------------------------------------------- /app/services/role_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask_smorest import abort 4 | from sqlalchemy import asc 5 | 6 | from app.db import db 7 | from app.models.permission_model import PermissionModel 8 | from app.models.role_model import RoleModel 9 | from app.models.role_permission_model import RolePermissionModel 10 | from app.models.user_role_model import UserRoleModel 11 | 12 | # Create logger for this module 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def get_all_role(): 17 | results = RoleModel.query.order_by(asc(RoleModel.id)).all() 18 | return results 19 | 20 | 21 | def post_role(role_data): 22 | name = role_data["name"] 23 | description = role_data["description"] 24 | 25 | try: 26 | new_row = RoleModel(name=name, description=description) 27 | 28 | # Add Permission 29 | for permission_id in role_data["permissions"]: 30 | permission = PermissionModel.query.filter_by(id=permission_id).first() 31 | new_row.permissions.append(permission) 32 | 33 | db.session.add(new_row) 34 | db.session.commit() 35 | except Exception as ex: 36 | db.session.rollback() 37 | logger.error(f"Can not add role! Error: {ex}") 38 | abort(400, message=f"Can not add role! Error: {ex}") 39 | 40 | return {"message": "Add successfully!"} 41 | 42 | 43 | def get_role(role_id): 44 | results = RoleModel.query.filter_by(id=role_id).first() 45 | return results 46 | 47 | 48 | def update_role(role_data, role_id): 49 | role = RoleModel.query.filter_by(id=role_id).first() 50 | 51 | if not role: 52 | logger.error("role doesn't exist, cannot update!") 53 | abort(400, message="role doesn't exist, cannot update!") 54 | 55 | # Updete role 56 | try: 57 | role.permissions = [] 58 | 59 | for permission_id in role_data["permissions"]: 60 | permission = PermissionModel.query.filter_by(id=permission_id).first() 61 | role.permissions.append(permission) 62 | 63 | if role_data["name"]: 64 | role.name = role_data["name"] 65 | 66 | if role_data["description"]: 67 | role.description = role_data["description"] 68 | 69 | db.session.add(role) 70 | db.session.commit() 71 | except Exception as ex: 72 | db.session.rollback() 73 | logger.error(f"Can not update role! Error: {ex}") 74 | abort(400, message=f"Can not update role! Error: {ex}") 75 | 76 | return {"message": "Update successfully!"} 77 | 78 | 79 | def delete_role(role_id): 80 | RolePermissionModel.query.filter_by(role_id=role_id).delete() 81 | UserRoleModel.query.filter_by(role_id=role_id).delete() 82 | role = RoleModel.query.filter_by(id=role_id).delete() 83 | 84 | if not role: 85 | logger.error("Role doesn't exist, cannot delete!") 86 | abort(400, message="Role doesn't exist, cannot delete!") 87 | 88 | db.session.commit() 89 | return {"message": "Delete successfully!"} 90 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # CI/CD Deploy 2 | 3 | ## Set up Github Actions 4 | 5 | 1. Create _Secrets_ on Github: 6 | 7 | - **AWS_ACCESS_KEY_ID**: access token 8 | - **AWS_SECRET_ACCESS_KEY**: secret access 9 | - **SSH_PRIVATE_KEY**: ssh key pair 10 | - **DOCKERHUB_USERNAME**: dockerhub username 11 | - **DOCKERHUB_PASSWORD**: dockerhub password 12 | 13 | 2. Create _Variables_ on Github: 14 | 15 | - **TAGS**: Tag for resources 16 | 17 | Example: 18 | 19 | ```sh 20 | [{ "Key": "ApplicationName", "Value": "Template_Application" }, 21 | { "Key": "Creator", "Value": "VectorNguyen" }] 22 | ``` 23 | 24 | ## Workflows 25 | 26 | ### Development - Build and Unittest 27 | 28 | #### File: [development_pipeline.yml](development_pipeline.yml) 29 | 30 | **Event:** On Pull Request → any branch into develop 31 | 32 | **Jobs:** 33 | 34 | - Install dependencies (caches) 35 | - Run isort 36 | - Run black 37 | - Run flake8 38 | - Build images (caches) 39 | - Push images to Docker Hub 40 | 41 | **Description:** 42 | This workflow is triggered on Pull Requests into the develop branch. It ensures a clean and standardized codebase by installing dependencies, checking code formatting with isort, black, and flake8, and finally building and pushing Docker images to Docker Hub. 43 | 44 | ### Staging - CI/CD Pipeline 45 | 46 | #### File: [staging_pipeline.yml](staging_pipeline.yml) 47 | 48 | **Event:** On Pull Request → any branch into staging 49 | 50 | **Jobs:** 51 | 52 | - Install dependencies (caches) 53 | - Run isort 54 | - Run black 55 | - Run flake8 56 | - Build images (caches) 57 | - Push images to Docker Hub 58 | - Create infrastructure 59 | - Configure infrastructure 60 | - Deploy application using Docker Compose 61 | - Clean up following the concept of A/B deploy 62 | 63 | **Description:** 64 | This pipeline is designed for the staging environment and is triggered on Pull Requests into the staging branch. It includes steps to ensure code quality, build and push Docker images, create and configure necessary infrastructure, and deploy the application using Docker Compose. The cleanup process follows the A/B deployment concept. 65 | 66 | ### Production - CI/CD Pipeline 67 | 68 | #### File: [production_pipeline.yml](production_pipeline.yml) 69 | 70 | **Event:** On Pull Request → any branch into master 71 | 72 | **Jobs:** 73 | 74 | - Install dependencies (caches) 75 | - Run isort 76 | - Run black 77 | - Run flake8 78 | - Build images (caches) 79 | - Push images to Docker Hub 80 | - Create infrastructure 81 | - Configure infrastructure 82 | - Deploy application using Docker Compose 83 | - Clean up following the concept of A/B deploy 84 | 85 | **Description:** 86 | The production pipeline is triggered on Pull Requests into the master branch, indicating changes are ready for deployment to the production environment. It follows a similar process to the staging pipeline but is specifically tailored for the production environment. The cleanup process adheres to the A/B deployment concept, ensuring a smooth transition between versions. 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 87 | __pypackages__/ 88 | 89 | # Celery stuff 90 | celerybeat-schedule 91 | celerybeat.pid 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Environment variable 97 | # .env 98 | # .env* 99 | 100 | # Environments 101 | .venv/ 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # It's better to unpack these files and commit the raw source because 128 | # git has its own built in compression methods. 129 | *.7z 130 | *.jar 131 | *.rar 132 | *.zip 133 | *.gz 134 | *.gzip 135 | *.tgz 136 | *.bzip 137 | *.bzip2 138 | *.bz2 139 | *.xz 140 | *.lzma 141 | *.cab 142 | *.xar 143 | 144 | # Packing-only formats 145 | *.iso 146 | *.tar 147 | 148 | # Package management formats 149 | *.dmg 150 | *.xpi 151 | *.gem 152 | *.egg 153 | *.deb 154 | *.rpm 155 | *.msi 156 | *.msm 157 | *.msp 158 | *.txz 159 | 160 | # Backup 161 | *.bak 162 | *.gho 163 | *.ori 164 | *.orig 165 | *.tmp 166 | 167 | # GPG 168 | secring.* 169 | 170 | # OpenSSL-related files best not committed 171 | ## Certificate Authority 172 | *.ca 173 | 174 | ## Certificate 175 | *.crt 176 | 177 | ## Certificate Sign Request 178 | *.csr 179 | 180 | ## Certificate 181 | *.der 182 | 183 | ## Key database file 184 | *.kdb 185 | 186 | ## OSCP request data 187 | *.org 188 | 189 | ## PKCS #12 190 | *.p12 191 | 192 | ## PEM-encoded certificate data 193 | *.pem 194 | 195 | ## Random number seed 196 | *.rnd 197 | 198 | ## SSLeay data 199 | *.ssleay 200 | 201 | ## S/MIME message 202 | *.smime 203 | 204 | # ide 205 | .idea/ 206 | 207 | # others 208 | migrations/ 209 | 210 | # BBDD 211 | *.db 212 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class DefaultConfig: 8 | """ 9 | Default Configuration 10 | """ 11 | 12 | # Flask Configuration 13 | APP_NAME = os.environ.get("APP_NAME") 14 | SECRET_KEY = os.environ.get("SECRET_KEY") 15 | PROPAGATE_EXCEPTIONS = True 16 | DEBUG = False 17 | TESTING = False 18 | 19 | # Configuration of Flask-JWT-Extended 20 | JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY") 21 | # Determines the minutes that the access token remains active 22 | JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(minutes=30) 23 | # Determines the days that the refresh token remains active 24 | JWT_REFRESH_TOKEN_EXPIRES = datetime.timedelta(days=30) 25 | # Algorithm used to generate the token 26 | JWT_ALGORITHM = "HS256" 27 | # Algorithm used to decode the token 28 | JWT_DECODE_ALGORITHMS = "HS256" 29 | # Header that should contain the JWT in a request 30 | JWT_HEADER_NAME = "Authorization" 31 | # Word that goes before the token in the Authorization header in this case empty 32 | JWT_HEADER_TYPE = "Bearer" 33 | # Where to look for a JWT when processing a request. 34 | JWT_TOKEN_LOCATION = "headers" 35 | 36 | # Config API documents 37 | API_TITLE = "Flask REST API Template" 38 | API_VERSION = "v1" 39 | OPENAPI_VERSION = "3.0.3" 40 | OPENAPI_URL_PREFIX = "/" 41 | OPENAPI_SWAGGER_UI_PATH = "/swagger-ui" 42 | OPENAPI_SWAGGER_UI_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/" 43 | 44 | # Database configuration 45 | SQLALCHEMY_TRACK_MODIFICATIONS = False 46 | SHOW_SQLALCHEMY_LOG_MESSAGES = False 47 | 48 | # App Environments 49 | APP_ENV_LOCAL = "local" 50 | APP_ENV_TESTING = "testing" 51 | APP_ENV_DEVELOP = "develop" 52 | APP_ENV_PRODUCTION = "production" 53 | APP_ENV = "" 54 | 55 | # Logging 56 | DATE_FMT = "%Y-%m-%d %H:%M:%S" 57 | LOG_FILE_API = f"{basedir}/logs/api.log" 58 | 59 | 60 | class DevelopConfig(DefaultConfig): 61 | # App environment 62 | APP_ENV = DefaultConfig.APP_ENV_DEVELOP 63 | 64 | # Activate debug mode 65 | DEBUG = True 66 | 67 | # Database configuration 68 | SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") 69 | 70 | 71 | class TestingConfig(DefaultConfig): 72 | # App environment 73 | APP_ENV = DefaultConfig.APP_ENV_TESTING 74 | 75 | # Flask disables error catching during request handling for better error reporting in tests 76 | TESTING = True 77 | 78 | # Activate debug mode 79 | DEBUG = True 80 | 81 | # False to disable CSRF protection during tests 82 | WTF_CSRF_ENABLED = False 83 | 84 | # Logging 85 | LOG_FILE_API = f"{basedir}/logs/api_tests.log" 86 | 87 | # Database configuration 88 | SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_TEST_URL") 89 | 90 | 91 | class LocalConfig(DefaultConfig): 92 | # App environment 93 | APP_ENV = DefaultConfig.APP_ENV_LOCAL 94 | 95 | # Activate debug mode 96 | DEBUG = False 97 | 98 | # Database configuration 99 | SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") 100 | 101 | 102 | class ProductionConfig(DefaultConfig): 103 | # App environment 104 | APP_ENV = DefaultConfig.APP_ENV_PRODUCTION 105 | 106 | # Activate debug mode 107 | DEBUG = False 108 | 109 | # Database configuration 110 | SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") 111 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | 5 | # Github 6 | .github/ 7 | */.github/ 8 | 9 | # Marckdown 10 | *.md 11 | 12 | # CI 13 | .codeclimate.yml 14 | .travis.yml 15 | .taskcluster.yml 16 | 17 | # Docker 18 | docker-compose.yml 19 | .docker 20 | Dockerfile* 21 | .dockerignore 22 | 23 | # Jupyter Notebooks 24 | .ipynb_checkpoints 25 | */.ipynb_checkpoints/* 26 | 27 | # IPython 28 | profile_default/ 29 | ipython_config.py 30 | 31 | # Remove previous ipynb_checkpoints 32 | # git rm -r .ipynb_checkpoints/ 33 | 34 | # OpenSSL-related files best not committed 35 | 36 | ## Certificate Authority 37 | *.ca 38 | 39 | ## Certificate 40 | *.crt 41 | 42 | ## Certificate Sign Request 43 | *.csr 44 | 45 | ## Certificate 46 | *.der 47 | 48 | ## Key database file 49 | *.kdb 50 | 51 | ## OSCP request data 52 | *.org 53 | 54 | ## PKCS #12 55 | *.p12 56 | 57 | ## PEM-encoded certificate data 58 | *.pem 59 | 60 | ## Random number seed 61 | *.rnd 62 | 63 | ## SSLeay data 64 | *.ssleay 65 | 66 | ## S/MIME message 67 | *.smime 68 | 69 | # Byte-compiled / optimized / DLL files 70 | __pycache__/ 71 | **/__pycache__/ 72 | */*/__pycache__/ 73 | */*/*/__pycache__/ 74 | */*/*/*/__pycache__/ 75 | *.py[cod] 76 | **/*.py[cod] 77 | */*/*.py[cod] 78 | */*/*/*.py[cod] 79 | *$py.class 80 | 81 | # C extensions 82 | *.so 83 | 84 | # Distribution / packaging 85 | .Python 86 | env/ 87 | build/ 88 | develop-eggs/ 89 | dist/ 90 | downloads/ 91 | eggs/ 92 | lib/ 93 | lib64/ 94 | parts/ 95 | sdist/ 96 | var/ 97 | *.egg-info/ 98 | .installed.cfg 99 | *.egg 100 | .eggs/ 101 | wheels/ 102 | share/python-wheels/ 103 | MANIFEST 104 | 105 | # Pipenv 106 | Pipfile 107 | Pipfile.lock 108 | 109 | # Installer logs 110 | pip-log.txt 111 | pip-delete-this-directory.txt 112 | 113 | # Unit test / coverage reports 114 | # tests/ 115 | htmlcov/ 116 | .tox/ 117 | .coverage 118 | .cache 119 | nosetests.xml 120 | coverage.xml 121 | .nox/ 122 | .coverage.* 123 | nosetests.xml 124 | coverage.xml 125 | *.cover 126 | *.py,cover 127 | .hypothesis/ 128 | .pytest_cache/ 129 | cover/ 130 | 131 | # Translations 132 | *.mo 133 | *.pot 134 | 135 | # Django stuff: 136 | **/*.log 137 | local_settings.py 138 | db.sqlite3 139 | db.sqlite3-journal 140 | 141 | # Flask stuff: 142 | instance/ 143 | .webassets-cache 144 | 145 | # Scrapy stuff: 146 | .scrapy 147 | 148 | # PyBuilder 149 | .pybuilder/ 150 | target/ 151 | 152 | # Sphinx documentation 153 | docs/_build/ 154 | 155 | # Jupyter Notebook 156 | .ipynb_checkpoints 157 | 158 | # IPython 159 | profile_default/ 160 | ipython_config.py 161 | 162 | # Celery stuff 163 | celerybeat-schedule 164 | celerybeat.pid 165 | 166 | # SageMath parsed files 167 | *.sage.py 168 | 169 | # Virtual environment 170 | .env 171 | .env* 172 | .venv 173 | env/ 174 | venv/ 175 | ENV/ 176 | env.bak/ 177 | venv.bak/ 178 | 179 | # PyCharm 180 | .idea 181 | 182 | # Python mode for VIM 183 | .ropeproject 184 | */.ropeproject 185 | */*/.ropeproject 186 | */*/*/.ropeproject 187 | 188 | # Vim swap files 189 | *.swp 190 | */*.swp 191 | */*/*.swp 192 | */*/*/*.swp 193 | 194 | # Spyder project settings 195 | .spyderproject 196 | .spyproject 197 | 198 | # Rope project settings 199 | .ropeproject 200 | 201 | # mkdocs documentation 202 | /site 203 | 204 | # mypy 205 | .mypy_cache/ 206 | .dmypy.json 207 | dmypy.json 208 | 209 | # Pyre type checker 210 | .pyre/ 211 | 212 | # pytype static type analyzer 213 | .pytype/ 214 | 215 | # Cython debug symbols 216 | cython_debug/ 217 | 218 | # It's better to unpack these files and commit the raw source because 219 | # git has its own built in compression methods. 220 | *.7z 221 | *.jar 222 | *.rar 223 | *.zip 224 | *.gz 225 | *.gzip 226 | *.tgz 227 | *.bzip 228 | *.bzip2 229 | *.bz2 230 | *.xz 231 | *.lzma 232 | *.cab 233 | *.xar 234 | 235 | # Packing-only formats 236 | *.iso 237 | *.tar 238 | 239 | # Package management formats 240 | *.dmg 241 | *.xpi 242 | *.gem 243 | *.egg 244 | *.deb 245 | *.rpm 246 | *.msi 247 | *.msm 248 | *.msp 249 | *.txz 250 | 251 | # Backup 252 | *.bak 253 | *.gho 254 | *.ori 255 | *.orig 256 | *.tmp 257 | 258 | # GPG 259 | secring.* 260 | 261 | # Especiales del proyecto 262 | services/ 263 | 264 | # BBDD 265 | *.db 266 | -------------------------------------------------------------------------------- /.github/workflows/cloudformations/server.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Creates EC2 Server 3 | Parameters: 4 | EnvironmentName: 5 | Description: An environment name that will be prefixed to resource names 6 | Type: String 7 | 8 | VpcCIDR: 9 | Description: "VPC range" 10 | Type: String 11 | Default: "" 12 | 13 | PublicSubnetCIDR: 14 | Description: "Public subnet CIDR block" 15 | Type: String 16 | Default: "" 17 | 18 | InstanceType: 19 | Type: String 20 | Description: "EC2 instance type" 21 | AllowedValues: 22 | - t2.micro 23 | - t3.micro 24 | - t3.medium 25 | 26 | KeyPairName: 27 | Description: "AWS key pair in us-east-1, stored in SSM Parameter Store" 28 | Type: String 29 | Default: "my-keypair" 30 | 31 | AMItoUse: 32 | Description: AMI to use for our base image - Canonical, Ubuntu, 22.04 LTS, amd64 jammy image build on 2023-05-16 33 | Type: String 34 | Default: "ami-053b0d53c279acc90" 35 | 36 | Resources: 37 | VPC: 38 | Type: AWS::EC2::VPC 39 | Properties: 40 | CidrBlock: !Ref VpcCIDR 41 | EnableDnsHostnames: true 42 | EnableDnsSupport: true 43 | Tags: 44 | - Key: Name 45 | Value: !Sub ${EnvironmentName} VPC 46 | 47 | InternetGateway: 48 | Type: AWS::EC2::InternetGateway 49 | Properties: 50 | Tags: 51 | - Key: Name 52 | Value: Internet Gateway 53 | 54 | AttachGateway: 55 | Type: AWS::EC2::VPCGatewayAttachment 56 | Properties: 57 | VpcId: !Ref VPC 58 | InternetGatewayId: !Ref InternetGateway 59 | 60 | PublicSubnet: 61 | Type: AWS::EC2::Subnet 62 | Properties: 63 | AvailabilityZone: !Select [0, !GetAZs ""] 64 | VpcId: !Ref VPC 65 | CidrBlock: !Ref PublicSubnetCIDR 66 | Tags: 67 | - Key: Name 68 | Value: !Sub ${EnvironmentName} Public Subnet 69 | 70 | PublicRouteTable: 71 | Type: AWS::EC2::RouteTable 72 | Properties: 73 | VpcId: !Ref VPC 74 | Tags: 75 | - Key: Name 76 | Value: "Public Route Table" 77 | 78 | SubnetRouteTableAssociationPub: 79 | Type: AWS::EC2::SubnetRouteTableAssociation 80 | Properties: 81 | SubnetId: !Ref PublicSubnet 82 | RouteTableId: !Ref PublicRouteTable 83 | 84 | PublicRouteNATGateway: 85 | Type: AWS::EC2::Route 86 | DependsOn: AttachGateway 87 | Properties: 88 | RouteTableId: !Ref PublicRouteTable 89 | DestinationCidrBlock: "0.0.0.0/0" 90 | GatewayId: !Ref InternetGateway 91 | 92 | SecurityGroup: 93 | Type: AWS::EC2::SecurityGroup 94 | Properties: 95 | GroupName: !Sub ${EnvironmentName}-SecurityGroup 96 | GroupDescription: Allow http to client host 97 | VpcId: !Ref VPC 98 | SecurityGroupIngress: 99 | - IpProtocol: tcp 100 | FromPort: 80 101 | ToPort: 80 102 | CidrIp: 0.0.0.0/0 103 | - IpProtocol: tcp 104 | FromPort: 443 105 | ToPort: 443 106 | CidrIp: 0.0.0.0/0 107 | - IpProtocol: tcp 108 | FromPort: 22 109 | ToPort: 22 110 | CidrIp: 0.0.0.0/0 111 | - IpProtocol: tcp 112 | FromPort: 5000 113 | ToPort: 5000 114 | CidrIp: 0.0.0.0/0 115 | - IpProtocol: tcp 116 | FromPort: 3000 117 | ToPort: 3000 118 | CidrIp: 0.0.0.0/0 119 | SecurityGroupEgress: 120 | - IpProtocol: "-1" 121 | FromPort: -1 122 | ToPort: -1 123 | CidrIp: 0.0.0.0/0 124 | 125 | # Server EC2 Instance 126 | ServerInstance: 127 | Type: AWS::EC2::Instance 128 | Properties: 129 | InstanceType: !Ref InstanceType 130 | ImageId: !Ref AMItoUse 131 | KeyName: !Ref KeyPairName 132 | NetworkInterfaces: 133 | - AssociatePublicIpAddress: true 134 | DeviceIndex: "0" 135 | GroupSet: 136 | - !Ref SecurityGroup 137 | SubnetId: !Ref PublicSubnet 138 | BlockDeviceMappings: 139 | - DeviceName: "/dev/sda1" 140 | Ebs: 141 | VolumeSize: 50 142 | Tags: 143 | - Key: Name 144 | Value: !Sub ${EnvironmentName}-Instance 145 | 146 | Outputs: 147 | EC2InstanceConnection: 148 | Description: The connection for the EC2 instance 149 | Value: !Join 150 | - "" 151 | - - "ubuntu@" 152 | - !GetAtt ServerInstance.PublicIp 153 | EC2InstancePublicDNS: 154 | Description: The instance public DNS name 155 | Value: !GetAtt ServerInstance.PublicDnsName 156 | Export: 157 | Name: !Join 158 | - "" 159 | - - !Ref AWS::StackName 160 | - "-PublicDNS" 161 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import click 4 | import coverage 5 | from passlib.hash import pbkdf2_sha256 6 | 7 | from app.db import db 8 | from app.models import ( 9 | PermissionModel, 10 | RoleModel, 11 | RolePermissionModel, 12 | UserModel, 13 | UserRoleModel, 14 | ) 15 | 16 | 17 | @click.option( 18 | "--pattern", default="tests_*.py", help="Test search pattern", required=False 19 | ) 20 | def cov(pattern): 21 | """ 22 | Run the unit tests with coverage 23 | """ 24 | cov = coverage.coverage(branch=True, include="app/*") 25 | cov.start() 26 | tests = unittest.TestLoader().discover("tests", pattern=pattern) 27 | result = unittest.TextTestRunner(verbosity=2).run(tests) 28 | if result.wasSuccessful(): 29 | cov.stop() 30 | cov.save() 31 | print("Coverage Summary:") 32 | cov.report() 33 | cov.erase() 34 | return 0 35 | return 1 36 | 37 | 38 | @click.option( 39 | "--pattern", default="tests_*.py", help="Test search pattern", required=False 40 | ) 41 | def cov_html(pattern): 42 | """ 43 | Run the unit tests with coverage and generate an HTML report. 44 | """ 45 | cov = coverage.coverage(branch=True, include="app/*") 46 | cov.start() 47 | 48 | tests = unittest.TestLoader().discover("tests", pattern=pattern) 49 | result = unittest.TextTestRunner(verbosity=2).run(tests) 50 | 51 | if result.wasSuccessful(): 52 | cov.stop() 53 | cov.save() 54 | 55 | print("Coverage Summary:") 56 | cov.report() 57 | cov.html_report(directory="report/htmlcov") 58 | cov.erase() 59 | return 0 60 | 61 | return 1 62 | 63 | 64 | @click.option("--pattern", default="tests_*.py", help="Test pattern", required=False) 65 | def tests(pattern): 66 | """ 67 | Run the tests without code coverage 68 | """ 69 | tests = unittest.TestLoader().discover("tests", pattern=pattern) 70 | result = unittest.TextTestRunner(verbosity=2).run(tests) 71 | if result.wasSuccessful(): 72 | return 0 73 | return 1 74 | 75 | 76 | def create_db(): 77 | """ 78 | Create Database. 79 | """ 80 | db.create_all() 81 | db.session.commit() 82 | 83 | 84 | def reset_db(): 85 | """ 86 | Reset Database. 87 | """ 88 | db.drop_all() 89 | db.create_all() 90 | db.session.commit() 91 | 92 | 93 | def drop_db(): 94 | """ 95 | Drop Database. 96 | """ 97 | db.drop_all() 98 | db.session.commit() 99 | 100 | 101 | def init_db_user(): 102 | # Insert Permission 103 | read_perrmission = PermissionModel(name="read", description="Read data") 104 | write_perrmission = PermissionModel(name="write", description="Write data") 105 | delete_perrmission = PermissionModel(name="delete", description="Delete data") 106 | db.session.add_all([read_perrmission, write_perrmission, delete_perrmission]) 107 | db.session.commit() 108 | 109 | # Insert Role 110 | admin_role = RoleModel(name="Admin", description="Full Permission") 111 | user_role = RoleModel(name="User", description="Can read, write data") 112 | guest_role = RoleModel(name="Guest", description="Just read data") 113 | db.session.add_all([admin_role, user_role, guest_role]) 114 | db.session.commit() 115 | 116 | # Insert Role_Permission 117 | role_permission_admin1 = RolePermissionModel(role_id=1, permission_id=1) 118 | role_permission_admin2 = RolePermissionModel(role_id=1, permission_id=2) 119 | role_permission_admin3 = RolePermissionModel(role_id=1, permission_id=3) 120 | role_permission_user1 = RolePermissionModel(role_id=2, permission_id=1) 121 | role_permission_user2 = RolePermissionModel(role_id=2, permission_id=2) 122 | role_permission_guest = RolePermissionModel(role_id=3, permission_id=1) 123 | db.session.add_all( 124 | [ 125 | role_permission_admin1, 126 | role_permission_admin2, 127 | role_permission_admin3, 128 | role_permission_user1, 129 | role_permission_user2, 130 | role_permission_guest, 131 | ] 132 | ) 133 | db.session.commit() 134 | 135 | # Insert User 136 | password = pbkdf2_sha256.hash("123456") 137 | admin_user = UserModel(username="admin", password=password) 138 | normal_user = UserModel(username="user", password=password) 139 | guest_user = UserModel(username="guest", password=password) 140 | db.session.add_all([admin_user, normal_user, guest_user]) 141 | db.session.commit() 142 | 143 | # Insert UserRole 144 | user_role1 = UserRoleModel(user_id=1, role_id=1) 145 | user_role2 = UserRoleModel(user_id=2, role_id=2) 146 | user_role3 = UserRoleModel(user_id=3, role_id=3) 147 | db.session.add_all([user_role1, user_role2, user_role3]) 148 | db.session.commit() 149 | 150 | 151 | def create_user_admin(username="admin"): 152 | """ 153 | Create User Admin. 154 | """ 155 | admin = UserModel.query.filter_by(username=username).first() 156 | 157 | if admin is None: 158 | print("user-admin is not created before!") 159 | init_db_user() 160 | else: 161 | print("user-admin is created!") 162 | 163 | 164 | def init_app(app): 165 | if app.config["APP_ENV"] == "production": 166 | commands = [create_db, reset_db, drop_db, create_user_admin] 167 | else: 168 | commands = [ 169 | create_db, 170 | reset_db, 171 | drop_db, 172 | create_user_admin, 173 | tests, 174 | cov_html, 175 | cov, 176 | ] 177 | 178 | for command in commands: 179 | app.cli.add_command(app.cli.command()(command)) 180 | -------------------------------------------------------------------------------- /app/services/user_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask_jwt_extended import ( 4 | create_access_token, 5 | create_refresh_token, 6 | get_jwt, 7 | get_jwt_identity, 8 | ) 9 | from flask_smorest import abort 10 | from passlib.hash import pbkdf2_sha256 11 | from sqlalchemy import asc 12 | 13 | from app.db import db 14 | from app.models.blocklist_model import BlocklistModel 15 | from app.models.role_model import RoleModel 16 | from app.models.user_model import UserModel 17 | from app.services import user_role_service 18 | 19 | # Create logger for this module 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def get_all_user(): 24 | results = UserModel.query.order_by(asc(UserModel.id)).all() 25 | return results 26 | 27 | 28 | def get_user(user_id): 29 | results = UserModel.query.filter_by(id=user_id).first() 30 | return results 31 | 32 | 33 | def update_user(user_data, user_id): 34 | user = UserModel.query.filter_by(id=user_id).first() 35 | if not user: 36 | logger.error("User doesn't exist, cannot update!") 37 | abort(400, message="User doesn't exist, cannot update!") 38 | 39 | try: 40 | if user_data["username"]: 41 | user.username = user_data["username"] 42 | 43 | if user_data["password"]: 44 | # Hash password 45 | password = pbkdf2_sha256.hash(user_data["password"]) 46 | user.password = password 47 | 48 | # Update roles 49 | user.roles = [] 50 | 51 | for role_id in user_data["roles"]: 52 | role = RoleModel.query.filter_by(id=role_id).first() 53 | user.roles.append(role) 54 | 55 | db.session.commit() 56 | except Exception as ex: 57 | db.session.rollback() 58 | logger.error(f"Can not update! Error: {ex}") 59 | abort(400, message=f"Can not update! Error: {ex}") 60 | 61 | return {"message": "Update successfully!"} 62 | 63 | 64 | def update_block_user(user_data, user_id): 65 | # Only admin can delete user 66 | jwt = get_jwt() 67 | if not jwt.get("is_admin"): 68 | logger.error("Admin privilege requierd.") 69 | abort(401, message="Admin privilege requierd.") 70 | 71 | if user_id == 1: 72 | logger.error("Can not block Super Admin!") 73 | abort(401, message="Can not block Super Admin!") 74 | 75 | user = UserModel.query.filter_by(id=user_id).first() 76 | 77 | if not user: 78 | logger.error("User doesn't exist, cannot update!") 79 | abort(400, message="User doesn't exist, cannot update!") 80 | 81 | try: 82 | # Update status block 83 | user.block = user_data["block"] 84 | 85 | db.session.add(user) 86 | db.session.commit() 87 | except Exception as ex: 88 | db.session.rollback() 89 | logger.error(f"Can not update status block User! Error: {ex}") 90 | abort(400, message=f"Can not update status block User! Error: {ex}") 91 | 92 | return {"message": "Block successfully!"} 93 | 94 | 95 | def delete_user(id): 96 | # Only admin can delete user 97 | jwt = get_jwt() 98 | if not jwt.get("is_admin"): 99 | logger.error("Admin privilege requierd.") 100 | abort(401, message="Admin privilege requierd.") 101 | 102 | result = UserModel.query.filter_by(id=id).delete() 103 | if not result: 104 | logger.error("User doesn't exist, cannot delete!") 105 | abort(400, message="User doesn't exist, cannot delete!") 106 | 107 | db.session.commit() 108 | return {"message": "Delete successfully!"} 109 | 110 | 111 | def login_user(user_data): 112 | # Check user name 113 | user = UserModel.query.filter(UserModel.username == user_data["username"]).first() 114 | 115 | # Verify 116 | if user and pbkdf2_sha256.verify(user_data["password"], user.password): 117 | # Create access_token 118 | access_token = create_access_token(identity=user.id, fresh=True) 119 | 120 | # Create refresh_token 121 | refresh_token = create_refresh_token(identity=user.id) 122 | 123 | logger.info(f"User login successfully! user_name: {user_data['username']}") 124 | 125 | return {"access_token": access_token, "refresh_token": refresh_token} 126 | 127 | logger.error("Invalid credentials.") 128 | abort(401, message="Invalid credentials.") 129 | 130 | 131 | def register_user(user_data): 132 | username = user_data["username"] 133 | password = user_data["password"] 134 | 135 | # Check user name exist 136 | user = UserModel.query.filter(UserModel.username == user_data["username"]).first() 137 | if user: 138 | logger.error("Username already exists.") 139 | abort(400, message="Username already exists.") 140 | 141 | # Hash password 142 | password = pbkdf2_sha256.hash(password) 143 | 144 | new_user = UserModel(username=username, password=password) 145 | 146 | try: 147 | db.session.add(new_user) 148 | db.session.commit() 149 | 150 | except Exception as ex: 151 | db.session.rollback() 152 | logger.error(f"Can not register! Error: {ex}") 153 | abort(400, message=f"Can not register! Error: {ex}") 154 | 155 | try: 156 | # Add default role for user 157 | user_role_service.link_roles_to_user(user_id=new_user.id, role_id=3) 158 | 159 | except Exception as ex: 160 | logger.error(f"Can not register! - Can not add role default. Error: {ex}") 161 | abort(400, message=f"Can not register! - Can not add role default. Error: {ex}") 162 | 163 | return {"message": "Register successfully!"} 164 | 165 | 166 | def refresh_token(): 167 | # Get id current user 168 | current_user_id = get_jwt_identity() 169 | 170 | # Create access_token 171 | access_token = create_access_token(identity=current_user_id, fresh=True) 172 | 173 | # Create refresh_token 174 | refresh_token = create_refresh_token(identity=current_user_id) 175 | 176 | # Block previous access_token 177 | jti = get_jwt()["jti"] 178 | 179 | # Block access token 180 | add_jti_blocklist(jti) 181 | 182 | return {"access_token": access_token, "refresh_token": refresh_token} 183 | 184 | 185 | def add_jti_blocklist(jti): 186 | # Add to blockist when remove jti 187 | new_row = BlocklistModel(jti_blocklist=str(jti)) 188 | 189 | try: 190 | db.session.add(new_row) 191 | db.session.commit() 192 | except Exception as ex: 193 | db.session.rollback() 194 | logger.error(f"Can not add jti! Error: {ex}") 195 | abort(400, message=f"Can not add jti! Error: {ex}") 196 | -------------------------------------------------------------------------------- /.github/workflows/production_pipeline.yml: -------------------------------------------------------------------------------- 1 | name: production 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build-test: 9 | runs-on: ubuntu-latest 10 | services: 11 | postgres: 12 | image: postgres:13.3 13 | env: 14 | POSTGRES_USER: db_user 15 | POSTGRES_PASSWORD: db_password 16 | POSTGRES_DB: db_test 17 | ports: 18 | - 5432:5432 19 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 20 | steps: 21 | - name: Checkout Repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: "3.10" 28 | cache: "pip" 29 | 30 | - name: Install Python dependencies 31 | run: python -m pip install -r requirements.txt 32 | 33 | - name: Run isort 34 | run: isort --check-only --profile=black . 35 | 36 | - name: Run black 37 | run: black --check . 38 | 39 | - name: Run flake8 40 | run: flake8 --ignore=E501,W503,F401 . 41 | 42 | - name: Unit Tests and Integration Tests 43 | env: 44 | DATABASE_TEST_URL: postgresql://db_user:db_password@localhost/db_test 45 | run: python -m flask tests 46 | 47 | - name: Set up Docker Buildx 48 | uses: docker/setup-buildx-action@v2 49 | 50 | - name: Login to Docker Hub 51 | id: docker_hub_auth 52 | uses: docker/login-action@v2 53 | with: 54 | username: ${{ secrets.DOCKERHUB_USERNAME }} 55 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 56 | 57 | - name: Build and push 58 | uses: docker/build-push-action@v4 59 | with: 60 | context: . 61 | push: true 62 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/flask_template_image:latest 63 | cache-from: type=gha 64 | cache-to: type=gha,mode=max 65 | 66 | create-config-infrastructure: 67 | runs-on: ubuntu-latest 68 | needs: 69 | - build-test 70 | steps: 71 | - name: Checkout Repository 72 | uses: actions/checkout@v3 73 | 74 | - name: Declare variables 75 | shell: bash 76 | run: | 77 | echo "SHA_SHORT=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV" 78 | echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/})" >> "$GITHUB_ENV" 79 | 80 | - name: Configure AWS credentials 81 | id: creds 82 | uses: aws-actions/configure-aws-credentials@v1 83 | with: 84 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 85 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 86 | aws-region: us-east-1 87 | 88 | - name: Deploy to AWS CloudFormation 89 | uses: aws-actions/aws-cloudformation-github-deploy@v1 90 | with: 91 | name: flask-template-prod-${{ env.SHA_SHORT }} 92 | template: ./.github/workflows/cloudformations/server.yml 93 | parameter-overrides: "file:///${{ github.workspace }}/.github/workflows/cloudformations/server-parameters.json" 94 | tags: ${{ vars.TAGS }} 95 | 96 | - name: Get Public DNS Server 97 | run: | 98 | # Create file 99 | backend_public_dns=flask-template-prod-${{ env.SHA_SHORT }}-PublicDNS 100 | # Pull the export value 101 | host=$(aws cloudformation list-exports \ 102 | --query "Exports[?Name==\`$backend_public_dns\`].Value" \ 103 | --no-paginate --output text) 104 | 105 | echo $host 106 | # Append the DNS to the inventory file 107 | echo $host >> $(eval echo "./.github/workflows/ansible/hosts") 108 | 109 | cat ./.github/workflows/ansible/hosts 110 | 111 | - name: Zip artifact files 112 | uses: montudor/action-zip@v1 113 | with: 114 | args: zip -qq -r artifact.zip . 115 | 116 | - name: Create files forlder in ansible 117 | run: mkdir -p ./.github/workflows/ansible/roles/deploy/files 118 | 119 | - name: Copy file 120 | uses: canastro/copy-file-action@master 121 | with: 122 | source: "artifact.zip" 123 | target: "./.github/workflows/ansible/roles/deploy/files/artifact.zip" 124 | 125 | - name: Run playbook 126 | uses: dawidd6/action-ansible-playbook@v2 127 | with: 128 | playbook: deploy_applications.yml 129 | directory: ./.github/workflows/ansible 130 | key: ${{secrets.SSH_PRIVATE_KEY}} 131 | options: | 132 | --inventory ./hosts 133 | 134 | - name: Remove stack on fail 135 | if: failure() 136 | run: | 137 | echo flask-template-prod-${{ env.SHA_SHORT }} 138 | # Get stack id for the delete_stack waiter 139 | stack_info=$(aws cloudformation describe-stacks --stack-name flask-template-prod-${{ env.SHA_SHORT }} --query "Stacks[*] | [0].StackId" 2>&1) 140 | if echo $stack_info | grep 'does not exist' > /dev/null 141 | then 142 | echo "Stack does not exist." 143 | echo $stack_info 144 | exit 0 145 | fi 146 | if echo $stack_info | grep 'ValidationError' > /dev/null 147 | then 148 | echo $stack_info 149 | exit 1 150 | else 151 | aws cloudformation delete-stack --stack-name flask-template-prod-${{ env.SHA_SHORT }} 152 | echo $stack_info 153 | aws cloudformation wait stack-delete-complete --stack-name flask-template-prod-${{ env.SHA_SHORT }} 154 | exit 0 155 | fi 156 | 157 | clean-up: 158 | runs-on: ubuntu-latest 159 | needs: 160 | - create-config-infrastructure 161 | steps: 162 | - name: Checkout Repository 163 | uses: actions/checkout@v3 164 | 165 | - name: Declare some variables 166 | shell: bash 167 | run: | 168 | echo "SHA_SHORT=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV" 169 | echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/})" >> "$GITHUB_ENV" 170 | 171 | - name: Configure AWS credentials 172 | id: creds 173 | uses: aws-actions/configure-aws-credentials@v1 174 | with: 175 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 176 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 177 | aws-region: us-east-1 178 | 179 | - name: Fetch stacks and save the old stack name 180 | run: | 181 | # Fetch the stack names 182 | export STACKS=( 183 | $(aws cloudformation list-stacks \ 184 | --query "StackSummaries[*].StackName" \ 185 | --no-paginate --output text \ 186 | --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE 187 | ) 188 | ) 189 | for stack in "${STACKS[@]}"; do 190 | if [[ ! "$stack" =~ "${{ env.SHA_SHORT }}" ]] && [[ "$stack" =~ "flask-template-prod" ]]; then 191 | echo "DESTROY_STACK=$stack" >> "$GITHUB_ENV" 192 | fi 193 | done 194 | 195 | - name: Remove the search engine infrastructure 196 | run: | 197 | # Check if DESTROY_STACK is not set 198 | if [ -z "${{ env.DESTROY_STACK }}" ]; then 199 | echo "DESTROY_STACK is not set" 200 | exit 0 201 | else 202 | echo "DESTROY_STACK is set to ${{ env.DESTROY_STACK }}" 203 | fi 204 | 205 | # Get stack id for the delete_stack waiter 206 | stack_info=$(aws cloudformation describe-stacks --stack-name ${{ env.DESTROY_STACK }} --query "Stacks[*] | [0].StackId" 2>&1) 207 | if echo $stack_info | grep 'does not exist' > /dev/null 208 | then 209 | echo "Stack does not exist." 210 | echo $stack_info 211 | exit 0 212 | fi 213 | if echo $stack_info | grep 'ValidationError' > /dev/null 214 | then 215 | echo $stack_info 216 | exit 1 217 | else 218 | aws cloudformation delete-stack --stack-name ${{ env.DESTROY_STACK }} 219 | echo $stack_info 220 | aws cloudformation wait stack-delete-complete --stack-name ${{ env.DESTROY_STACK }} 221 | exit 0 222 | fi 223 | -------------------------------------------------------------------------------- /.github/workflows/staging_pipeline.yml: -------------------------------------------------------------------------------- 1 | name: staging 2 | on: 3 | pull_request: 4 | branches: 5 | - staging 6 | 7 | jobs: 8 | build-test: 9 | runs-on: ubuntu-latest 10 | services: 11 | postgres: 12 | image: postgres:13.3 13 | env: 14 | POSTGRES_USER: db_user 15 | POSTGRES_PASSWORD: db_password 16 | POSTGRES_DB: db_test 17 | ports: 18 | - 5432:5432 19 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 20 | steps: 21 | - name: Checkout Repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: "3.10" 28 | cache: "pip" 29 | 30 | - name: Install Python dependencies 31 | run: python -m pip install -r requirements.txt 32 | 33 | - name: Run isort 34 | run: isort --check-only --profile=black . 35 | 36 | - name: Run black 37 | run: black --check . 38 | 39 | - name: Run flake8 40 | run: flake8 --ignore=E501,W503,F401 . 41 | 42 | - name: Unit Tests and Integration Tests 43 | env: 44 | DATABASE_TEST_URL: postgresql://db_user:db_password@localhost/db_test 45 | run: python -m flask tests 46 | 47 | - name: Set up Docker Buildx 48 | uses: docker/setup-buildx-action@v2 49 | 50 | - name: Login to Docker Hub 51 | id: docker_hub_auth 52 | uses: docker/login-action@v2 53 | with: 54 | username: ${{ secrets.DOCKERHUB_USERNAME }} 55 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 56 | 57 | - name: Build and push 58 | uses: docker/build-push-action@v4 59 | with: 60 | context: . 61 | push: true 62 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/flask_template_image:latest 63 | cache-from: type=gha 64 | cache-to: type=gha,mode=max 65 | 66 | create-config-infrastructure: 67 | runs-on: ubuntu-latest 68 | needs: 69 | - build-test 70 | steps: 71 | - name: Checkout Repository 72 | uses: actions/checkout@v3 73 | 74 | - name: Declare variables 75 | shell: bash 76 | run: | 77 | echo "SHA_SHORT=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV" 78 | echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/})" >> "$GITHUB_ENV" 79 | 80 | - name: Configure AWS credentials 81 | id: creds 82 | uses: aws-actions/configure-aws-credentials@v1 83 | with: 84 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 85 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 86 | aws-region: us-east-1 87 | 88 | - name: Deploy to AWS CloudFormation 89 | uses: aws-actions/aws-cloudformation-github-deploy@v1 90 | with: 91 | name: flask-template-staging-${{ env.SHA_SHORT }} 92 | template: ./.github/workflows/cloudformations/server.yml 93 | parameter-overrides: "file:///${{ github.workspace }}/.github/workflows/cloudformations/server-parameters.json" 94 | tags: ${{ vars.TAGS }} 95 | 96 | - name: Get Public DNS Server 97 | run: | 98 | # Create file 99 | backend_public_dns=flask-template-staging-${{ env.SHA_SHORT }}-PublicDNS 100 | # Pull the export value 101 | host=$(aws cloudformation list-exports \ 102 | --query "Exports[?Name==\`$backend_public_dns\`].Value" \ 103 | --no-paginate --output text) 104 | 105 | echo $host 106 | # Append the DNS to the inventory file 107 | echo $host >> $(eval echo "./.github/workflows/ansible/hosts") 108 | 109 | cat ./.github/workflows/ansible/hosts 110 | 111 | - name: Zip artifact files 112 | uses: montudor/action-zip@v1 113 | with: 114 | args: zip -qq -r artifact.zip . 115 | 116 | - name: Create files forlder in ansible 117 | run: mkdir -p ./.github/workflows/ansible/roles/deploy/files 118 | 119 | - name: Copy file 120 | uses: canastro/copy-file-action@master 121 | with: 122 | source: "artifact.zip" 123 | target: "./.github/workflows/ansible/roles/deploy/files/artifact.zip" 124 | 125 | - name: Run playbook 126 | uses: dawidd6/action-ansible-playbook@v2 127 | with: 128 | playbook: deploy_applications.yml 129 | directory: ./.github/workflows/ansible 130 | key: ${{secrets.SSH_PRIVATE_KEY}} 131 | options: | 132 | --inventory ./hosts 133 | 134 | - name: Remove stack on fail 135 | if: failure() 136 | run: | 137 | echo flask-template-staging-${{ env.SHA_SHORT }} 138 | # Get stack id for the delete_stack waiter 139 | stack_info=$(aws cloudformation describe-stacks --stack-name flask-template-staging-${{ env.SHA_SHORT }} --query "Stacks[*] | [0].StackId" 2>&1) 140 | if echo $stack_info | grep 'does not exist' > /dev/null 141 | then 142 | echo "Stack does not exist." 143 | echo $stack_info 144 | exit 0 145 | fi 146 | if echo $stack_info | grep 'ValidationError' > /dev/null 147 | then 148 | echo $stack_info 149 | exit 1 150 | else 151 | aws cloudformation delete-stack --stack-name flask-template-staging-${{ env.SHA_SHORT }} 152 | echo $stack_info 153 | aws cloudformation wait stack-delete-complete --stack-name flask-template-staging-${{ env.SHA_SHORT }} 154 | exit 0 155 | fi 156 | 157 | clean-up: 158 | runs-on: ubuntu-latest 159 | needs: 160 | - create-config-infrastructure 161 | steps: 162 | - name: Checkout Repository 163 | uses: actions/checkout@v3 164 | 165 | - name: Declare some variables 166 | shell: bash 167 | run: | 168 | echo "SHA_SHORT=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV" 169 | echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/})" >> "$GITHUB_ENV" 170 | 171 | - name: Configure AWS credentials 172 | id: creds 173 | uses: aws-actions/configure-aws-credentials@v1 174 | with: 175 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 176 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 177 | aws-region: us-east-1 178 | 179 | - name: Fetch stacks and save the old stack name 180 | run: | 181 | # Fetch the stack names 182 | export STACKS=( 183 | $(aws cloudformation list-stacks \ 184 | --query "StackSummaries[*].StackName" \ 185 | --no-paginate --output text \ 186 | --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE 187 | ) 188 | ) 189 | for stack in "${STACKS[@]}"; do 190 | if [[ ! "$stack" =~ "${{ env.SHA_SHORT }}" ]] && [[ "$stack" =~ "flask-template-staging" ]]; then 191 | echo "DESTROY_STACK=$stack" >> "$GITHUB_ENV" 192 | fi 193 | done 194 | 195 | - name: Remove the search engine infrastructure 196 | run: | 197 | # Check if DESTROY_STACK is not set 198 | if [ -z "${{ env.DESTROY_STACK }}" ]; then 199 | echo "DESTROY_STACK is not set" 200 | exit 0 201 | else 202 | echo "DESTROY_STACK is set to ${{ env.DESTROY_STACK }}" 203 | fi 204 | 205 | # Get stack id for the delete_stack waiter 206 | stack_info=$(aws cloudformation describe-stacks --stack-name ${{ env.DESTROY_STACK }} --query "Stacks[*] | [0].StackId" 2>&1) 207 | if echo $stack_info | grep 'does not exist' > /dev/null 208 | then 209 | echo "Stack does not exist." 210 | echo $stack_info 211 | exit 0 212 | fi 213 | if echo $stack_info | grep 'ValidationError' > /dev/null 214 | then 215 | echo $stack_info 216 | exit 1 217 | else 218 | aws cloudformation delete-stack --stack-name ${{ env.DESTROY_STACK }} 219 | echo $stack_info 220 | aws cloudformation wait stack-delete-complete --stack-name ${{ env.DESTROY_STACK }} 221 | exit 0 222 | fi 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask API REST Template 2 | 3 | [![Development](https://github.com/vectornguyen76/flask-rest-api-template/actions/workflows/development_pipeline.yml/badge.svg)](https://github.com/vectornguyen76/flask-rest-api-template/actions/workflows/development_pipeline.yml) 4 | [![Staging](https://github.com/vectornguyen76/flask-rest-api-template/actions/workflows/staging_pipeline.yml/badge.svg)](https://github.com/vectornguyen76/flask-rest-api-template/actions/workflows/staging_pipeline.yml) 5 | [![Production](https://github.com/vectornguyen76/flask-rest-api-template/actions/workflows/production_pipeline.yml/badge.svg)](https://github.com/vectornguyen76/flask-rest-api-template/actions/workflows/production_pipeline.yml) 6 | 7 |

8 | Logo 9 |

10 | 11 | Rest API template developed in Python with the Flask framework. The template covers user management, jwt tokens for authentication, and assign permissions for each user with Flask Principal. In the local environment, it uses docker to create an environment made up of several services such as api (flask), database (postgresql), reverse-proxy (nginx). 12 | 13 | ## Index 14 | 15 | - [Technology](#technology) 16 | - [Requirements](#requirements) 17 | - [Environments](#environments) 18 | - [Develop](#develop) 19 | - [Testing](#testing) 20 | - [Local](#local) 21 | - [Production](#production) 22 | - [Flask Commands](#flask-commands) 23 | - [Flask-cli](#flask-cli) 24 | - [Database commands](#bbdd-commands) 25 | - [Flask-migrate](#flask-migrate) 26 | - [Swagger](#swagger) 27 | - [Reference](#reference) 28 | - [Contribution](#contribution) 29 | 30 | ## Technology 31 | 32 | - **Operating System:** Ubuntu 33 | - **Web Framework:** Flask 34 | - **ORM:** Flask-sqlalchemy 35 | - **Swagger:** Swagger-UI 36 | - **Authentication:** Flask Json Web Token 37 | - **Permission:** JWT Decorator 38 | - **Serialization, Deserialization and Validation:** Marshmallow 39 | - **Migration Database:** Flask-migrate 40 | - **Environment manager:** Anaconda/Miniconda 41 | - **Containerization:** Docker, docker-compose 42 | - **Database:** PostgreSQL 43 | - **Python WSGI HTTP Server:** Gunicorn 44 | - **Proxy:** Nginx 45 | - **Tests:** Unittest 46 | - **Deployment platform:** AWS 47 | - **CI/CD:** Github Actions 48 | 49 | ## Requirements 50 | 51 | - [Python](https://www.python.org/downloads/) 52 | - [Anaconda/Miniconda](instructions/anaconda-miniconda.md) 53 | - [Docker](instructions/docker-dockercompose.md) 54 | - [Docker-Compose](instructions/docker-dockercompose.md) 55 | - [Github](https://github.com) 56 | 57 | ## Environments 58 | 59 | ### Develop 60 | 61 | Development environment that uses PostgreSQL in local and uses the server flask in debug mode. 62 | 63 | 1. **Create environment and install packages** 64 | 65 | ```shell 66 | conda create -n backend python=3.10 67 | 68 | conda activate backend 69 | 70 | pip install -r requirements.txt 71 | ``` 72 | 73 | 2. **Create PosgresSQL on Ubuntu** 74 | 75 | ```shell 76 | # Install PosgresSQL 77 | sudo apt-get install postgresql-12 78 | 79 | # Access to PosgresSQL 80 | sudo -u postgres psql 81 | 82 | # Create user and password 83 | CREATE USER db_user WITH PASSWORD 'db_password'; 84 | 85 | # Create Database dev 86 | CREATE DATABASE db_dev; 87 | 88 | # Add permission User to Database 89 | GRANT ALL PRIVILEGES ON DATABASE db_dev TO db_user; 90 | ``` 91 | 92 | 3. **Create or update `.env` file** 93 | 94 | ```shell 95 | # APP configuration 96 | APP_NAME=Flask API Rest Template 97 | APP_ENV=develop 98 | 99 | # Flask Configuration 100 | FLASK_APP=app:app 101 | FLASK_DEBUG=true 102 | APP_SETTINGS_MODULE=config.DevelopConfig 103 | FLASK_RUN_HOST=0.0.0.0 104 | FLASK_RUN_PORT=5000 105 | 106 | # Secret key 107 | SECRET_KEY= 108 | JWT_SECRET_KEY= 109 | 110 | # Database service configuration 111 | DATABASE_URL=postgresql://db_user:db_password@localhost/db_dev 112 | ``` 113 | 114 | 4. **Run application** 115 | 116 | ```shell 117 | # Create database 118 | flask create-db 119 | 120 | # Create user admin 121 | flask create-user-admin 122 | 123 | # Run a development server 124 | flask run 125 | ``` 126 | 127 | ### Testing 128 | 129 | Testing environment that uses PostgreSQL as database (db_test) and performs unit tests, integration tests and API tests. 130 | 131 | 1. **Create test environment** 132 | 133 | 2. **Create Test Database** 134 | 135 | 3. **Create or update `.env` file** 136 | 137 | ```shell 138 | # APP configuration 139 | APP_NAME=Flask API Rest Template 140 | APP_ENV=testing 141 | 142 | # Flask Configuration 143 | FLASK_APP=app:app 144 | FLASK_DEBUG=true 145 | APP_SETTINGS_MODULE=config.TestingConfig 146 | FLASK_RUN_HOST=0.0.0.0 147 | FLASK_RUN_PORT=3000 148 | 149 | # Secret key 150 | SECRET_KEY= 151 | JWT_SECRET_KEY= 152 | 153 | # Database service configuration 154 | DATABASE_TEST_URL=postgresql://db_user:db_password@localhost/db_test 155 | ``` 156 | 157 | 4. **Init database** 158 | 159 | ```shell 160 | # Create database 161 | flask create-db 162 | 163 | # Create user admin 164 | flask create-user-admin 165 | ``` 166 | 167 | 5. **Run all the tests** 168 | 169 | ```shell 170 | flask tests 171 | ``` 172 | 173 | 6. **Run unit tests** 174 | 175 | ```shell 176 | flask tests_unit 177 | ``` 178 | 179 | 7. **Run integration tests** 180 | 181 | ```shell 182 | flask tests_integration 183 | ``` 184 | 185 | 8. **Run API tests** 186 | 187 | ```shell 188 | flask tests_api 189 | ``` 190 | 191 | 9. **Run coverage** 192 | 193 | ```shell 194 | flask coverage 195 | ``` 196 | 197 | 10. **Run coverage report** 198 | 199 | ```shell 200 | flask coverage_report 201 | ``` 202 | 203 | ### Local 204 | 205 | Containerized services separately with PostgreSQL databases (db), API (api) and Nginx reverse proxy (nginx) with Docker and docker-compose. 206 | 207 | 1. **Create `.env.api.local`, `.env.db.local` files** 208 | 209 | 1. **.env.api.local** 210 | 211 | ```shell 212 | # APP configuration 213 | APP_NAME=[Name APP] # For example Flask API Rest Template 214 | APP_ENV=local 215 | 216 | # Flask configuration 217 | API_ENTRYPOINT=app:app 218 | APP_SETTINGS_MODULE=config.LocalConfig 219 | APP_TEST_SETTINGS_MODULE=config.TestingConfig 220 | 221 | # API service configuration 222 | API_HOST= # For example 0.0.0.0 223 | API_PORT= # For example 5000 224 | 225 | # Database service configuration 226 | DATABASE=postgres 227 | DB_HOST= # For example db_service (name service in docker-compose) 228 | DB_PORT= # For example 5432 (port service in docker-compose) 229 | POSTGRES_DB= # For example db_dev 230 | POSTGRES_USER= # For example db_user 231 | PGPASSWORD= # For example db_password 232 | 233 | # Secret key 234 | SECRET_KEY= 235 | JWT_SECRET_KEY= 236 | 237 | DATABASE_TEST_URL= # For example postgresql+psycopg2://db_user:db_password@db_service:5432/db_test 238 | DATABASE_URL= # For example postgresql+psycopg2://db_user:db_password@db_service:5432/db_dev 239 | ``` 240 | 241 | 2. **.env.db.local**: 242 | 243 | ```shell 244 | POSTGRES_USER= # For example db_user 245 | POSTGRES_PASSWORD= # For example db_password 246 | POSTGRES_DB= # For example db_dev 247 | ``` 248 | 249 | 2. **Build and run services** 250 | `shell docker-compose up --build ` 2. Stop services: 251 | `shell docker-compose stop ` 3. Delete services: 252 | `shell docker compose down ` 4. Remove services (removing volumes): 253 | `shell docker-compose down -v ` 4. Remove services (removing volumes and images): 254 | `shell docker-compose down -v --rmi all ` 5. View services: 255 | `shell docker-compose ps ` 256 | **NOTE:** The Rest API defaults to host _localhost_ and port _80_. 257 | 258 | ### Production 259 | 260 | Apply CI/CD with Github Actions to automatically deployed to AWS platform use EC2, RDS PostgresSQL. 261 | 262 | 1. Create file **.env.pro** and enter the environment variables needed for production. For example: 263 | 264 | ```shell 265 | # APP configuration 266 | APP_NAME=Flask API Rest Template 267 | APP_ENV=production 268 | 269 | # Flask configuration 270 | API_ENTRYPOINT=app:app 271 | APP_SETTINGS_MODULE=config.ProductionConfig 272 | 273 | # API service configuration 274 | API_HOST= # For example 0.0.0.0 275 | 276 | # Secret key 277 | SECRET_KEY= 278 | JWT_SECRET_KEY= 279 | 280 | # Database service configuration 281 | DATABASE_URL= # For example sqlite:///production.db 282 | 283 | # Deploy platform 284 | PLATFORM_DEPLOY=AWS 285 | ``` 286 | 287 | ## Flask Commands 288 | 289 | ### Flask-cli 290 | 291 | - Create all tables in the database: 292 | 293 | ```sh 294 | flask create_db 295 | ``` 296 | 297 | - Delete all tables in the database: 298 | 299 | ```sh 300 | flask drop_db 301 | ``` 302 | 303 | - Create admin user for the Rest API: 304 | 305 | ```sh 306 | flask create-user-admin 307 | ``` 308 | 309 | - Database reset: 310 | 311 | ```sh 312 | flask reset-db 313 | ``` 314 | 315 | - Run tests without coverage: 316 | 317 | ```sh 318 | flask reset-db 319 | ``` 320 | 321 | - Run tests with coverage without report in html: 322 | 323 | ```sh 324 | flask cov 325 | ``` 326 | 327 | - Run tests with coverage with report in html: 328 | ```sh 329 | flask cov-html 330 | ``` 331 | 332 | ## Database commands 333 | 334 | ### Flask-migrate 335 | 336 | - Create a migration repository: 337 | 338 | ```sh 339 | flask db init 340 | ``` 341 | 342 | - Generate a migration version: 343 | 344 | ```sh 345 | flask db migrate -m "Init" 346 | ``` 347 | 348 | - Apply migration to the Database: 349 | ```sh 350 | flask db upgrade 351 | ``` 352 | 353 | ## Swagger 354 | 355 | ``` 356 | http://localhost:/swagger-ui 357 | ``` 358 | 359 |

360 | Swagger 361 |

362 | 363 | ## Reference 364 | 365 | - [Udemy - REST APIs with Flask and Python in 2023](https://www.udemy.com/course/rest-api-flask-and-python/) 366 | - [Github - Flask API REST Template](https://github.com/igp7/flask-rest-api-template) 367 | - [Github - Uvicorn Gunicorn Fastapi Docker](https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker) 368 | 369 | ## Contribution 370 | 371 | Feel free to make any suggestions or improvements to the project. 372 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tests/postman/FlaskTemplate.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "03bd1377-f65e-4a5d-b478-2af9d4f41f18", 4 | "name": "FlaskTemplate", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "20498029", 7 | "_collection_link": "https://dark-escape-873868.postman.co/workspace/My-Workspace~fd59678e-5c9a-4e36-8407-3c2ba6dcae38/collection/20498029-03bd1377-f65e-4a5d-b478-2af9d4f41f18?action=share&source=collection_link&creator=20498029" 8 | }, 9 | "item": [ 10 | { 11 | "name": "User", 12 | "item": [ 13 | { 14 | "name": "GetAllUser", 15 | "request": { 16 | "auth": { 17 | "type": "bearer", 18 | "bearer": [ 19 | { 20 | "key": "token", 21 | "value": "{{TOKEN}}", 22 | "type": "string" 23 | } 24 | ] 25 | }, 26 | "method": "GET", 27 | "header": [], 28 | "url": { 29 | "raw": "{{HOST}}/user", 30 | "host": ["{{HOST}}"], 31 | "path": ["user"] 32 | } 33 | }, 34 | "response": [] 35 | }, 36 | { 37 | "name": "GetUser", 38 | "request": { 39 | "auth": { 40 | "type": "bearer", 41 | "bearer": [ 42 | { 43 | "key": "token", 44 | "value": "{{TOKEN}}", 45 | "type": "string" 46 | } 47 | ] 48 | }, 49 | "method": "GET", 50 | "header": [], 51 | "url": { 52 | "raw": "{{HOST}}/user/2", 53 | "host": ["{{HOST}}"], 54 | "path": ["user", "2"] 55 | } 56 | }, 57 | "response": [] 58 | }, 59 | { 60 | "name": "Update", 61 | "request": { 62 | "auth": { 63 | "type": "bearer", 64 | "bearer": [ 65 | { 66 | "key": "token", 67 | "value": "{{TOKEN}}", 68 | "type": "string" 69 | } 70 | ] 71 | }, 72 | "method": "PUT", 73 | "header": [], 74 | "body": { 75 | "mode": "raw", 76 | "raw": "{\r\n \"username\": null,\r\n \"password\": \"123456\",\r\n \"roles\": [1,2,3]\r\n}", 77 | "options": { 78 | "raw": { 79 | "language": "json" 80 | } 81 | } 82 | }, 83 | "url": { 84 | "raw": "{{HOST}}/user/1", 85 | "host": ["{{HOST}}"], 86 | "path": ["user", "1"] 87 | } 88 | }, 89 | "response": [] 90 | }, 91 | { 92 | "name": "Block User", 93 | "event": [ 94 | { 95 | "listen": "test", 96 | "script": { 97 | "exec": [""], 98 | "type": "text/javascript" 99 | } 100 | } 101 | ], 102 | "request": { 103 | "auth": { 104 | "type": "bearer", 105 | "bearer": [ 106 | { 107 | "key": "token", 108 | "value": "{{TOKEN}}", 109 | "type": "string" 110 | } 111 | ] 112 | }, 113 | "method": "PUT", 114 | "header": [], 115 | "body": { 116 | "mode": "raw", 117 | "raw": "{\r\n \"block\": true\r\n}", 118 | "options": { 119 | "raw": { 120 | "language": "json" 121 | } 122 | } 123 | }, 124 | "url": { 125 | "raw": "{{HOST}}/block-user/3", 126 | "host": ["{{HOST}}"], 127 | "path": ["block-user", "3"] 128 | } 129 | }, 130 | "response": [] 131 | } 132 | ] 133 | }, 134 | { 135 | "name": "Login", 136 | "item": [ 137 | { 138 | "name": "Login", 139 | "event": [ 140 | { 141 | "listen": "test", 142 | "script": { 143 | "exec": [ 144 | "var jsonData = JSON.parse(responseBody);\r", 145 | "pm.environment.set(\"TOKEN\", jsonData.access_token);\r", 146 | "pm.environment.set(\"REFRESHTOKEN\", jsonData.refresh_token);" 147 | ], 148 | "type": "text/javascript" 149 | } 150 | } 151 | ], 152 | "request": { 153 | "method": "POST", 154 | "header": [], 155 | "body": { 156 | "mode": "raw", 157 | "raw": "{\r\n \"username\": \"admin\",\r\n \"password\": \"123456\"\r\n}", 158 | "options": { 159 | "raw": { 160 | "language": "json" 161 | } 162 | } 163 | }, 164 | "url": { 165 | "raw": "{{HOST}}/login", 166 | "host": ["{{HOST}}"], 167 | "path": ["login"] 168 | } 169 | }, 170 | "response": [] 171 | }, 172 | { 173 | "name": "Logout", 174 | "request": { 175 | "auth": { 176 | "type": "bearer", 177 | "bearer": [ 178 | { 179 | "key": "token", 180 | "value": "{{TOKEN}}", 181 | "type": "string" 182 | } 183 | ] 184 | }, 185 | "method": "POST", 186 | "header": [], 187 | "url": { 188 | "raw": "{{HOST}}/logout", 189 | "host": ["{{HOST}}"], 190 | "path": ["logout"] 191 | } 192 | }, 193 | "response": [] 194 | }, 195 | { 196 | "name": "RefreshToken", 197 | "event": [ 198 | { 199 | "listen": "test", 200 | "script": { 201 | "exec": [ 202 | "var jsonData = JSON.parse(responseBody);\r", 203 | "pm.environment.set(\"TOKEN\", jsonData.access_token);\r", 204 | "pm.environment.set(\"REFRESHTOKEN\", jsonData.refresh_token);" 205 | ], 206 | "type": "text/javascript" 207 | } 208 | } 209 | ], 210 | "request": { 211 | "auth": { 212 | "type": "bearer", 213 | "bearer": [ 214 | { 215 | "key": "token", 216 | "value": "{{REFRESHTOKEN}}", 217 | "type": "string" 218 | } 219 | ] 220 | }, 221 | "method": "POST", 222 | "header": [], 223 | "url": { 224 | "raw": "{{HOST}}/refresh", 225 | "host": ["{{HOST}}"], 226 | "path": ["refresh"] 227 | } 228 | }, 229 | "response": [] 230 | } 231 | ] 232 | }, 233 | { 234 | "name": "Register", 235 | "item": [ 236 | { 237 | "name": "Register", 238 | "request": { 239 | "method": "POST", 240 | "header": [], 241 | "body": { 242 | "mode": "raw", 243 | "raw": "{\r\n \"email\": \"phuoc@gmail.com\",\r\n \"password\": \"Phuoc12345@\"\r\n}", 244 | "options": { 245 | "raw": { 246 | "language": "json" 247 | } 248 | } 249 | }, 250 | "url": { 251 | "raw": "{{HOST}}/auth/users", 252 | "host": ["{{HOST}}"], 253 | "path": ["auth", "users"] 254 | } 255 | }, 256 | "response": [] 257 | } 258 | ] 259 | }, 260 | { 261 | "name": "Role", 262 | "item": [ 263 | { 264 | "name": "Get all", 265 | "request": { 266 | "auth": { 267 | "type": "bearer", 268 | "bearer": [ 269 | { 270 | "key": "token", 271 | "value": "{{TOKEN}}", 272 | "type": "string" 273 | } 274 | ] 275 | }, 276 | "method": "GET", 277 | "header": [], 278 | "url": { 279 | "raw": "{{HOST}}/role", 280 | "host": ["{{HOST}}"], 281 | "path": ["role"] 282 | } 283 | }, 284 | "response": [] 285 | }, 286 | { 287 | "name": "Get by id", 288 | "request": { 289 | "auth": { 290 | "type": "bearer", 291 | "bearer": [ 292 | { 293 | "key": "token", 294 | "value": "{{TOKEN}}", 295 | "type": "string" 296 | } 297 | ] 298 | }, 299 | "method": "GET", 300 | "header": [], 301 | "url": { 302 | "raw": "{{HOST}}/role/1", 303 | "host": ["{{HOST}}"], 304 | "path": ["role", "1"] 305 | } 306 | }, 307 | "response": [] 308 | }, 309 | { 310 | "name": "Add new", 311 | "request": { 312 | "auth": { 313 | "type": "bearer", 314 | "bearer": [ 315 | { 316 | "key": "token", 317 | "value": "{{TOKEN}}", 318 | "type": "string" 319 | } 320 | ] 321 | }, 322 | "method": "POST", 323 | "header": [], 324 | "body": { 325 | "mode": "raw", 326 | "raw": "{\r\n \"name\": \"nhân viên2\",\r\n \"description\": \"Nhân viên nè\",\r\n \"permissions\": [1,3]\r\n}", 327 | "options": { 328 | "raw": { 329 | "language": "json" 330 | } 331 | } 332 | }, 333 | "url": { 334 | "raw": "{{HOST}}/role", 335 | "host": ["{{HOST}}"], 336 | "path": ["role"] 337 | } 338 | }, 339 | "response": [] 340 | }, 341 | { 342 | "name": "Update", 343 | "request": { 344 | "auth": { 345 | "type": "bearer", 346 | "bearer": [ 347 | { 348 | "key": "token", 349 | "value": "{{TOKEN}}", 350 | "type": "string" 351 | } 352 | ] 353 | }, 354 | "method": "PUT", 355 | "header": [], 356 | "body": { 357 | "mode": "raw", 358 | "raw": "{\r\n \"name\": \"Super admin\",\r\n \"description\": \"Full permissions\",\r\n \"permissions\": [1]\r\n}", 359 | "options": { 360 | "raw": { 361 | "language": "json" 362 | } 363 | } 364 | }, 365 | "url": { 366 | "raw": "{{HOST}}/role/1", 367 | "host": ["{{HOST}}"], 368 | "path": ["role", "1"] 369 | } 370 | }, 371 | "response": [] 372 | }, 373 | { 374 | "name": "Delete", 375 | "request": { 376 | "auth": { 377 | "type": "bearer", 378 | "bearer": [ 379 | { 380 | "key": "token", 381 | "value": "{{TOKEN}}", 382 | "type": "string" 383 | } 384 | ] 385 | }, 386 | "method": "DELETE", 387 | "header": [], 388 | "body": { 389 | "mode": "raw", 390 | "raw": "{\r\n \"display_name\": \"\",\r\n \"email\": \"\"\r\n}", 391 | "options": { 392 | "raw": { 393 | "language": "json" 394 | } 395 | } 396 | }, 397 | "url": { 398 | "raw": "{{HOST}}/role/13", 399 | "host": ["{{HOST}}"], 400 | "path": ["role", "13"] 401 | } 402 | }, 403 | "response": [] 404 | } 405 | ] 406 | }, 407 | { 408 | "name": "Permission", 409 | "item": [ 410 | { 411 | "name": "Get all", 412 | "request": { 413 | "auth": { 414 | "type": "bearer", 415 | "bearer": [ 416 | { 417 | "key": "token", 418 | "value": "{{TOKEN}}", 419 | "type": "string" 420 | } 421 | ] 422 | }, 423 | "method": "GET", 424 | "header": [], 425 | "url": { 426 | "raw": "{{HOST}}/permission", 427 | "host": ["{{HOST}}"], 428 | "path": ["permission"] 429 | } 430 | }, 431 | "response": [] 432 | }, 433 | { 434 | "name": "Get by id", 435 | "request": { 436 | "auth": { 437 | "type": "bearer", 438 | "bearer": [ 439 | { 440 | "key": "token", 441 | "value": "{{TOKEN}}", 442 | "type": "string" 443 | } 444 | ] 445 | }, 446 | "method": "GET", 447 | "header": [], 448 | "url": { 449 | "raw": "{{HOST}}/permission/1", 450 | "host": ["{{HOST}}"], 451 | "path": ["permission", "1"] 452 | } 453 | }, 454 | "response": [] 455 | }, 456 | { 457 | "name": "Add new", 458 | "request": { 459 | "auth": { 460 | "type": "bearer", 461 | "bearer": [ 462 | { 463 | "key": "token", 464 | "value": "{{TOKEN}}", 465 | "type": "string" 466 | } 467 | ] 468 | }, 469 | "method": "POST", 470 | "header": [], 471 | "body": { 472 | "mode": "raw", 473 | "raw": "{\r\n \"name\": \"read\",\r\n \"description\": \"Read data\"\r\n}", 474 | "options": { 475 | "raw": { 476 | "language": "json" 477 | } 478 | } 479 | }, 480 | "url": { 481 | "raw": "{{HOST}}/permission", 482 | "host": ["{{HOST}}"], 483 | "path": ["permission"] 484 | } 485 | }, 486 | "response": [] 487 | }, 488 | { 489 | "name": "Update", 490 | "request": { 491 | "auth": { 492 | "type": "bearer", 493 | "bearer": [ 494 | { 495 | "key": "token", 496 | "value": "{{TOKEN}}", 497 | "type": "string" 498 | } 499 | ] 500 | }, 501 | "method": "PUT", 502 | "header": [], 503 | "body": { 504 | "mode": "raw", 505 | "raw": "{\r\n \"name\": \"read\",\r\n \"description\": \"Read data\"\r\n}", 506 | "options": { 507 | "raw": { 508 | "language": "json" 509 | } 510 | } 511 | }, 512 | "url": { 513 | "raw": "{{HOST}}/permission/4", 514 | "host": ["{{HOST}}"], 515 | "path": ["permission", "4"] 516 | } 517 | }, 518 | "response": [] 519 | }, 520 | { 521 | "name": "Update Roles", 522 | "request": { 523 | "auth": { 524 | "type": "bearer", 525 | "bearer": [ 526 | { 527 | "key": "token", 528 | "value": "{{TOKEN}}", 529 | "type": "string" 530 | } 531 | ] 532 | }, 533 | "method": "PUT", 534 | "header": [], 535 | "body": { 536 | "mode": "raw", 537 | "raw": "{\r\n \"data_update\" : {\"1\" : [1,2,3], \"2\": [2]}\r\n}", 538 | "options": { 539 | "raw": { 540 | "language": "json" 541 | } 542 | } 543 | }, 544 | "url": { 545 | "raw": "{{HOST}}/permission-role-update", 546 | "host": ["{{HOST}}"], 547 | "path": ["permission-role-update"] 548 | } 549 | }, 550 | "response": [] 551 | } 552 | ] 553 | }, 554 | { 555 | "name": "User Role", 556 | "item": [ 557 | { 558 | "name": "Link Roles to User", 559 | "request": { 560 | "auth": { 561 | "type": "bearer", 562 | "bearer": [ 563 | { 564 | "key": "token", 565 | "value": "{{TOKEN}}", 566 | "type": "string" 567 | } 568 | ] 569 | }, 570 | "method": "POST", 571 | "header": [], 572 | "url": { 573 | "raw": "{{HOST}}/user/1/role/2", 574 | "host": ["{{HOST}}"], 575 | "path": ["user", "1", "role", "2"] 576 | } 577 | }, 578 | "response": [] 579 | }, 580 | { 581 | "name": "Delete Roles to User", 582 | "request": { 583 | "auth": { 584 | "type": "bearer", 585 | "bearer": [ 586 | { 587 | "key": "token", 588 | "value": "{{TOKEN}}", 589 | "type": "string" 590 | } 591 | ] 592 | }, 593 | "method": "DELETE", 594 | "header": [], 595 | "url": { 596 | "raw": "{{HOST}}/user/1/role/2", 597 | "host": ["{{HOST}}"], 598 | "path": ["user", "1", "role", "2"] 599 | } 600 | }, 601 | "response": [] 602 | } 603 | ] 604 | }, 605 | { 606 | "name": "Role Permission", 607 | "item": [ 608 | { 609 | "name": "Get All", 610 | "request": { 611 | "auth": { 612 | "type": "bearer", 613 | "bearer": [ 614 | { 615 | "key": "token", 616 | "value": "{{TOKEN}}", 617 | "type": "string" 618 | } 619 | ] 620 | }, 621 | "method": "GET", 622 | "header": [], 623 | "url": { 624 | "raw": "{{HOST}}/role-permission", 625 | "host": ["{{HOST}}"], 626 | "path": ["role-permission"] 627 | } 628 | }, 629 | "response": [] 630 | } 631 | ] 632 | } 633 | ] 634 | } 635 | --------------------------------------------------------------------------------