Repository: fossasia/open-event-server

Issue:
Deleted user cannot signup with the same email
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
Once a user gets deleted by admin or if he himself deletes the account, then later he cannot signup with the same email. An error `User already exists.` persists

**To Reproduce**
Steps to reproduce the behavior:
1. Go to `Admin > Users`
2. Delete a test user
3. Try to signup again using same email as of test user
4. See error

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
The user must be allowed to signup with that email

**Additional context**
<!-- Add any other context about the problem here. -->
Related to https://github.com/fossasia/open-event-server/issues/5805
Will work on it



Discussion:
@shreyanshdwivedi is the user able to login with the following credentials 
Nope. When you try to login using the old creds, it raises error `Your credentials were incorrect.`
Yeah. When deleting a user, append an underscore in their email, and keep appending until it is unique
Also create a migration to do the same to previously deleted users
@shreyanshdwivedi let me know if you need any help regarding this?
@iamareebjamal you mean when deleting a user on server using this code  -
```
user.deleted_at = data.get('deleted_at)
```
I should also update email of the user like this -
```
user.email = '_' + user.email  #till it becomes unique
user.deleted_at = data.get('deleted_at)
```
Right?
```user.email = user.email + '_' ```

Yes
@uds5501 @iamareebjamal I've a query regarding this. Suppose I updated the email of user by apapending `_` to his/her email when admin deleted his/her account. 
Now comes the real problem -
 - When admin views his/her deleted users list, the email will be the new appended email. However when I add this code block -
```python
if has_access('is_admin') and data.get('deleted_at') != user.deleted_at:
            notUniqueEmail = 1
            userEmail = user.email
            while notUniqueEmail:
                print(userEmail)
                try:
                    db.session.query(User).filter_by(email=userEmail).one()
                except NoResultFound:
                    user.email = userEmail
                    notUniqueEmail = 0
                else:
                    userEmail = userEmail + '_'
            user.deleted_at = data.get('deleted_at')
```
the deleted users list produces this error `TypeError: list indices must be integers or slices, not str`.

 - The second issue. If admin deleted a user and now user signs up with same email and admin deletes this user too, we have two entries for same user. And admin can restore both the users, this will be really bad experience bcoz it'll definitely create a number of issues with it.

Your views?
@shreyanshdwivedk I was thinking about the same issue , as the deleted
users still have their data stored stored in database which can be restored
by superadmin , we can have two approaches here -:
1.) Erase the user data from database .
2.) Do something like facebook , if a deleted user sign in again , restore
his data except events and orders .
Your views @uds5501 , @iamareebjamal

On Fri, 17 May 2019 at 4:01 AM, Shreyansh Dwivedi <notifications@github.com>
wrote:

> @uds5501 <https://github.com/uds5501> @iamareebjamal
> <https://github.com/iamareebjamal> I've a query regarding this. Suppose I
> updated the email of user by apapending _ to his/her email when admin
> deleted his/her account.
> Now comes the real problem -
>
>    - When admin views his/her deleted users list, the email will be the
>    new appended email. However when I add this code block -
>
> if has_access('is_admin') and data.get('deleted_at') != user.deleted_at:
>             notUniqueEmail = 1
>             userEmail = user.email
>             while notUniqueEmail:
>                 print(userEmail)
>                 try:
>                     db.session.query(User).filter_by(email=userEmail).one()
>                 except NoResultFound:
>                     user.email = userEmail
>                     notUniqueEmail = 0
>                 else:
>                     userEmail = userEmail + '_'
>             user.deleted_at = data.get('deleted_at')
>
> the deleted users list produces this error TypeError: list indices must
> be integers or slices, not str.
>
>    - The second issue. If admin deleted a user and now user signs up with
>    same email and admin deletes this user too, we have two entries for same
>    user. And admin can restore both the users, this will be really bad
>    experience bcoz it'll definitely create a number of issues with it.
>
> Your views?
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/fossasia/open-event-server/issues/5889?email_source=notifications&email_token=AKQMTLQ4IFRO6BGYBQKQIQTPVXOERA5CNFSM4HNHHM62YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVTHKZY#issuecomment-493253991>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AKQMTLSNLQBHKEBXDQXFP43PVXOERANCNFSM4HNHHM6Q>
> .
>

> @uds5501 @iamareebjamal I've a query regarding this. Suppose I updated the email of user by apapending `_` to his/her email when admin deleted his/her account.
> Now comes the real problem -
> 
> * When admin views his/her deleted users list, the email will be the new appended email. However when I add this code block -
> 
> ```python
> if has_access('is_admin') and data.get('deleted_at') != user.deleted_at:
>             notUniqueEmail = 1
>             userEmail = user.email
>             while notUniqueEmail:
>                 print(userEmail)
>                 try:
>                     db.session.query(User).filter_by(email=userEmail).one()
>                 except NoResultFound:
>                     user.email = userEmail
>                     notUniqueEmail = 0
>                 else:
>                     userEmail = userEmail + '_'
>             user.deleted_at = data.get('deleted_at')
> ```
> 
> the deleted users list produces this error `TypeError: list indices must be integers or slices, not str`.
> 
> * The second issue. If admin deleted a user and now user signs up with same email and admin deletes this user too, we have two entries for same user. And admin can restore both the users, this will be really bad experience bcoz it'll definitely create a number of issues with it.
> 
> Your views?

The error here created is due to the email now created after is not a form of unicode email which has been recently solved by @CosmicCoder96 in frontend
@shreyanshdwivedi Or we could simply not use SoftDeletionSchema for this? But that would finish the restoration feature.

I like @kushthedude idea. We can see the last login / deleted-at timestamp and simply login the same user all over again if it's less than 15 days Otherwise, reallocate the same user entry to the new user except, with the user centric data wiped out and a password reset link being sent to the user.

@iamareebjamal @mariobehling Please see this.
I don't understand what's so complicated here that this much of analysis is required. We want the email field to be unique, none of the above solutions work with that. How can you have a unique field with duplicate emails? Also, restoration will and should not work in case someone has created a new account with that email. 

> When admin views his/her deleted users list, the email will be the new appended email

As it should be

> Erase the user data from database .

No

> Do something like facebook , if a deleted user sign in again , restore
his data except events and orders .

We don't want the deleted user to sign in. We want it to sign up. Which will not be possible because unique key constraint will prevent that from creating an account, and we don't want this much complication where we restore an account to someone else. Where will we keep the previous data? How will we verify that it is the same person who deleted the account? 


> And admin can restore both the users

Restoring will be a very rare manual process, and you can't still have 2 users with same email because of unique contraint, that's why we want to keep it and that's why we are appending _ to its end. This will not be a problem ever. For one email, tere can be infinite deleted users, but only one active. Same as happens with twitter. You can change you username and other person can take that username and repeat

@iamareebjamal @shreyanshdwivedi Apparently, adding an `_` at the end won't be of much help. How about instead of 
`xyz@email.com` to `xyz@email.com_`, we do `xyz_@email.com`

The former one gives a unicode violation and hence this page :
![image](https://user-images.githubusercontent.com/21087061/57912314-a71bcf80-78a7-11e9-92ed-c617a4fb53d1.png)

>the deleted users list produces this error TypeError: list indices must be integers or slices, not str.

uds5501 exactly. Even I mentioned it earlier. Besides the error u mentioned, the list page is also breaking.

>How about instead of
xyz@email.com to xyz@email.com_, we do xyz_@email.com

iamareebjamal your thoughts?
@uds5501 it is more likely a violation of the field of type ‘email’. It
might not be a valid email address according to marshmallow validations
defined in user schema. Not a Unicode issue. Xyz_@email.com if allowed
sounds good.

On Fri, 17 May 2019 at 6:25 PM, Shreyansh Dwivedi <notifications@github.com>
wrote:

> the deleted users list produces this error TypeError: list indices must be
> integers or slices, not str.
>
> uds5501 exactly. Even I mentioned it earlier. Besides the error u
> mentioned, the list page is also breaking.
>
> How about instead of
> xyz@email.com to xyz@email.com_, we do xyz_@email.com
>
> iamareebjamal your thoughts?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/fossasia/open-event-server/issues/5889?email_source=notifications&email_token=AEDUDRKBQ2NKYXXXE5VIWRDPV2B3LA5CNFSM4HNHHM62YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVUMJMA#issuecomment-493405360>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AEDUDRIZN5TFGTQKQRCHWZLPV2B3LANCNFSM4HNHHM6Q>
> .
>

@CosmicCoder96 I have checked, the XYz_ works fine and yes, it's a schema validation issue. 
It can be worked upon only after https://github.com/fossasia/open-event-server/pull/5882 gets merged.
`xyz_@email.com` is a problem. Now we taking up a new email which may belong to other person

Try `xyz@email.com.deleted`
@iamareebjamal `.deleted` works 
@shreyanshdwivedi apply these changes in the migration?
Yeah, once the mentioned PR gets merged, I'll push the changes alongwith a migration file to update the email of deleted users.

Resolving patch:
diff --git a/app/api/helpers/user.py b/app/api/helpers/user.py
new file mode 100644
--- /dev/null
+++ b/app/api/helpers/user.py
@@ -0,0 +1,48 @@
+from sqlalchemy.orm.exc import NoResultFound
+from app.api.helpers.exceptions import ForbiddenException
+from app.models import db
+from app.api.helpers.db import save_to_db
+from app.models.user import User
+
+
+def modify_email_for_user_to_be_deleted(user):
+    """
+    Update the email ID of user which is to be deleted.
+    Adds '.deleted' substring to email of a user to be deleted.
+    :param order: User to be deleted.
+    :return:
+    """
+    not_unique_email = True
+    user_email = user.email
+    while not_unique_email:
+        try:
+            db.session.query(User).filter_by(email=user_email).one()
+        except NoResultFound:
+            user.email = user_email
+            save_to_db(user)
+            not_unique_email = False
+        else:
+            user_email = user_email + '.deleted'
+    return user
+
+
+def modify_email_for_user_to_be_restored(user):
+    """
+    Update the email ID of user which is to be restored.
+    Removes '.deleted' substring from a user to be restored.
+    :param order: User to be restored.
+    :return:
+    """
+    user_email = user.email
+    remove_str = '.deleted'
+    if user_email.endswith(remove_str):
+        user_email = user_email[:-len(remove_str)]
+        try:
+            db.session.query(User).filter_by(email=user_email).one()
+        except NoResultFound:
+            user.email = user_email
+            save_to_db(user)
+        else:
+            raise ForbiddenException({'pointer': '/data/attributes/email'},
+                                     "This email is already registered! Manually edit and then try restoring")
+    return user
diff --git a/app/api/users.py b/app/api/users.py
--- a/app/api/users.py
+++ b/app/api/users.py
@@ -14,6 +14,7 @@
 from app.api.helpers.mail import send_email_confirmation, send_email_change_user_email, send_email_with_action
 from app.api.helpers.permission_manager import has_access
 from app.api.helpers.permissions import is_user_itself
+from app.api.helpers.user import modify_email_for_user_to_be_deleted, modify_email_for_user_to_be_restored
 from app.api.helpers.utilities import get_serializer, str_generator
 from app.api.schema.users import UserSchema, UserSchemaPublic
 from app.models import db
@@ -207,21 +208,27 @@ def before_update_object(self, user, data, view_kwargs):
         #     data['small_image_url'] = uploaded_images['thumbnail_image_url']
         #     data['thumbnail_image_url'] = uploaded_images['thumbnail_image_url']
         #     data['icon_image_url'] = uploaded_images['icon_image_url']
+
+        if data.get('deleted_at') != user.deleted_at:
+            if has_access('is_user_itself', user_id=user.id) or has_access('is_admin'):
+                if data.get('deleted_at'):
+                    if len(user.events) != 0:
+                        raise ForbiddenException({'source': ''}, "Users associated with events cannot be deleted")
+                    elif len(user.orders) != 0:
+                        raise ForbiddenException({'source': ''}, "Users associated with orders cannot be deleted")
+                    else:
+                        modify_email_for_user_to_be_deleted(user)
+                else:
+                    modify_email_for_user_to_be_restored(user)
+                    data['email'] = user.email
+                user.deleted_at = data.get('deleted_at')
+            else:
+                raise ForbiddenException({'source': ''}, "You are not authorized to update this information.")
+
         users_email = data.get('email', None)
         if users_email is not None:
             users_email = users_email.strip()
 
-        if has_access('is_admin') and data.get('deleted_at') != user.deleted_at:
-            user.deleted_at = data.get('deleted_at')
-
-        if has_access('is_user_itself', user_id=user.id) and data.get('deleted_at') != user.deleted_at:
-            if len(user.events) != 0:
-                raise ForbiddenException({'source': ''}, "Users associated with events cannot be deleted")
-            elif len(user.orders) != 0:
-                raise ForbiddenException({'source': ''}, "Users associated with orders cannot be deleted")
-            else:
-                user.deleted_at = data.get('deleted_at')
-
         if users_email is not None and users_email != user.email:
             try:
                 db.session.query(User).filter_by(email=users_email).one()
diff --git a/migrations/versions/6f7b6fad3f56_refactor_deleted_users_email.py b/migrations/versions/6f7b6fad3f56_refactor_deleted_users_email.py
new file mode 100644
--- /dev/null
+++ b/migrations/versions/6f7b6fad3f56_refactor_deleted_users_email.py
@@ -0,0 +1,20 @@
+"""Refactor deleted users email
+Revision ID: 6f7b6fad3f56
+Revises: 0e80c49a6e28
+Create Date: 2019-05-24 03:26:25
+"""
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '6f7b6fad3f56'
+down_revision = '0e80c49a6e28'
+
+
+def upgrade():
+    op.execute("UPDATE users SET _email = concat(_email, '.deleted') where deleted_at IS NOT NULL;",
+               execution_options=None)
+
+def downgrade():
+    op.execute("UPDATE users SET _email = left(_email, length(_email)-8) where right(_email, 8) = '.deleted' and deleted_at IS NOT NULL;",
+               execution_options=None)


Test patch: