mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-05-19 04:11:00 -04:00
feat: Integrate django-invitations for user invitation management and update settings
This commit is contained in:
@@ -44,6 +44,7 @@ ALLOWED_HOSTS = ['*'] # In production, restrict to known hosts.
|
||||
# Installed Apps
|
||||
# ---------------------------------------------------------------------------
|
||||
INSTALLED_APPS = (
|
||||
"allauth_ui",
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
@@ -51,7 +52,6 @@ INSTALLED_APPS = (
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
# "allauth_ui",
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'allauth',
|
||||
@@ -61,6 +61,7 @@ INSTALLED_APPS = (
|
||||
'allauth.socialaccount',
|
||||
'allauth.socialaccount.providers.github',
|
||||
'allauth.socialaccount.providers.openid_connect',
|
||||
'invitations',
|
||||
'drf_yasg',
|
||||
'djmoney',
|
||||
'corsheaders',
|
||||
@@ -70,8 +71,8 @@ INSTALLED_APPS = (
|
||||
'integrations',
|
||||
'django.contrib.gis',
|
||||
# 'achievements', # Not done yet, will be added later in a future update
|
||||
# 'widget_tweaks',
|
||||
# 'slippers',
|
||||
'widget_tweaks',
|
||||
'slippers',
|
||||
|
||||
)
|
||||
|
||||
@@ -220,6 +221,8 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
ALLAUTH_UI_THEME = "dim"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Authentication & Accounts
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -230,6 +233,9 @@ SOCIALACCOUNT_ALLOW_SIGNUP = getenv('SOCIALACCOUNT_ALLOW_SIGNUP', 'false').lower
|
||||
|
||||
AUTH_USER_MODEL = 'users.CustomUser'
|
||||
ACCOUNT_ADAPTER = 'users.adapters.CustomAccountAdapter'
|
||||
INVITATIONS_ADAPTER = ACCOUNT_ADAPTER
|
||||
INVITATIONS_ACCEPT_INVITE_AFTER_SIGNUP = True
|
||||
INVITATIONS_EMAIL_SUBJECT_PREFIX = 'AdventureLog: '
|
||||
SOCIALACCOUNT_ADAPTER = 'users.adapters.CustomSocialAccountAdapter'
|
||||
ACCOUNT_SIGNUP_FORM_CLASS = 'users.form_overrides.CustomSignupForm'
|
||||
|
||||
@@ -237,6 +243,8 @@ SESSION_SAVE_EVERY_REQUEST = True
|
||||
LOGIN_REDIRECT_URL = FRONTEND_URL # Redirect to frontend after login
|
||||
|
||||
SOCIALACCOUNT_LOGIN_ON_GET = True
|
||||
INVITATIONS_INVITE_FORM = 'users.form_overrides.UseAdminInviteForm'
|
||||
INVITATIONS_SIGNUP_REDIRECT_URL = f"{FRONTEND_URL}/signup"
|
||||
|
||||
HEADLESS_FRONTEND_URLS = {
|
||||
"account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}",
|
||||
|
||||
@@ -33,6 +33,8 @@ urlpatterns = [
|
||||
|
||||
path('csrf/', get_csrf_token, name='get_csrf_token'),
|
||||
path('public-url/', get_public_url, name='get_public_url'),
|
||||
|
||||
path("invitations/", include('invitations.urls', namespace='invitations')),
|
||||
|
||||
path('', TemplateView.as_view(template_name='home.html')),
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ Django==5.2.8
|
||||
djangorestframework>=3.15.2
|
||||
django-allauth==0.63.3
|
||||
django-money==3.5.4
|
||||
django-invitations==2.1.0
|
||||
drf-yasg==1.21.4
|
||||
django-cors-headers==4.4.0
|
||||
coreapi==2.3.3
|
||||
@@ -15,7 +16,7 @@ setuptools==79.0.1
|
||||
gunicorn==23.0.0
|
||||
qrcode==8.0
|
||||
slippers==0.6.2
|
||||
django-allauth-ui==1.5.1
|
||||
django-allauth-ui==1.7.0
|
||||
django-widget-tweaks==1.5.0
|
||||
django-ical==1.9.2
|
||||
icalendar==6.1.0
|
||||
|
||||
BIN
backend/server/static/favicon.png
Normal file
BIN
backend/server/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
3
backend/server/templates/admin/base.html
Normal file
3
backend/server/templates/admin/base.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{% extends "admin/base.html" %} {% load static %} {% block extrahead %}
|
||||
<link rel="icon" href="{% static 'favicon.png' %}" />
|
||||
{% endblock %}
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="AdventureLog API Server" />
|
||||
<link rel="icon" href="/static/favicon.png" />
|
||||
<meta name="author" content="Sean Morley" />
|
||||
<title>AdventureLog API Server</title>
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{% load i18n %} {% autoescape off %} {% blocktrans %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>You're Invited to AdventureLog!</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: #f0f4f8;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
|
||||
padding: 40px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
img.logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #111827;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
a.button {
|
||||
display: inline-block;
|
||||
padding: 16px 32px;
|
||||
background: linear-gradient(90deg, #4f46e5, #6366f1);
|
||||
color: #fff !important;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
a.button:hover {
|
||||
background: linear-gradient(90deg, #4338ca, #4f46e5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
}
|
||||
.adventure {
|
||||
font-weight: bold;
|
||||
color: #4f46e5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img
|
||||
src="https://adventurelog.app/adventurelog.png"
|
||||
alt="AdventureLog Logo"
|
||||
class="logo"
|
||||
/>
|
||||
<h1>You're Invited to AdventureLog!</h1>
|
||||
<p>Hello <strong>{{ email }}</strong>,</p>
|
||||
<p>
|
||||
Adventure awaits! You've been invited to join
|
||||
<span class="adventure">AdventureLog</span>, the ultimate travel
|
||||
companion to track, plan, and collaborate on your journeys.
|
||||
</p>
|
||||
<p>Hit the button below to accept your invitation and start exploring!</p>
|
||||
<p><a href="{{ invite_url }}" class="button">Join AdventureLog</a></p>
|
||||
<div class="footer">
|
||||
If you weren't expecting this email, no worries—you can safely ignore
|
||||
it.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{% endblocktrans %} {% endautoescape %}
|
||||
23
backend/server/templates/invitations/forms/_invite.html
Normal file
23
backend/server/templates/invitations/forms/_invite.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{# templates/invitations/invite.html #} {% load i18n %}
|
||||
|
||||
<h4 class="title">{% trans "Send Invitation" %}</h4>
|
||||
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans %}To send an invitation, please go to the Django admin and
|
||||
create a new Invitation object.{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a
|
||||
href="{% url 'admin:invitations_invitation_add' %}"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{% trans "Go to Django Admin" %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
<p class="text-muted small">
|
||||
{% blocktrans %}Only staff users can send invitations.{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
39
backend/server/templates/mfa/authenticate.html
Normal file
39
backend/server/templates/mfa/authenticate.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "mfa/authenticate.html" %}
|
||||
{% load allauth %}
|
||||
{% load allauth_ui %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
{% trans "Two-Factor Authentication" as heading %}
|
||||
{% blocktranslate asvar subheading %}Your account is protected by two-factor authentication. Please enter an authenticator code:{% endblocktranslate %}
|
||||
{% url 'mfa_authenticate' as action_url %}
|
||||
{% #container heading=heading subheading=subheading %}
|
||||
{% translate "Activate" as button_text %}
|
||||
{% trans "Sign In" as button_text %}
|
||||
{% #form form=form url=action_url button_text=button_text %}
|
||||
{% csrf_token %}
|
||||
{% /form %}
|
||||
{% if "webauthn" in MFA_SUPPORTED_TYPES %}
|
||||
<div class="divider"></div>
|
||||
<h2 class="my-3 text-lg">{% translate "Alternative options" %}</h2>
|
||||
{% #form form=webauthn_form url=action_url use_default_button="false" %}
|
||||
<button type="submit" class="btn btn-neutral">
|
||||
{% trans "Use a security key" %}
|
||||
</button>
|
||||
<a href="{% url "account_login" %}" class="btn btn-accent">{% trans "Cancel" %}</a>
|
||||
{% csrf_token %}
|
||||
{% /form %}
|
||||
{% endif %}
|
||||
{% /container %}
|
||||
{{ js_data|json_script:"js_data" }}
|
||||
{# djlint:off #}
|
||||
<script type="text/javascript">
|
||||
allauth.webauthn.forms.authenticateForm({
|
||||
ids: {
|
||||
authenticate: "mfa_webauthn_authenticate",
|
||||
credential: "{{ webauthn_form.credential.auto_id }}"
|
||||
},
|
||||
data: JSON.parse(document.getElementById('js_data').textContent)
|
||||
})
|
||||
</script>
|
||||
{# djlint:on #}
|
||||
{% endblock content %}
|
||||
@@ -2,18 +2,48 @@
|
||||
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from allauth.account.signals import user_signed_up
|
||||
from django.conf import settings
|
||||
from django.urls import resolve, Resolver404
|
||||
from invitations.models import Invitation
|
||||
|
||||
|
||||
class CustomAccountAdapter(DefaultAccountAdapter):
|
||||
"""Control regular signup based on DISABLE_REGISTRATION setting"""
|
||||
"""Control regular signup based on DISABLE_REGISTRATION, but allow invites."""
|
||||
|
||||
def is_open_for_signup(self, request):
|
||||
"""
|
||||
Determines if regular signup is allowed.
|
||||
Check DISABLE_REGISTRATION env variable.
|
||||
Allow signup only if:
|
||||
- DISABLE_REGISTRATION is False, OR
|
||||
- the request is for the invitation acceptance URL, OR
|
||||
- there's a valid invitation key in the request parameters.
|
||||
"""
|
||||
return settings.DISABLE_REGISTRATION is False
|
||||
# If registration is globally open, allow as usual
|
||||
if settings.DISABLE_REGISTRATION is False:
|
||||
return True
|
||||
|
||||
# If an invitation-verified email is stashed in the session, allow signup
|
||||
if hasattr(request, "session") and request.session.get("account_verified_email"):
|
||||
return True
|
||||
|
||||
# When disabled, allow signups via invitation accept URL
|
||||
try:
|
||||
match = resolve(request.path_info)
|
||||
print("Resolved view name:", match.view_name)
|
||||
if match.view_name == "invitations:accept-invite":
|
||||
return True
|
||||
except Resolver404:
|
||||
pass
|
||||
|
||||
|
||||
# Block any other signup
|
||||
return False
|
||||
|
||||
def get_user_signed_up_signal(self):
|
||||
"""Return the allauth `user_signed_up` signal for compatibility with
|
||||
django-invitations which expects this method on the adapter.
|
||||
"""
|
||||
return user_signed_up
|
||||
|
||||
|
||||
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
|
||||
|
||||
@@ -14,4 +14,17 @@ class CustomSignupForm(forms.Form):
|
||||
|
||||
# Save the user instance
|
||||
user.save()
|
||||
return user
|
||||
return user
|
||||
|
||||
class UseAdminInviteForm(forms.Form):
|
||||
"""
|
||||
Dummy form that just tells admins to use the Django admin to send invites.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Remove any fields; we only want to show a message
|
||||
self.fields.clear()
|
||||
|
||||
def as_widget(self):
|
||||
# This is not needed; we’ll just use a template
|
||||
pass
|
||||
Reference in New Issue
Block a user