aboutsummaryrefslogtreecommitdiffstats
path: root/pysite
diff options
context:
space:
mode:
authorGravatar Nate the great <[email protected]>2018-03-29 04:52:29 -0400
committerGravatar Gareth Coles <[email protected]>2018-03-29 09:52:29 +0100
commit1889f3248ae83cc773a5443c6ba0f62834d73b7b (patch)
tree7603d1da7f64006075cf3a8b58548e1a49ca6840 /pysite
parentReorder resources.json (diff)
Oauth (#45)
* Creating a OAUTH login for the site. We still need to have a way to fill in credentials however. Signed-off-by: Zwork101 <[email protected]> * How ya like me now Travis? Signed-off-by: Zwork101 <[email protected]> * Fix slight error in database insertion * Revert "Fix slight error in database insertion" (wrong branch) This reverts commit 9ac6cbb * Don't have snekchek working, but I'll try again. Signed-off-by: Zwork101 <[email protected]> * Please enter the commit message for your changes. Lines starting * Adding Oauth2 login to site. * Add prefix * Add prefix * This never happened * Flipping Travis I still can't get snekchek to work locally. * Added a whole bunch of stuff, ready to be used. Signed-off-by: Zwork101 <[email protected]> * Making sessions more secure, and future safe. Signed-off-by: Zwork101 <[email protected]> * Adding some quick stuff Signed-off-by: Zwork101 <[email protected]> * Appease the flake8 gods Signed-off-by: Zwork101 <[email protected]> * Appease the flake8 gods Signed-off-by: Zwork101 <[email protected]> * Whoops Signed-off-by: Zwork101 <[email protected]> * Add comments, fix user_data function. Signed-off-by: Zwork101 <[email protected]> * Whooops, forgot to flake :/ Signed-off-by: Zwork101 <[email protected]> * Make it look nicer, thanks Aperture. Signed-off-by: Zwork101 <[email protected]> * Fixing login issues and added button * Add a OauthMixin to allow for easy data access. Clean stuff up. Signed-off-by: Zwork101 <[email protected]> * Fix a test, and use self.assertEqual rather then self.assertEquals! Signed-off-by: Zwork101 <[email protected]> * Please don't ask how that happened. Signed-off-by: Zwork101 <[email protected]> * Added some tests, moved a bunch of stuff around. Mainly cleaned stuff up. Signed-off-by: Zwork101 <[email protected]> * Add a ton of tests, try to please the coverall gods :D, moved some code into a function for testing. Signed-off-by: Zwork101 <[email protected]> * Just some stupid stuff I missed. Signed-off-by: Zwork101 <[email protected]> * Fix an issue with the test, and add docs Signed-off-by: Zwork101 <[email protected]> * Remove pointless function. (join_user) Signed-off-by: Zwork101 <[email protected]> * Fix test consistency
Diffstat (limited to 'pysite')
-rw-r--r--pysite/base_route.py4
-rw-r--r--pysite/constants.py21
-rw-r--r--pysite/database.py8
-rw-r--r--pysite/mixins.py48
-rw-r--r--pysite/oauth.py77
-rw-r--r--pysite/route_manager.py20
-rw-r--r--pysite/views/main/index.py3
-rw-r--r--pysite/views/main/logout.py15
-rw-r--r--pysite/views/tests/index.py1
9 files changed, 186 insertions, 11 deletions
diff --git a/pysite/base_route.py b/pysite/base_route.py
index 4e1a63a7..71a4c894 100644
--- a/pysite/base_route.py
+++ b/pysite/base_route.py
@@ -6,9 +6,10 @@ from flask import Blueprint, Response, jsonify, render_template
from flask.views import MethodView
from pysite.constants import ErrorCodes
+from pysite.mixins import OauthMixin
-class BaseView(MethodView):
+class BaseView(MethodView, OauthMixin):
"""
Base view class with functions and attributes that should be common to all view classes.
@@ -27,6 +28,7 @@ class BaseView(MethodView):
"""
context["current_page"] = self.name
context["view"] = self
+ context["logged_in"] = self.logged_in
return render_template(template_names, **context)
diff --git a/pysite/constants.py b/pysite/constants.py
index c84ca245..7df4674e 100644
--- a/pysite/constants.py
+++ b/pysite/constants.py
@@ -1,7 +1,7 @@
# coding=utf-8
from enum import Enum, IntEnum
-import os
+from os import environ
class ErrorCodes(IntEnum):
@@ -22,6 +22,17 @@ ADMIN_ROLE = 267628507062992896
MODERATOR_ROLE = 267629731250176001
HELPER_ROLE = 267630620367257601
+SERVER_ID = 267624335836053506
+
+DISCORD_API_ENDPOINT = "https://discordapp.com/api"
+
+DISCORD_OAUTH_REDIRECT = "/auth/discord"
+DISCORD_OAUTH_AUTHORIZED = "/auth/discord/authorized"
+DISCORD_OAUTH_ID = environ.get('DISCORD_OAUTH_ID', '')
+DISCORD_OAUTH_SECRET = environ.get('DISCORD_OAUTH_SECRET', '')
+DISCORD_OAUTH_SCOPE = 'identify email guilds.join'
+OAUTH_DATABASE = "oauth_data"
+
ERROR_DESCRIPTIONS = {
# 5XX
500: "The server encountered an unexpected error ._.",
@@ -46,9 +57,9 @@ ERROR_DESCRIPTIONS = {
}
# PaperTrail logging
-PAPERTRAIL_ADDRESS = os.environ.get("PAPERTRAIL_ADDRESS") or None
-PAPERTRAIL_PORT = int(os.environ.get("PAPERTRAIL_PORT") or 0)
+PAPERTRAIL_ADDRESS = environ.get("PAPERTRAIL_ADDRESS") or None
+PAPERTRAIL_PORT = int(environ.get("PAPERTRAIL_PORT") or 0)
# DataDog logging
-DATADOG_ADDRESS = os.environ.get("DATADOG_ADDRESS") or None
-DATADOG_PORT = int(os.environ.get("DATADOG_PORT") or 0)
+DATADOG_ADDRESS = environ.get("DATADOG_ADDRESS") or None
+DATADOG_PORT = int(environ.get("DATADOG_PORT") or 0)
diff --git a/pysite/database.py b/pysite/database.py
index add76923..4c2153fe 100644
--- a/pysite/database.py
+++ b/pysite/database.py
@@ -103,9 +103,11 @@ class RethinkDB:
self.log.debug(f"Table created: '{table_name}'")
return True
- def delete(self, table_name: str, primary_key: Optional[str] = None,
- durability: str = "hard", return_changes: Union[bool, str] = False
- ) -> Union[Dict[str, Any], None]:
+ def delete(self,
+ table_name: str,
+ primary_key: Union[str, None] = None,
+ durability: str="hard",
+ return_changes: Union[bool, str] = False) -> dict:
"""
Delete one or all documents from a table. This can only delete
either the contents of an entire table, or a single document.
diff --git a/pysite/mixins.py b/pysite/mixins.py
index 059f871d..5b1a780f 100644
--- a/pysite/mixins.py
+++ b/pysite/mixins.py
@@ -6,7 +6,7 @@ from _weakref import ref
from pysite.database import RethinkDB
-class DBMixin():
+class DBMixin:
"""
Mixin for classes that make use of RethinkDB. It can automatically create a table with the specified primary
key using the attributes set at class-level.
@@ -59,3 +59,49 @@ class DBMixin():
@property
def db(self) -> RethinkDB:
return self._db()
+
+
+class OauthMixin:
+ """
+ Mixin for the classes that need access to a logged in user's information. This class should be used
+ to grant route's access to user information, such as name, email, id, ect.
+
+ There will almost never be a need for someone to inherit this, as BaseView does that for you.
+
+ This class will add 3 properties to your route:
+
+ * logged_in (bool): True if user is registered with the site, False else wise.
+
+ * user_data (dict): A dict that looks like this:
+
+ {
+ "user_id": Their discord ID,
+ "username": Their discord username (without discriminator),
+ "discriminator": Their discord discriminator,
+ "email": Their email, in which is connected to discord
+ }
+
+ user_data returns None, if the user isn't logged in.
+
+ * oauth (OauthBackend): The instance of pysite.oauth.OauthBackend, connected to the RouteManager.
+ """
+
+ @classmethod
+ def setup(cls: "OauthMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
+
+ if hasattr(super(), "setup"):
+ super().setup(manager, blueprint) # pragma: no cover
+
+ cls._oauth = ref(manager.oauth_backend)
+
+ @property
+ def logged_in(self) -> bool:
+ return self.user_data is not None
+
+ @property
+ def user_data(self) -> dict:
+ return self.oauth.user_data()
+
+ @property
+ def oauth(self):
+ return self._oauth()
diff --git a/pysite/oauth.py b/pysite/oauth.py
new file mode 100644
index 00000000..8370b713
--- /dev/null
+++ b/pysite/oauth.py
@@ -0,0 +1,77 @@
+import logging
+from uuid import uuid4, uuid5
+
+from flask import session
+from flask_dance.consumer.backend import BaseBackend
+from flask_dance.contrib.discord import discord
+
+
+from pysite.constants import DISCORD_API_ENDPOINT, OAUTH_DATABASE
+
+
+class OauthBackend(BaseBackend):
+ """
+ This is the backend for the oauth
+
+ This is used to manage users that have completed
+ an oauth dance. It contains 3 functions, get, set,
+ and delete, however we only use set.
+
+ Inherits:
+ flake_dance.consumer.backend.BaseBackend
+ pysite.mixins.DBmixin
+
+ Properties:
+ key: The app's secret, we use it too make session IDs
+ """
+
+ def __init__(self, manager):
+ super().__init__()
+ self.db = manager.db
+ self.key = manager.app.secret_key
+ self.db.create_table(OAUTH_DATABASE, primary_key="id")
+
+ def get(self, *args, **kwargs): # Not used
+ pass
+
+ def set(self, blueprint, token):
+
+ user = self.get_user()
+ self.join_discord(token["access_token"], user["id"])
+ sess_id = str(uuid5(uuid4(), self.key))
+ self.add_user(token, user, sess_id)
+
+ def delete(self, blueprint): # Not used
+ pass
+
+ def add_user(self, token_data: dict, user_data: dict, session_id: str):
+ session["session_id"] = session_id
+
+ self.db.insert(OAUTH_DATABASE, {"id": session_id,
+ "access_token": token_data["access_token"],
+ "refresh_token": token_data["refresh_token"],
+ "expires_at": token_data["expires_at"],
+ "snowflake": user_data["id"]})
+
+ self.db.insert("users", {"user_id": user_data["id"],
+ "username": user_data["username"],
+ "discriminator": user_data["discriminator"],
+ "email": user_data["email"]})
+
+ def get_user(self) -> dict:
+ resp = discord.get(DISCORD_API_ENDPOINT + "/users/@me") # 'discord' is a request.Session with oauth information
+ if resp.status_code != 200:
+ logging.warning("Unable to get user information: " + str(resp.json()))
+ return resp.json()
+
+ def user_data(self):
+ user_id = session.get("session_id")
+ if user_id: # If the user is logged in, get user info.
+ creds = self.db.get(OAUTH_DATABASE, user_id)
+ if creds:
+ return self.db.get("users", creds["snowflake"])
+
+ def logout(self):
+ sess_id = session.get("session_id")
+ if sess_id and self.db.get(OAUTH_DATABASE, sess_id): # If user exists in db,
+ self.db.delete(OAUTH_DATABASE, sess_id) # remove them (at least, their session)
diff --git a/pysite/route_manager.py b/pysite/route_manager.py
index 72517a3c..9ecd3ced 100644
--- a/pysite/route_manager.py
+++ b/pysite/route_manager.py
@@ -5,10 +5,15 @@ import logging
import os
from flask import Blueprint, Flask
+from flask_dance.contrib.discord import make_discord_blueprint
from flask_sockets import Sockets
from pysite.base_route import APIView, BaseView, ErrorView, RouteView
+from pysite.constants import (
+ DISCORD_OAUTH_ID, DISCORD_OAUTH_SCOPE, DISCORD_OAUTH_SECRET, DISCORD_OAUTH_REDIRECT, DISCORD_OAUTH_AUTHORIZED
+)
from pysite.database import RethinkDB
+from pysite.oauth import OauthBackend
from pysite.websockets import WS
TEMPLATES_PATH = "../templates"
@@ -31,6 +36,21 @@ class RouteManager:
self.app.before_request(self.db.before_request)
self.app.teardown_request(self.db.teardown_request)
+ # Load the oauth blueprint
+ self.oauth_backend = OauthBackend(self)
+ self.oauth_blueprint = make_discord_blueprint(
+ DISCORD_OAUTH_ID,
+ DISCORD_OAUTH_SECRET,
+ DISCORD_OAUTH_SCOPE,
+ '/',
+ login_url=DISCORD_OAUTH_REDIRECT,
+ authorized_url=DISCORD_OAUTH_AUTHORIZED,
+ backend=self.oauth_backend
+ )
+ self.log.debug(f"Loading Blueprint: {self.oauth_blueprint.name}")
+ self.app.register_blueprint(self.oauth_blueprint)
+ self.log.debug("")
+
# Load the main blueprint
self.main_blueprint = Blueprint("main", __name__)
self.log.debug(f"Loading Blueprint: {self.main_blueprint.name}")
diff --git a/pysite/views/main/index.py b/pysite/views/main/index.py
index 210eb057..8d0cb349 100644
--- a/pysite/views/main/index.py
+++ b/pysite/views/main/index.py
@@ -1,5 +1,6 @@
# coding=utf-8
from pysite.base_route import RouteView
+from pysite.constants import DISCORD_OAUTH_REDIRECT
class IndexView(RouteView):
@@ -7,4 +8,4 @@ class IndexView(RouteView):
name = "index"
def get(self):
- return self.render("main/index.html")
+ return self.render("main/index.html", login_url=DISCORD_OAUTH_REDIRECT)
diff --git a/pysite/views/main/logout.py b/pysite/views/main/logout.py
new file mode 100644
index 00000000..fce30972
--- /dev/null
+++ b/pysite/views/main/logout.py
@@ -0,0 +1,15 @@
+from flask import redirect, session
+
+from pysite.base_route import RouteView
+
+
+class LogoutView(RouteView):
+ name = "logout"
+ path = "/auth/logout"
+
+ def get(self):
+ if self.logged_in:
+ # remove user's session
+ del session["session_id"]
+ self.oauth.logout()
+ return redirect("/")
diff --git a/pysite/views/tests/index.py b/pysite/views/tests/index.py
index 3071bf0e..2a55a112 100644
--- a/pysite/views/tests/index.py
+++ b/pysite/views/tests/index.py
@@ -7,6 +7,7 @@ from pysite.base_route import RouteView
from pysite.constants import ValidationTypes
from pysite.decorators import api_params
+
SCHEMA = Schema([{"test": str}])
REQUIRED_KEYS = ["test"]