Track user last login option and fixes (#1312)

This also hides the pop up notifications after 6 seconds

For review, a review and merge of #1311 on which this is built would
make this easier to review.
This commit is contained in:
daveoconnor
2024-10-04 10:23:25 -07:00
committed by GitHub
parent 4c17724f00
commit 274777a2e6
14 changed files with 96 additions and 20 deletions

View File

@@ -12,9 +12,7 @@
{% block content %}
<div class="py-0 px-3 mb-3 md:py-6 md:px-0"
x-data="{ loginMethod: localStorage.getItem('_x_boostlogin') }"
>
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">
<div class="md:pt-11 md:mt-11 w-full bg-white dark:bg-charcoal mx-auto rounded py-6 px-3">
<div class="md:w-full">
@@ -57,13 +55,9 @@
<div class="w-full md:w-1/2">
<h2 class="text-xl text-center items-center">
Or Log In with Email <span x-cloak class="text-xs bg-emerald-400 text-slate rounded px-1 ml-2" x-show='loginMethod === "\"email\""'>Last Log In</span>
Or Log In with Email <span x-cloak class="text-xs bg-emerald-400 text-slate rounded px-1 ml-2" x-show="providerMatchesLastLogin('email')">Last Log In</span>
</h2>
<form class="login"
method="POST"
action="{% url 'account_login' %}"
x-data="{ boostlogin: $persist('email') }"
>
<form class="login" method="POST" action="{% url 'account_login' %}">
<div class="mx-auto space-y-4 w-2/3" id="signup_form">
{% csrf_token %}
@@ -94,7 +88,6 @@
<div class="flex justify-between mb-4">
<a class="text-sky-600 dark:text-sky-300 hover:text-orange dark:hover:text-orange" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
<button type="submit"
x-on:click="boostlogin = 'email'"
class="py-3 px-8 text-sm text-base font-medium text-white uppercase rounded-md border md:py-1 md:px-4 md:text-lg bg-orange hover:bg-orange/80 border-orange dark:bg-slate dark:hover:bg-charcoal dark:text-white hover:drop-shadow-md">{% trans "Log in" %}</button>
</div>
</div>

View File

@@ -74,6 +74,9 @@
<script>
var r = document.querySelector(':root');
const messageVisibilitySeconds = 6;
const opacityTransitionTime = 0.15; // matches the tailwind transition-opacity time
const delay = ms => new Promise(res => setTimeout(res, ms));
function modalSize(h,w) {
var rs = getComputedStyle(r);
@@ -96,6 +99,39 @@
modal.classList.remove('show-modal');
}
const hideMessage = async () => {
const message = document.getElementById('messages');
message.classList.add('opacity-0');
await delay(opacityTransitionTime * 1000);
const messageButton = message.querySelector('button');
if (messageButton) {
messageButton.click();
}
}
const providerMatchesLastLogin = async (provider) => {
const lastLoginProvider = localStorage.getItem('boostLoginMethod') || null;
return provider === lastLoginProvider;
}
const trackLoginUpdateCheck = async () => {
const footer = document.querySelector('footer');
const indicator = footer.dataset.trackLoginMethod; // intentionally not cast to boolean
const loginMethod = footer.dataset.loginMethod;
if (indicator) {
if (indicator === 'True') {
localStorage.setItem('boostLoginMethod', loginMethod);
} else {
localStorage.removeItem('boostLoginMethod');
}
}
}
(async () => {
await trackLoginUpdateCheck();
await delay(messageVisibilitySeconds * 1000);
await hideMessage();
})();
</script>
</body>

View File

@@ -77,7 +77,7 @@
}
}
</style>
<footer class="footer">
<footer class="footer"{% if user.is_authenticated %} data-track-login-method="{{ request.user.indicate_last_login_method }}" data-login-method="{{ request.session.boost_login_method }}"{% endif %}>
<div class="footer-container">
<div class="links">
<a href="https://lists.boost.org/mailman/listinfo.cgi/boost-users">Contact</a>

View File

@@ -1,4 +1,4 @@
<div id="messages" class="w-full text-center" x-data="{show: true}">
<div id="messages" class="w-full text-center transition-opacity" x-data="{show: true}">
{% if messages %}
{% for message in messages %}
<div x-show="show" class="w-2/3 mx-auto text-left items-center text-slate dark:text-white rounded text-base px-3 py-2 {% if 'error' in message.tags %}bg-red-500{% else %}bg-green/70{% endif %} fade show">

View File

@@ -15,11 +15,12 @@
<p>{% blocktrans with provider.name as provider %}You are about to log in using a third party account from {{ provider }}.{% endblocktrans %}</p>
{% endif %}
<form method="post" x-data="{ boostlogin: $persist('{{ provider.name }}') }">
<form method="post">
{% csrf_token %}
<button type="submit"
x-on:click="boostlogin = '{{ provider.name }}'"
class="py-3 px-8 text-sm text-base font-medium text-white uppercase rounded-md border md:py-1 md:px-4 md:text-lg bg-orange hover:bg-orange/80 border-orange dark:bg-slate dark:hover:bg-charcoal dark:text-white hover:drop-shadow-md">{% trans "Continue" %}</button>
<button
type="submit"
class="py-3 px-8 text-sm text-base font-medium text-white uppercase rounded-md border md:py-1 md:px-4 md:text-lg bg-orange hover:bg-orange/80 border-orange dark:bg-slate dark:hover:bg-charcoal dark:text-white hover:drop-shadow-md"
>{% trans "Continue" %}</button>
</form>
</div>
</div>

View File

@@ -16,7 +16,7 @@
class="relative w-2/3 mx-auto block px-8 py-3 text-base font-medium rounded-md border border-orange !text-white hover:!text-white bg-orange hover:bg-orange/80 dark:bg-slate dark:hover:bg-charcoal hover:drop-shadow-md md:py-4 md:text-lg md:px-10 {{provider.id}}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}"
>
<span x-cloak class="absolute right-1 top-1 text-xs bg-white text-slate rounded p-1" x-show='loginMethod === "\"GitHub\""'>Last Log in</span>
<span x-cloak class="absolute right-1 top-1 text-xs bg-emerald-400 text-slate rounded px-1 ml-2" x-show="providerMatchesLastLogin('github')">Last Log in</span>
<i class="fab fa-github"></i>
Use {{provider.name}}
</a>
@@ -27,7 +27,7 @@
class="relative w-2/3 mx-auto block px-8 py-3 text-base font-medium rounded-md border border-orange !text-white hover:!text-white bg-orange hover:bg-orange/80 dark:bg-slate dark:hover:bg-charcoal hover:drop-shadow-md md:py-4 md:text-lg md:px-10 {{provider.id}}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}"
>
<span x-cloak class="absolute right-1 top-1 text-xs bg-white text-slate rounded p-1" x-show='loginMethod === "\"Google\""'>Last Log in</span>
<span x-cloak class="absolute right-1 top-1 text-xs bg-emerald-400 text-slate rounded px-1 ml-2" x-show="providerMatchesLastLogin('google')">Last Log in</span>
<i class="fab fa-google"></i>
Use {{provider.name}}
</a>

View File

@@ -87,6 +87,7 @@
{% endif %}
</div>
{% if not social_accounts %}
<div class="rounded bg-white dark:bg-charcoal p-4">
<h3>{% trans "Set Password" %}</h3>
<form method="POST" action="." class="password_set space-y-3">
@@ -103,13 +104,15 @@
</div>
</form>
</div>
{% else %}
<div class="rounded bg-white dark:bg-charcoal p-4">
<h3>{% trans "Account Connections" %}</h3>
<div class="mt-4">
<a href="{% url 'socialaccount_connections' %}"><button class="py-2 px-3 text-sm text-white rounded bg-orange">{% trans 'Manage Account Connections' %}</button></a>
</div>
</div>
{% endif %}
</div>
</div>
</div>

1
users/constants.py Normal file
View File

@@ -0,0 +1 @@
LOGIN_METHOD_SESSION_FIELD_NAME = "boost_login_method"

View File

@@ -79,7 +79,7 @@ class PreferencesForm(forms.ModelForm):
class UserProfileForm(forms.ModelForm):
class Meta:
model = User
fields = ["email", "first_name", "last_name"]
fields = ["email", "first_name", "last_name", "indicate_last_login_method"]
class CustomClearableFileInput(forms.ClearableFileInput):

View File

@@ -0,0 +1,21 @@
# Generated by Django 4.2.15 on 2024-10-02 18:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0012_user_can_update_image"),
]
operations = [
migrations.AddField(
model_name="user",
name="indicate_last_login_method",
field=models.BooleanField(
default=False,
help_text="Indicate on the login page the last login method used.",
),
),
]

View File

@@ -242,6 +242,10 @@ class User(BaseUser):
"a user's ability to update their own profile photo, uncheck this box."
),
)
indicate_last_login_method = models.BooleanField(
default=False,
help_text="Indicate on the login page the last login method used.",
)
def save_image_from_github(self, avatar_url):
response = requests.get(avatar_url)

View File

@@ -1,8 +1,11 @@
from allauth.account.signals import user_logged_in
from django.dispatch import receiver
from django.db.models.signals import post_save
from allauth.socialaccount.models import SocialAccount
from users.constants import LOGIN_METHOD_SESSION_FIELD_NAME
GITHUB = "github"
@@ -26,3 +29,14 @@ def import_social_profile_data(sender, instance, created, **kwargs):
if avatar_url:
instance.user.save_image_from_github(avatar_url)
@receiver(user_logged_in)
def user_logged_in_handler(request, user, **kwargs):
# We trigger this here as well as on the profile update in case there are two users
# on one machine, we need to reflag for the cookie update
try:
method = request.session["account_authentication_methods"][0].get("provider")
except (KeyError, IndexError):
method = None
request.session[LOGIN_METHOD_SESSION_FIELD_NAME] = method or "email"

View File

@@ -15,6 +15,7 @@ def user(db):
email="user@example.com",
first_name="Regular",
last_name="User",
indicate_last_login_method=False,
last_login=timezone.now(),
image=None,
)

View File

@@ -146,11 +146,13 @@ def test_user_profile_form(user):
"first_name",
"last_name",
"email",
"indicate_last_login_method",
}
assert form.initial == {
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
"indicate_last_login_method": user.indicate_last_login_method,
}
form = UserProfileForm(instance=user, data={"email": "test@example.com"})
assert form.is_valid()