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