Commit 3586cfaba4678c93c4ec985beb50d7c65937d2ab
1 parent
5b95825d
Exists in
master
and in
1 other branch
fixed async login
Showing
2 changed files
with
79 additions
and
52 deletions
Show diff stats
learnapp.py
| 1 | 1 | ||
| 2 | # python standard library | 2 | # python standard library |
| 3 | -from contextlib import contextmanager # `with` statement in db sessions | ||
| 4 | -import logging | ||
| 5 | from os import path, sys | 3 | from os import path, sys |
| 4 | +import logging | ||
| 5 | +from contextlib import contextmanager # `with` statement in db sessions | ||
| 6 | +import asyncio | ||
| 6 | from datetime import datetime | 7 | from datetime import datetime |
| 7 | 8 | ||
| 8 | # user installed libraries | 9 | # user installed libraries |
| @@ -20,15 +21,42 @@ from tools import load_yaml | @@ -20,15 +21,42 @@ from tools import load_yaml | ||
| 20 | # setup logger for this module | 21 | # setup logger for this module |
| 21 | logger = logging.getLogger(__name__) | 22 | logger = logging.getLogger(__name__) |
| 22 | 23 | ||
| 23 | - | 24 | +# ============================================================================ |
| 24 | class LearnAppException(Exception): | 25 | class LearnAppException(Exception): |
| 25 | pass | 26 | pass |
| 26 | 27 | ||
| 27 | 28 | ||
| 28 | # ============================================================================ | 29 | # ============================================================================ |
| 30 | +# helper functions | ||
| 31 | +# ============================================================================ | ||
| 32 | +async def check_password(try_pw, password): | ||
| 33 | + try_pw = try_pw.encode('utf-8') | ||
| 34 | + loop = asyncio.get_running_loop() | ||
| 35 | + hashed_pw = await loop.run_in_executor(None, bcrypt.hashpw, try_pw, password) | ||
| 36 | + return password == hashed_pw | ||
| 37 | + | ||
| 38 | + | ||
| 39 | +# ============================================================================ | ||
| 29 | # LearnApp - application logic | 40 | # LearnApp - application logic |
| 30 | # ============================================================================ | 41 | # ============================================================================ |
| 31 | class LearnApp(object): | 42 | class LearnApp(object): |
| 43 | + # ------------------------------------------------------------------------ | ||
| 44 | + # helper to manage db sessions using the `with` statement, for example | ||
| 45 | + # with self.db_session() as s: s.query(...) | ||
| 46 | + # ------------------------------------------------------------------------ | ||
| 47 | + @contextmanager | ||
| 48 | + def db_session(self, **kw): | ||
| 49 | + session = self.Session(**kw) | ||
| 50 | + try: | ||
| 51 | + yield session | ||
| 52 | + session.commit() | ||
| 53 | + except: | ||
| 54 | + session.rollback() | ||
| 55 | + logger.error('DB rollback!!!') | ||
| 56 | + finally: | ||
| 57 | + session.close() | ||
| 58 | + | ||
| 59 | + # ------------------------------------------------------------------------ | ||
| 32 | def __init__(self, config_file): | 60 | def __init__(self, config_file): |
| 33 | # state of online students | 61 | # state of online students |
| 34 | self.online = {} | 62 | self.online = {} |
| @@ -47,38 +75,44 @@ class LearnApp(object): | @@ -47,38 +75,44 @@ class LearnApp(object): | ||
| 47 | # ------------------------------------------------------------------------ | 75 | # ------------------------------------------------------------------------ |
| 48 | # login | 76 | # login |
| 49 | # ------------------------------------------------------------------------ | 77 | # ------------------------------------------------------------------------ |
| 50 | - def login(self, uid, pw): | 78 | + async def login(self, uid, try_pw): |
| 79 | + if uid.startswith('l'): # remove prefix 'l' | ||
| 80 | + uid = uid[1:] | ||
| 51 | 81 | ||
| 52 | with self.db_session() as s: | 82 | with self.db_session() as s: |
| 53 | - student = s.query(Student).filter(Student.id == uid).one_or_none() | ||
| 54 | - if student is None: | 83 | + try: |
| 84 | + name, password = s.query(Student.name, Student.password).filter_by(id=uid).one() | ||
| 85 | + except: | ||
| 55 | logger.info(f'User "{uid}" does not exist!') | 86 | logger.info(f'User "{uid}" does not exist!') |
| 56 | - return False # student does not exist | ||
| 57 | - | ||
| 58 | - if bcrypt.checkpw(pw.encode('utf-8'), student.password): | ||
| 59 | - if uid in self.online: | ||
| 60 | - logger.warning(f'User "{uid}" already logged in, overwriting state') | ||
| 61 | - else: | ||
| 62 | - logger.info(f'User "{uid}" logged in') | ||
| 63 | - | ||
| 64 | - tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid) | ||
| 65 | - state = {t.topic_id: | ||
| 66 | - { | ||
| 67 | - 'level': t.level, | ||
| 68 | - 'date': datetime.strptime(t.date, "%Y-%m-%d %H:%M:%S.%f") | ||
| 69 | - } for t in tt} | ||
| 70 | - | ||
| 71 | - self.online[uid] = { | ||
| 72 | - 'number': student.id, | ||
| 73 | - 'name': student.name, | ||
| 74 | - 'state': StudentKnowledge(self.deps, state=state), | ||
| 75 | - # 'learning': None, # current topic learning | ||
| 76 | - } | ||
| 77 | - return True | 87 | + return False |
| 78 | 88 | ||
| 89 | + pw_ok = await check_password(try_pw, password) # async bcrypt | ||
| 90 | + if pw_ok: | ||
| 91 | + if uid in self.online: | ||
| 92 | + logger.warning(f'User "{uid}" already logged in, overwriting state') | ||
| 79 | else: | 93 | else: |
| 80 | - logger.info(f'User "{uid}" wrong password!') | ||
| 81 | - return False | 94 | + logger.info(f'User "{uid}" logged in successfully') |
| 95 | + | ||
| 96 | + with self.db_session() as s: | ||
| 97 | + tt = s.query(StudentTopic).filter_by(student_id=uid) | ||
| 98 | + | ||
| 99 | + state = {t.topic_id: | ||
| 100 | + { | ||
| 101 | + 'level': t.level, | ||
| 102 | + 'date': datetime.strptime(t.date, "%Y-%m-%d %H:%M:%S.%f") | ||
| 103 | + } for t in tt} | ||
| 104 | + | ||
| 105 | + self.online[uid] = { | ||
| 106 | + 'number': uid, | ||
| 107 | + 'name': name, | ||
| 108 | + 'state': StudentKnowledge(self.deps, state=state), | ||
| 109 | + # 'learning': None, # current topic learning | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + else: | ||
| 113 | + logger.info(f'User "{uid}" wrong password!') | ||
| 114 | + | ||
| 115 | + return pw_ok | ||
| 82 | 116 | ||
| 83 | # ------------------------------------------------------------------------ | 117 | # ------------------------------------------------------------------------ |
| 84 | # logout | 118 | # logout |
| @@ -206,31 +240,14 @@ class LearnApp(object): | @@ -206,31 +240,14 @@ class LearnApp(object): | ||
| 206 | n = s.query(Student).count() | 240 | n = s.query(Student).count() |
| 207 | m = s.query(Topic).count() | 241 | m = s.query(Topic).count() |
| 208 | q = s.query(Answer).count() | 242 | q = s.query(Answer).count() |
| 209 | - except Exception: | 243 | + except Exception as e: |
| 210 | logger.critical(f'Database "{db}" not usable!') | 244 | logger.critical(f'Database "{db}" not usable!') |
| 211 | - sys.exit(1) | 245 | + raise e |
| 212 | else: | 246 | else: |
| 213 | logger.info(f'{n:6} students') | 247 | logger.info(f'{n:6} students') |
| 214 | logger.info(f'{m:6} topics') | 248 | logger.info(f'{m:6} topics') |
| 215 | logger.info(f'{q:6} answers') | 249 | logger.info(f'{q:6} answers') |
| 216 | 250 | ||
| 217 | - # ------------------------------------------------------------------------ | ||
| 218 | - # helper to manage db sessions using the `with` statement, for example | ||
| 219 | - # with self.db_session() as s: s.query(...) | ||
| 220 | - # ------------------------------------------------------------------------ | ||
| 221 | - @contextmanager | ||
| 222 | - def db_session(self, **kw): | ||
| 223 | - session = self.Session(**kw) | ||
| 224 | - try: | ||
| 225 | - yield session | ||
| 226 | - session.commit() | ||
| 227 | - except Exception as e: | ||
| 228 | - session.rollback() | ||
| 229 | - raise e | ||
| 230 | - finally: | ||
| 231 | - session.close() | ||
| 232 | - | ||
| 233 | - | ||
| 234 | 251 | ||
| 235 | # ======================================================================== | 252 | # ======================================================================== |
| 236 | # methods that do not change state (pure functions) | 253 | # methods that do not change state (pure functions) |
serve.py
| @@ -16,12 +16,23 @@ import functools | @@ -16,12 +16,23 @@ import functools | ||
| 16 | import tornado.ioloop | 16 | import tornado.ioloop |
| 17 | import tornado.web | 17 | import tornado.web |
| 18 | import tornado.httpserver | 18 | import tornado.httpserver |
| 19 | -from tornado import iostream | 19 | +# from tornado import iostream |
| 20 | 20 | ||
| 21 | # this project | 21 | # this project |
| 22 | from learnapp import LearnApp | 22 | from learnapp import LearnApp |
| 23 | from tools import load_yaml, md_to_html | 23 | from tools import load_yaml, md_to_html |
| 24 | 24 | ||
| 25 | +# ---------------------------------------------------------------------------- | ||
| 26 | +# Decorator used to restrict access to the administrator | ||
| 27 | +# ---------------------------------------------------------------------------- | ||
| 28 | +def admin_only(func): | ||
| 29 | + @functools.wraps(func) | ||
| 30 | + def wrapper(self, *args, **kwargs): | ||
| 31 | + if self.current_user != '0': | ||
| 32 | + raise tornado.web.HTTPError(403) # forbidden | ||
| 33 | + else: | ||
| 34 | + func(self, *args, **kwargs) | ||
| 35 | + return wrapper | ||
| 25 | 36 | ||
| 26 | # ============================================================================ | 37 | # ============================================================================ |
| 27 | # WebApplication - Tornado Web Server | 38 | # WebApplication - Tornado Web Server |
| @@ -82,8 +93,7 @@ class LoginHandler(BaseHandler): | @@ -82,8 +93,7 @@ class LoginHandler(BaseHandler): | ||
| 82 | uid = self.get_body_argument('uid') | 93 | uid = self.get_body_argument('uid') |
| 83 | pw = self.get_body_argument('pw') | 94 | pw = self.get_body_argument('pw') |
| 84 | 95 | ||
| 85 | - loop = asyncio.get_event_loop() | ||
| 86 | - login_ok = await loop.run_in_executor(None, self.learn.login, uid, pw) | 96 | + login_ok = await self.learn.login(uid, pw) |
| 87 | 97 | ||
| 88 | if login_ok: | 98 | if login_ok: |
| 89 | self.set_secure_cookie("user", str(uid), expires_days=30) | 99 | self.set_secure_cookie("user", str(uid), expires_days=30) |