Skip to content

Commit

Permalink
Merge pull request #13 from israelias/backend
Browse files Browse the repository at this point in the history
Backend
  • Loading branch information
israelias authored Aug 28, 2021
2 parents 5f78896 + acc3546 commit 15d4ffe
Show file tree
Hide file tree
Showing 15 changed files with 370 additions and 7 deletions.
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn backend.app:app
16 changes: 16 additions & 0 deletions backend/admin/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from flask_admin import Admin
from .mongoview import MyAdminIndexView


admin = Admin(
name="Cheat-Hub Backend",
index_view=MyAdminIndexView(),
endpoint="admin",
url="/admin",
template_mode="bootstrap4",
)


def initialize_admin(app):
admin.init_app(app)
app.config["FLASK_ADMIN_SWATCH"] = "cerulean"
10 changes: 10 additions & 0 deletions backend/admin/basicauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import os
from flask_basicauth import BasicAuth

basic_auth = BasicAuth()


def initialize_basicauth(app):
basic_auth.init_app(app)
app.config["BASIC_AUTH_USERNAME"] = os.environ.get("BASIC_AUTH_USERNAME")
app.config["BASIC_AUTH_PASSWORD"] = os.environ.get("BASIC_AUTH_PASSWORD")
31 changes: 31 additions & 0 deletions backend/admin/mail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os
from flask_mail import Mail

if not os.path.exists("env.py"):
pass
else:
import env

mail = Mail()


def initialize_mail(app):
mail.init_app(app)
app.config["MAIL_SERVER"] = os.environ.get("MAIL_SERVER")
app.config["MAIL_PORT"] = int(os.environ.get("MAIL_PORT"))
app.config["MAIL_USERNAME"] = os.environ.get("MAIL_USERNAME")
app.config["MAIL_PASSWORD"] = os.environ.get("MAIL_PASSWORD")
app.config["MAIL_DEFAULT_SENDER"] = os.environ.get("MAIL_DEFAULT_SENDER")
app.config["MAIL_USE_SSL"] = True

# MAIL_SERVER : default ‘localhost’
# MAIL_PORT : default 25
# MAIL_USE_TLS : default False
# MAIL_USE_SSL : default False
# MAIL_DEBUG : default app.debug
# MAIL_USERNAME : default None
# MAIL_PASSWORD : default None
# MAIL_DEFAULT_SENDER : default None
# MAIL_MAX_EMAILS : default None
# MAIL_SUPPRESS_SEND : default app.testing
# MAIL_ASCII_ATTACHMENTS : default False
44 changes: 44 additions & 0 deletions backend/admin/mongoview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from flask import Response, redirect
from flask_admin import AdminIndexView, expose
from flask_admin.contrib.mongoengine.view import ModelView
from werkzeug.exceptions import HTTPException
from .basicauth import basic_auth

"""
The following three classes are inherited from their respective base class,
and are customized, to make flask_admin compatible with BasicAuth.
"""


class AuthException(HTTPException):
def __init__(self, message):
super().__init__(
message,
Response(
"You could not be authenticated. Please refresh the page.",
401,
{"WWW-Authenticate": 'Basic realm="Login Required"'},
),
)


class MyModelView(ModelView):
def is_accessible(self):
if not basic_auth.authenticate():
raise AuthException("Not authenticated.")
else:
return True

def inaccessible_callback(self, name, **kwargs):
return redirect(basic_auth.challenge())


class MyAdminIndexView(AdminIndexView):
def is_accessible(self):
if not basic_auth.authenticate():
raise AuthException("Not authenticated.")
else:
return True

def inaccessible_callback(self, name, **kwargs):
return redirect(basic_auth.challenge())
13 changes: 13 additions & 0 deletions backend/admin/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .admin import admin
from flask_admin.contrib.mongoengine.view import ModelView
from .mongoview import MyModelView
from database.models import User, Snippet, Collection, TokenBlocklist

# https://flask-admin.readthedocs.io/en/latest/api/mod_contrib_mongoengine/


def initialize_views():
admin.add_view(MyModelView(User))
admin.add_view(MyModelView(Snippet))
admin.add_view(MyModelView(Collection))
admin.add_view(MyModelView(TokenBlocklist))
26 changes: 21 additions & 5 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
from flask_cors import CORS
from flask_bcrypt import Bcrypt
from flask_jwt_extended import JWTManager
from flask_mail import Mail

from admin.basicauth import initialize_basicauth
from admin.admin import initialize_admin
from admin.views import initialize_views

from database.db import initialize_db
from flask_restful import Api
from resources.errors import errors
from resources.routes import initialize_routes


if not os.path.exists("env.py"):
pass
Expand All @@ -18,13 +23,24 @@

# ===========================================================================
# * `Flask App and Configs`
# ? Executes Flask app deployment
# ? Executes Flask app deployment
# Initializes app and packages by order of dependency requirements
# ===========================================================================


app = Flask(__name__)

app.config["MAIL_SERVER"] = os.environ.get("MAIL_SERVER")
app.config["MAIL_PORT"] = int(os.environ.get("MAIL_PORT"))
app.config["MAIL_USERNAME"] = os.environ.get("MAIL_USERNAME")
app.config["MAIL_PASSWORD"] = os.environ.get("MAIL_PASSWORD")
app.config["MAIL_DEFAULT_SENDER"] = os.environ.get("MAIL_DEFAULT_SENDER")
app.config["MAIL_USE_TLS"] = True

mail = Mail(app)

# initialize_mail(app)
from resources.routes import initialize_routes

api = Api(app, errors=errors)
bcrypt = Bcrypt(app)
Expand All @@ -35,16 +51,16 @@
app.config["MONGODB_PORT"] = int(os.environ.get("MONGODB_PORT"))
app.secret_key = os.environ.get("SECRET_KEY")
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
app.config["FLASK_ADMIN_SWATCH"] = "cerulean"
app.config["DEBUG_TB_ENABLED"] = True
app.config["JSON_SORT_KEYS"] = False


initialize_db(app)
initialize_routes(api)



initialize_basicauth(app)
initialize_admin(app)
initialize_views()


if __name__ == "__main__":
Expand Down
27 changes: 25 additions & 2 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
aniso8601==9.0.1
aniso8601==7.0.0
appdirs==1.4.4
astroid==2.5.6
bcrypt==3.2.0
black==20.8b1
blinker==1.4
cffi==1.14.5
click==7.1.2
dnspython==2.1.0
email-validator==1.1.3
Flask==1.1.2
Flask-Admin==1.5.8
Flask-BasicAuth==0.2.0
Flask-Bcrypt==0.7.1
Flask-Cors==3.0.10
Flask-JWT-Extended==4.1.0
Flask-Mail==0.9.1
flask-mongoengine==1.0.0
Flask-RESTful==0.3.8
Flask-WTF==0.14.3
idna==3.1
install==1.3.4
iso8601==0.1.14
isort==5.8.0
itsdangerous==1.1.0
Jinja2==2.11.3
lazy-object-proxy==1.6.0
MarkupSafe==1.1.1
mccabe==0.6.1
mongoengine==0.23.0
mypy-extensions==0.4.3
pathspec==0.8.1
promise==2.3
pycparser==2.20
PyJWT==2.0.1
pylint==2.8.3
pymongo==3.11.3
pytz==2021.1
regex==2021.4.4
Rx==1.6.1
sentinels==1.0.0
singledispatch==3.6.1
six==1.15.0
toml==0.10.2
typed-ast==1.4.2
typing-extensions==3.7.4.3
Werkzeug==1.0.1
wrapt==1.12.1
WTForms==2.3.3
gunicorn
5 changes: 5 additions & 0 deletions backend/resources/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ class BadTokenError(Exception):
pass


class ExpiredTokenError(Exception):
pass


errors = {
"InternalServerError": {"message": "Something went wrong.", "status": 500},
"SchemaValidationError": {
Expand Down Expand Up @@ -100,4 +104,5 @@ class BadTokenError(Exception):
"status": 400,
},
"BadTokenError": {"message": "Invalid token.", "status": 407},
"ExpiredTokenError": {"message": "Token is expired.", "status": 410},
}
88 changes: 88 additions & 0 deletions backend/resources/reset_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from flask import request, render_template
from flask_jwt_extended import create_access_token, decode_token

from database.models import User
from flask_restful import Resource
from services.mail_service import send_email

from resources.errors import (
SchemaValidationError,
InternalServerError,
EmailDoesNotExistError,
BadTokenError,
ExpiredTokenError,
)
from jwt.exceptions import (
ExpiredSignatureError,
DecodeError,
InvalidTokenError,
)

import datetime


class ForgotPassword(Resource):
def post(self):
url = request.host_url + "reset/"
try:
body = request.get_json()
email = body.get("email")
if not email:
raise SchemaValidationError

user = User.objects.get(email=email)
if not user:
raise EmailDoesNotExistError

expires = datetime.timedelta(hours=24)
reset_token = create_access_token(str(user.id), expires_delta=expires)

return send_email(
"[Cheat-hub] Reset Your Password",
sender="[email protected]",
recipients=[user.email],
text_body=render_template("reset_password.txt", url=url + reset_token),
html_body=render_template("reset_password.html", url=url + reset_token),
)
except SchemaValidationError:
raise SchemaValidationError
except EmailDoesNotExistError:
raise EmailDoesNotExistError
except Exception as e:
raise InternalServerError


class ResetPassword(Resource):
def post(self):
url = request.host_url + "reset/"
try:
body = request.get_json()
reset_token = body.get("reset_token")
password = body.get("password")

if not reset_token or not password:
raise SchemaValidationError

user_id = decode_token(reset_token)["sub"]
user = User.objects.get(id=user_id)

user.modify(password=password)
user.hash_password()
user.save()

return send_email(
"[Cheat-hub] Password reset successful",
sender="[email protected]",
recipients=[user.email],
text_body="Password reset was successful",
html_body="<p>Password reset was successful</p>",
)

except SchemaValidationError:
raise SchemaValidationError
except ExpiredSignatureError:
raise ExpiredTokenError
except (DecodeError, InvalidTokenError):
raise BadTokenError
except Exception as e:
raise InternalServerError
5 changes: 5 additions & 0 deletions backend/resources/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from .tags import TagsApi

from .reset_password import ForgotPassword, ResetPassword

# ===========================================================================
# * Initialize Routes
# ? Initializes assignment of defined resource classes to a url.
Expand Down Expand Up @@ -81,3 +83,6 @@ def initialize_routes(api):

"""Tag list endpoint: List of all current tags returned. """
api.add_resource(TagsApi, "/api/tags")

api.add_resource(ForgotPassword, "/api/auth/forgot")
api.add_resource(ResetPassword, "/api/auth/reset")
Loading

1 comment on commit 15d4ffe

@vercel
Copy link

@vercel vercel bot commented on 15d4ffe Aug 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.