Repository: fossasia/open-event-server

Issue:
Email IDs are case-sensitive, allowing multiple accounts with one handle
**Describe the bug**
Currently, email id is stored in the database as raw string i.e. as entered by the user and then during auth the user is expected to enter the exact case-sensitive email id. This can lead to creation of multiple accounts with one email id and is a major bug.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to login
2. Go to create account
3. Enter email id and password
4. Click Register
5. Again go to Create Account
6. Enter the same email id and password, however this time 
    change the casing of some letters of email
7. Click Register
8. You would have successfully created two different accounts with the same email id

**Expected behavior**
When we try to create the second account, it should give an error: Email already registered.

**Additional context**
I am working on it.



Discussion:
@iamareebjamal @CosmicCoder96 this change will have to be made at multiple places. Essentially everytime we take email id as input from the user we will have to lowercase it for consistency in the database. Can you suggest what all places should I look into to fix the issue?
The only place it needs to change is registration
@iamareebjamal I will have to make the following changes if we want to allow the users to enter the 'email' in any 'case'.

## Changes for Email consistency:
### app/api/users.py
`UserList` - `before_create_object` (change)
`UserDetail` - `before_update_object` (change)
`is_email_available` (change)

### app/api/events.py
`EventList` - `before_create_object` (add)
`EventDetail` - `before_update_object` (change)

### app/api/speakers.py
`SpeakerListPost` - `before_create_object` (add)
`SpeakerDetail` - before_update_object (change)

### app/api/auth.py
`resend_verification_email` (change)
`reset_password` - method:POST (change)

### app/api/user_emails.py
`UserEmailListPost` - `before_create_object` (add)

### app/api/role_invite.py
`RoleInvitePost` - `before_create_object` (add)

### app/api/helpers/jwt.py
`jwt_authenticate` (change)

Note: The add/change written in the bracket refers to whether I will be changing the method or adding it.

Can you please review this once and give me a go ahead if you are satisfied with the proposed changes?
Just create an index on email fields to be unique lower

```
CREATE INDEX idx_groups_email ON groups lower(email);
```
This is a good solution however, I feel it will miss some cases. For example: while logging in, if the user entered the email in a different case then the might fail. Moreover, the data in database won't be as consistent, I think it's better if all the emails are stored in lowercase. Therefore, I think the best way to solve this issue might be that whenever we take email as input from the user we should convert it to lowercase before we do any further processing on it.
> while logging in, if the user entered the email in a different case then the might fail.  

Why will it fail?

> Therefore, I think the best way to solve this issue might be that whenever we take email as input from the user we should convert it to lowercase before we do any further processing on it.

How does your solution work on pre-existing data? And ensures on database level that data will not be inconsistent
I will explain what I am trying to say with an example:

## Example
### During Registration
email entered by the user: "EXAMPLE@EXAMPLE.COM"

### Database
email stored in the database: "EXAMPLE@EXAMPLE.COM"
email in the index: "example@example.com"

### During Logging In
email entered by the user: "EXAMPLE@example.com"

### Problem
The email id entered by the user during logging in, does not match the email in database or the lowercase email on which index is constructed.

### Additional Problem
Also, while coming up with a solution we will also have to keep in mind that there might be existing mixed-case emails in the database.

### Solution
Every time user enters email, we convert it to lowercase and then store it in the database or do any further processing on it (like querying the database).

### Auxiliary Solution
We will also construct the index (as you mentioned) on lowercase emails to avoid the clashed with existing mixed-case emails.
We need a solution which works for previous emails and prevents inconsistency on database level
My plan was to use both the things mentioned below.
> ### Solution
> Every time user enters email, we convert it to lowercase and then store it in the database or do any further processing on it (like querying the database).

This will prevent future inconsistency.
> ### Auxiliary Solution
> We will also construct the index (as you mentioned) on lowercase emails to avoid the clashed with existing mixed-case emails.

This will make sure the system works for previously  existing emails in database.
I think this will solve the issue.
OK
@divyam-goel if you have not invested much time on this enhancement, can I take over?

@divyam-goel @uds5501 Anyone working on this issue?
@prateekj117 No, I am not, you should probably take over

@uds5501 Ok.

Resolving patch:
diff --git a/migrations/versions/7bb5891a9f2e_lowercase_email.py b/migrations/versions/7bb5891a9f2e_lowercase_email.py
new file mode 100644
--- /dev/null
+++ b/migrations/versions/7bb5891a9f2e_lowercase_email.py
@@ -0,0 +1,35 @@
+"""lowercase_email
+
+Revision ID: 7bb5891a9f2e
+Revises: 6e5c574cbfb8
+Create Date: 2019-03-30 12:51:48.134800
+
+"""
+
+from alembic import op
+from sqlalchemy import func
+import sqlalchemy as sa
+import sqlalchemy_utils
+
+
+# revision identifiers, used by Alembic.
+revision = '7bb5891a9f2e'
+down_revision = '6e5c574cbfb8'
+
+
+def upgrade():
+    op.execute("UPDATE users SET deleted_at = current_timestamp, _email = concat(_email, '_') where _email not in (SELECT DISTINCT ON (upper(_email)) _email FROM users);",
+               execution_options=None)
+    op.execute("create extension citext;",
+               execution_options=None)
+    op.execute("alter table users alter column _email type citext;",
+               execution_options=None)
+
+
+def downgrade():
+    op.execute("alter table users alter column _email type text;",
+               execution_options=None)
+    op.execute("UPDATE users SET deleted_at = null, _email = left(_email, length(_email)-1) where right(_email, 1) = '_';",
+               execution_options=None)
+    op.execute("drop extension citext;",
+               execution_options=None)


Test patch: