From 1195ddeb2c2e125d8624260a1f2fcdd2a8bd38ec Mon Sep 17 00:00:00 2001
From: Martin Mares <mj@ucw.cz>
Date: Fri, 13 Jan 2023 21:59:42 +0100
Subject: [PATCH] =?UTF-8?q?Registrace=20o=C5=A1et=C5=99uje=20kolize=20p?=
 =?UTF-8?q?=C5=99i=20zm=C4=9Bn=C4=9B=20e-mailu?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Při implementaci registrace jsme na to úplně zapomněli.

Closes #307 and #321.
---
 mo/web/acct.py | 47 +++++++++++++++++++++++++++++++----------------
 1 file changed, 31 insertions(+), 16 deletions(-)

diff --git a/mo/web/acct.py b/mo/web/acct.py
index e54311a2..24f34bc2 100644
--- a/mo/web/acct.py
+++ b/mo/web/acct.py
@@ -193,19 +193,23 @@ def user_settings_personal():
             sess.commit()
             flash('Heslo změněno.', 'success')
         if form.email.data != user.email:
-            rr = mo.users.new_reg_request(db.RegReqType.change_email, 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.email.send_confirm_change_email(user, rr.email_token)
-            else:
-                app.logger.info('Settings: Rate limit')
-                flash('Příliš mnoho požadavků na změny e-mailu. Počkejte prosím chvíli a zkuste to znovu.', 'danger')
+            if mo.users.user_by_email(form.email.data) is not None:
+                flash('Tuto e-mailovou adresu už používá jiný uživatel.', 'danger')
                 ok = False
+            else:
+                rr = mo.users.new_reg_request(db.RegReqType.change_email, 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.email.send_confirm_change_email(user, rr.email_token)
+                else:
+                    app.logger.info('Settings: Rate limit')
+                    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'))
 
@@ -436,6 +440,7 @@ class Reg2:
             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.',
+            RegStatus.already_exists: 'Tuto adresu už použivá jiný účet.',
         },
         db.RegReqType.reset_passwd: {
             RegStatus.new: 'Chybný kód pro obnovení hesla. Zkontrolujte, že jste odkaz z e-mailu zkopírovali správně.',
@@ -519,9 +524,16 @@ class Reg2:
         self.user = user
         return True
 
-    def change_email(self):
+    def change_email(self) -> bool:
         sess = db.get_session()
         user = self.rr.user
+
+        if mo.users.user_by_email(self.rr.email) is not None:
+            app.logger.info(f'Reg2: Uživatel #{user.user_id} si chce změnit email na <{user.email}>, ale už je použitý jiným účtem.')
+            self.status = RegStatus.already_exists
+            return False
+
+        # Tady je krátké okénko, kdy může nastat race condition. Chytí ji integritní omezení v DB a vznikne výjimka.
         user.email = self.rr.email
 
         app.logger.info(f'Reg2: Uživatel #{user.user_id} si změnil email na <{user.email}>')
@@ -536,6 +548,7 @@ class Reg2:
 
         self.rr.used_at = mo.now
         sess.commit()
+        return True
 
     def change_passwd(self, new_passwd: str):
         sess = db.get_session()
@@ -610,13 +623,15 @@ def confirm_email():
     form = ConfirmEmailForm()
     if form.validate_on_submit():
         if form.submit.data:
-            reg2.change_email()
-            flash('E-mail změněn.', 'success')
+            if reg2.change_email():
+                flash('E-mail změněn.', 'success')
+                return redirect(url_for('user_settings'))
         elif form.cancel.data:
             reg2.spend_request()
             flash('Požadavek na změnu e-mailu zrušen.', 'success')
-        return redirect(url_for('user_settings'))
+            return redirect(url_for('user_settings'))
 
+    reg2.flash_message()
     form.orig_email.data = reg2.rr.user.email
     form.new_email.data = reg2.rr.email
 
-- 
GitLab