Skip to content

Commit b8dff02

Browse files
committed
- Changed role name from not_confirmed into unverified
- Added a unique salt into confirmation link for each user - Added a test
2 parents c2ffa37 + bed689c commit b8dff02

File tree

16 files changed

+506
-124
lines changed

16 files changed

+506
-124
lines changed

lms/lmsdb/models.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
class RoleOptions(enum.Enum):
3636
BANNED = 'Banned'
37-
NOT_CONFIRMED = 'Not_Confirmed'
37+
UNVERIFIED = 'Unverified'
3838
STUDENT = 'Student'
3939
STAFF = 'Staff'
4040
VIEWER = 'Viewer'
@@ -68,7 +68,7 @@ class Role(BaseModel):
6868
(RoleOptions.STAFF.value, RoleOptions.STAFF.value),
6969
(RoleOptions.VIEWER.value, RoleOptions.VIEWER.value),
7070
(RoleOptions.STUDENT.value, RoleOptions.STUDENT.value),
71-
(RoleOptions.NOT_CONFIRMED.value, RoleOptions.NOT_CONFIRMED.value),
71+
(RoleOptions.UNVERIFIED.value, RoleOptions.UNVERIFIED.value),
7272
(RoleOptions.BANNED.value, RoleOptions.BANNED.value),
7373
))
7474

@@ -80,8 +80,8 @@ def get_banned_role(cls) -> 'Role':
8080
return cls.get(Role.name == RoleOptions.BANNED.value)
8181

8282
@classmethod
83-
def get_not_confirmed_role(cls) -> 'Role':
84-
return cls.get(Role.name == RoleOptions.NOT_CONFIRMED.value)
83+
def get_unverified_role(cls) -> 'Role':
84+
return cls.get(Role.name == RoleOptions.UNVERIFIED.value)
8585

8686
@classmethod
8787
def get_student_role(cls) -> 'Role':
@@ -107,8 +107,8 @@ def is_banned(self) -> bool:
107107
return self.name == RoleOptions.BANNED.value
108108

109109
@property
110-
def is_not_confirmed(self) -> bool:
111-
return self.name == RoleOptions.NOT_CONFIRMED.value
110+
def is_unverified(self) -> bool:
111+
return self.name == RoleOptions.UNVERIFIED.value
112112

113113
@property
114114
def is_student(self) -> bool:

lms/lmsweb/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,6 @@ class AdminCommentTextView(AdminModelView):
6969
admin = Admin(
7070
webapp,
7171
name='LMS',
72-
template_mode='bootstrap3',
72+
template_mode='bootstrap4',
7373
index_view=MyAdminIndexView(), # NOQA
7474
)

lms/lmsweb/config.py.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ SECRET_KEY = ''
88
MAILGUN_API_KEY = os.getenv('MAILGUN_API_KEY')
99
MAILGUN_DOMAIN = os.getenv('MAILGUN_DOMAIN', 'mail.pythonic.guru')
1010
SERVER_ADDRESS = os.getenv('SERVER_ADDRESS', '127.0.0.1:5000')
11+
SITE_NAME = 'Learning Python'
1112

1213
# MAIL CONFIGURATION
1314
MAIL_SERVER = 'smtp.gmail.com'
@@ -17,6 +18,9 @@ MAIL_USE_TLS = False
1718
MAIL_USERNAME = 'username@gmail.com'
1819
MAIL_PASSWORD = 'password'
1920

21+
# ADMIN PANEL
22+
FLASK_ADMIN_FLUID_LAYOUT = True
23+
2024
# SESSION_COOKIE_SECURE = True
2125
SESSION_COOKIE_HTTPONLY = True
2226
SESSION_COOKIE_SAMESITE = 'Lax'

lms/lmsweb/forms/register.py

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

lms/lmsweb/tools/registration.py

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,16 @@
11
import csv
2-
from lms.lmsweb.tools.validators import (
3-
UniqueEmailRequired, UniqueUsernameRequired,
4-
)
52
import os
63
import typing
74

8-
from flask import url_for
95
from flask_babel import gettext as _ # type: ignore
10-
from flask_mail import Message # type: ignore
11-
from flask_wtf import FlaskForm
12-
from itsdangerous import URLSafeTimedSerializer
13-
from wtforms import PasswordField, StringField
14-
from wtforms.validators import Email, EqualTo, InputRequired, Length
156

167
from lms.lmsdb import models
17-
from lms.lmsweb import config, webapp, webmail
8+
from lms.lmsweb import config
189
from lms.utils.log import log
1910

2011
import requests
2112

2213

23-
SERIALIZER = URLSafeTimedSerializer(config.SECRET_KEY)
24-
25-
26-
class RegisterForm(FlaskForm):
27-
email = StringField(
28-
'Email', validators=[
29-
InputRequired(), Email(message=_('אימייל לא תקין')),
30-
UniqueEmailRequired, Length(max=60),
31-
],
32-
)
33-
username = StringField(
34-
'Username', validators=[
35-
InputRequired(), UniqueUsernameRequired, Length(min=4, max=20),
36-
],
37-
)
38-
fullname = StringField(
39-
'Full Name', validators=[InputRequired(), Length(min=3, max=60)],
40-
)
41-
password = PasswordField(
42-
'Password', validators=[InputRequired(), Length(min=8)], id='password',
43-
)
44-
confirm = PasswordField(
45-
'Password Confirmation', validators=[
46-
InputRequired(),
47-
EqualTo('password', message=_('הסיסמאות שהוקלדו אינן זהות')),
48-
],
49-
)
50-
51-
5214
class UserToCreate(typing.NamedTuple):
5315
name: str
5416
email: str
@@ -167,22 +129,6 @@ def _build_user_text(user: UserToCreate) -> str:
167129
return msg
168130

169131

170-
def generate_confirmation_token(email: str) -> str:
171-
return SERIALIZER.dumps(email, salt='email-confirmation')
172-
173-
174-
def send_confirmation_mail(email: str, fullname: str) -> None:
175-
token = generate_confirmation_token(email)
176-
msg = Message(
177-
'Confirmation Email - Learn Python',
178-
sender=f'lms@{config.MAILGUN_DOMAIN}', recipients=[email],
179-
)
180-
link = url_for('confirm_email', token=token, _external=True)
181-
msg.body = f'Hey {fullname},\nYour confirmation link is: {link}'
182-
if not webapp.config.get('TESTING'):
183-
webmail.send(msg)
184-
185-
186132
if __name__ == '__main__':
187133
registration = UserRegistrationCreator.from_csv_file(config.USERS_CSV)
188134
print(registration.users_to_create) # noqa: T001

lms/lmsweb/translations/en/LC_MESSAGES/messages.po

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ msgid ""
77
msgstr ""
88
"Project-Id-Version: lmsweb-1.0\n"
99
"Report-Msgid-Bugs-To: bugs@mesicka.com\n"
10-
"POT-Creation-Date: 2021-09-11 09:07+0300\n"
10+
"POT-Creation-Date: 2021-09-11 19:16+0300\n"
1111
"PO-Revision-Date: 2020-09-16 18:29+0300\n"
1212
"Last-Translator: Or Ronai\n"
1313
"Language: en\n"
@@ -45,36 +45,36 @@ msgstr "The automatic checker couldn't run your code."
4545
msgid "אחי, בדקת את הקוד שלך?"
4646
msgstr "Bro, did you check your code?"
4747

48-
#: lmsweb/views.py:113
48+
#: lmsweb/views.py:115
4949
#, fuzzy
5050
msgid "שם המשתמש או הסיסמה שהוזנו לא תקינים"
5151
msgstr "Invalid username or password"
5252

53-
#: lmsweb/views.py:115
53+
#: lmsweb/views.py:120
5454
msgid "עליך לאשר את המייל"
5555
msgstr "You have to confirm your registration with the link sent to your email"
5656

57-
#: lmsweb/views.py:139
57+
#: lmsweb/views.py:144
5858
msgid "ההרשמה בוצעה בהצלחה"
5959
msgstr "Registration successfully"
6060

61-
#: lmsweb/views.py:165
61+
#: lmsweb/views.py:168
6262
msgid "המשתמש שלך אומת בהצלחה, כעת הינך יכול להתחבר למערכת"
6363
msgstr "Your user has been successfully confirmed, you can now login"
6464

65-
#: lmsweb/views.py:172
65+
#: lmsweb/views.py:176
6666
msgid "קישור האימות פג תוקף, קישור חדש נשלח אל תיבת המייל שלך"
6767
msgstr "The confirmation link is expired, new link has been sent to your email"
6868

69-
#: lmsweb/tools/registration.py:29
69+
#: lmsweb/forms/register.py:14
7070
msgid "אימייל לא תקין"
7171
msgstr "Invalid email"
7272

73-
#: lmsweb/tools/registration.py:47
73+
#: lmsweb/forms/register.py:32
7474
msgid "הסיסמאות שהוקלדו אינן זהות"
7575
msgstr "The passwords are not identical"
7676

77-
#: lmsweb/tools/registration.py:143
77+
#: lmsweb/tools/registration.py:105
7878
msgid "מערכת הגשת התרגילים"
7979
msgstr "Exercuse submission system"
8080

@@ -86,6 +86,16 @@ msgstr "The username already in use"
8686
msgid "האימייל כבר נמצא בשימוש"
8787
msgstr "The email already in use"
8888

89+
#: models/register.py:31
90+
#, python-format
91+
msgid "מייל אימות - %(site_name)s"
92+
msgstr "Confirmation mail - %(site_name)s"
93+
94+
#: models/register.py:39
95+
#, python-format
96+
msgid "שלום %(fullname)s,\n לינק האימות שלך למערכת הוא: %(link)s"
97+
msgstr "Hello %(fullname)s,\n Your confirmation link is: %(link)s"
98+
8999
#: models/solutions.py:50
90100
#, python-format
91101
msgid "%(solver)s הגיב לך על בדיקת תרגיל \"%(subject)s\"."

lms/lmsweb/views.py

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@
2525
from lms.lmsweb.config import (
2626
LANGUAGES, LIMITS_PER_HOUR, LIMITS_PER_MINUTE, LOCALE, MAX_UPLOAD_SIZE,
2727
)
28+
from lms.lmsweb.forms.register import RegisterForm
2829
from lms.lmsweb.manifest import MANIFEST
2930
from lms.lmsweb.redirections import (
3031
PERMISSIVE_CORS, get_next_url, login_manager,
3132
)
32-
from lms.lmsweb.tools.registration import (
33-
RegisterForm, SERIALIZER, send_confirmation_mail,
34-
)
3533
from lms.models import (
3634
comments, notes, notifications, share_link, solutions, upload,
3735
)
3836
from lms.models.errors import FileSizeError, LmsError, UploadError, fail
37+
from lms.models.register import (
38+
SERIALIZER, retrieve_salt, send_confirmation_mail,
39+
)
3940
from lms.utils.consts import RTL_LANGUAGES
4041
from lms.utils.files import (
4142
get_language_name_by_extension, get_mime_type_by_extention,
@@ -102,17 +103,21 @@ def login(login_message: Optional[str] = None):
102103
if request.method == 'POST':
103104
if (
104105
user is not None and user.is_password_valid(password)
105-
and not user.role.is_not_confirmed
106+
and not user.role.is_unverified
106107
):
107108
login_user(user)
108109
return get_next_url(next_page)
110+
109111
elif (
110112
user is None or not user.is_password_valid(password)
111-
or user.role.is_not_confirmed
113+
or user.role.is_unverified
112114
):
113115
login_message = _('שם המשתמש או הסיסמה שהוזנו לא תקינים')
114-
if user is not None and user.role.is_not_confirmed:
115-
login_message = _('עליך לאשר את המייל')
116+
error_details = {'next': next_page, 'login_message': login_message}
117+
return redirect(url_for('login', **error_details))
118+
119+
elif user.is_unverified:
120+
login_message = _('עליך לאשר את המייל')
116121
error_details = {'next': next_page, 'login_message': login_message}
117122
return redirect(url_for('login', **error_details))
118123

@@ -122,39 +127,37 @@ def login(login_message: Optional[str] = None):
122127
@webapp.route('/signup', methods=['GET', 'POST'])
123128
def signup():
124129
form = RegisterForm()
125-
if form.validate_on_submit():
126-
User.get_or_create(**{
127-
User.mail_address.name: form.email.data,
128-
User.username.name: form.username.data,
129-
}, defaults={
130-
User.fullname.name: form.fullname.data,
131-
User.role.name: Role.get_not_confirmed_role(),
132-
User.password.name: form.password.data,
133-
User.api_key.name: User.random_password(),
134-
})
135-
136-
send_confirmation_mail(form.email.data, form.fullname.data)
137-
138-
return redirect(url_for(
139-
'login', login_message=_('ההרשמה בוצעה בהצלחה'),
140-
))
141-
142-
return render_template('signup.html', form=form)
130+
if not form.validate_on_submit():
131+
return render_template('signup.html', form=form)
132+
133+
user = User.get_or_create(**{
134+
User.mail_address.name: form.email.data,
135+
User.username.name: form.username.data,
136+
}, defaults={
137+
User.fullname.name: form.fullname.data,
138+
User.role.name: Role.get_unverified_role(),
139+
User.password.name: form.password.data,
140+
User.api_key.name: User.random_password(),
141+
})
142+
send_confirmation_mail(user[0])
143+
return redirect(url_for(
144+
'login', login_message=_('ההרשמה בוצעה בהצלחה'),
145+
))
143146

144147

145-
@webapp.route('/confirm-email/<token>')
146-
def confirm_email(token: str):
148+
@webapp.route('/confirm-email/<int:user_id>/<token>')
149+
def confirm_email(user_id: int, token: str):
147150
try:
148-
email = SERIALIZER.loads(
149-
token, salt='email-confirmation', max_age=3600,
150-
)
151-
user = User.get_or_none(User.mail_address == email)
151+
user = User.get_or_none(User.id == user_id)
152152
if user is None:
153-
return fail(404, f'No such user with email {email}.')
154-
if not user.role.is_not_confirmed:
153+
return fail(404, f'No such user with id {user_id}.')
154+
155+
if not user.role.is_unverified:
155156
return fail(
156157
403, f'User has been already confirmed {user.username}',
157158
)
159+
160+
SERIALIZER.loads(token, salt=retrieve_salt(user), max_age=3600)
158161
update = User.update(
159162
role=Role.get_student_role(),
160163
).where(User.username == user.username)
@@ -165,8 +168,9 @@ def confirm_email(token: str):
165168
_('המשתמש שלך אומת בהצלחה, כעת הינך יכול להתחבר למערכת'),
166169
),
167170
))
171+
168172
except SignatureExpired:
169-
send_confirmation_mail(email, user.fullname)
173+
send_confirmation_mail(user)
170174
return redirect(url_for(
171175
'login', login_message=(
172176
_('קישור האימות פג תוקף, קישור חדש נשלח אל תיבת המייל שלך'),

0 commit comments

Comments
 (0)