Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7f6f246
Added get and post methods
Subhadeep0506 Jan 15, 2022
ee3d121
Added README
Subhadeep0506 Jan 15, 2022
0e38618
Initial Commit
Subhadeep0506 Jan 16, 2022
335d0a8
added requirements.txt
Subhadeep0506 Jan 18, 2022
c85794b
added authentication to app
Subhadeep0506 Jan 18, 2022
5773e1c
added delete and put methods
Subhadeep0506 Jan 18, 2022
6976738
modified README
Subhadeep0506 Jan 18, 2022
1b65a98
minor changes
Subhadeep0506 Jan 20, 2022
c8007f1
added database and user registration
Subhadeep0506 Jan 20, 2022
191924f
added database integration to get and post
Subhadeep0506 Jan 20, 2022
281696e
implemeted delete method
Subhadeep0506 Jan 21, 2022
933e106
minor changes
Subhadeep0506 Jan 21, 2022
71f90dc
implemented database into put and get items
Subhadeep0506 Jan 21, 2022
453932f
minor changes
Subhadeep0506 Jan 21, 2022
6858880
made seperate folders for item and user
Subhadeep0506 Jan 24, 2022
815d0b0
Added Store functions
Subhadeep0506 Jan 25, 2022
f235ca9
initial commit
Subhadeep0506 Jan 25, 2022
02cb51e
created login endpoint
Subhadeep0506 Jan 26, 2022
8ea1275
Added blocklist and other jwt configs
Subhadeep0506 Jan 27, 2022
60d5cf8
added logout feature
Subhadeep0506 Jan 27, 2022
e7c80af
files exclusion
Subhadeep0506 Jan 27, 2022
f4b1c83
minor changes
Subhadeep0506 Feb 3, 2022
447a3ec
added type-hinying to all methods
Subhadeep0506 Feb 23, 2022
b251732
added marshmallow serialization and deserialization
Subhadeep0506 Feb 25, 2022
a4eaa65
added flask-marshmallow to user resource
Subhadeep0506 Feb 25, 2022
1005ec1
removed unnecessary directories
Subhadeep0506 Feb 25, 2022
8ac4006
added marshmallow to item and store
Subhadeep0506 Feb 26, 2022
c430faa
changed ModelSchema to SQLAlchemyAutoSchema
Subhadeep0506 Feb 26, 2022
e9c4b97
modified gitignore
Subhadeep0506 Feb 26, 2022
d6bc3b0
Added user confirmation
Subhadeep0506 Feb 28, 2022
c29a1c0
Added email confirmation
Subhadeep0506 Mar 1, 2022
5d4d5a9
Added environment variables
Subhadeep0506 Mar 2, 2022
af8d3cb
Minor changes
Subhadeep0506 Mar 5, 2022
12480d9
minor unfortunate changes
Subhadeep0506 Mar 5, 2022
a7ff629
minor changes
Subhadeep0506 Mar 5, 2022
b2f02e8
Re-added confirmation page
Subhadeep0506 Mar 5, 2022
3daf439
minor changes
Subhadeep0506 Mar 5, 2022
ee46742
minor changes
Subhadeep0506 Mar 6, 2022
1f1b360
Added confirmation model and schema
Subhadeep0506 Mar 8, 2022
908cf4d
Added confirmation resources
Subhadeep0506 Mar 8, 2022
0238ae1
minor changes
Subhadeep0506 Mar 8, 2022
ee9865b
minor changes
Subhadeep0506 Mar 9, 2022
215029d
Initial commit
Subhadeep0506 Mar 9, 2022
b36c8b9
Added Image and avatar feature
Subhadeep0506 Mar 13, 2022
713b2b9
Migrated database to ElephantSQL
Subhadeep0506 Mar 13, 2022
3da8452
Changed README
Subhadeep0506 Mar 13, 2022
a79d86d
Changed README
Subhadeep0506 Mar 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
MAILGUN_DOMAIN=
MAILGUN_API_KEY=
JWT_SECRET_KEY="secret-key-here"
APPLICATION_SETTINGS=default_config.py
DATABASE_URL=database-uri-here
APP_SECRET_KEY="komraishumtirkomchuri"
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
test/test.py
test/test.db
__pycache__
.vscode
.idea
/.env
database/data.db
resources/__pycache__
model/__pycache__
static/images
migrations
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/PythonRESTapi.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"python.pythonPath": "/usr/sbin/python3.9",
"python.formatting.provider": "black"
}
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# REST API application made with flask.

Contains <> branches

Final and stable application is in the master branch

NOTE: Use a virtual environment with python 3.9 to run the application

## INSATLLATION

Run the following command to install dependencies:

```
pip install -r requirements.txt
```

## RUN

Run the following command to start the application:

```
python app.py
```

## IMPORTANT

Make sure to delete the `data.db` file from `database` directory before fresh run.

## Steps to database migration

1. ```
flask db init
```

To initiate migration process.

2. Edit the `alembic.ini` and `env.py` according to requirements.
3. ```
flask db migrate
```
To create migration instance.
4. Edit the version file accordingly.
5. ```
flask db upgrade
```
To commit the upgrade.
Binary file added __pycache__/blacklist.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/database.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/item.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/security.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/user.cpython-39.pyc
Binary file not shown.
152 changes: 142 additions & 10 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,142 @@
from flask import Flask
# creating an app from Flask class
app = Flask(__name__)
# letting the application know what requests it will understand
@app.route('/')
def home():
return "Hello World!"

# run the app in specified folder
app.run(port=5000)
from flask import Flask, jsonify
from flask_restful import Api
from flask_jwt_extended import JWTManager
from flask_uploads import configure_uploads, patch_request_class
from flask_migrate import Migrate
from marshmallow import ValidationError
from dotenv import load_dotenv

from ma import ma
from resources.user import (
UserRegister,
User,
UserLogin,
UserLogout,
TokenRefresh,
)
from resources.item import Item, ItemList
from resources.store import Store, StoreList
from resources.confirmation import Confirmation, ConfirmationByUser
from resources.image import ImageUpload, Image, AvatarUpload, Avatar
from libs.image_helper import IMAGE_SET
from blacklist import BLACKLIST
from database import db

app = Flask(__name__)
load_dotenv(".env", verbose=True)
# .env has to be loaded manually here beacuse it is loaded automatically when app runs.
# But here it will not load automatically as the app has not started at the point
app.config.from_object("default_config") # Loads the default_config.py file
app.config.from_envvar("APPLICATION_SETTINGS")
patch_request_class(app, 10 * 1024 * 1024) # Used to limit the max size of image that can be uploaded, here: 10mb
configure_uploads(app, IMAGE_SET)
api = Api(app)
migrate = Migrate(app, db) # Establishes a link between app and the remote database


@app.errorhandler(ValidationError)
def handle_marshmallow_validation(err):
return jsonify(err.messages), 400


@app.before_first_request
def create_tables():
db.create_all()
# above function creates all the tables before the 1st request is made
# unless they exist already


# JWT() creates a new endpoint: /auth
# we send username and password to /auth
# JWT() gets the username and password, and sends it to authenticate function
# the authenticate function maps the username and checks the password
# if all goes well, the authenticate function returns user
# which is the identity or jwt(or token)
# jwt = JWT(app, authenticate, identity)
jwt = JWTManager(app) # JwtManager links up to the application, doesn't create /auth point


# below function returns True, if the token that is sent is in the blacklist
@jwt.token_in_blocklist_loader
def check_if_token_in_blacklist(jwt_header, jwt_data):
# print("Log Message:", jwt_data)
return jwt_data["jti"] in BLACKLIST


@jwt.additional_claims_loader # modifies the below function, and links it with JWTManager, which in turn is linked with our app
def add_claims_to_jwt(identity):
if identity == 1: # instead of hard-coding this, we should read it from a config file or database
return {"is_admin": True}

return {"is_admin": False}


# JWT Configurations
@jwt.expired_token_loader
def expired_token_callback(header, data):
return (
jsonify({"description": "The token has expired.", "error": "token_expired"}),
401,
)


@jwt.invalid_token_loader
def invalid_token_callback(error):
return (
jsonify({"description": "Signature verification failed.", "error": "invalid_token"}),
401,
)


@jwt.unauthorized_loader
def missing_token_callback(error): # when no jwt is sent
return (
jsonify(
{
"description": "Request doesn't contain a access token.",
"error": "authorization_required",
}
),
401,
)


@jwt.needs_fresh_token_loader
def token_not_fresh_callback(self, callback):
# print("Log:", callback)
return (
jsonify({"description": "The token is not fresh.", "error": "fresh_token_required"}),
401,
)


@jwt.revoked_token_loader
def revoked_token_callback(self, callback):
# print("Log:", callback)
return (
jsonify({"description": "The token has been revoked.", "error": "token_revoked"}),
401,
)


api.add_resource(Item, "/item/<string:name>")
api.add_resource(Store, "/store/<string:name>")
api.add_resource(ItemList, "/items")
api.add_resource(StoreList, "/stores")
api.add_resource(UserRegister, "/register")
api.add_resource(Confirmation, "/user_confirm/<string:confirmation_id>")
api.add_resource(ConfirmationByUser, "/confirmation/user/<int:user_id>")
api.add_resource(ImageUpload, "/upload/image/")
api.add_resource(Image, "/image/<string:filename>")
api.add_resource(AvatarUpload, "/upload/avatar")
api.add_resource(Avatar, "/avatar/<int:user_id>")
api.add_resource(User, "/user/<int:user_id>")
api.add_resource(UserLogin, "/login")
api.add_resource(UserLogout, "/logout")
api.add_resource(TokenRefresh, "/refresh")

db.init_app(app)

if __name__ == "__main__":
ma.init_app(app) # tells marshmallow that it should be communicating with this flask app
app.run(port=5000, debug=True)
2 changes: 2 additions & 0 deletions blacklist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
BLACKLIST = set() # user ids that will be denied access

5 changes: 5 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os

# When app will be deployed to server, app will use config.py
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///database/data.db")
3 changes: 3 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
12 changes: 12 additions & 0 deletions default_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import os

DEBUG = True
# SQLALCHEMY_DATABASE_URI = "sqlite:///database/data.db"
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///database/data.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
PROPAGATE_EXCEPTIONS = True
JWT_SECRET_KEY = os.environ["JWT_SECRET_KEY"] # If there is no secret key set, app would crash
JWT_BLACKLIST_ENABLED = True
JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"]
# APP_SECRET_KEY = os.environ["APP_SECRET_KEY"]
UPLOADED_IMAGES_DEST = os.path.join("static", "images")
Empty file added libs/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions libs/image_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import os
import re
from typing import Union
from werkzeug.datastructures import FileStorage

from flask_uploads import UploadSet, IMAGES

IMAGE_SET = UploadSet("images", IMAGES) # Set name and allowed extensions


def save_image(image: FileStorage, folder: str = None, name: str = None) -> str:
# Takes filestorage and saves it to a folder
return IMAGE_SET.save(image, folder, name) # images will be saved in static/images


def get_path(filename: str = None, folder: str = None) -> str:
# Takes an image and returns its full path
return IMAGE_SET.path(filename, folder)


def find_image_any_format(filename: str, folder: str) -> Union[str, None]:
# Takes filename and reutrns an image on any of the accepted formats
for _format in IMAGES:
image = f"{filename}.{_format}"
image_path = IMAGE_SET.path(filename=image, folder=folder)
if os.path.isfile(image_path):
return image_path

return None


def _retrieve_filename(file: Union[str, FileStorage]) -> str:
# Take a file storage and return the filename.
# Allows our functions to call with both filename or FileStorages
# and always gets back filename
# If it is a file it gives the filename
if isinstance(file, FileStorage):
return file.filename
return file # Else it gives the filename


def is_filename_safe(file: Union[str, FileStorage]) -> bool:
# Check our regex and returns whether the string matches or not
filename = _retrieve_filename(file)

allowed_format = "|".join(IMAGES) # jpg|jpeg|png|jpe
regex = f"^[a-zA-Z0-9][a-zA-Z0-9_()-\.]*\.({allowed_format})$"
return re.match(regex, filename) is not None


def get_basename(file: Union[str, FileStorage]) -> str:
# Returns fullname of image in path
# get_basename("some/folder/path/image.jpg") -> returns "image.jpg"
filename = _retrieve_filename(file)
return os.path.split(filename)[1] # for /static/images/image.png -> image.png


def get_extension(file: Union[str, FileStorage]) -> str:
# Returns file extension
filename = _retrieve_filename(file)
return os.path.splitext(filename)[1] # for image.png -> png
Loading