Skip to content

Commit 33c8dc8

Browse files
orronaiyammesicka
andauthored
feat: Add change password and forgot my password (#299)
* feat: Add registration page - Added a html template of the signup page - Added a href button to the signup page from the login page - Added the form of the signup to the backend and db * feat: Add registration page - Added confirmation email system - Added some tests * Fixed sourcey-ai issues * fixed resend email confirmation * Added translations * Removed unused module * Changed versions of requirements * Changed versions of requirements * Changed versions of requirements * Changed versions of requirements * Changed versions of requirements * Changed versions of requirements * Removed versions change * Fixed tests * Fixed a test * Fixed test and updated client fixture * Added tests for coverage * Added a test for signature expired * Removed unnecessary condition * Added role attribute * - Fixed babel translations - Added few _('...') instead of some english phrases - Moved out the auth logic into models/users - Added MAIL_DEFAULT_SENDER variable to the config - Changed the retrieve_salt method * Fixed a test to check bad signature token * Fixed a test * Moved out the HASHED_PASSWORD in order to be global variable, and added an error message to the UnhashedPasswordError * Added a configuration of registration open, and a test * feat: Add change password page - Added tests - Added a ref link from user's page - Added the form background - Added the template and the css style * Added a comma * Added a comma * - Moved the send mails methods into utils/mail.py - Added _invalid_tries to session in order to limit the tries of invalid change password - Added INVALID_TRIES variable to the config * Added a test for invalid_tries change password * Added a test for invalid_tries change password * Added a test for invalid_tries change password * - Added forgot my password template and css style - Added a link from login page - Added in the backend the forms - Added mails to reset password - Added tests * Fixed some changes requested * Fixed translations * Fixed some issues * Changed session token to be uuid * Added a test and changed get_or_none to get * fix: check test send mail * Added skipif for sending mail test and split a test function * Changed skipif condition and split a test * fix: test mails * Removed unused import * Changed fixture to be session instead of class scope * changed some code style and changed uuidfield * Add migration * feat: add migration * fix: bool expression * Changed html name files * Removed unused imports and moved out to conftest some client.post's * Removed TestResponse objects Co-authored-by: Yam Mesicka <yammesicka@gmail.com>
1 parent ad7f8d0 commit 33c8dc8

23 files changed

+722
-207
lines changed

lms/lmsdb/bootstrap.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Any, Callable, Optional, Tuple, Type
2+
from uuid import uuid4
23

34
from peewee import (
45
Database, Entity, Expression, Field, Model, OP,
@@ -88,9 +89,7 @@ def get_details(table: Model, column: Field) -> Tuple[bool, str, str]:
8889
column_name = column.column_name
8990

9091
cols = {col.name for col in db_config.database.get_columns(table_name)}
91-
if column_name in cols:
92-
return True, table_name, column_name
93-
return False, table_name, column_name
92+
return column_name in cols, table_name, column_name
9493

9594

9695
def _add_not_null_column(
@@ -227,22 +226,39 @@ def _add_api_keys_to_users_table(table: Model, _column: Field) -> None:
227226
user.save()
228227

229228

229+
def _add_uuid_to_users_table(table: Model, _column: Field) -> None:
230+
log.info('Adding UUIDs for all users, might take some extra time...')
231+
with db_config.database.transaction():
232+
for user in table:
233+
user.uuid = uuid4()
234+
user.save()
235+
236+
230237
def _api_keys_migration() -> bool:
231238
User = models.User
232239
_add_not_null_column(User, User.api_key, _add_api_keys_to_users_table)
233240
return True
234241

235242

243+
def _uuid_migration() -> bool:
244+
User = models.User
245+
_add_not_null_column(User, User.uuid, _add_uuid_to_users_table)
246+
return True
247+
248+
236249
def main():
237250
with models.database.connection_context():
251+
if models.database.table_exists(models.User.__name__.lower()):
252+
_api_keys_migration()
253+
_uuid_migration()
254+
238255
models.database.create_tables(models.ALL_MODELS, safe=True)
239256

240257
if models.Role.select().count() == 0:
241258
models.create_basic_roles()
242259
if models.User.select().count() == 0:
243260
models.create_demo_users()
244261

245-
_api_keys_migration()
246262
text_fixer.fix_texts()
247263
import_tests.load_tests_from_path('/app_dir/notebooks-tests')
248264

lms/lmsdb/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
from collections import Counter
21
import enum
32
import html
43
import secrets
54
import string
5+
from collections import Counter
66
from datetime import datetime
77
from typing import (
88
Any, Dict, Iterable, List, Optional, TYPE_CHECKING, Tuple,
99
Type, Union, cast,
1010
)
11+
from uuid import uuid4
1112

1213
from flask_babel import gettext as _ # type: ignore
1314
from flask_login import UserMixin, current_user # type: ignore
1415
from peewee import ( # type: ignore
1516
BooleanField, Case, CharField, Check, DateTimeField, ForeignKeyField,
16-
IntegerField, JOIN, ManyToManyField, TextField, fn,
17+
IntegerField, JOIN, ManyToManyField, TextField, UUIDField, fn,
1718
)
1819
from playhouse.signals import Model, post_save, pre_save # type: ignore
1920
from werkzeug.security import (
@@ -138,6 +139,10 @@ class User(UserMixin, BaseModel):
138139
password = CharField()
139140
role = ForeignKeyField(Role, backref='users')
140141
api_key = CharField()
142+
uuid = UUIDField(default=uuid4, unique=True)
143+
144+
def get_id(self):
145+
return str(self.uuid)
141146

142147
def is_password_valid(self, password):
143148
return check_password_hash(self.password, password)
@@ -201,6 +206,7 @@ def on_save_handler(model_class, instance, created):
201206
is_password_changed = not instance.password.startswith('pbkdf2:sha256')
202207
if created or is_password_changed:
203208
instance.password = generate_password_hash(instance.password)
209+
instance.uuid = uuid4()
204210

205211
is_api_key_changed = not instance.api_key.startswith('pbkdf2:sha256')
206212
if created or is_api_key_changed:

lms/lmsweb/config.py.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,6 @@ LOCALE = 'en'
6363
# Limiter settings
6464
LIMITS_PER_MINUTE = 5
6565
LIMITS_PER_HOUR = 50
66+
67+
# Change password settings
68+
MAX_INVALID_PASSWORD_TRIES = 5

lms/lmsweb/forms/change_password.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from flask import session
2+
from flask_babel import gettext as _ # type: ignore
3+
from flask_wtf import FlaskForm
4+
from wtforms import PasswordField
5+
from wtforms.validators import EqualTo, InputRequired, Length, ValidationError
6+
7+
from lms.lmsweb.config import MAX_INVALID_PASSWORD_TRIES
8+
9+
10+
class ChangePasswordForm(FlaskForm):
11+
current_password = PasswordField(
12+
'Password', validators=[InputRequired(), Length(min=8)], id='password',
13+
)
14+
password = PasswordField(
15+
'Password', validators=[InputRequired(), Length(min=8)], id='password',
16+
)
17+
confirm = PasswordField(
18+
'Password Confirmation', validators=[
19+
InputRequired(),
20+
EqualTo('password', message=_('הסיסמאות שהוקלדו אינן זהות')),
21+
],
22+
)
23+
24+
def __init__(self, user, *args, **kwargs):
25+
super(ChangePasswordForm, self).__init__(*args, **kwargs)
26+
self.user = user
27+
28+
def validate_current_password(self, field):
29+
if session['_invalid_password_tries'] >= MAX_INVALID_PASSWORD_TRIES:
30+
raise ValidationError(_('הזנת סיסמה שגויה מספר רב מדי של פעמים'))
31+
if not self.user.is_password_valid(field.data):
32+
session['_invalid_password_tries'] += 1
33+
raise ValidationError(_('הסיסמה הנוכחית שהוזנה שגויה'))

lms/lmsweb/forms/reset_password.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from lms.lmsweb.tools.validators import EmailNotExists
2+
from flask_babel import gettext as _ # type: ignore
3+
from flask_wtf import FlaskForm
4+
from wtforms import StringField
5+
from wtforms.fields.simple import PasswordField
6+
from wtforms.validators import Email, EqualTo, InputRequired, Length
7+
8+
9+
class ResetPassForm(FlaskForm):
10+
email = StringField(
11+
'Email', validators=[
12+
InputRequired(), Email(message=_('אימייל לא תקין')),
13+
EmailNotExists,
14+
],
15+
)
16+
17+
18+
class RecoverPassForm(FlaskForm):
19+
password = PasswordField(
20+
'Password', validators=[InputRequired(), Length(min=8)], id='password',
21+
)
22+
confirm = PasswordField(
23+
'Password Confirmation', validators=[
24+
InputRequired(),
25+
EqualTo('password', message=_('הסיסמאות שהוקלדו אינן זהות')),
26+
],
27+
)

lms/lmsweb/tools/validators.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,11 @@ def UniqueEmailRequired(
1919
email_exists = User.get_or_none(User.mail_address == field.data)
2020
if email_exists:
2121
raise ValidationError(_('האימייל כבר נמצא בשימוש'))
22+
23+
24+
def EmailNotExists(
25+
_form: 'ResetPassForm', field: StringField, # type: ignore # NOQA: F821
26+
) -> None:
27+
email_exists = User.get_or_none(User.mail_address == field.data)
28+
if not email_exists:
29+
raise ValidationError(_('האימייל לא רשום במערכת'))

0 commit comments

Comments
 (0)