Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User auth #3 ready #71

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/web/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
DB_DEMO_PATH = os.path.join("..", "..", "database", "demo_user.json")
DB_MANUAL_PATH = os.path.join("..", "..", "database", "tl_manual.json")
DB_LORE_PATH = os.path.join("..", "..", "database", "tl_lore.json")
DB_KEYS_PATH = os.path.join("..", "..", "database", "keys.json")
Copy link
Member

Choose a reason for hiding this comment

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

key.json n'est pas un nom significatif

Copy link
Member

Choose a reason for hiding this comment

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

"Contains keys and secrets needed for third-party authentification."
Pourquoi pas le renommer auth.json?

Copy link
Member

Choose a reason for hiding this comment

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

DONE, renommé pour auth.json



def main():
Expand Down Expand Up @@ -74,6 +75,7 @@ def parse_args():
_parser.db_demo_path = DB_DEMO_PATH
_parser.db_manual_path = DB_MANUAL_PATH
_parser.db_lore_path = DB_LORE_PATH
_parser.db_keys_path = DB_KEYS_PATH
return _parser


Expand Down
24 changes: 18 additions & 6 deletions src/web/base_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
# -*- coding: utf-8 -*-

import tornado.web
# from py_class import user
import json
from py_class import db


# TODO add user detection here
# @userapp.tornado.config(app_id=user.USER_APP_ID)
class BaseHandler(tornado.web.RequestHandler):
_debug = None
_manual = None
Expand All @@ -25,9 +24,22 @@ def initialize(self, **kwargs):
"db": self._db,
"disable_character": kwargs.get("disable_character"),
"disable_admin": kwargs.get("disable_admin"),
"disable_login": kwargs.get("disable_login")
"disable_login": kwargs.get("disable_login"),
"url": kwargs.get("url"),
}

def get_current_user(self):
# TODO not work
return self._db.get_user(_uuid=self.get_secure_cookie("user"))
user_cookie = self.get_secure_cookie("user")
#trim private data
return self._db.get_user(id_type="user", user_id=eval(user_cookie).get("user_id")) if user_cookie else None

def give_cookie(self, user_id, twitter_access_token=None, facebook_access_token=None, google_access_token=None):
if user_id:
self.set_secure_cookie("user", str({"user_id": user_id,
"twitter_access_token": twitter_access_token,
"facebook_access_token": facebook_access_token,
"google_access_token": google_access_token}),
httpOnly=True, expires_days=3)
self.redirect("/")
else:
print("User doesn't have an id.", file=sys.stderr)
3 changes: 2 additions & 1 deletion src/web/bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"angular-schema-form-dynamic-select": "master",
"ngPrint": "master",
"components-font-awesome": "master",
"angular-qrcode": "master"
"angular-qrcode": "master",
"jsSHA": "master"
}
}
265 changes: 230 additions & 35 deletions src/web/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
import tornado
import tornado.web
import hashlib
import tornado.auth
import sys
import base_handler
import jsonhandler
Expand Down Expand Up @@ -44,54 +44,231 @@ def get(self):
class LoginHandler(base_handler.BaseHandler):
@tornado.web.asynchronous
def get(self):
if self.get_secure_cookie("user"):
self.redirect("/")

invalid_login = self.get_argument("invalid", default=None)
if self._global_arg["disable_login"]:
return
self.render('login.html', **self._global_arg)
invalid_login = "disable_login"

self.render('login.html', invalid_login=invalid_login, **self._global_arg)

@tornado.web.asynchronous
def post(self):
if self._global_arg["disable_login"]:
return
email = self.get_argument("username")
name = self.get_argument("name")
password = self.get_argument("password")
self.redirect("/login?invalid=disable_login")

if self.get_secure_cookie("user"):
print("Need to logout before login or sign in.", file=sys.stderr)
print("Need to logout before login or sign up.", file=sys.stderr)
return
if not email:
print("User name is empty.", file=sys.stderr)

#EXTREMELY IMPORTANT to prevent accessing accounts that do not yet have a password.
password = self.get_argument("password")
if not password:
print("Password is empty.", file=sys.stderr)
secure_pass = hashlib.sha256(password.encode('UTF-8')).hexdigest()
if name:
user = self._db.create_user(email, name, secure_pass)
self.redirect("/login?invalid=password")


#Login
if self.get_arguments("username") == []:

username_or_email = self.get_argument("username_or_email")
if not username_or_email:
print("Email or Username is empty.", file=sys.stderr)
self.redirect("/login?invalid=username_or_email")

#Try finding the user by mail...
user = None
if "@" in username_or_email:
print("in mail")
user = self._db.get_user(email=username_or_email, password=password)
#... or by name.
if not user:
print("not in mail")
user = self._db.get_user(name=username_or_email, password=password)

#If user is found, give him a secure cookie based on his user id
if user:
self.give_cookie(user.get("user_id"))
else:
print("Invalid email/password combination", file=sys.stderr)
self.redirect("/login?invalid=login")

#Sign Up
else:
user = self._db.get_user(email, secure_pass)
name = self.get_argument("username")
if not name:
print("Username is empty.", file=sys.stderr)
self.redirect("/login?invalid=username")

email = self.get_argument("email", default=None)

if user:
uuid = user.get("uuid")
if uuid:
self.set_secure_cookie("user", uuid)
self.redirect("/")
password_mail = self.get_argument("pwconfirm")
if not password_mail:
print("Password is empty.", file=sys.stderr)
self.redirect("/login?invalid=password")

if self._db.create_user(name, email, password, password_mail):
self.redirect("/login")
else:
self.redirect("/login?invalid=signup")

class GoogleOAuth2LoginHandler(base_handler.BaseHandler, tornado.auth.GoogleOAuth2Mixin):
@tornado.gen.coroutine
def get(self):
if self.get_argument('code', False):
try:
google_user = yield self.get_authenticated_user(
redirect_uri=self._global_arg["url"]+'/cmd/auth/google',
code=self.get_argument('code'))
access_token = google_user["access_token"]
google_user = yield self.oauth2_request(
"https://www.googleapis.com/oauth2/v1/userinfo",
access_token=access_token)

# Save the user with e.g. set_secure_cookie
google_id = google_user["id"]
user = self._db.get_user(id_type="google", user_id=google_id)

#Login
#If user is found, give him a secure cookie based on his user_id and Google access_token
if user:
self.give_cookie(user.get("user_id"), google_access_token=access_token)

#Sign up
else:
name = google_user["name"]
email = google_user.get("email")
user = self._db.create_user(name, email, google_id=google_id)
if user:
self.give_cookie(user.get("user_id"), google_access_token=access_token)
else:
self.redirect("/login?invalid=google")

except KeyError as exception:
print("KeyError: "+str(exception)+" in GoogleOAuth2LoginHandler", file=sys.stderr)
self.redirect("/login?invalid=google")
return

else:
yield self.authorize_redirect(
redirect_uri=self._global_arg["url"]+'/cmd/auth/google',
client_id=self.settings['google_oauth']['key'],
scope=['profile', 'email'],
response_type='code',
extra_params={'approval_prompt': 'auto'})

class FacebookGraphLoginHandler(base_handler.BaseHandler, tornado.auth.FacebookGraphMixin):
@tornado.gen.coroutine
def get(self):
if self.get_argument("code", None):
try:
facebook_user = yield self.get_authenticated_user(
redirect_uri=self._global_arg["url"]+'/cmd/auth/facebook',
client_id=self.settings["facebook_api_key"],
client_secret=self.settings["facebook_secret"],
code=self.get_argument("code"),
extra_fields=["email"])
access_token = facebook_user["access_token"]

facebook_id = facebook_user["id"]
user = self._db.get_user(id_type="facebook", user_id=facebook_id)

#Login
#If user is found, give him a secure cookie based on his user_id and Facebook access_token
if user:
self.give_cookie(user.get("user_id"), facebook_access_token=access_token)

#Sign up
else:
name = facebook_user["name"]
email = facebook_user.get("email")
user = self._db.create_user(name, email, facebook_id=facebook_id)
if user:
self.give_cookie(user.get("user_id"), facebook_access_token=access_token)
else:
self.redirect("/login?invalid=facebook")

except KeyError as exception:
print("KeyError: "+str(exception)+" in FacebookGraphLoginHandler", file=sys.stderr)
self.redirect("/login?invalid=facebook")
return

else:
yield self.authorize_redirect(
redirect_uri=self._global_arg["url"]+'/cmd/auth/facebook',
client_id=self.settings["facebook_api_key"],
#Permissions: https://developers.facebook.com/docs/facebook-login/permissions
extra_params={"scope": "email"})

class TwitterLoginHandler(base_handler.BaseHandler, tornado.auth.TwitterMixin):
@tornado.gen.coroutine
def get(self):
if self.get_argument("oauth_token", None):
try:
twitter_user = yield self.get_authenticated_user()
access_token = twitter_user.get("access_token")
twitter_user = yield self.twitter_request("/account/verify_credentials",
access_token=access_token, include_email="true")

twitter_id = twitter_user["id_str"]
user = self._db.get_user(id_type="twitter", user_id=twitter_id)

#Login
#If user is found, give him a secure cookie based on his user_id and Twitter access_token
if user:
self.give_cookie(user.get("user_id"), twitter_access_token=access_token)

#Sign up
else:
name = twitter_user["name"]
email = twitter_user.get("email")
user = self._db.create_user(name, email, twitter_id=twitter_id)
if user:
self.give_cookie(user.get("user_id"), twitter_access_token=access_token)
else:
self.redirect("/login?invalid=twitter")
except KeyError as exception:
print("KeyError: "+str(exception)+" in TwitterLoginHandler", file=sys.stderr)
self.redirect("/login?invalid=twitter")
return
else:
yield self.authenticate_redirect()

class LogoutHandler(base_handler.BaseHandler):
def get(self):
if self._global_arg["disable_login"]:
return
self.clear_cookie("user")
self.redirect(u"/")
if self.current_user:
self.clear_cookie("user")
self.redirect("/")
else:
self.redirect("/login")


class AdminHandler(base_handler.BaseHandler):
@tornado.web.asynchronous
# @userapp.tornado.authorized()
# @userapp.tornado.has_permission('admin')
@tornado.web.authenticated
def get(self):
if self._global_arg["disable_admin"]:
return
self.render('admin_character.html', **self._global_arg)
if self.current_user.get("permission") == "Admin":
self.render('admin_character.html', **self._global_arg)
else:
print("Insufficient persmissions", file=sys.stderr)
self.redirect("/") #TODO : HTTP error 403: Forbidden

class ProfileHandler(base_handler.BaseHandler):
@tornado.web.asynchronous
@tornado.web.authenticated
def get(self, user_id=None):
if self._global_arg["disable_character"]:
return
if user_id:
user = self._db.get_user(user_id=user_id)
else:
user = self.current_user
self.render('profile.html', user=user, **self._global_arg)
Copy link
Member

Choose a reason for hiding this comment

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

serait-ce mieux renommer profile.html pour user_profile.html ?
Car dans le contexte, il peut avoir d'autre profile comme celui du character.

Copy link
Member

Choose a reason for hiding this comment

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

DONE On abandonne pour le moment ce commentaire.


class CharacterHandler(base_handler.BaseHandler):
@tornado.web.asynchronous
Expand All @@ -107,9 +284,9 @@ def get(self):
if self._global_arg["disable_character"]:
return

player_id = self.request.query[len("player_id="):]
user_id = self.request.query[len("user_id="):]
Copy link
Member

Choose a reason for hiding this comment

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

Il va falloir migrer la base de donnée avec les nouvelles clés utilisés ou reprendre les anciennes clés et les modifier plus tard pour des noms plus juste.

Copy link
Member

Choose a reason for hiding this comment

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

player_id -> user_id

Copy link
Member

Choose a reason for hiding this comment

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

DONE - ce n'est plus pertinant dans le contexte qu'on efface la base de donnée du passé.

is_admin = self.request.query == "is_admin"
if player_id == "" and not is_admin:
if user_id == "" and not is_admin:
# leave now, missing permission
self.finish()
return
Expand All @@ -118,7 +295,7 @@ def get(self):
if is_admin:
data = json.dumps(self._db.get_all_user())
else:
data = json.dumps(self._db.get_all_user(id=player_id))
data = json.dumps(self._db.get_all_user(user_id=user_id))

self.write(data)
self.finish()
Expand All @@ -130,17 +307,17 @@ def post(self):
self.prepare_json()

# user_id = self.get_argument("user_id")
player = self.get_argument("player")
user = self.get_argument("user")
character = self.get_argument("character")
delete_player_id = self.get_argument("delete_player_id")
delete_character_id = self.get_argument("delete_character_id")
delete_user_by_id = self.get_argument("delete_user_by_id")
delete_character_by_id = self.get_argument("delete_character_by_id")

# exception, if delete_player_id, create player if not exist
if not player and delete_player_id:
player = {"id": delete_player_id}
# exception, if delete_user_by_id, create user if not exist
if not user and delete_user_by_id:
user = {"user_id": delete_user_by_id}

self._db.update_player(player, character, delete_player_id=delete_player_id,
delete_character_id=delete_character_id)
self._db.update_user(user, character, delete_user_by_id=delete_user_by_id,
delete_character_by_id=delete_character_by_id)


class ManualHandler(jsonhandler.JsonHandler):
Expand All @@ -155,3 +332,21 @@ class LoreHandler(jsonhandler.JsonHandler):
def get(self):
self.write(self._lore.get_str_all())
self.finish()


class ValidateAuthHandler(base_handler.BaseHandler):
"""This class is designed purely for client-side validation"""
@tornado.web.asynchronous
def get(self):
name = self.get_argument("username", default=None)
email = self.get_argument("email", default=None)

if name:
self.write("0" if (self._db.user_exists(name=name) or self._db.user_exists(email=name)) else "1")
Copy link
Member

Choose a reason for hiding this comment

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

pourquoi ne pas retourner un json?

Copy link
Member

Choose a reason for hiding this comment

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

Puis mettre une boolean au lieu de "0" ou "1"?

Copy link
Member

Choose a reason for hiding this comment

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

DONE - ça va être bon pour le moment. La requête est envoyé à chaque caractère. Ça doit être optimisé dans le future.

elif email:
self.write("0" if (self._db.user_exists(email=email) or self._db.user_exists(name=email)) else "1")

# Produce a missing argument error
else:
self.get_argument("username or email")
Copy link
Member

Choose a reason for hiding this comment

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

Est-ce que c'est ceci qu'il faut aller chercher? «username_or_email»

Copy link
Member

Choose a reason for hiding this comment

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

De toute façon, on ne fait rien avec. On veut créer une erreur? Je ne comprend pas la raison de cette ligne.

Copy link
Member

Choose a reason for hiding this comment

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

DONE - Via l'interface usagé web, on ne peut pas se rendre à cette étape. Seul des tests unitaires ou des requêtes http en ne passant pas via l'interface usagé pourra exécuter cette ligne. Un todo a été ajouté.

self.finish()
Loading