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 | 2 | # python standard library |
| 3 | -from contextlib import contextmanager # `with` statement in db sessions | |
| 4 | -import logging | |
| 5 | 3 | from os import path, sys |
| 4 | +import logging | |
| 5 | +from contextlib import contextmanager # `with` statement in db sessions | |
| 6 | +import asyncio | |
| 6 | 7 | from datetime import datetime |
| 7 | 8 | |
| 8 | 9 | # user installed libraries |
| ... | ... | @@ -20,15 +21,42 @@ from tools import load_yaml |
| 20 | 21 | # setup logger for this module |
| 21 | 22 | logger = logging.getLogger(__name__) |
| 22 | 23 | |
| 23 | - | |
| 24 | +# ============================================================================ | |
| 24 | 25 | class LearnAppException(Exception): |
| 25 | 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 | 40 | # LearnApp - application logic |
| 30 | 41 | # ============================================================================ |
| 31 | 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 | 60 | def __init__(self, config_file): |
| 33 | 61 | # state of online students |
| 34 | 62 | self.online = {} |
| ... | ... | @@ -47,38 +75,44 @@ class LearnApp(object): |
| 47 | 75 | # ------------------------------------------------------------------------ |
| 48 | 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 | 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 | 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 | 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 | 118 | # logout |
| ... | ... | @@ -206,31 +240,14 @@ class LearnApp(object): |
| 206 | 240 | n = s.query(Student).count() |
| 207 | 241 | m = s.query(Topic).count() |
| 208 | 242 | q = s.query(Answer).count() |
| 209 | - except Exception: | |
| 243 | + except Exception as e: | |
| 210 | 244 | logger.critical(f'Database "{db}" not usable!') |
| 211 | - sys.exit(1) | |
| 245 | + raise e | |
| 212 | 246 | else: |
| 213 | 247 | logger.info(f'{n:6} students') |
| 214 | 248 | logger.info(f'{m:6} topics') |
| 215 | 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 | 253 | # methods that do not change state (pure functions) | ... | ... |
serve.py
| ... | ... | @@ -16,12 +16,23 @@ import functools |
| 16 | 16 | import tornado.ioloop |
| 17 | 17 | import tornado.web |
| 18 | 18 | import tornado.httpserver |
| 19 | -from tornado import iostream | |
| 19 | +# from tornado import iostream | |
| 20 | 20 | |
| 21 | 21 | # this project |
| 22 | 22 | from learnapp import LearnApp |
| 23 | 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 | 38 | # WebApplication - Tornado Web Server |
| ... | ... | @@ -82,8 +93,7 @@ class LoginHandler(BaseHandler): |
| 82 | 93 | uid = self.get_body_argument('uid') |
| 83 | 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 | 98 | if login_ok: |
| 89 | 99 | self.set_secure_cookie("user", str(uid), expires_days=30) | ... | ... |