Skip to content
Snippets Groups Projects
Commit 251bd5f5 authored by Martin Mareš's avatar Martin Mareš
Browse files

Uživatelé si mohou měnit heslo a e-mail

Pro změnu obojího je potřeba zadat staré heslo, aby se minimalizovalo
riziko únosu účtu, má-li útočník chvíli přístup k session.

Změna e-mailové adresy podléhá potvrzení e-mailem.
parent c0b41ed1
No related branches found
No related tags found
1 merge request!86Registrace
This commit is part of merge request !86. Comments created here will be created in the context of that merge request.
...@@ -118,7 +118,11 @@ def password_reset_url(token: str) -> str: ...@@ -118,7 +118,11 @@ def password_reset_url(token: str) -> str:
def confirm_create_url(token: str) -> str: def confirm_create_url(token: str) -> str:
return config.WEB_ROOT + 'auth/confirm?' + urllib.parse.urlencode({'token': token}, safe=':') return config.WEB_ROOT + 'auth/confirm/r?' + urllib.parse.urlencode({'token': token}, safe=':')
def confirm_email_url(token: str) -> str:
return config.WEB_ROOT + 'auth/confirm/e?' + urllib.parse.urlencode({'token': token}, safe=':')
def send_new_account_email(user: db.User, token: str) -> bool: def send_new_account_email(user: db.User, token: str) -> bool:
...@@ -158,6 +162,18 @@ def send_confirm_create_email(user: db.User, token: str) -> bool: ...@@ -158,6 +162,18 @@ def send_confirm_create_email(user: db.User, token: str) -> bool:
'''.format(confirm_create_url(token)))) '''.format(confirm_create_url(token))))
def send_confirm_change_email(user: db.User, token: str) -> bool:
return send_user_email(user, 'Změna e-mailové adresy', textwrap.dedent('''\
Někdo (pravděpodobně Vy) požádal o nastavení e-mailové adresy k účtu
v Odevzdávacím systému Matematické olympiády na tuto adresu.
Pokud změnu chcete provést, následujte tento odkaz:
{}
Váš OSMO
'''.format(confirm_email_url(token))))
def die(msg: str) -> NoReturn: def die(msg: str) -> NoReturn:
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
sys.exit(1) sys.exit(1)
... ...
......
...@@ -118,13 +118,73 @@ def incarnate(id): ...@@ -118,13 +118,73 @@ def incarnate(id):
@app.route('/user/settings') @app.route('/user/settings')
def user_settings(): def user_settings():
sess = db.get_session() sess = db.get_session()
roles = []
if g.user:
roles = (sess.query(db.UserRole) roles = (sess.query(db.UserRole)
.filter_by(user_id=g.user.user_id) .filter_by(user_id=g.user.user_id)
.options(joinedload(db.UserRole.place)) .options(joinedload(db.UserRole.place))
.all()) .all())
return render_template('settings.html', roles=roles, roles_by_type=mo.rights.roles_by_type)
if g.user.is_org or g.user.is_admin:
pant = None
else:
pant = sess.query(db.Participant).get((g.user.user_id, mo.current_year))
return render_template('settings.html', user=g.user, pant=pant, roles=roles, roles_by_type=mo.rights.roles_by_type)
class SettingsForm(FlaskForm):
email = mo_fields.Email(validators=[validators.DataRequired()])
current_passwd = wtforms.PasswordField('Aktuální heslo', validators=[validators.DataRequired()])
new_passwd = mo_fields.NewPassword(
description=mo.users.password_help + ' Pokud nechcete heslo měnit, ponechte toto políčko prázdné.',
)
new_passwd2 = mo_fields.RepeatPassword()
submit = wtforms.SubmitField('Nastavit')
def validate_current_passwd(form, field):
if not mo.users.check_password(g.user, field.data):
raise ValidationError('Chybné heslo.')
@app.route('/user/settings/change', methods=('GET', 'POST'))
def user_settings_change():
sess = db.get_session()
user = g.user
form = SettingsForm()
if not form.submit.data:
form.email.data = user.email
if form.validate_on_submit():
ok = True
if form.new_passwd.data:
app.logger.info(f'Settings: Změněno heslo uživatele #{user.user_id}')
mo.users.set_password(user, form.new_passwd.data)
mo.util.log(
type=db.LogType.user,
what=user.user_id,
details={'action': 'change-passwd'},
)
sess.commit()
flash('Heslo změněno.', 'success')
if form.email.data != user.email:
rr = mo.users.new_reg_request(db.RegReqType.change, request.remote_addr)
if rr:
rr.user_id = user.user_id
rr.email = form.email.data
sess.add(rr)
sess.commit()
app.logger.info(f'Settings: Požadavek na změnu e-mailu uživatele #{user.user_id}')
flash('Odeslán e-mail s odkazem na potvrzení nové adresy.', 'success')
mo.util.send_confirm_change_email(user, rr.email_token)
else:
app.logger.info('Settings: Rate limit')
flash('Příliš mnoho požadavků na změny hesla. Počkejte prosím chvíli a zkuste to znovu.', 'danger')
ok = False
if ok:
return redirect(url_for('user_settings'))
return render_template('settings_change.html', form=form)
@app.errorhandler(NeedLoginError) @app.errorhandler(NeedLoginError)
...@@ -361,6 +421,11 @@ class Reg2: ...@@ -361,6 +421,11 @@ class Reg2:
RegStatus.already_spent: 'Tento odkaz na potvrzení registrace byl již využit.', RegStatus.already_spent: 'Tento odkaz na potvrzení registrace byl již využit.',
RegStatus.already_exists: 'Účet s touto adresou už existuje.', RegStatus.already_exists: 'Účet s touto adresou už existuje.',
}, },
db.RegReqType.change: {
RegStatus.new: 'Chybný potvrzovací kód. Zkontrolujte, že jste odkaz z e-mailu zkopírovali správně.',
RegStatus.expired: 'Vypršela platnost potvrzovacího kódu, požádejte prosím o změnu e-mailu znovu.',
RegStatus.already_spent: 'Tento odkaz na potvrzení změny e-mailu byl již využit.',
},
} }
def __init__(self, token: str, expected_type: db.RegReqType): def __init__(self, token: str, expected_type: db.RegReqType):
...@@ -445,6 +510,28 @@ class Reg2: ...@@ -445,6 +510,28 @@ class Reg2:
self.user = user self.user = user
return True return True
def change_email(self):
sess = db.get_session()
user = g.user
user.email = self.rr.email
app.logger.info(f'Reg2: Uživatel #{user.user_id} si změnil email na <{user.email}>')
mo.util.log(
type=db.LogType.user,
what=user.user_id,
details={
'action': 'change-settings',
'changes': db.get_object_changes(user),
},
)
self.rr.used_at = mo.now
sess.commit()
def spend_request(self):
self.rr.used_at = mo.now
db.get_session().commit()
def flash_message(self): def flash_message(self):
msgs = self.messages[self.reg_type] msgs = self.messages[self.reg_type]
if self.status in msgs: if self.status in msgs:
...@@ -484,6 +571,36 @@ def confirm_reg(): ...@@ -484,6 +571,36 @@ def confirm_reg():
return render_template('acct_reg2.html', form=form) return render_template('acct_reg2.html', form=form)
class ConfirmEmailForm(FlaskForm):
orig_email = wtforms.StringField('Původní e-mail', render_kw={"disabled": "disabled"})
new_email = wtforms.StringField('Nový e-mail', render_kw={"disabled": "disabled"})
submit = wtforms.SubmitField('Potvrdit změnu')
cancel = wtforms.SubmitField('Zrušit požadavek')
@app.route('/auth/confirm/e', methods=('GET', 'POST'))
def confirm_email():
reg2 = Reg2(request.args.get('token'), db.RegReqType.change)
if reg2.status != RegStatus.ok:
reg2.flash_message()
return redirect(url_for('user_settings'))
form = ConfirmEmailForm()
if form.validate_on_submit():
if form.submit.data:
reg2.change_email()
flash('E-mail změněn.', 'success')
elif form.cancel.data:
reg2.spend_request()
flash('Požadavek na změnu e-mailu zrušen.', 'success')
return redirect(url_for('user_settings'))
form.orig_email.data = g.user.email
form.new_email.data = reg2.rr.email
return render_template('acct_confirm_email.html', form=form)
@app.errorhandler(werkzeug.exceptions.Forbidden) @app.errorhandler(werkzeug.exceptions.Forbidden)
def handle_forbidden(e): def handle_forbidden(e):
return render_template('forbidden.html') return render_template('forbidden.html')
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Změna e-mailu{% endblock %}
{% block body %}
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{% endblock %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Uživatel {{ g.user.full_name() }}{% endblock %} {% block title %}Uživatel {{ user.full_name() }}{% endblock %}
{% block body %} {% block body %}
<h3>Osobní údaje</h3>
<table class=table>
<tr><th>Jméno<td>{{ user.first_name }}
<tr><th>Příjmení<td>{{ user.last_name }}
<tr><th>E-mail<td>{{ user.email }}
{% if pant %}
<tr><th>Škola<td>{{ pant.school_place.name }}
<tr><th>Ročník<td>{{ pant.grade }}
<tr><th>Rok narození<td>{{ pant.birth_year }}
{% endif %}
</table>
<p><a class='btn btn-primary' href='{{ url_for('user_settings_change') }}'>Změnit e-mail nebo heslo</a>
<p>Pokud potřebujete změnit jiné údaje, ozvěte se svému učiteli nebo garantovi.
Neuspějete-li u nich, napište správci OSMO (kontakt viz patička stránky).
{% if user.is_admin or user.is_org %}
<h3>Práva</h3> <h3>Práva</h3>
{% if g.user.is_admin %} {% if user.is_admin %}
<p>Správce systému <p>Správce systému
{% endif %} {% endif %}
{% if g.user.is_org %} {% if user.is_org %}
<p>Organizátor s následujícími rolemi: <p>Organizátor s následujícími rolemi:
<table class=data> <table class=table>
<tr> <tr>
<th>Role <th>Role
<th>Oblast <th>Oblast
...@@ -25,8 +45,7 @@ ...@@ -25,8 +45,7 @@
{% endfor %} {% endfor %}
</table> </table>
{% endif %} {% endif %}
{% if not g.user.is_admin and not g.user.is_org %}
<p>Běžný uživatel
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Změna osobních údajů{% endblock %}
{% block body %}
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{% endblock %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment