lib.itmens/users/account.py

510 lines
17 KiB
Python
Raw Normal View History

from datetime import timedelta
from urllib.parse import quote
import django_rq
2023-07-04 17:21:17 -04:00
from django import forms
from django.conf import settings
from django.contrib import auth, messages
from django.contrib.auth import authenticate
from django.contrib.auth.decorators import login_required
from django.core.exceptions import BadRequest, ObjectDoesNotExist
2023-07-04 17:21:17 -04:00
from django.core.validators import EmailValidator
2024-07-01 17:29:38 -04:00
from django.db import transaction
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils import timezone
2024-06-07 22:29:10 -04:00
from django.utils.translation import gettext as _
2023-12-25 17:27:31 -05:00
from django.views.decorators.http import require_http_methods
from loguru import logger
from common.config import *
2023-08-13 23:11:12 -04:00
from common.utils import AuthedHttpRequest
from journal.models import remove_data_by_user
2024-07-01 17:29:38 -04:00
from mastodon.models import Email, Mastodon
from mastodon.models.common import Platform, SocialAccount
from mastodon.models.email import EmailAccount
2023-09-03 20:11:46 +00:00
from takahe.utils import Takahe
2024-06-07 22:29:10 -04:00
from .models import User
from .tasks import *
2024-04-23 23:57:49 -04:00
2024-07-01 17:29:38 -04:00
@require_http_methods(["GET"])
def login(request):
2024-07-01 17:29:38 -04:00
selected_domain = request.GET.get("domain", default="")
sites = Mastodon.get_sites()
2024-04-23 23:57:49 -04:00
if request.GET.get("next"):
request.session["next_url"] = request.GET.get("next")
invite_status = -1 if settings.INVITE_ONLY else 0
if settings.INVITE_ONLY and request.GET.get("invite"):
if Takahe.verify_invite(request.GET.get("invite")):
invite_status = 1
request.session["invite"] = request.GET.get("invite")
else:
invite_status = -2
return render(
request,
"users/login.html",
{
"sites": sites,
"scope": quote(settings.MASTODON_CLIENT_SCOPE),
2024-07-01 17:29:38 -04:00
"selected_domain": selected_domain,
2024-04-23 23:57:49 -04:00
"allow_any_site": settings.MASTODON_ALLOW_ANY_SITE,
2024-07-01 17:29:38 -04:00
"enable_email": settings.ENABLE_LOGIN_EMAIL,
"enable_threads": settings.ENABLE_LOGIN_THREADS,
"enable_bluesky": settings.ENABLE_LOGIN_BLUESKY,
2024-04-23 23:57:49 -04:00
"invite_status": invite_status,
},
)
2023-07-04 17:21:17 -04:00
# connect will send verification email or redirect to mastodon server
2024-07-01 17:29:38 -04:00
@require_http_methods(["GET", "POST"])
def connect(request):
2023-07-04 17:21:17 -04:00
if request.method == "POST" and request.POST.get("method") == "email":
login_email = request.POST.get("email", "")
try:
EmailValidator()(login_email)
2024-04-06 00:13:50 -04:00
except Exception:
2023-07-04 17:21:17 -04:00
return render(
request,
"common/error.html",
2024-05-19 16:32:59 -04:00
{"msg": _("Invalid email address")},
2023-07-04 17:21:17 -04:00
)
2024-07-01 17:29:38 -04:00
Email.send_login_email(request, login_email, "login")
2023-07-04 17:21:17 -04:00
return render(
request,
2024-07-01 17:29:38 -04:00
"users/verify.html",
2023-07-04 17:21:17 -04:00
{
2024-05-19 16:32:59 -04:00
"msg": _("Verification"),
"secondary_msg": _(
"Verification email is being sent, please check your inbox."
),
2024-07-01 17:29:38 -04:00
"action": "login",
2023-07-04 17:21:17 -04:00
},
)
login_domain = (
request.session["swap_domain"]
if request.session.get("swap_login")
2023-07-04 17:21:17 -04:00
else (request.POST.get("domain") or request.GET.get("domain"))
)
if not login_domain:
return render(
request,
"common/error.html",
{
2024-05-19 16:32:59 -04:00
"msg": _("Missing instance domain"),
"secondary_msg": "",
},
)
login_domain = (
login_domain.strip().lower().split("//")[-1].split("/")[0].split("@")[-1]
)
2023-02-14 17:19:00 -05:00
try:
2024-07-01 17:29:38 -04:00
login_url = Mastodon.generate_auth_url(login_domain, request)
return redirect(login_url)
2023-02-14 17:19:00 -05:00
except Exception as e:
return render(
request,
"common/error.html",
{
2024-05-19 16:32:59 -04:00
"msg": _("Error connecting to instance"),
"secondary_msg": f"{login_domain} {e}",
},
)
# mastodon server redirect back to here
2023-12-25 17:27:31 -05:00
@require_http_methods(["GET"])
2023-12-25 22:38:09 -05:00
def connect_redirect_back(request):
code = request.GET.get("code")
2023-01-10 16:52:00 -05:00
if not code:
return render(
request,
"common/error.html",
2024-05-19 16:32:59 -04:00
{
"msg": _("Authentication failed"),
2024-05-25 23:38:11 -04:00
"secondary_msg": _("Invalid response from Fediverse instance."),
2024-05-19 16:32:59 -04:00
},
2023-01-10 16:52:00 -05:00
)
2023-12-25 22:38:09 -05:00
site = request.session.get("mastodon_domain")
if not site:
2023-01-10 16:52:00 -05:00
return render(
request,
"common/error.html",
2024-05-19 16:32:59 -04:00
{
"msg": _("Authentication failed"),
"secondary_msg": _("Invalid cookie data."),
},
2023-01-10 16:52:00 -05:00
)
try:
2024-07-01 17:29:38 -04:00
token, refresh_token = Mastodon.obtain_token(site, code, request)
except ObjectDoesNotExist:
2024-04-23 23:57:49 -04:00
raise BadRequest(_("Invalid instance domain"))
if not token:
2023-01-10 16:52:00 -05:00
return render(
request,
"common/error.html",
2024-05-19 16:32:59 -04:00
{
"msg": _("Authentication failed"),
2024-05-25 23:38:11 -04:00
"secondary_msg": _("Invalid token from Fediverse instance."),
2024-05-19 16:32:59 -04:00
},
2023-01-10 16:52:00 -05:00
)
2024-07-01 17:29:38 -04:00
if request.session.get("swap_login", False) and request.user.is_authenticated:
# swap login for existing user
return swap_login(request, token, site, refresh_token)
2024-07-01 17:29:38 -04:00
account = Mastodon.authenticate(site, token, refresh_token)
if not account:
return render(
request,
"common/error.html",
{
"msg": _("Authentication failed"),
"secondary_msg": _("Invalid account data from Fediverse instance."),
},
)
if account.user: # existing user
user: User | None = authenticate(request, social_account=account) # type: ignore
if not user:
2024-05-19 16:32:59 -04:00
return render(
request,
"common/error.html",
{
"msg": _("Authentication failed"),
2024-07-01 17:29:38 -04:00
"secondary_msg": _("Invalid user."),
2024-05-19 16:32:59 -04:00
},
)
2024-07-01 17:29:38 -04:00
return login_existing_user(request, user)
elif not settings.MASTODON_ALLOW_ANY_SITE: # directly create a new user
new_user = User.register(
account=account,
username=account.username,
)
2024-07-01 17:29:38 -04:00
auth_login(request, new_user)
return render(request, "users/welcome.html")
else: # check invite and ask for username
return register_new_user(request, account)
2023-07-04 17:21:17 -04:00
2024-07-01 17:29:38 -04:00
def register_new_user(request, account: SocialAccount):
2023-09-03 20:11:46 +00:00
if settings.INVITE_ONLY:
if not Takahe.verify_invite(request.session.get("invite")):
return render(
request,
"common/error.html",
{
2024-05-19 16:32:59 -04:00
"msg": _("Authentication failed"),
"secondary_msg": _("Registration is for invitation only"),
2023-09-03 20:11:46 +00:00
},
)
2024-07-01 17:29:38 -04:00
del request.session["invite"]
if request.user.is_authenticated:
auth.logout(request)
request.session["verified_account"] = account.to_dict()
if account.platform == Platform.EMAIL:
email_readyonly = True
data = {"email": account.handle}
else:
email_readyonly = False
data = {"email": ""}
form = RegistrationForm(data)
return render(
request,
"users/register.html",
{"form": form, "email_readyonly": email_readyonly},
)
2023-08-13 23:11:12 -04:00
def login_existing_user(request, existing_user):
auth_login(request, existing_user)
if not existing_user.username or not existing_user.identity:
2023-12-06 00:11:48 -05:00
response = redirect(reverse("users:register"))
2023-08-13 23:11:12 -04:00
elif request.session.get("next_url") is not None:
response = redirect(request.session.get("next_url"))
del request.session["next_url"]
else:
response = redirect(reverse("common:home"))
2023-12-10 19:13:45 -05:00
response.delete_cookie(settings.TAKAHE_SESSION_COOKIE_NAME)
2023-08-13 23:11:12 -04:00
return response
@login_required
def logout(request):
2024-04-23 23:57:49 -04:00
return auth_logout(request)
@login_required
2024-04-23 23:57:49 -04:00
@require_http_methods(["POST"])
def reconnect(request):
if request.META.get("HTTP_AUTHORIZATION"):
raise BadRequest("Only for web login")
2024-04-23 23:57:49 -04:00
request.session["swap_login"] = True
request.session["swap_domain"] = request.POST["domain"]
return connect(request)
2023-07-04 17:21:17 -04:00
class RegistrationForm(forms.ModelForm):
email = forms.EmailField(required=False)
class Meta:
model = User
fields = ["username"]
def clean_username(self):
username = self.cleaned_data.get("username")
if username and self.instance and self.instance.username:
username = self.instance.username
2023-07-08 00:44:22 -04:00
elif (
username
and User.objects.filter(username__iexact=username)
.exclude(pk=self.instance.pk if self.instance else -1)
.exists()
):
raise forms.ValidationError(_("This username is already in use."))
2023-07-04 17:21:17 -04:00
return username
def clean_email(self):
2024-07-01 17:29:38 -04:00
email = self.cleaned_data.get("email", "").strip()
2023-07-04 17:21:17 -04:00
if (
email
2024-07-01 17:29:38 -04:00
and EmailAccount.objects.filter(handle__iexact=email)
.exclude(user_id=self.instance.pk if self.instance else -1)
2023-07-04 17:21:17 -04:00
.exists()
):
raise forms.ValidationError(_("This email address is already in use."))
return email
2024-07-01 17:29:38 -04:00
@require_http_methods(["GET", "POST"])
2024-04-12 20:42:36 -04:00
def verify_code(request):
2024-07-01 17:29:38 -04:00
if request.method == "GET":
return render(request, "users/verify.html")
code = request.POST.get("code", "").strip()
2024-04-12 20:42:36 -04:00
if not code:
return render(
request,
2024-07-01 17:29:38 -04:00
"users/verify.html",
2024-04-12 20:42:36 -04:00
{
2024-05-19 16:32:59 -04:00
"error": _("Invalid verification code"),
2024-04-12 20:42:36 -04:00
},
)
2024-07-01 17:29:38 -04:00
account = Email.authenticate(request, code)
if not account:
2024-04-12 20:42:36 -04:00
return render(
request,
2024-07-01 17:29:38 -04:00
"users/verify.html",
2024-04-12 20:42:36 -04:00
{
2024-05-19 16:32:59 -04:00
"error": _("Invalid verification code"),
2024-04-12 20:42:36 -04:00
},
)
2024-07-01 17:29:38 -04:00
if request.user.is_authenticated:
# existing logged in user to verify a pending email
if request.user.email_account == account:
# same email, nothing to do
return render(request, "users/welcome.html")
if account.user and account.user != request.user:
# email used by another user
return render(
request,
"common/error.html",
{
"msg": _("Authentication failed"),
"secondary_msg": _("Email already in use"),
},
)
with transaction.atomic():
if request.user.email_account:
request.user.email_account.delete()
account.user = request.user
account.save()
if request.session.get("new_user", 0):
try:
del request.session["new_user"]
except KeyError:
pass
return render(request, "users/welcome.html")
2023-07-04 17:21:17 -04:00
else:
2024-07-01 17:29:38 -04:00
return redirect(reverse("users:info"))
if account.user:
# existing user: log back in
user = authenticate(request, social_account=account)
if user:
return login_existing_user(request, user)
else:
return render(
request,
"common/error.html",
{
"msg": _("Authentication failed"),
"secondary_msg": _("Invalid user."),
},
)
# new user: check invite and ask for username
return register_new_user(request, account)
2023-07-04 17:21:17 -04:00
2024-07-01 17:29:38 -04:00
@require_http_methods(["GET", "POST"])
2023-08-13 23:11:12 -04:00
def register(request: AuthedHttpRequest):
2024-07-01 17:29:38 -04:00
if not settings.MASTODON_ALLOW_ANY_SITE:
return render(request, "users/welcome.html")
form = RegistrationForm(
request.POST,
instance=(
2023-07-04 17:21:17 -04:00
User.objects.get(pk=request.user.pk)
if request.user.is_authenticated
else None
2024-07-01 17:29:38 -04:00
),
)
verified_account = SocialAccount.from_dict(request.session.get("verified_account"))
email_readonly = (
verified_account is not None and verified_account.platform == Platform.EMAIL
)
error = None
if request.method == "POST" and form.is_valid():
if request.user.is_authenticated:
# logged in user to change email
current_email = (
request.user.email_account.handle
if request.user.email_account
else None
)
if (
form.cleaned_data["email"]
and form.cleaned_data["email"] != current_email
):
Email.send_login_email(request, form.cleaned_data["email"], "verify")
return render(request, "users/verify.html")
else:
# new user finishes login process
if not form.cleaned_data["username"]:
error = _("Valid username required")
elif User.objects.filter(
2023-07-08 00:44:22 -04:00
username__iexact=form.cleaned_data["username"]
).exists():
2024-07-01 17:29:38 -04:00
error = _("Username in use")
else:
# create new user
new_user = User.register(
username=form.cleaned_data["username"], account=verified_account
2023-07-04 17:21:17 -04:00
)
2024-07-01 17:29:38 -04:00
auth_login(request, new_user)
if not email_readonly and form.cleaned_data["email"]:
# verify email if presented
Email.send_login_email(
request, form.cleaned_data["email"], "verify"
2023-07-04 17:21:17 -04:00
)
2024-07-01 17:29:38 -04:00
request.session["new_user"] = 1
return render(request, "users/verify.html")
return render(request, "users/welcome.html")
return render(
request,
"users/register.html",
{"form": form, "email_readonly": email_readonly, "error": error},
)
def swap_login(request, token, site, refresh_token):
del request.session["swap_login"]
del request.session["swap_domain"]
2024-07-01 17:29:38 -04:00
account = Mastodon.authenticate(site, token, refresh_token)
current_user = request.user
2024-07-01 17:29:38 -04:00
if account:
if account.user == current_user:
messages.add_message(
2024-05-19 16:32:59 -04:00
request,
messages.ERROR,
_("Unable to update login information: identical identity."),
)
2024-07-01 17:29:38 -04:00
elif account.user:
messages.add_message(
request,
messages.ERROR,
_("Unable to update login information: identity in use."),
)
else:
2024-07-01 17:29:38 -04:00
with transaction.atomic():
if current_user.mastodon:
current_user.mastodon.delete()
account.user = current_user
account.save()
current_user.mastodon_username = account.username
current_user.mastodon_id = account.account_data["id"]
current_user.mastodon_site = account.domain
current_user.mastodon_token = account.access_token
current_user.mastodon_refresh_token = account.refresh_token
current_user.mastodon_account = account.account_data
current_user.save(
update_fields=[
"username",
"mastodon_id",
"mastodon_username",
"mastodon_site",
"mastodon_token",
"mastodon_refresh_token",
"mastodon_account",
]
)
2024-07-01 17:29:38 -04:00
django_rq.get_queue("mastodon").enqueue(
refresh_mastodon_data_task, current_user.pk, token
)
messages.add_message(
request,
messages.INFO,
_("Login information updated.") + account.handle,
)
else:
2024-05-19 16:32:59 -04:00
messages.add_message(
2024-05-25 23:38:11 -04:00
request, messages.ERROR, _("Invalid account data from Fediverse instance.")
2024-05-19 16:32:59 -04:00
)
2024-07-01 17:29:38 -04:00
return redirect(reverse("users:info"))
2023-07-12 01:11:15 -04:00
def clear_preference_cache(request):
for key in list(request.session.keys()):
if key.startswith("p_"):
del request.session[key]
def auth_login(request, user):
2023-06-03 19:20:30 -04:00
auth.login(request, user, backend="mastodon.auth.OAuth2Backend")
2024-07-01 17:29:38 -04:00
request.session.pop("verified_account", None)
2023-07-12 01:11:15 -04:00
clear_preference_cache(request)
if (
user.mastodon_last_refresh < timezone.now() - timedelta(hours=1)
or user.mastodon_account == {}
):
django_rq.get_queue("mastodon").enqueue(refresh_mastodon_data_task, user.pk)
def auth_logout(request):
auth.logout(request)
2023-12-10 19:38:00 -05:00
response = redirect("/")
response.delete_cookie(settings.TAKAHE_SESSION_COOKIE_NAME)
return response
2023-07-04 17:21:17 -04:00
def clear_data_task(user_id):
user = User.objects.get(pk=user_id)
user_str = str(user)
2023-07-20 21:59:49 -04:00
if user.identity:
remove_data_by_user(user.identity)
2023-12-10 19:38:00 -05:00
Takahe.delete_identity(user.identity.pk)
2023-07-04 17:21:17 -04:00
user.clear()
logger.warning(f"User {user_str} data cleared.")
@login_required
def clear_data(request):
if request.META.get("HTTP_AUTHORIZATION"):
raise BadRequest("Only for web login")
if request.method == "POST":
v = request.POST.get("verification")
if v and (v == request.user.mastodon_acct or v == request.user.email):
2023-07-04 17:21:17 -04:00
django_rq.get_queue("mastodon").enqueue(clear_data_task, request.user.id)
2023-12-10 19:38:00 -05:00
return auth_logout(request)
else:
2024-05-19 16:32:59 -04:00
messages.add_message(request, messages.ERROR, _("Account mismatch."))
return redirect(reverse("users:data"))