Basic dashboard support for Authenticated users

This commit is contained in:
Jeremy Zhang 2017-03-25 00:52:56 -07:00
parent 11c326b35d
commit 2e5f6fafbf
11 changed files with 284 additions and 33 deletions

26
run.py
View File

@ -3,5 +3,31 @@ from titanembeds.app import app
if __name__ == "__main__": if __name__ == "__main__":
import os import os
from flask import jsonify, request
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # Testing oauthlib os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # Testing oauthlib
# Session viewer https://gist.github.com/babldev/502364a3f7c9bafaa6db
def decode_flask_cookie(secret_key, cookie_str):
import hashlib
from itsdangerous import URLSafeTimedSerializer
from flask.sessions import TaggedJSONSerializer
salt = 'cookie-session'
serializer = TaggedJSONSerializer()
signer_kwargs = {
'key_derivation': 'hmac',
'digest_method': hashlib.sha1
}
s = URLSafeTimedSerializer(secret_key, salt=salt, serializer=serializer, signer_kwargs=signer_kwargs)
return s.loads(cookie_str)
@app.route("/session")
def session():
cookie = request.cookies.get('session')
if cookie:
decoded = decode_flask_cookie(app.secret_key, request.cookies.get('session'))
else:
decoded = None
return jsonify(session_cookie=decoded)
app.run(host="0.0.0.0",port=3000,debug=True) app.run(host="0.0.0.0",port=3000,debug=True)

View File

@ -2,7 +2,8 @@ session:
- unauthenticated T/F - unauthenticated T/F
- user_id - random generated 4 digit discrim upon nick creation, otherwise the discord client id if authed - user_id - random generated 4 digit discrim upon nick creation, otherwise the discord client id if authed
- username - username
- user_keys {guildid: key, ...} - unused if authed - avatar
- user_keys {guildid: key, ...} - replaced with discord token dict if authed
database: database:

View File

@ -1,13 +1,13 @@
from config import config from config import config
from database import db from database import db
from flask import Flask, render_template, request, session, url_for, redirect from flask import Flask, render_template, request, session, url_for, redirect, jsonify
import blueprints.api import blueprints.api
import blueprints.user import blueprints.user
import os import os
os.chdir(config['app-location']) os.chdir(config['app-location'])
app = Flask(__name__) app = Flask(__name__, static_folder="static")
app.config['SQLALCHEMY_DATABASE_URI'] = config['database-uri'] app.config['SQLALCHEMY_DATABASE_URI'] = config['database-uri']
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Suppress the warning/no need this on for now. app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Suppress the warning/no need this on for now.
app.secret_key = config['app-secret'] app.secret_key = config['app-secret']
@ -27,14 +27,11 @@ def post_set_username(guildid, channelid):
return redirect(url_for("embed_get", guildid=guildid, channelid=channelid)) return redirect(url_for("embed_get", guildid=guildid, channelid=channelid))
@app.route("/") @app.route("/")
def hello(): def index():
return "This page is not blank" return render_template("index.html.jinja2")
@app.route("/embed/<guildid>/<channelid>") @app.route("/embed/<guildid>/<channelid>")
def embed_get(guildid, channelid): def embed_get(guildid, channelid):
if 'username' not in session: if 'username' not in session:
return redirect(url_for("get_set_username", guildid=guildid, channelid=channelid)) return redirect(url_for("get_set_username", guildid=guildid, channelid=channelid))
return render_template("embed.html") return render_template("embed.html")
if __name__ == "__main__":
app.run(host="0.0.0.0",port=3000,debug=True)

View File

@ -1,5 +1,5 @@
from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans, AuthenticatedUsers
from titanembeds.decorators import valid_session_required from titanembeds.decorators import valid_session_required, discord_users_only
from titanembeds.discordrest import DiscordREST from titanembeds.discordrest import DiscordREST
from flask import Blueprint, abort, jsonify, session, request from flask import Blueprint, abort, jsonify, session, request
from sqlalchemy import and_ from sqlalchemy import and_
@ -141,8 +141,21 @@ def create_unauthenticated_user():
status = {'banned': True} status = {'banned': True}
return jsonify(status=status) return jsonify(status=status)
@api.route("/new_guild", methods=["POST"])
@discord_users_only(api=True)
def post_new_guild():
pass
@api.route("/query_guild", methods=["GET"]) @api.route("/query_guild", methods=["GET"])
@valid_session_required(api=True) @valid_session_required(api=True)
def query_guild(): def query_guild():
guild_id = request.args.get('guild_id') guild_id = request.args.get('guild_id')
return jsonify(exists=check_guild_existance(guild_id)) return jsonify(exists=check_guild_existance(guild_id))
@api.route("/check_discord_authentication", methods=["GET"])
@discord_users_only(api=True)
def check_discord_authentication():
if not session['unauthenticated']:
return jsonify(error=False)
else:
return jsonify(error=True)

View File

@ -1,35 +1,64 @@
from flask import Blueprint, request, redirect, jsonify, abort, session from flask import Blueprint, request, redirect, jsonify, abort, session, url_for, render_template
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from config import config from config import config
from titanembeds.decorators import discord_users_only
user = Blueprint("user", __name__) user = Blueprint("user", __name__)
redirect_url = config['app-base-url'] + "/user/callback" redirect_url = config['app-base-url'] + "/user/callback"
authorize_url = "https://discordapp.com/api/oauth2/authorize" authorize_url = "https://discordapp.com/api/oauth2/authorize"
token_url = "https://discordapp.com/api/oauth2/token" token_url = "https://discordapp.com/api/oauth2/token"
avatar_base_url = "https://cdn.discordapp.com/avatars/" avatar_base_url = "https://cdn.discordapp.com/avatars/"
guild_icon_url = "https://cdn.discordapp.com/icons/"
def make_session(token=None, state=None, scope=None): def make_authenticated_session(token=None, state=None, scope=None):
return OAuth2Session( return OAuth2Session(
client_id=config['client-id'], client_id=config['client-id'],
token=token, token=token,
state=state, state=state,
scope=scope, scope=scope,
redirect_uri=redirect_url, redirect_uri=request.url_root + "user/callback",
) )
def get_current_user(): def discordrest_from_user(endpoint):
token = session['discord_token'] token = session['user_keys']
discord = make_session(token=token) discord = make_authenticated_session(token=token)
req = discord.get("https://discordapp.com/api/users/@me") req = discord.get("https://discordapp.com/api/v6{}".format(endpoint))
return req
def get_current_authenticated_user():
req = discordrest_from_user("/users/@me")
if req.status_code != 200: if req.status_code != 200:
abort(req.status_code) abort(req.status_code)
user = req.json() user = req.json()
return user return user
def user_has_permission(permission, index):
return bool((int(permission) >> index) & 1)
def get_user_guilds():
req = discordrest_from_user("/users/@me/guilds")
return req
def get_user_managed_servers():
guilds = get_user_guilds().json()
filtered = []
for guild in guilds:
permission = guild['permissions'] # Manage Server, Ban Members, Kick Members
if guild['owner'] or user_has_permission(permission, 5) or user_has_permission(permission, 2) or user_has_permission(permission, 1):
filtered.append(guild)
return filtered
def generate_avatar_url(id, av):
return avatar_base_url + str(id) + '/' + str(av) + '.jpg'
def generate_guild_icon_url(id, hash):
return guild_icon_url + str(id) + "/" + str(hash) + ".jpg"
@user.route("/login_authenticated", methods=["GET"]) @user.route("/login_authenticated", methods=["GET"])
def login_authenticated(): def login_authenticated():
session["redirect"] = request.args.get("redirect")
scope = ['identify', 'guilds', 'guilds.join'] scope = ['identify', 'guilds', 'guilds.join']
discord = make_session(scope=scope) discord = make_authenticated_session(scope=scope)
authorization_url, state = discord.authorization_url( authorization_url, state = discord.authorization_url(
authorize_url, authorize_url,
access_type="offline" access_type="offline"
@ -41,27 +70,40 @@ def login_authenticated():
def callback(): def callback():
state = session.get('oauth2_state') state = session.get('oauth2_state')
if not state or request.values.get('error'): if not state or request.values.get('error'):
return "state error" return redirect(url_for('user.logout'))
discord = make_session(state=state) discord = make_authenticated_session(state=state)
discord_token = discord.fetch_token( discord_token = discord.fetch_token(
token_url, token_url,
client_secret=config['client-secret'], client_secret=config['client-secret'],
authorization_response=request.url) authorization_response=request.url)
if not discord_token: if not discord_token:
return "no discord token" return redirect(url_for('user.logout'))
session['discord_token'] = discord_token session['user_keys'] = discord_token
return str(discord_token) session['unauthenticated'] = False
user = get_current_authenticated_user()
session['user_id'] = user['id']
session['username'] = user['username']
session['avatar'] = generate_avatar_url(user['id'], user['avatar'])
if session["redirect"]:
return redirect(session["redirect"])
return redirect(url_for("user.dashboard"))
@user.route('/logout', methods=["GET"]) @user.route('/logout', methods=["GET"])
def logout(): def logout():
session.clear() session.clear()
return "logged out" return redirect(url_for("index"))
@user.route("/dashboard")
@discord_users_only()
def dashboard():
return render_template("dashboard.html.jinja2", servers=get_user_managed_servers(), icon_generate=generate_guild_icon_url)
@user.route("/administrate_guild/<guild_id>")
@discord_users_only()
def administrate_guild(guild_id):
return str(guild_id)
@user.route('/me') @user.route('/me')
@discord_users_only()
def me(): def me():
return jsonify(user=get_current_user()) return jsonify(user=get_current_authenticated_user())
@user.route('/avatar')
def avatar():
user = get_current_user()
return avatar_base_url + str(user['id']) + '/' + str(user['avatar']) + '.jpg'

View File

@ -1,17 +1,28 @@
from functools import wraps from functools import wraps
from flask import url_for, redirect, session, jsonify from flask import url_for, redirect, session, jsonify, abort
def valid_session_required(api=False): def valid_session_required(api=False):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if 'unauthenticated' not in session or 'user_id' not in session or 'username' not in session: if 'unauthenticated' not in session or 'user_id' not in session or 'username' not in session:
session.clear()
if api: if api:
return jsonify(error=True, message="Unauthenticated session"), 403 return jsonify(error=True, message="Unauthenticated session"), 403
redirect(url_for('index')) redirect(url_for('user.logout'))
if session['unauthenticated'] and 'user_keys' not in session: if session['unauthenticated'] and 'user_keys' not in session:
session['user_keys'] = {} session['user_keys'] = {}
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
return decorator return decorator
def discord_users_only(api=False):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'unauthenticated' not in session or session['unauthenticated']:
if api:
return jsonify(error=True, message="Not logged in as a discord user"), 403
return redirect(url_for("user.login_authenticated"))
return f(*args, **kwargs)
return decorated_function
return decorator

View File

@ -105,6 +105,11 @@ class DiscordREST:
# Guild # Guild
##################### #####################
def get_guild(self, guild_id):
_endpoint = "/guilds/{guild_id}".format(guild_id=guild_id)
r = self.request("GET", _endpoint)
return r
def get_guild_channels(self, guild_id): def get_guild_channels(self, guild_id):
_endpoint = "/guilds/{guild_id}/channels".format(guild_id=guild_id) _endpoint = "/guilds/{guild_id}/channels".format(guild_id=guild_id)
r = self.request("GET", _endpoint) r = self.request("GET", _endpoint)

View File

@ -0,0 +1,52 @@
html {
background-color: #7986cb;
color: white;
}
body {
display: flex;
min-height: 100vh;
flex-direction: column;
}
main {
flex: 1 0 auto;
}
nav {
background-color: #3f51b5;
background: linear-gradient(rgba(63, 81, 181, 1), rgba(255,0,0,0));
box-shadow: none;
}
.page-footer {
background-color: transparent;
}
@media only screen and (max-width: 992px) {
nav .brand-logo {
left: 10%;
}
}
.btn {
background-color: #303f9f;
}
.btn:hover {
background-color: #3f51b5;
}
.btn:focus {
background-color: #536dfe;
}
.avatar_menu {
background-size: contain;
}
.center_content {
display: block;
margin-left: auto;
margin-right: auto;
}

View File

@ -0,0 +1,32 @@
{% extends 'site_layout.html.jinja2' %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>User Dashboard</h1>
<p class="flow-text">Select a server to configure Titan Embeds.</p>
<p>*List missing some servers? It's because you must have either <strong>Manage Server</strong>, <strong>Kick Members</strong>, or <strong>Ban Members</strong> permissions to modify embed settings.</p>
<div class="row">
{% for server in servers %}
<div class="col s6">
<div class="card-panel grey lighten-5 z-depth-1">
<div class="row valign-wrapper">
<div class="col s2">
{% if server.icon %}
<img src="{{ icon_generate(server.id, server.icon) }}" alt="" class="circle responsive-img">
{% else %}
<span class="black-text">No icon :(</span>
{% endif %}
</div>
<div class="col s10">
<span class="black-text">
<p class="flow-text">{{ server.name }}</p>
<br>
<a class="waves-effect waves-light btn" href="{{url_for('user.administrate_guild', guild_id=server['id'])}}">Modify</a>
</span>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'site_layout.html.jinja2' %}
{% block title %}Index{% endblock %}
{% block content %}
<h1 class="center-align">Embed Discord like a<br><strong>true Titan</strong></h1>
<p class="flow-text center-align">Add <strong>Titan</strong> to your discord server to create your own personalized chat embed!</p>
<a class="waves-effect waves-light btn btn-large center_content" href="{{ url_for('user.dashboard') }}">Start here!</a>
<br /><br />
<div style="display: flex;align-items: center;">
<video preload="true" loop="" style="width:100%; border-radius: 10px;">
<source src="http://mee6.xyz/static/mee6.mp4" type="video/mp4">
<source src="http://mee6.xyz/static/mee6.webm" type="video/webm; codecs=vp8, vorbis">
<source type="video/ogg; codecs=theora, vorbis" src="http://mee6.xyz/static/mee6.ogg">
Your browser does not support the video tag.
</video> <!-- TODO: Fix video and add autoplay -->
</div>
{% endblock %}

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<!--Import Google Icon Font-->
<link href="http://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!--Import materialize.css-->
<link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/css/materialize.min.css" integrity="sha256-6DQKO56c9MZL0LAc7QNtxqJyqSa3rS9Gq5FVcIhtA+w=" crossorigin="anonymous" media="screen,projection"/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!--Let browser know website is optimized for mobile-->
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{% block title %}{% endblock %} - Titan Embeds for Discord</title>
</head>
<body>
<main>
{% if session['unauthenticated'] is defined and not session['unauthenticated'] %}
<ul id="menu_dropdown" class="dropdown-content">
<li><a href="{{ url_for('user.dashboard') }}">Dashboard</a></li>
<li class="divider"></li>
<li><a href="{{ url_for('user.logout') }}">Logout</a></li>
</ul>
{% endif %}
<nav>
<div class="nav-wrapper container">
<a href="/" class="brand-logo"><b>Titan</b>Embeds</a>
<ul id="nav-mobile" class="right">
<li><a href="#" class="waves-effect btn z-depth-3">Visit Us!</a></li> <!-- TODO: Add discord guild invite -->
{% if session['unauthenticated'] is defined and not session['unauthenticated'] %}
<li><a id="menu_drop" data-activates="menu_dropdown" class="waves-effect btn z-depth-3 btn-floating dropdown-button avatar_menu" style='background-image: url(" {{ session['avatar'] }} ")'></a></li>
{% else %}
<li><a href="{{ url_for('user.login_authenticated') }}" class="waves-effect btn z-depth-3">Login</a></li>
{% endif %}
</ul>
</div>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
<footer class="page-footer">
<div class="footer-copyright">
<div class="container">
A project by EndenDragon
<a class="grey-text text-lighten-4 right" href="https://github.com/EndenDragon/Titan">GitHub Repo</a>
</div>
</div>
</footer>
<!--Import jQuery before materialize.js-->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/js/materialize.min.js" integrity="sha256-ToPQhpo/E89yaCd7+V8LUCjobNRkjilRXfho6x3twLU=" crossorigin="anonymous"></script>
</body>
</html>