Commit 3586cfaba4678c93c4ec985beb50d7c65937d2ab

Authored by Miguel Barao
1 parent 5b95825d
Exists in master and in 1 other branch dev

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)
... ...