diff --git a/boofilsic/settings.py b/boofilsic/settings.py index 32318f24..9457f6c0 100644 --- a/boofilsic/settings.py +++ b/boofilsic/settings.py @@ -91,6 +91,7 @@ SETUP_ADMIN_USERNAMES = [ u for u in os.environ.get("NEODB_ADMIN_USERNAMES", "").split(",") if u ] +INVITE_ONLY = os.environ.get("NEODB_INVITE_ONLY", "") != "" # Mastodon/Pleroma instance allowed to login, keep empty to allow any instance to login MASTODON_ALLOWED_SITES = [] diff --git a/catalog/views.py b/catalog/views.py index 301f6d86..480d86fc 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -7,6 +7,7 @@ from django.core.paginator import Paginator from django.db.models import Count from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.http import require_http_methods @@ -272,6 +273,8 @@ def discover(request): # gallery["items"] = Item.objects.filter(id__in=ids) if request.user.is_authenticated: + if not request.user.registration_complete: + return redirect(reverse("users:register")) layout = request.user.preference.discover_layout identity = request.user.identity podcast_ids = [ diff --git a/common/views.py b/common/views.py index 1502b9f6..ebbc5add 100644 --- a/common/views.py +++ b/common/views.py @@ -11,11 +11,15 @@ from users.models import User @login_required def me(request): + if not request.user.registration_complete: + return redirect(reverse("users:register")) return redirect(request.user.identity.url) def home(request): if request.user.is_authenticated: + if not request.user.registration_complete: + return redirect(reverse("users:register")) home = request.user.preference.classic_homepage if home == 1: return redirect(request.user.url) diff --git a/docker-compose.yml b/docker-compose.yml index 8955509f..5175dcbd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,7 @@ x-shared: NEODB_DEBUG: NEODB_SECRET_KEY: NEODB_ADMIN_USERNAMES: + NEODB_INVITE_ONLY: NEODB_DB_NAME: neodb NEODB_DB_USER: neodb NEODB_DB_PASSWORD: aubergine diff --git a/social/views.py b/social/views.py index 757cb2f4..22bfd505 100644 --- a/social/views.py +++ b/social/views.py @@ -2,7 +2,8 @@ import logging from django.contrib.auth.decorators import login_required from django.core.exceptions import BadRequest -from django.shortcuts import render +from django.shortcuts import redirect, render +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from catalog.models import * @@ -19,6 +20,8 @@ PAGE_SIZE = 10 def feed(request): if request.method != "GET": raise BadRequest() + if not request.user.registration_complete: + return redirect(reverse("users:register")) user = request.user podcast_ids = [ p.item_id diff --git a/takahe/models.py b/takahe/models.py index e16e7df5..43cb733b 100644 --- a/takahe/models.py +++ b/takahe/models.py @@ -1,5 +1,6 @@ import datetime import os +import random import re import secrets import ssl @@ -137,6 +138,51 @@ class RsaKeys: return private_key_serialized, public_key_serialized +class Invite(models.Model): + """ + An invite token, good for one signup. + """ + + class Meta: + # managed = False + db_table = "users_invite" + + # Should always be lowercase + token = models.CharField(max_length=500, unique=True) + + # Admin note about this code + note = models.TextField(null=True, blank=True) + + # Uses remaining (null means "infinite") + uses = models.IntegerField(null=True, blank=True) + + # Expiry date + expires = models.DateTimeField(null=True, blank=True) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + @classmethod + def create_random(cls, uses=None, expires=None, note=None): + return cls.objects.create( + token="".join( + random.choice("abcdefghkmnpqrstuvwxyz23456789") for i in range(20) + ), + uses=uses, + expires=expires, + note=note, + ) + + @property + def valid(self): + if self.uses is not None: + if self.uses <= 0: + return False + if self.expires is not None: + return self.expires >= timezone.now() + return True + + class User(AbstractBaseUser): identities: "RelatedManager[Identity]" diff --git a/takahe/utils.py b/takahe/utils.py index ca66644f..34931d02 100644 --- a/takahe/utils.py +++ b/takahe/utils.py @@ -592,3 +592,10 @@ class Takahe: ) cache.set(cache_key, peers, timeout=1800) return peers + + @staticmethod + def verify_invite(token): + if not token: + return False + invite = Invite.objects.filter(token=token).first() + return invite and invite.valid diff --git a/users/account.py b/users/account.py index 195712eb..f3bdf2b9 100644 --- a/users/account.py +++ b/users/account.py @@ -25,6 +25,7 @@ from journal.models import remove_data_by_user from mastodon import mastodon_request_included from mastodon.api import * from mastodon.api import verify_account +from takahe.utils import Takahe from .models import Preference, User from .tasks import * @@ -49,7 +50,13 @@ def login(request): # store redirect url in the cookie 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", @@ -58,6 +65,7 @@ def login(request): "scope": quote(settings.MASTODON_CLIENT_SCOPE), "selected_site": selected_site, "allow_any_site": settings.MASTODON_ALLOW_ANY_SITE, + "invite_status": invite_status, }, ) else: @@ -188,6 +196,18 @@ def OAuth2_login(request): def register_new_user(request, **param): + if settings.INVITE_ONLY: + if not Takahe.verify_invite(request.session.get("invite")): + return render( + request, + "common/error.html", + { + "msg": _("注册失败😫"), + "secondary_msg": _("本站仅限邀请注册"), + }, + ) + else: + del request.session["invite"] new_user = User.register(**param) request.session["new_user"] = True auth_login(request, new_user) diff --git a/users/models/user.py b/users/models/user.py index e8121d74..a273fc82 100644 --- a/users/models/user.py +++ b/users/models/user.py @@ -203,6 +203,10 @@ class User(AbstractUser): def __str__(self): return f'{self.pk}:{self.username or ""}:{self.mastodon_acct}' + @property + def registration_complete(self): + return self.username is not None + def clear(self): if self.mastodon_site == "removed" and not self.is_active: return diff --git a/users/templates/users/login.html b/users/templates/users/login.html index 8291eeac..6c12f3f0 100644 --- a/users/templates/users/login.html +++ b/users/templates/users/login.html @@ -132,13 +132,24 @@ }); {% else %} - {% for site in sites %} {% endfor %} {% endif %} + {% if invite_status %} + + {% if invite_status == 1 %} + 邀请链接有效,可注册新用户 + {% elif invite_status == -1 %} + 本站目前为邀请注册,已有账户可直接登入,新用户请使用有效邀请链接注册 + {% elif invite_status == -2 %} + 邀请链接无效,已有账户可直接登入,新用户请使用有效邀请链接注册 + {% endif %} + + {% endif %} {% endif %}
部分模块加载超时,请检查网络(翻墙)设置。