Skip to content

Implementation of Auth0 to Signup flow #7974

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ services:
- JSON_LOG=1
# - LOG_LEVEL=DEBUG
volumes:
- ./mydata:/label-studio/data:rw
- label-studio-data:/label-studio/data:rw
command: label-studio-uwsgi

db:
Expand All @@ -62,3 +62,7 @@ services:
volumes:
- ${POSTGRES_DATA_DIR:-./postgres-data}:/var/lib/postgresql/data
- ./deploy/pgsql/certs:/var/lib/postgresql/certs:ro

volumes:
postgres-data:
label-studio-data:
16 changes: 10 additions & 6 deletions label_studio/manage.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#!/usr/bin/env python
"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
"""
"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license."""

import os
import sys

if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.label_studio')
from dotenv import load_dotenv

load_dotenv()

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings.label_studio")
# os.environ.setdefault('DEBUG', 'True')
try:
from django.conf import settings
Expand All @@ -17,7 +21,7 @@
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
'available on your PYTHONPATH environment variable? Did you '
'forget to activate a virtual environment?'
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
96 changes: 51 additions & 45 deletions label_studio/users/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
"""
"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license."""

import logging

from django import forms
Expand All @@ -12,18 +12,18 @@
EMAIL_MAX_LENGTH = 256
USERNAME_MAX_LENGTH = 30
DISPLAY_NAME_LENGTH = 100
USERNAME_LENGTH_ERR = f'Please enter a username {USERNAME_MAX_LENGTH} characters or fewer in length'
DISPLAY_NAME_LENGTH_ERR = f'Please enter a display name {DISPLAY_NAME_LENGTH} characters or fewer in length'
INVALID_USER_ERROR = "The email and password you entered don't match."
USERNAME_LENGTH_ERR = f"Please enter a username {USERNAME_MAX_LENGTH} characters or fewer in length"
DISPLAY_NAME_LENGTH_ERR = f"Please enter a display name {DISPLAY_NAME_LENGTH} characters or fewer in length"
INVALID_USER_ERROR = "Invalid Credentials."

FOUND_US_ELABORATE = 'Other'
FOUND_US_ELABORATE = "Other"
FOUND_US_OPTIONS = (
('Gi', 'Github'),
('Em', 'Email or newsletter'),
('Se', 'Search engine'),
('Fr', 'Friend or coworker'),
('Ad', 'Ad'),
('Ot', FOUND_US_ELABORATE),
("Gi", "Github"),
("Em", "Email or newsletter"),
("Se", "Search engine"),
("Fr", "Friend or coworker"),
("Ad", "Ad"),
("Ot", FOUND_US_ELABORATE),
)

logger = logging.getLogger(__name__)
Expand All @@ -33,76 +33,82 @@ class LoginForm(forms.Form):
"""For logging in to the app and all - session based"""

# use username instead of email when LDAP enabled
email = forms.CharField(label='User') if settings.USE_USERNAME_FOR_LOGIN else forms.EmailField(label='Email')
email = forms.CharField(label="User") if settings.USE_USERNAME_FOR_LOGIN else forms.EmailField(label="Email")
password = forms.CharField(widget=forms.PasswordInput())
persist_session = forms.BooleanField(widget=forms.CheckboxInput(), required=False)

def clean(self, *args, **kwargs):
cleaned = super(LoginForm, self).clean()
email = cleaned.get('email', '').lower()
password = cleaned.get('password', '')
if len(email) >= EMAIL_MAX_LENGTH:
raise forms.ValidationError('Email is too long')
email = cleaned.get("email", "").lower()
password = cleaned.get("password", "")

# advanced way for user auth
user = settings.USER_AUTH(User, email, password)
if len(email) >= EMAIL_MAX_LENGTH:
raise forms.ValidationError("Email is too long")

# regular access
if user is None:
user = auth.authenticate(email=email, password=password)
# Consider the native DB as the source of truth.
# The signup logic keeps the native DB in sync with Auth0.
# If there is no user with the given email, simply raise an error.
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
raise forms.ValidationError(INVALID_USER_ERROR)

if user and user.is_active:
persist_session = cleaned.get('persist_session', False)
return {'user': user, 'persist_session': persist_session}
else:
if not user.is_active:
raise forms.ValidationError(INVALID_USER_ERROR)

persist_session = cleaned.get("persist_session", False)
# This dict decides the attributes populated on form.cleaned_data
return {"user": user, "persist_session": persist_session, "email": email, "password": password}


class UserSignupForm(forms.Form):
email = forms.EmailField(label='Work Email', error_messages={'required': 'Invalid email'})
password = forms.CharField(widget=forms.TextInput(attrs={'type': 'password'}))
email = forms.EmailField(label="Work Email", error_messages={"required": "Invalid email"})
password = forms.CharField(widget=forms.TextInput(attrs={"type": "password"}))
allow_newsletters = forms.BooleanField(required=False)
how_find_us = forms.CharField(required=False)
elaborate = forms.CharField(required=False)

def clean_password(self):
password = self.cleaned_data.get('password')
password = self.cleaned_data.get("password")
try:
validate_password(password)
except DjangoValidationError as e:
raise forms.ValidationError(e.messages)
return password

def clean_username(self):
username = self.cleaned_data.get('username')
username = self.cleaned_data.get("username")
if username and User.objects.filter(username=username.lower()).exists():
raise forms.ValidationError('User with username already exists')
raise forms.ValidationError("User with username already exists")
return username

def clean_email(self):
email = self.cleaned_data.get('email').lower()
email = self.cleaned_data.get("email").lower()
if len(email) >= EMAIL_MAX_LENGTH:
raise forms.ValidationError('Email is too long')
raise forms.ValidationError("Email is too long")

if email and User.objects.filter(email=email).exists():
raise forms.ValidationError('User with this email already exists')
raise forms.ValidationError("User with this email already exists")

return email

def save(self):
print("save function")
cleaned = self.cleaned_data
password = cleaned['password']
email = cleaned['email'].lower()
print(f"Cleaned data: {cleaned}")
email = cleaned["email"].lower()
allow_newsletters = None
how_find_us = None
if 'allow_newsletters' in cleaned:
allow_newsletters = cleaned['allow_newsletters']
if 'how_find_us' in cleaned:
how_find_us = cleaned['how_find_us']
if 'elaborate' in cleaned and how_find_us == FOUND_US_ELABORATE:
cleaned['elaborate']

user = User.objects.create_user(email, password, allow_newsletters=allow_newsletters)
if "allow_newsletters" in cleaned:
allow_newsletters = cleaned["allow_newsletters"]
if "how_find_us" in cleaned:
how_find_us = cleaned["how_find_us"]
if "elaborate" in cleaned and how_find_us == FOUND_US_ELABORATE:
cleaned["elaborate"]

user = User.objects.create(email=email, allow_newsletters=allow_newsletters)
# Mark the user as having no password
user.set_unusable_password()
return user


Expand All @@ -111,4 +117,4 @@ class UserProfileForm(forms.ModelForm):

class Meta:
model = User
fields = ('first_name', 'last_name', 'phone', 'allow_newsletters')
fields = ("first_name", "last_name", "phone", "allow_newsletters")
7 changes: 7 additions & 0 deletions label_studio/users/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,15 @@ def check_avatar(files):

def save_user(request, next_page, user_form):
"""Save user instance to DB"""
print("save_user 1")
user = user_form.save()
print("save_user 2")
user.username = user.email.split('@')[0]
print("save_user 3")
user.save()

print("save_user 2")

if Organization.objects.exists():
org = Organization.objects.first()
org.add_user(user)
Expand Down Expand Up @@ -88,7 +93,9 @@ def proceed_registration(request, user_form, organization_form, next_page):
"""Register a new user for POST user_signup"""
# save user to db
save_user = load_func(settings.SAVE_USER)
print("proceeding 1")
response = save_user(request, next_page, user_form)
print("proceeding 2")

return response

Expand Down
83 changes: 50 additions & 33 deletions label_studio/users/templates/users/new-ui/user_login.html
Original file line number Diff line number Diff line change
@@ -1,48 +1,65 @@
{% extends 'users/new-ui/user_base.html' %}

{% block content %}
{{ block.super }}
<script nonce="{{request.csp_nonce}}">
{{ block.super }}
<script nonce="{{request.csp_nonce}}">
gaClientIdTrackingIframe('users.login.view');
// Give time for `ls_gaclient_id` to be set
setTimeout(() => {
const ls_gaclient_id = sessionStorage.getItem('ls_gaclient_id');
__lsa('users.login.view', { ls_gaclient_id });
const ls_gaclient_id = sessionStorage.getItem('ls_gaclient_id');
__lsa('users.login.view', { ls_gaclient_id });
}, 2000);
</script>
</script>
{% endblock %}

{% block user_content %}
<div class="form-wrapper">
<div class="form-wrapper">
<h2>Log in</h2>
<form id="login-form" action="{% url 'user-login' %}?next={{ next }}" method="post">
{% csrf_token %}
<div class="input-wrapper">
<label>Email Address</label>
<input type="text" class="lsf-input-ls" name="email" id="email" value="{{ form.data.email }}">
</div>
<div class="input-wrapper">
<label>Password</label>
<input type="password" class="lsf-input-ls" name="password" id="password">
</div>
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="error">
{{ error }}
</p>
{% endfor %}
{% endif %}
<div class="form-group">
<input type="checkbox" id="persist_session" name="persist_session" class="lsf-checkbox-ls" checked="checked" style="width: auto;" />
<label for="persist_session">Keep me logged in this browser</label>
</div>
<button type="submit" aria-label="Log In" class="login-button">Log in</button>
{% csrf_token %}
<div class="input-wrapper">
<label>Email Address</label>
<input type="text" class="lsf-input-ls" name="email" id="email" value="{{ form.data.email }}">
</div>
<div class="input-wrapper">
<label>Password</label>
<input type="password" class="lsf-input-ls" name="password" id="password">
</div>
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="error"> {{ error }} </p>
{% endfor %}
{% endif %}
{% if error_msg %}
<p class="error"> {{ error_msg }} </p>
{% endif %}
<div class="form-group">
<input type="checkbox" id="persist_session" name="persist_session" class="lsf-checkbox-ls" checked="checked"
style="width: auto;" />
<label for="persist_session">Keep me logged in this browser</label>
</div>
<button type="submit" aria-label="Log In" class="login-button">Log in</button>
<button type="button" aria-label="sign in with google" class="login-button" onclick="handleGoogleSignIn()">
Sign in with Google
</button>
</form>
</div>
{% if not settings.DISABLE_SIGNUP_WITHOUT_LINK %}
<div class="text-wrapper">
</div>
{% if not settings.DISABLE_SIGNUP_WITHOUT_LINK %}
<div class="text-wrapper">
<p class="">Don't have an account?</p>
<a href="{% url 'user-signup' %}{% querystring %}">Sign up</a>
</div>
{% endif %}
{% endblock %}
</div>
{% endif %}
<script>
async function handleGoogleSignIn() {
const query = new URLSearchParams({
response_type: "code",
connection: "google-oauth2",
client_id: "{{ client_id }}",
redirect_uri: "{{ redirect_uri }}",
scope: "openid profile email"
});
location.href = `https://{{ auth0_domain }}/authorize?${query}`
}
</script>
{% endblock %}
20 changes: 18 additions & 2 deletions label_studio/users/templates/users/new-ui/user_signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,19 @@ <h2>Sign Up</h2>
</label>
</div>

{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
{% if user_form.non_field_errors %}
{% for error in user_form.non_field_errors %}
<p class="error">
{{ error }}
</p>
{% endfor %}
{% endif %}
<p><button type="submit" aria-label="Create Account" class="lsf-button-ls lsf-button-ls_look_primary">Create Account</button></p>
<p>
<button type="button" aria-label="sign in with google" class="lsf-button-ls lsf-button-ls_look_primary" onclick="handleGoogleSignIn()">
Sign in with Google
</button>
</p>
</form>
</div>
<div class="text-wrapper">
Expand All @@ -88,5 +93,16 @@ <h2>Sign Up</h2>
const isOther = e.target.value == '{{ elaborate }}';
document.querySelector("#elaborateContainer").style.display = isOther ? 'block' : 'none';
});

async function handleGoogleSignIn() {
const query = new URLSearchParams({
response_type: "code",
connection: "google-oauth2",
client_id: "{{ client_id }}",
redirect_uri: "{{ redirect_uri }}",
scope: "openid profile email"
});
location.href = `https://{{ auth0_domain }}/authorize?${query}`
}
</script>
{% endblock %}
1 change: 1 addition & 0 deletions label_studio/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# Authentication
path('user/login/', views.user_login, name='user-login'),
path('user/signup/', views.user_signup, name='user-signup'),
path('oauth/google/callback', views.google_callback_handler, name='google-oauth'),
path('user/account/', views.user_account, name='user-account'),
path('user/account/<sub_path>', views.user_account, name='user-account-anything'),
re_path(r'^logout/?$', views.logout, name='logout'),
Expand Down
Loading
Loading