diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..4756ef5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+MAILGUN_DOMAIN=
+MAILGUN_API_KEY=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..178d4d0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+test/test.py
+test/test.db
+__pycache__
+.vscode
+.idea
+/.env
+database/data.db
+resources/__pycache__
+model/__pycache__
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/PythonRESTapi.iml b/.idea/PythonRESTapi.iml
new file mode 100644
index 0000000..e469c68
--- /dev/null
+++ b/.idea/PythonRESTapi.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..e7ffb93
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..bb14381
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..2c6c77b
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "python.pythonPath": "/usr/sbin/python3.9",
+ "python.formatting.provider": "black"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7eca575
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+# 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.
diff --git a/__pycache__/blacklist.cpython-39.pyc b/__pycache__/blacklist.cpython-39.pyc
new file mode 100644
index 0000000..1a3d504
Binary files /dev/null and b/__pycache__/blacklist.cpython-39.pyc differ
diff --git a/__pycache__/database.cpython-39.pyc b/__pycache__/database.cpython-39.pyc
new file mode 100644
index 0000000..eeefcfc
Binary files /dev/null and b/__pycache__/database.cpython-39.pyc differ
diff --git a/__pycache__/item.cpython-39.pyc b/__pycache__/item.cpython-39.pyc
new file mode 100644
index 0000000..19f4cec
Binary files /dev/null and b/__pycache__/item.cpython-39.pyc differ
diff --git a/__pycache__/security.cpython-39.pyc b/__pycache__/security.cpython-39.pyc
new file mode 100644
index 0000000..0289371
Binary files /dev/null and b/__pycache__/security.cpython-39.pyc differ
diff --git a/__pycache__/user.cpython-39.pyc b/__pycache__/user.cpython-39.pyc
new file mode 100644
index 0000000..5c46dce
Binary files /dev/null and b/__pycache__/user.cpython-39.pyc differ
diff --git a/app.py b/app.py
index 5fbfd70..dde98a9 100644
--- a/app.py
+++ b/app.py
@@ -1,10 +1,137 @@
-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)
\ No newline at end of file
+from flask import Flask, jsonify
+from flask_restful import Api
+from flask_jwt_extended import JWTManager
+from marshmallow import ValidationError
+
+from ma import ma
+from resources.user import (
+ UserConfirm,
+ UserRegister,
+ User,
+ UserLogin,
+ UserLogout,
+ TokenRefresh,
+)
+from resources.item import Item, ItemList
+from resources.store import Store, StoreList
+from blacklist import BLACKLIST
+from database import db
+
+app = Flask(__name__)
+
+app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database/data.db"
+
+app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False # turns of flask_sqlalchemy modification tracker
+app.config["PROPAGATE_EXCEPTIONS"] = True # if flask_jwt raises an error, the flask app will check the error if this is set to true
+app.config["JWT_BLACKLIST_ENABLED"] = True
+app.config["JWT_BLACKLIST_TOKEN_CHECKS"] = [
+ "access",
+ "refresh",
+] # both access and refresh tokens will be denied for the user ids
+
+app.secret_key = "komraishumtirkomchuri"
+# app.config["JWT_SECRET_KEY"] = "YOUR KEY HERE"
+
+api = Api(app)
+
+
+@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():
+ 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/")
+api.add_resource(Store, "/store/")
+api.add_resource(ItemList, "/items")
+api.add_resource(StoreList, "/stores")
+api.add_resource(UserRegister, "/register")
+api.add_resource(UserConfirm, "/activate/")
+api.add_resource(User, "/user/")
+api.add_resource(UserLogin, "/login")
+api.add_resource(UserLogout, "/logout")
+api.add_resource(TokenRefresh, "/refresh")
+
+if __name__ == "__main__":
+ db.init_app(app)
+ ma.init_app(app) # tells marshmallow that it should be communicating with this flask app
+ app.run(port=5000, debug=True)
diff --git a/blacklist.py b/blacklist.py
new file mode 100644
index 0000000..ec72982
--- /dev/null
+++ b/blacklist.py
@@ -0,0 +1,2 @@
+BLACKLIST = set() # user ids that will be denied access
+
diff --git a/database.py b/database.py
new file mode 100644
index 0000000..d43f174
--- /dev/null
+++ b/database.py
@@ -0,0 +1,3 @@
+from flask_sqlalchemy import SQLAlchemy
+
+db = SQLAlchemy()
diff --git a/database/data.db b/database/data.db
new file mode 100644
index 0000000..6f30f8c
Binary files /dev/null and b/database/data.db differ
diff --git a/libs/__init__.py b/libs/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/libs/mailgun.py b/libs/mailgun.py
new file mode 100644
index 0000000..5ec6ea6
--- /dev/null
+++ b/libs/mailgun.py
@@ -0,0 +1,51 @@
+import os
+from typing import List
+from requests import Response, post
+
+
+FAILED_LOAD_API_KEY = "Falied to load Mailgun API key."
+FAILED_LOAD_DOMAIN_NAME = "Failde to load Mailgun domain."
+FAILED_SEND_CONFIRMATION_MAIL = "Error in sending confirmation email, user registration failed."
+
+
+class MailgunException(Exception):
+ def __init__(self, message: str):
+ super().__init__(message)
+
+
+class Mailgun:
+
+ MAILGUN_DOMAIN = os.environ.get("MAILGUN_DOMAIN") # can be None
+ MAILGUN_API_KEY = os.environ.get("MAILGUN_API_KEY") # can be None
+ FROM_TITLE = "Stores RestAPI"
+ # FROM_EMAIL = "Your Mailgun Email"
+ FROM_EMAIL = "postmaster@sandboxb3ef8f2c0e20406cb3f834bc39735ab4.mailgun.org"
+
+ # This method will interact with Mailgun API and return the response sent
+ @classmethod
+ def send_email(cls, email: List[str], subject: str, text: str, html: str) -> Response:
+ if cls.MAILGUN_API_KEY is None:
+ raise MailgunException(FAILED_LOAD_API_KEY)
+
+ if cls.MAILGUN_DOMAIN is None:
+ raise MailgunException(FAILED_LOAD_DOMAIN_NAME)
+
+ response = post(
+ f"https://api.mailgun.net/v3/{cls.MAILGUN_DOMAIN}/messages",
+ auth=(
+ "api",
+ cls.MAILGUN_API_KEY,
+ ),
+ data={
+ "from": f"{cls.FROM_TITLE} <{cls.FROM_EMAIL}>",
+ "to": email,
+ "subject": subject,
+ "text": text,
+ "html": html,
+ },
+ )
+
+ if response.status_code != 200:
+ raise MailgunException(FAILED_SEND_CONFIRMATION_MAIL)
+
+ return response
diff --git a/ma.py b/ma.py
new file mode 100644
index 0000000..cf7687a
--- /dev/null
+++ b/ma.py
@@ -0,0 +1,3 @@
+from flask_marshmallow import Marshmallow
+
+ma = Marshmallow()
diff --git a/models/__init__.py b/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/models/__pycache__/__init__.cpython-39.pyc b/models/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000..9c283e0
Binary files /dev/null and b/models/__pycache__/__init__.cpython-39.pyc differ
diff --git a/models/__pycache__/item.cpython-39.pyc b/models/__pycache__/item.cpython-39.pyc
new file mode 100644
index 0000000..a85569a
Binary files /dev/null and b/models/__pycache__/item.cpython-39.pyc differ
diff --git a/models/__pycache__/store.cpython-39.pyc b/models/__pycache__/store.cpython-39.pyc
new file mode 100644
index 0000000..92a3462
Binary files /dev/null and b/models/__pycache__/store.cpython-39.pyc differ
diff --git a/models/__pycache__/user.cpython-39.pyc b/models/__pycache__/user.cpython-39.pyc
new file mode 100644
index 0000000..11b3d83
Binary files /dev/null and b/models/__pycache__/user.cpython-39.pyc differ
diff --git a/models/item.py b/models/item.py
new file mode 100644
index 0000000..0a137cd
--- /dev/null
+++ b/models/item.py
@@ -0,0 +1,36 @@
+from typing import List
+
+from database import db
+
+
+class ItemModel(db.Model): # tells SQLAlchemy that it is something that will be saved to database and will be retrieved from database
+
+ __tablename__ = "items"
+
+ # Columns
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(80), nullable=False, unique=True)
+ price = db.Column(db.Float(precision=2), nullable=False) # precision: numbers after decimal point
+
+ store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
+ store = db.relationship("StoreModel", back_populates="items")
+
+ # searches the database for items using name
+ @classmethod
+ def find_item_by_name(cls, name: str) -> "ItemModel":
+ # return cls.query.filter_by(name=name) # SELECT name FROM __tablename__ WHERE name=name
+ # this function would return a ItemModel object
+ return cls.query.filter_by(name=name).first() # SELECT name FROM __tablename__ WHERE name=name LIMIT 1
+
+ @classmethod
+ def find_all(cls) -> List["ItemModel"]:
+ return cls.query.all()
+
+ # method to insert or update an item into database
+ def save_to_database(self) -> None:
+ db.session.add(self) # session here is a collection of objects that wil be written to database
+ db.session.commit()
+
+ def delete_from_database(self) -> None:
+ db.session.delete(self)
+ db.session.commit()
diff --git a/models/store.py b/models/store.py
new file mode 100644
index 0000000..deae614
--- /dev/null
+++ b/models/store.py
@@ -0,0 +1,35 @@
+from typing import List
+
+from database import db
+
+
+class StoreModel(db.Model): # tells SQLAlchemy that it is something that will be saved to database and will be retrieved from database
+
+ __tablename__ = "stores"
+
+ # Columns
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(80), nullable=False, unique=True)
+
+ items = db.relationship("ItemModel", lazy="dynamic") # this will allow us to check which item is in store whose id is equal to it's id.
+ # lazy="dynamic" tells sqlalchemy to not create seperate objects for each item that is created
+
+ # searches the database for items using name
+ @classmethod
+ def find_store_by_name(cls, name: str) -> "StoreModel":
+ # return cls.query.filter_by(name=name) # SELECT name FROM __tablename__ WHERE name=name
+ # this function would return a StoreModel object
+ return cls.query.filter_by(name=name).first() # SELECT name FROM __tablename__ WHERE name=name LIMIT 1
+
+ @classmethod
+ def find_all(cls) -> List["StoreModel"]:
+ return cls.query.all()
+
+ # method to insert or update an item into database
+ def save_to_database(self) -> None:
+ db.session.add(self) # session here is a collection of objects that wil be written to database
+ db.session.commit()
+
+ def delete_from_database(self) -> None:
+ db.session.delete(self)
+ db.session.commit()
diff --git a/models/user.py b/models/user.py
new file mode 100644
index 0000000..60fddb9
--- /dev/null
+++ b/models/user.py
@@ -0,0 +1,50 @@
+from typing import Dict, Union
+from requests import Response, post
+from flask import request, url_for
+
+from database import db
+from libs.mailgun import Mailgun
+
+
+class UserModel(db.Model):
+
+ __tablename__ = "users" # will be used to tell sqlalchemy the table name for users
+
+ # table columns for users table
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(80), nullable=False, unique=True)
+ email = db.Column(db.String(50), nullable=False, unique=True)
+ password = db.Column(db.String(80), nullable=False)
+ activated = db.Column(db.Boolean, default=False)
+
+ @classmethod
+ def find_by_username(cls, username: str) -> "UserModel":
+ return cls.query.filter_by(username=username).first()
+
+ @classmethod
+ def find_by_id(cls, _id: int) -> "UserModel":
+ return cls.query.filter_by(id=_id).first()
+
+ @classmethod
+ def find_by_email(cls, email: str) -> "UserModel":
+ return cls.query.filter_by(email=email).first()
+
+ # This method will interact with Mailgun API and return the response sent
+ def send_confirmation_email(self) -> Response:
+ # http://127.0.0.1:5000 - is the 'url_root'
+ # url_for("userconfirm") - this must mathch the name of user confirmation endpoint
+ link = request.url_root[:-1] + url_for("userconfirm", user_id=self.id)
+
+ subject = "CONFIRM REGISTRATION"
+ text = f"Click the link to confirm ragistration: {link}"
+ html = f'Click the link to confirm ragistration: {link}'
+
+ return Mailgun.send_email([self.email], subject, text, html)
+
+ def save_to_database(self) -> None:
+ db.session.add(self)
+ db.session.commit()
+
+ def delete_from_database(self) -> None:
+ db.session.delete(self)
+ db.session.commit()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..40cb649
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+Flask
+Flask-RESTful
+Flask_-JWT-Extended
+Flask-SQLAlchemy
+Marshmallow
+Flask-Marshmallow
+Marshmallow-SQLAlchemy
+Python-Dotenv
\ No newline at end of file
diff --git a/resources/__init__.py b/resources/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resources/__pycache__/__init__.cpython-39.pyc b/resources/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000..a900378
Binary files /dev/null and b/resources/__pycache__/__init__.cpython-39.pyc differ
diff --git a/resources/__pycache__/item.cpython-39.pyc b/resources/__pycache__/item.cpython-39.pyc
new file mode 100644
index 0000000..624e8a7
Binary files /dev/null and b/resources/__pycache__/item.cpython-39.pyc differ
diff --git a/resources/__pycache__/store.cpython-39.pyc b/resources/__pycache__/store.cpython-39.pyc
new file mode 100644
index 0000000..d63844a
Binary files /dev/null and b/resources/__pycache__/store.cpython-39.pyc differ
diff --git a/resources/__pycache__/user.cpython-39.pyc b/resources/__pycache__/user.cpython-39.pyc
new file mode 100644
index 0000000..eb492b4
Binary files /dev/null and b/resources/__pycache__/user.cpython-39.pyc differ
diff --git a/resources/item.py b/resources/item.py
new file mode 100644
index 0000000..82306b3
--- /dev/null
+++ b/resources/item.py
@@ -0,0 +1,109 @@
+from typing import Dict
+from flask import request
+from flask_restful import Resource
+from flask_jwt_extended import (
+ jwt_required,
+ get_jwt,
+ get_jwt_identity,
+)
+
+from models.item import ItemModel
+from schemas.item import ItemSchema
+
+BLANK_ERROR = "'{}' cannot be blank"
+ITEM_NOT_FOUND = "item not found."
+NAME_ALREADY_EXISTS = "item '{}' already exists"
+ERROR_INSERTING = "An error occured while inserting the item."
+ITEM_DELETED = "Item deleted"
+item_schema = ItemSchema()
+item_list_schema = ItemSchema(many=True)
+
+
+class Item(Resource):
+
+ # TO GET ITEM WITH NAME
+ @classmethod
+ @jwt_required()
+ def get(cls, name: str) -> Dict:
+ item = ItemModel.find_item_by_name(name)
+ if item:
+ return item_schema.dump(item), 200
+
+ return {"message": ITEM_NOT_FOUND}, 404
+
+ # TO POST AN ITEM
+ @classmethod
+ @jwt_required(fresh=True)
+ def post(cls, name: str):
+ # if there already exists an item with "name", show a messege, and donot add the item
+ if ItemModel.find_item_by_name(name):
+ return {"messege": NAME_ALREADY_EXISTS.format(name)}, 400
+
+ item_json = request.get_json()
+ item_json["name"] = name # this part is necessary to populate the payload with item name
+
+ item = item_schema.load(item_json)
+
+ # TODO: put these in above try except
+ try:
+ item.save_to_database()
+ except:
+ return {"messege": ERROR_INSERTING}, 500
+
+ return item_schema.dump(item), 201 # 201 is for CREATED status
+
+ # TO DELETE AN ITEM
+ @classmethod
+ @jwt_required()
+ def delete(cls, name: str):
+ claims = get_jwt()
+
+ if not claims["is_admin"]:
+ return {"message": "Admin privilages required"}, 401
+
+ item = ItemModel.find_item_by_name(name)
+ if item:
+ item.delete_from_database()
+ return {"messege": ITEM_DELETED}
+
+ # if doesn't exist, skip deleting
+ return {"messege": ITEM_NOT_FOUND}, 400
+
+ # TO ADD OR UPDATE AN ITEM
+ @classmethod
+ def put(cls, name: str):
+ item_json = request.get_json()
+
+ item = ItemModel.find_item_by_name(name)
+
+ # if item is not available, add it
+ if item is None:
+ item_json["name"] = name
+ item = item_schema.load(item_json)
+
+ # if item exists, update it
+ else:
+ item.price = item_json["price"]
+ item.store_id = item_json["store_id"]
+
+ # whether item is changed or inserted, it has to be saved to db
+ item.save_to_database()
+ return item_schema.dump(item), 200
+
+
+class ItemList(Resource):
+ @classmethod
+ @jwt_required(optional=True)
+ def get(cls):
+ user_id = get_jwt_identity()
+ items = [item_list_schema.dump(ItemModel.find_all())]
+
+ # if user id is given, then display full details
+ if user_id:
+ return {"items": items}, 200
+
+ # else display only item name
+ return {
+ "items": [item["name"] for item in items],
+ "message": "Login to view more data.",
+ }, 200
diff --git a/resources/store.py b/resources/store.py
new file mode 100644
index 0000000..15b7a15
--- /dev/null
+++ b/resources/store.py
@@ -0,0 +1,50 @@
+from flask import request
+from flask_restful import Resource
+
+from schemas.store import StoreSchema
+from models.store import StoreModel
+
+store_schema = StoreSchema()
+store_list_schema = StoreSchema(many=True)
+
+
+class Store(Resource):
+ @classmethod
+ def get(cls, name: str):
+ store = StoreModel.find_store_by_name(name)
+ if store:
+ return store_schema.dump(store), 200
+
+ return {"message": "Store not found."}, 404
+
+ @classmethod
+ def post(cls, name: str):
+ if StoreModel.find_store_by_name(name):
+ return {"message": "Store alrady exists."}, 400
+
+ store = StoreModel(name=name) # since __init__() method was removed from StoreModel, the name needs to be passed manually as such
+ # as StoreModel is a model subclass, model class acceps keyword arguements and maps them to columns
+ # so here, passing name as a keyword argument will map to name key in store model
+
+ try:
+ store.save_to_database()
+ except:
+ return {"message": "An error occured while creating the store."}, 500
+
+ return store_schema.dump(store), 201
+
+ @classmethod
+ def delete(cls, name: str):
+ store = StoreModel.find_store_by_name(name)
+ if store:
+ store.delete_from_database()
+ return {"message": "store deleted."}
+
+ return {"message": "store don't exist"}
+
+
+class StoreList(Resource):
+ @classmethod
+ def get(cls):
+ # return {"item": list(map(lambda x: x.json(), ItemModel.query.all()))}
+ return {"stores": [store_list_schema.dump(StoreModel.find_all())]}, 200
diff --git a/resources/user.py b/resources/user.py
new file mode 100644
index 0000000..9dc76a1
--- /dev/null
+++ b/resources/user.py
@@ -0,0 +1,159 @@
+from flask import request
+from flask_restful import Resource
+from marshmallow import ValidationError
+from werkzeug.security import safe_str_cmp
+from flask_jwt_extended import (
+ create_access_token,
+ create_refresh_token,
+ jwt_required,
+ get_jwt_identity,
+ get_jwt,
+)
+import traceback
+from flask import make_response, render_template, request
+
+
+from models.user import UserModel
+from schemas.user import UserSchema
+from blacklist import BLACKLIST
+from libs.mailgun import MailgunException
+
+# extracted parser variable for global use, and made it private
+# _user_parser = reqparse.RequestParser()
+# _user_parser.add_argument(
+# "username",
+# type=str,
+# required=True,
+# help="This field cannot be empty"
+# )
+# _user_parser.add_argument(
+# "password",
+# type=str,
+# required=True,
+# help="This field cannot be empty"
+# )
+
+user_schema = UserSchema()
+
+
+# New user registration class
+class UserRegister(Resource):
+
+ # calls to post a new user (new user registration)
+ @classmethod
+ def post(cls):
+ user = user_schema.load(request.get_json())
+
+ # First check if that user is present or not
+ if UserModel.find_by_username(user.username):
+ # if exists, then don't add
+ return {"message": "An user with that username already exists."}, 400
+ if UserModel.find_by_email(user.email):
+ # if exists, then don't add
+ return {"message": "An user with that email already exists."}, 400
+
+ # user = UserModel(data["username"], data["password"])
+ # user = UserModel(**user_data) # since parser only takes in username and password, only those two will be added.
+ # flask_marshmallow already creates a user model, so we need not do it manually
+ try:
+ user.save_to_database()
+ user.send_confirmation_email()
+ return {
+ "messege": "Account created successfully, an email with activation link has been sent to your email.",
+ }, 201
+ # Delete user from database in case of any Mailgun error
+ except MailgunException as e:
+ user.delete_from_database()
+ return {"message", str(e)}, 500
+ except:
+ # print(err.messages)
+ traceback.print_exc()
+ return {"message": "Internal server error, failed to create user"}
+
+
+class User(Resource):
+ @classmethod
+ def get(cls, user_id: int):
+
+ user = UserModel.find_by_id(user_id)
+ if not user:
+ return {"message": "User not found."}, 404
+
+ return user_schema.dump(user), 200
+
+ @classmethod
+ def delete(cls, user_id: int):
+ user = UserModel.find_by_id(user_id)
+ if not user:
+ return {"message": "User not found."}, 404
+
+ user.delete_from_database()
+ return {"message": "User deleted."}, 200
+
+
+class UserLogin(Resource):
+ @classmethod
+ def post(cls):
+ # get data from user to login. Include email to optional field.
+ user_data = user_schema.load(request.get_json(), partial=("email",))
+
+ # find user in database
+ user = UserModel.find_by_username(user_data.username)
+
+ # check password
+ # this here is what authenticate() function used to do
+ if user and safe_str_cmp(user.password, user_data.password):
+ # Check if user is activated
+ if user.activated:
+ # create access and refresh tokens
+ access_token = create_access_token(identity=user.id, fresh=True) # here, identity=user.id is what identity() used to do previously
+ refresh_token = create_refresh_token(identity=user.id)
+ # print("user logged in")
+
+ return {"access_token": access_token, "refresh_token": refresh_token}, 200
+ # If user is not activated
+ return {"message": "You have not confirmed registration, please check your email."}
+
+ return {"message": "Invalid credentials."}, 401 # Unauthorized
+
+
+class UserLogout(Resource):
+ # Loggig out requirees jwt as if user is not logged in they cannot log out
+ @classmethod
+ @jwt_required()
+ def post(cls):
+ jti = get_jwt()["jti"] # jti is JWT ID, unique identifier for a JWT
+ BLACKLIST.add(jti)
+ return {"message": "Successfully logged out."}, 200
+
+
+class TokenRefresh(Resource):
+ @classmethod
+ @jwt_required(refresh=True)
+ def post(cls):
+ current_user = get_jwt_identity()
+ new_token = create_access_token(identity=current_user, fresh=False) # fresh=Flase means that user have logged in days ago.
+
+ return {"access_token": new_token}, 200
+
+
+class UserConfirm(Resource):
+ @classmethod
+ def get(cls, user_id: int):
+ user = UserModel.find_by_id(user_id)
+
+ # If user is found, activate their profile
+ if user:
+ user.activated = True
+ user.save_to_database()
+ headers = {"Content-Type": "text/html"}
+ return make_response(
+ render_template(
+ "confirmation_page.html",
+ email=user.username,
+ ),
+ 200,
+ headers,
+ )
+
+ return {"meggase": "User not found"}, 404
diff --git a/schemas/item.py b/schemas/item.py
new file mode 100644
index 0000000..8df7cc0
--- /dev/null
+++ b/schemas/item.py
@@ -0,0 +1,16 @@
+from xml.etree.ElementInclude import include
+from ma import ma
+from models.item import ItemModel
+from models.store import StoreModel
+
+
+class ItemSchema(ma.SQLAlchemyAutoSchema):
+ # If the below Meta class is excluded, while fetching user information, we also fetch
+ # the user password. So, password is included in the load_only tuple so that password field
+ # is only loaded and not displayed.
+ class Meta:
+ model = ItemModel
+ load_instance = True
+ load_only = ("store",) # makes 'store' field load_only. store_id will be displayed
+ dump_only = ("id",) # makes 'id' field dump_only.
+ include_fk = True
diff --git a/schemas/store.py b/schemas/store.py
new file mode 100644
index 0000000..a6a7444
--- /dev/null
+++ b/schemas/store.py
@@ -0,0 +1,14 @@
+from ma import ma
+from models.store import StoreModel
+from models.item import ItemModel
+from schemas.item import ItemSchema
+
+
+class StoreSchema(ma.SQLAlchemyAutoSchema):
+ items = ma.Nested(ItemSchema, many=True)
+
+ class Meta:
+ model = StoreModel
+ load_instance = True
+ dump_only = ("id",) # makes 'id' field dump_only.
+ include_fk = True
diff --git a/schemas/user.py b/schemas/user.py
new file mode 100644
index 0000000..c8d0a65
--- /dev/null
+++ b/schemas/user.py
@@ -0,0 +1,17 @@
+from ma import ma
+from models.user import UserModel
+
+
+# use SQLAlchemyAutoSchema. ModelSchema is deprecated
+class UserSchema(ma.SQLAlchemyAutoSchema):
+ # If the below Meta class is excluded, while fetching user information, we also fetch
+ # the user password. So, password is included in the load_only tuple so that password field
+ # is only loaded and not displayed.
+ class Meta:
+ model = UserModel
+ load_instance = True
+ load_only = ("password",) # makes 'password' field load_only
+ dump_only = (
+ "id",
+ "activated",
+ ) # makes 'id' field dump_only.
diff --git a/static/confirmation_page.css b/static/confirmation_page.css
new file mode 100644
index 0000000..5a74cf6
--- /dev/null
+++ b/static/confirmation_page.css
@@ -0,0 +1,5 @@
+.full-height {
+ height: 80vh;
+ margin: 10px;
+ background-color: aquamarine;
+}
diff --git a/templates/confirmation_page.html b/templates/confirmation_page.html
new file mode 100644
index 0000000..4eb558e
--- /dev/null
+++ b/templates/confirmation_page.html
@@ -0,0 +1,31 @@
+
+
+
+
+ Registration Confirmation
+
+
+
+
+
+
+
+
Thank You
+
Your registration has been confirmed through < {{ email }} >.