From f84702804a02cc684184c5e6524cfda47104a75d Mon Sep 17 00:00:00 2001
From: Martin Mares <mj@ucw.cz>
Date: Sun, 25 Jul 2021 23:41:56 +0200
Subject: [PATCH] =?UTF-8?q?Reset=20hesla=20odd=C4=9Blen=20od=20aktivace=20?=
 =?UTF-8?q?=C3=BA=C4=8Dtu?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reset hesla používá rate-limitované tokeny uložené v DB. Aktivace
používá podepsané tokeny s timestampem, ale už funguje jen na účty,
ke kterým se dosud nikdo nepřihlásil.

Org už nemůže požádat o reset hesla uživatele, jen o znovuposlání
aktivačního odkazu.

Timestamp resetu hesla v DB změnil sémantiku, teď se aktualizuje
jak při provedených resetech/aktivacích, tak při žádostech o ně.
---
 bin/create-user                         |   9 +-
 bin/reset-user                          |  13 +--
 db/db.ddl                               |   2 +-
 mo/imports.py                           |   2 +-
 mo/users.py                             |  87 ++++++++----------
 mo/util.py                              |   8 +-
 mo/web/auth.py                          | 115 ++++++++++++++----------
 mo/web/org_contest.py                   |   2 +
 mo/web/org_users.py                     |  28 +++---
 mo/web/templates/acct_activate.html     |   8 ++
 mo/web/templates/acct_reset_passwd.html |   9 ++
 mo/web/templates/org_user.html          |   2 +-
 mo/web/templates/reset.html             |  17 ----
 13 files changed, 152 insertions(+), 150 deletions(-)
 create mode 100644 mo/web/templates/acct_activate.html
 create mode 100644 mo/web/templates/acct_reset_passwd.html
 delete mode 100644 mo/web/templates/reset.html

diff --git a/bin/create-user b/bin/create-user
index 09fbe9e1..c6f86c6b 100755
--- a/bin/create-user
+++ b/bin/create-user
@@ -13,8 +13,7 @@ parser.add_argument(dest='first_name', help='křestní jméno (jedno nebo více)
 parser.add_argument(dest='last_name', help='příjmení (jedno nebo více)')
 parser.add_argument('--org', default=False, action='store_true', help='přidělí uživateli organizátorská práva')
 parser.add_argument('--admin', default=False, action='store_true', help='přidělí uživateli správcovská práva')
-parser.add_argument('--passwd', type=str, help='nastaví počáteční heslo')
-parser.add_argument('--mail', default=False, action='store_true', help='pošle uživateli mail o založení účtu')
+parser.add_argument('--passwd', type=str, help='nastaví počáteční heslo (jinak pošle aktivační mail)')
 
 args = parser.parse_args()
 email = mo.users.normalize_email(args.email)
@@ -45,11 +44,9 @@ mo.util.log(db.LogType.user, user.user_id, {
 
 if args.passwd is not None:
     mo.users.set_password(user, args.passwd)
-
-if args.mail:
-    token = mo.users.ask_reset_password(user)
+    token = mo.users.make_activation_token(user)
 
 session.commit()
 
-if args.mail:
+if args.passwd is None:
     mo.util.send_new_account_email(user, token)
diff --git a/bin/reset-user b/bin/reset-user
index 0b6f8c70..88bb6953 100755
--- a/bin/reset-user
+++ b/bin/reset-user
@@ -1,16 +1,14 @@
 #!/usr/bin/env python3
 
 import argparse
-import sys
 
-import mo.config as config
+import mo.config
 import mo.db as db
 import mo.users
 import mo.util
 
-parser = argparse.ArgumentParser(description='Resetuje uživateli heslo a pošle mail')
+parser = argparse.ArgumentParser(description='Pošle uživateli nový aktivační mail')
 parser.add_argument(dest='email', help='e-mailová adresa')
-parser.add_argument('--new', default=False, action='store_true', help='pošle mail o založení účtu')
 parser.add_argument('--mail-instead', metavar='EMAIL', default=None, help='pošle mail někomu jinému')
 
 args = parser.parse_args()
@@ -22,13 +20,10 @@ user = mo.users.user_by_email(args.email)
 if user is None:
     mo.util.die('Tento uživatel neexistuje')
 
-token = mo.users.ask_reset_password(user)
+token = mo.users.make_activation_token(user)
 session.commit()
 
 if args.mail_instead:
     mo.config.MAIL_INSTEAD = args.mail_instead
 
-if args.new:
-    mo.util.send_new_account_email(user, token)
-else:
-    mo.util.send_password_reset_email(user, token)
+mo.util.send_new_account_email(user, token)
diff --git a/db/db.ddl b/db/db.ddl
index 0800fb38..9979743c 100644
--- a/db/db.ddl
+++ b/db/db.ddl
@@ -15,7 +15,7 @@ CREATE TABLE users (
 	is_test		boolean		NOT NULL DEFAULT false,		-- testovací účastník, není vidět ve výsledkovkách
 	created_at	timestamp with time zone	NOT NULL DEFAULT CURRENT_TIMESTAMP,
 	last_login_at	timestamp with time zone	DEFAULT NULL,
-	reset_at	timestamp with time zone	DEFAULT NULL,	-- poslední požadavek na reset hesla
+	reset_at	timestamp with time zone	DEFAULT NULL,	-- poslední reset/aktivace nebo žádost o ně
 	password_hash	varchar(255)	DEFAULT NULL,			-- heš hesla (je-li nastaveno)
 	note		text		NOT NULL DEFAULT ''		-- poznámka viditelná pro orgy
 );
diff --git a/mo/imports.py b/mo/imports.py
index 33284537..3645ea65 100644
--- a/mo/imports.py
+++ b/mo/imports.py
@@ -372,7 +372,7 @@ class Import:
         for uid in self.new_user_ids:
             u = sess.query(db.User).get(uid)
             if u and not u.password_hash and not u.reset_at:
-                token = mo.users.ask_reset_password(u)
+                token = mo.users.make_activation_token(u)
                 sess.commit()
                 mo.util.send_new_account_email(u, token)
             else:
diff --git a/mo/users.py b/mo/users.py
index 7e0c90b3..44667834 100644
--- a/mo/users.py
+++ b/mo/users.py
@@ -2,6 +2,7 @@
 
 import bcrypt
 import datetime
+import dateutil.tz
 import email.errors
 import email.headerregistry
 import re
@@ -174,12 +175,17 @@ def validate_password(passwd: str) -> bool:
     return len(passwd) >= 8
 
 
-def set_password(user: db.User, passwd: str):
+def set_password(user: db.User, passwd: str, reset: bool = False):
     salt = bcrypt.gensalt()
     hashed = bcrypt.hashpw(passwd.encode('utf-8'), salt)
     user.password_hash = hashed.decode('us-ascii')
-    user.reset_at = None
-    user.last_login_at = datetime.datetime.now()
+    if reset:
+        user.reset_at = mo.now
+        mo.util.log(
+            type=db.LogType.user,
+            what=user.user_id,
+            details={'action': 'do-reset'},
+        )
 
 
 def check_password(user: db.User, passwd: str):
@@ -188,63 +194,30 @@ def check_password(user: db.User, passwd: str):
 
 
 def login(user: db.User):
-    user.last_login_at = datetime.datetime.now()
-    user.reset_at = None
+    user.last_login_at = mo.now
 
 
-def ask_reset_password(user: db.User) -> str:
-    user.reset_at = datetime.datetime.now()
-    when = int(user.reset_at.timestamp())
-    token = mo.tokens.sign_token([str(user.user_id), str(when)], 'reset')
-
-    mo.util.log(
-        type=db.LogType.user,
-        what=user.user_id,
-        details={'action': 'ask-reset'},
-    )
-
-    return token
+def make_activation_token(user: db.User) -> str:
+    user.reset_at = mo.now
+    when = int(mo.now.timestamp())
+    return mo.tokens.sign_token([str(user.user_id), str(when)], 'activate')
 
 
-def check_reset_password(token: str) -> Optional[db.User]:
+def check_activation_token(token: str) -> Optional[db.User]:
     token = mo.util.clean_up_token(token)
-    fields = mo.tokens.verify_token(token, 'reset')
+    fields = mo.tokens.verify_token(token, 'activate')
     if not fields or len(fields) != 2:
         return None
-    user = db.get_session().query(db.User).filter_by(user_id=int(fields[0])).first()
+    user_id = int(fields[0])
+    token_time = datetime.datetime.fromtimestamp(int(fields[1]), tz=dateutil.tz.UTC)
 
-    if user.password_hash is None:
-        reset_token_validity_time = datetime.timedelta(days=28)
+    user = user_by_uid(user_id)
+    if not user:
+        return None
+    elif token_time < mo.now - datetime.timedelta(days=28):
+        return None
     else:
-        reset_token_validity_time = datetime.timedelta(hours=24)
-
-    now = datetime.datetime.now().astimezone()
-
-    if (user
-            and user.reset_at is not None
-            and fields[1] == str(int(user.reset_at.timestamp()))
-            and now - user.reset_at < reset_token_validity_time):
         return user
-    else:
-        return None
-
-
-def cancel_reset_password(user: db.User):
-    user.reset_at = None
-    mo.util.log(
-        type=db.LogType.user,
-        what=user.user_id,
-        details={'action': 'cancel-reset'},
-    )
-
-
-def do_reset_password(user: db.User):
-    user.reset_at = None
-    mo.util.log(
-        type=db.LogType.user,
-        what=user.user_id,
-        details={'action': 'do-reset'},
-    )
 
 
 def new_reg_request(type: db.RegReqType, client: str) -> Optional[db.RegRequest]:
@@ -272,3 +245,17 @@ def expire_reg_requests():
     table = db.RegRequest.__table__
     conn.execute(table.delete().where(table.c.expires_at < mo.now))
     sess.commit()
+
+
+def request_reset_password(user: db.User, client: str) -> Optional[db.RegRequest]:
+    logger.info('Login: Požadavek na reset hesla pro <%s>', user.email)
+    rr = new_reg_request(db.RegReqType.reset, client)
+    if rr:
+        db.get_session().add(rr)
+        rr.user_id = user.user_id
+        mo.util.log(
+            type=db.LogType.user,
+            what=user.user_id,
+            details={'action': 'ask-reset'},
+        )
+    return rr
diff --git a/mo/util.py b/mo/util.py
index 42d0a813..d22fadb4 100644
--- a/mo/util.py
+++ b/mo/util.py
@@ -113,8 +113,12 @@ def send_user_email(user: db.User, subject: str, body: str) -> bool:
     return True
 
 
+def activate_url(token: str) -> str:
+    return config.WEB_ROOT + 'auth/activate?' + urllib.parse.urlencode({'token': token}, safe=':')
+
+
 def password_reset_url(token: str) -> str:
-    return config.WEB_ROOT + 'auth/reset?' + urllib.parse.urlencode({'token': token}, safe=':')
+    return config.WEB_ROOT + 'auth/confirm/p?' + urllib.parse.urlencode({'token': token}, safe=':')
 
 
 def confirm_create_url(token: str) -> str:
@@ -135,7 +139,7 @@ def send_new_account_email(user: db.User, token: str) -> bool:
                 {}
 
         Váš OSMO
-    '''.format(password_reset_url(token))))
+    '''.format(activate_url(token))))
 
 
 def send_password_reset_email(user: db.User, token: str) -> bool:
diff --git a/mo/web/auth.py b/mo/web/auth.py
index 3ecc1f5c..7b547e59 100644
--- a/mo/web/auth.py
+++ b/mo/web/auth.py
@@ -62,20 +62,13 @@ def login():
         app.logger.error('Login: Neznámý uživatel <%s>', email)
         flash('Neznámý uživatel', 'danger')
     elif form.reset.data:
-        app.logger.info('Login: Požadavek na reset hesla pro <%s>', email)
-
-        min_time_between_resets = datetime.timedelta(minutes=1)
-        now = datetime.datetime.now().astimezone()
-        if (user.reset_at is not None
-                and now - user.reset_at < min_time_between_resets):
-            flash('Poslední požadavek na obnovení hesla byl odeslán příliš nedávno', 'danger')
-        else:
-            token = mo.users.ask_reset_password(user)
+        rr = mo.users.request_reset_password(user, request.remote_addr)
+        if rr:
             db.get_session().commit()
-
-            mo.util.send_password_reset_email(user, token)
-            flash('Na uvedenou adresu byl odeslán e-mail s odkazem na obnovu hesla', 'success')
-
+            mo.util.send_password_reset_email(user, rr.email_token)
+            flash('Na uvedenou adresu byl odeslán e-mail s odkazem na obnovu hesla.', 'success')
+        else:
+            flash('Příliš časté požadavky na obnovu hesla.', 'danger')
     elif not form.passwd.data or not mo.users.check_password(user, form.passwd.data):
         app.logger.error('Login: Špatné heslo pro uživatele <%s>', email)
         flash('Chybné heslo', 'danger')
@@ -179,7 +172,7 @@ def user_settings_change():
                 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')
+                flash('Příliš mnoho požadavků na změny e-mailu. Počkejte prosím chvíli a zkuste to znovu.', 'danger')
                 ok = False
         if ok:
             return redirect(url_for('user_settings'))
@@ -195,53 +188,38 @@ def handle_need_login(e):
 
 
 class ResetForm(FlaskForm):
-    email = EmailField('E-mail', description='Účet pro který se nastavuje nové heslo', render_kw={"disabled": "disabled"})
-    token = wtforms.HiddenField()
-    passwd = wtforms.PasswordField('Nové heslo', description=mo.users.password_help)
+    email = wtforms.StringField('E-mail', description='Účet pro který se nastavuje nové heslo', render_kw={"disabled": "disabled"})
+    new_passwd = mo_fields.NewPassword(validators=[validators.DataRequired()])
+    new_passwd2 = mo_fields.RepeatPassword(validators=[validators.DataRequired()])
     submit = wtforms.SubmitField('Nastavit heslo')
-    cancel = wtforms.SubmitField('Zrušit obnovu hesla')
 
 
-@app.route('/auth/reset', methods=('GET', 'POST'))
-def reset():
+@app.route('/auth/activate', methods=('GET', 'POST'))
+def activate():
     token = request.args.get('token')
     if not token:
-        flash('Žádný token pro resetování hesla', 'danger')
+        flash('Chybí token pro aktivaci účtu', 'danger')
         return redirect(url_for('login'))
 
-    user = mo.users.check_reset_password(token)
+    user = mo.users.check_activation_token(token)
     if not user:
-        flash('Neplatný požadavek na obnovu hesla', 'danger')
+        flash('Neplatný kód pro aktivaci účtu. Zkontrolujte, že jste odkaz z e-mailu zkopírovali správně.', 'danger')
+        return redirect(url_for('login'))
+
+    if user.last_login_at is not None:
+        flash('Tento účet už byl aktivován. Pokud neznáte heslo, použijte tlačítko pro obnovu hesla.', 'danger')
         return redirect(url_for('login'))
 
-    form = ResetForm(token=token, email=user.email)
+    form = ResetForm(email=user.email)
     ok = form.validate_on_submit()
     if not ok:
-        return render_template('reset.html', form=form)
+        return render_template('acct_activate.html', form=form)
 
-    if form.cancel.data:
-        mo.users.cancel_reset_password(user)
-        app.logger.info('Login: Zrušen reset hesla pro uživatele <%s>', user.email)
-        db.get_session().commit()
-        flash('Obnova hesla zrušena', 'warning')
-        return redirect(url_for('login'))
-    elif not mo.users.validate_password(form.passwd.data):
-        flash(mo.users.password_help, 'danger')
-        return render_template('reset.html', form=form)
-    else:
-        mo.users.do_reset_password(user)
-        mo.users.set_password(user, form.passwd.data)
-        app.logger.info('Login: Reset hesla pro uživatele <%s>', user.email)
-        mo.util.log(
-            type=db.LogType.user,
-            what=user.user_id,
-            details={'action': 'reset-passwd'},
-        )
-        mo.users.login(user)
-        app.logger.info('Login: Přihlásil se uživatel <%s> po resetování hesla', user.email)
-        db.get_session().commit()
-        flash('Nastavení nového hesla a přihlášení do systému proběhlo úspěšně', 'success')
-        return login_and_redirect(user, flash_msg='Heslo nastaveno')
+    app.logger.info('Login: Aktivace účtu uživatele <%s>', user.email)
+    mo.users.set_password(user, form.new_passwd.data, reset=True)
+    mo.users.login(user)
+    db.get_session().commit()
+    return login_and_redirect(user, flash_msg='Nastavení nového hesla a přihlášení do systému proběhlo úspěšně')
 
 
 class RegStatus(Enum):
@@ -426,6 +404,11 @@ class Reg2:
             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.',
         },
+        db.RegReqType.reset: {
+            RegStatus.new: 'Chybný kód pro obnovení hesla. Zkontrolujte, že jste odkaz z e-mailu zkopírovali správně.',
+            RegStatus.expired: 'Vypršela platnost kódu pro obnovení hesla, požádejte prosím o obnovu znovu.',
+            RegStatus.already_spent: 'Tento odkaz na obnovení hesla byl již využit.',
+        },
     }
 
     def __init__(self, token: str, expected_type: db.RegReqType):
@@ -528,6 +511,17 @@ class Reg2:
         self.rr.used_at = mo.now
         sess.commit()
 
+    def change_passwd(self, new_passwd: str):
+        sess = db.get_session()
+        user = self.rr.user
+
+        app.logger.info(f'Reg2: Uživatel #{user.user_id} si resetoval heslo')
+        mo.users.set_password(user, new_passwd, reset=True)
+        mo.users.login(user)
+
+        self.rr.used_at = mo.now
+        sess.commit()
+
     def spend_request(self):
         self.rr.used_at = mo.now
         db.get_session().commit()
@@ -601,6 +595,31 @@ def confirm_email():
     return render_template('acct_confirm_email.html', form=form)
 
 
+class CancelResetForm(FlaskForm):
+    cancel = wtforms.SubmitField('Zrušit obnovu hesla')
+
+
+@app.route('/auth/confirm/p', methods=('GET', 'POST'))
+def confirm_reset():
+    reg2 = Reg2(request.args.get('token'), db.RegReqType.reset)
+    if reg2.status != RegStatus.ok:
+        reg2.flash_message()
+        return redirect(url_for('login'))
+
+    form = ResetForm(email=reg2.rr.user.email)
+    if form.validate_on_submit() and form.submit.data:
+        reg2.change_passwd(form.new_passwd.data)
+        return login_and_redirect(reg2.rr.user, flash_msg='Nastavení nového hesla a přihlášení do systému proběhlo úspěšně')
+
+    cform = CancelResetForm()
+    if cform.validate_on_submit() and cform.cancel.data:
+        reg2.spend_request()
+        flash('Požadavek na změnu hesla zrušen.', 'success')
+        return redirect(url_for('user_settings'))
+
+    return render_template('acct_reset_passwd.html', form=form, cancel_form=cform)
+
+
 @app.errorhandler(werkzeug.exceptions.Forbidden)
 def handle_forbidden(e):
     return render_template('forbidden.html')
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 28c42d63..03e23ada 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -1674,6 +1674,8 @@ def org_contest_add_user(id: int, site_id: Optional[int] = None):
             db.get_session().commit()
             if is_new_user:
                 flash("Založen nový uživatel.", "info")
+                token = mo.users.make_activation_token(user)
+                mo.email.send_new_account_email(user, token)
             if is_new_participant:
                 flash("Založena nová registrace do ročníku.", "info")
             if is_new_participation:
diff --git a/mo/web/org_users.py b/mo/web/org_users.py
index e885537a..981d8c0b 100644
--- a/mo/web/org_users.py
+++ b/mo/web/org_users.py
@@ -316,14 +316,15 @@ class ResendInviteForm(FlaskForm):
     resend_invite = SubmitField()
 
     def do(self, user: db.User):
-        token = mo.users.ask_reset_password(user)
-        db.get_session().commit()
-        if user.last_login_at is None and mo.util.send_new_account_email(user, token):
-            flash('Uvítací e-mail s odkazem pro nastavení hesla odeslán na {}'.format(user.email), 'success')
-        elif mo.util.send_password_reset_email(user, token):
-            flash('E-mail s odkazem pro resetování hesla odeslán na {}'.format(user.email), 'success')
+        if user.last_login_at is None:
+            token = mo.users.make_activation_token(user)
+            db.get_session().commit()
+            if mo.util.send_new_account_email(user, token):
+                flash('Uvítací e-mail s odkazem na aktivaci účtu odeslán na {}.'.format(user.email), 'success')
+            else:
+                flash('Problém při odesílání e-mailu s odkazem na aktivaci účtu.', 'danger')
         else:
-            flash('Problém při odesílání e-mailu s odkazem pro nastavení hesla', 'danger')
+            flash('Tento uživatel už má účet aktivovaný.', 'danger')
 
 
 @app.route('/org/org/<int:id>/', methods=('GET', 'POST'))
@@ -339,7 +340,7 @@ def org_org(id: int):
     can_assign_rights = rr.have_right(Right.assign_rights)
 
     resend_invite_form: Optional[ResendInviteForm] = None
-    if rr.can_edit_user(user):
+    if user.last_login_at is None and rr.can_edit_user(user):
         resend_invite_form = ResendInviteForm()
         if resend_invite_form.resend_invite.data and resend_invite_form.validate_on_submit():
             resend_invite_form.do(user)
@@ -425,7 +426,7 @@ def org_user(id: int):
     rr = g.gatekeeper.rights_generic()
 
     resend_invite_form: Optional[ResendInviteForm] = None
-    if rr.can_edit_user(user):
+    if user.last_login_at is None and rr.can_edit_user(user):
         resend_invite_form = ResendInviteForm()
         if resend_invite_form.resend_invite.data and resend_invite_form.validate_on_submit():
             resend_invite_form.do(user)
@@ -567,17 +568,14 @@ def org_user_new():
                 details={'action': 'new', 'user': db.row2dict(new_user)},
             )
 
+            token = mo.users.make_activation_token(new_user)
             sess.commit()
             flash('Nový uživatel vytvořen', 'success')
 
-            # Send password (re)set link
-            token = mo.users.ask_reset_password(new_user)
-            db.get_session().commit()
-
             if mo.util.send_new_account_email(new_user, token):
-                flash('E-mail s odkazem pro nastavení hesla odeslán na {}'.format(new_user.email), 'success')
+                flash('E-mail s odkazem na aktivaci účtu odeslán na {}.'.format(new_user.email), 'success')
             else:
-                flash('Problém při odesílání e-mailu s odkazem pro nastavení hesla', 'danger')
+                flash('Problém při odesílání e-mailu s odkazem na aktivaci účtu.', 'danger')
 
             if is_org:
                 return redirect(url_for('org_org', id=new_user.user_id))
diff --git a/mo/web/templates/acct_activate.html b/mo/web/templates/acct_activate.html
new file mode 100644
index 00000000..1117b6c9
--- /dev/null
+++ b/mo/web/templates/acct_activate.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+{% block title %}Aktivace nového účtu{% endblock %}
+{% block body %}
+
+{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
+
+{% endblock %}
diff --git a/mo/web/templates/acct_reset_passwd.html b/mo/web/templates/acct_reset_passwd.html
new file mode 100644
index 00000000..1342edea
--- /dev/null
+++ b/mo/web/templates/acct_reset_passwd.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+{% block title %}Nastavení nového hesla{% endblock %}
+{% block body %}
+
+{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
+{{ wtf.quick_form(cancel_form, form_type='horizontal') }}
+
+{% endblock %}
diff --git a/mo/web/templates/org_user.html b/mo/web/templates/org_user.html
index 966d4b94..05463d5f 100644
--- a/mo/web/templates/org_user.html
+++ b/mo/web/templates/org_user.html
@@ -24,7 +24,7 @@
 <form method=POST class='btn-group' onsubmit='return confirm("Poslat účastníkovi e-mail s odkazem na vytvoření hesla?");'>
 	{{ resend_invite_form.csrf_token }}
 	<button class="btn btn-default" type='submit' name='resend_invite' value='yes'>
-		{% if user.last_login_at %}Resetovat heslo{% else %}Znovu poslat zvací e-mail{% endif %}
+		Znovu poslat zvací e-mail
 	</button>
 </form>
 {% endif %}
diff --git a/mo/web/templates/reset.html b/mo/web/templates/reset.html
deleted file mode 100644
index bb382b0a..00000000
--- a/mo/web/templates/reset.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{% extends "base.html" %}
-{% import "bootstrap/wtf.html" as wtf %}
-{% block title %}Nastavení nového hesla{% endblock %}
-{% block body %}
-
-	<form method="POST" class="form form-horizontal" action="">
-		{{ form.csrf_token }}
-		{{ form.token() }}
-		{{ wtf.form_field(form.email, form_type='horizontal') }}
-		{{ wtf.form_field(form.passwd, form_type='horizontal') }}
-		<div class="btn-group col-lg-offset-2">
-			{{ wtf.form_field(form.submit, class="btn btn-primary") }}
-			{{ wtf.form_field(form.cancel) }}
-		</div>
-	</form>
-
-{% endblock %}
-- 
GitLab