set username and login via email

This commit is contained in:
Your Name 2023-07-04 17:21:17 -04:00 committed by Henri Dickson
parent 0f1ac51032
commit fbb4bbc437
42 changed files with 786 additions and 287 deletions

View file

@ -273,6 +273,9 @@ EXPORT_FILE_PATH_ROOT = "export/"
# Allow user to login via any Mastodon/Pleroma sites
MASTODON_ALLOW_ANY_SITE = False
# Allow user to create account with email (and link to Mastodon account later)
ALLOW_EMAIL_ONLY_ACCOUNT = False
# Timeout of requests to Mastodon, in seconds
MASTODON_TIMEOUT = 30

View file

@ -18,12 +18,21 @@ from django.urls import path, include
from django.conf import settings
from users.views import login
from common.api import api
from django.views.generic import RedirectView
urlpatterns = [
path("api/", api.urls), # type: ignore
path("login/", login),
path("markdownx/", include("markdownx.urls")),
path("users/", include("users.urls")),
path("account/", include("users.urls")),
path(
"users/connect/",
RedirectView.as_view(url="/account/connect", query_string=True),
),
path(
"users/OAuth2_login/",
RedirectView.as_view(url="/account/login/oauth", query_string=True),
),
path("", include("catalog.urls")),
path("", include("journal.urls")),
path("timeline/", include("social.urls")),

View file

@ -109,7 +109,7 @@ class Command(BaseCommand):
else:
self.stdout.write(f"! no season {i} : {i.absolute_url}?skipcheck=1")
if self.fix:
i.recast_to(TVShow)
i.recast_to(i.merged_to_item.__class__)
self.stdout.write(f"Checking TVSeason is child of other class...")
for i in TVSeason.objects.filter(show__isnull=False).exclude(

View file

@ -40,7 +40,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if comment.metadata.shared_link %} href="{{ comment.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if comment.metadata.shared_link %} href="{{ comment.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
</span>
<span>

View file

@ -58,7 +58,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if comment.metadata.shared_link %} href="{{ comment.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if comment.metadata.shared_link %} href="{{ comment.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
</span>
<span>

View file

@ -18,7 +18,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if review.metadata.shared_link %} href="{{ review.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if review.metadata.shared_link %} href="{{ review.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
</span>
<span>

View file

@ -60,7 +60,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if mark.comment.metadata.shared_link %} href="{{ mark.comment.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if mark.comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if mark.comment.metadata.shared_link %} href="{{ mark.comment.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if mark.comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
{% comment %} <span class="timestamp">{{ mark.comment.created_time|date }}</span> {% endcomment %}
</span>
@ -83,7 +83,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if comment.metadata.shared_link %} href="{{ comment.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if comment.metadata.shared_link %} href="{{ comment.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
{% comment %} <span class="timestamp">{{ comment.created_time|date }}</span> {% endcomment %}
</span>

View file

@ -43,7 +43,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if mark.metadata.shared_link %} href="{{ mark.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if mark.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if mark.metadata.shared_link %} href="{{ mark.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if mark.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
<span class="timestamp">{{ mark.created_time|date }}</span>
</div>

View file

@ -31,7 +31,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if review.metadata.shared_link %} href="{{ review.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if review.metadata.shared_link %} href="{{ review.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
<span>
{% liked_piece review as liked %}

View file

@ -65,10 +65,13 @@
<ul role="listbox" style="min-width:-webkit-max-content;" dir="rtl">
{% if request.user.is_authenticated %}
<li>
<a href="{% url 'users:data' %}">数据</a>
<a href="{% url 'users:data' %}">数据管理</a>
</li>
<li>
<a href="{% url 'users:preferences' %}">设置</a>
<a href="{% url 'users:preferences' %}">使用设置</a>
</li>
<li>
<a href="{% url 'users:info' %}">账号信息</a>
</li>
<li>
<a href="{% url 'users:logout' %}">登出</a>

View file

@ -15,10 +15,10 @@
<i class="fa-solid fa-puzzle-piece"></i> &nbsp; {{ site_name }}致力于提供一个涵盖书籍、影视、音乐、游戏、播客的自由开放互联的收藏评论空间。 你可以在这里记录你的收藏和想法,以及发现新的内容和朋友。
</p>
<p>
<i class="fa-solid fa-globe"></i> &nbsp; 登录{{ site_name }}需要一个联邦网络Fediverse也被称为长毛象实例账号如果你还没有账号可以<a href="https://joinmastodon.org/zh/servers" target="_blank">到这里注册</a>
<i class="fa-solid fa-globe"></i> &nbsp; 登录{{ site_name }}需要一个联邦宇宙Fediverse也被称为长毛象实例账号如果你还没有账号可以<a href="https://joinmastodon.org/zh/servers" target="_blank">到这里注册</a>
</p>
<p>
<i class="fa-solid fa-circle-question"></i> &nbsp; 如果有任何问题或建议,欢迎通过<a href="https://mastodon.social/@neodb">联邦网络</a><a href="https://discord.gg/uprvcH8gqD">Discord</a>和我们联系。
<i class="fa-solid fa-circle-question"></i> &nbsp; 如果有任何问题或建议,欢迎通过<a href="https://mastodon.social/@neodb">联邦宇宙</a><a href="https://discord.gg/uprvcH8gqD">Discord</a>和我们联系。
</p>
<p>
<i class="fa-solid fa-door-open"></i> &nbsp; <a href="{% url 'users:login' %}">点击这里</a>登录。

View file

@ -0,0 +1,29 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }}</title>
{% include "common_libs.html" with jquery=0 v2=1 %}
</head>
<body>
{% include "_header.html" %}
<main class="container">
<article class="error">
<header>
<h3>{{ msg }}</h3>
</header>
{{ secondary_msg|default:"" }}
</article>
</main>
{% include "_footer.html" %}
</body>
</html>

View file

@ -10,12 +10,12 @@ most settings resides in `settings.py`, a few notable ones:
- `SITE_INFO['site_name']` change by you need
- `CLIENT_NAME` site name shown in Mastodon app page
- `APP_WEBSITE` external root url for your side
- `REDIRECT_URIS` this should be `APP_WEBSITE + "/users/OAuth2_login/"` . It can be multiple urls separated by `\n` , but not all Fediverse software support it well. Also note changing this later may invalidate app token granted previously
- `REDIRECT_URIS` this should be `APP_WEBSITE + "/account/login/oauth"` . It can be multiple urls separated by `\n` , but not all Fediverse software support it well. Also note changing this later may invalidate app token granted previously
- `MASTODON_ALLOW_ANY_SITE` set to `True` so that user can login via any Mastodon API compatible sites (e.g. Mastodon/Pleroma)
- `MASTODON_CLIENT_SCOPE` change it later may invalidate app token granted previously
- `ADMIN_URL` admin page url, keep it private
- `SEARCH_BACKEND` should be either `TYPESENSE` or `MEILISEARCH` so that search and index can function. `None` will use default database search, which is for development only and may gets deprecated soon.
Settings for Scrapers
---------------------

View file

@ -16,7 +16,7 @@ class ReviewForm(forms.ModelForm):
title = forms.CharField(label=_("评论标题"))
body = MarkdownxFormField(label=_("评论正文 (Markdown格式可参考下方语法范例)"), strip=False)
share_to_mastodon = forms.BooleanField(
label=_("分享到联邦网络"), initial=False, required=False
label=_("分享到联邦宇宙"), initial=False, required=False
)
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
visibility = forms.TypedChoiceField(
@ -38,7 +38,7 @@ class CollectionForm(forms.ModelForm):
# id = forms.IntegerField(required=False, widget=forms.HiddenInput())
title = forms.CharField(label=_("标题"))
brief = MarkdownxFormField(label=_("介绍 (Markdown)"), strip=False)
# share_to_mastodon = forms.BooleanField(label=_("分享到联邦网络"), initial=True, required=False)
# share_to_mastodon = forms.BooleanField(label=_("分享到联邦宇宙"), initial=True, required=False)
visibility = forms.TypedChoiceField(
label=_("可见性"),
initial=0,

View file

@ -55,7 +55,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if mark.metadata.shared_link %} href="{{ mark.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if mark.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if mark.metadata.shared_link %} href="{{ mark.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if mark.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
<span class="timestamp">{{ mark.created_time|date }}</span>
</div>
@ -91,7 +91,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if mark.review.metadata.shared_link %} href="{{ mark.review.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if mark.review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if mark.review.metadata.shared_link %} href="{{ mark.review.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if mark.review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
<span class="timestamp">{{ mark.review.created_time|date }}</span>
</span>

View file

@ -44,7 +44,7 @@
hx-get="{% url 'journal:collection_share' collection.uuid %}"
hx-target="body"
hx-swap="beforeend"
title="分享到联邦网络"><i class="fa-solid fa-share-nodes"></i></a>
title="分享到联邦宇宙"><i class="fa-solid fa-share-nodes"></i></a>
</span>
{% endif %}
</div>

View file

@ -36,7 +36,7 @@
id="id_share_to_mastodon"
value="1"
{% if not request.user.preference.default_no_share %}checked{% endif %}>
分享到联邦网络
分享到联邦宇宙
</label>
</div>
<div class="mark-modal__option" style="width:max-content;">

View file

@ -66,7 +66,7 @@
<textarea name="text"
rows="5"
autofocus
placeholder="提示: 善用 &gt;!文字!&lt; 标记可隐藏剧透; 超过360字可能无法分享到联邦网络实例时间线。"
placeholder="提示: 善用 &gt;!文字!&lt; 标记可隐藏剧透; 超过360字可能无法分享到联邦宇宙实例时间线。"
id="id_text">{{ mark.comment_text|default:"" }}</textarea>
</fieldset>
</div>
@ -114,7 +114,7 @@
id="id_share_to_mastodon"
value="1"
{% if not request.user.preference.default_no_share %}checked{% endif %}>
分享到联邦网络
分享到联邦宇宙
</label>
</fieldset>
</div>

View file

@ -41,7 +41,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if review.metadata.shared_link %} href="{{ review.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if review.metadata.shared_link %} href="{{ review.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
{% if request.user == review.owner %}{% endif %}
</div>

View file

@ -37,7 +37,7 @@
<span>
<a target="_blank"
rel="noopener"
{% if collection.metadata.shared_link %} href="{{ collection.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if collection.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
{% if collection.metadata.shared_link %} href="{{ collection.metadata.shared_link }}" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if collection.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
</span>
<span class="timestamp">{{ collection.created_time|date }}</span>
</div>

View file

@ -9,7 +9,7 @@ class CollectionTest(TestCase):
def setUp(self):
self.book1 = Edition.objects.create(title="Hyperion")
self.book2 = Edition.objects.create(title="Andymion")
self.user = User.objects.create()
self.user = User.objects.create(email="a@b.com")
pass
def test_collection(self):
@ -39,7 +39,7 @@ class ShelfTest(TestCase):
pass
def test_shelf(self):
user = User.objects.create(mastodon_site="site", username="name")
user = User.objects.create(mastodon_site="site", mastodon_username="name")
shelf_manager = ShelfManager(user=user)
self.assertEqual(user.shelf_set.all().count(), 3)
book1 = Edition.objects.create(title="Hyperion")
@ -102,9 +102,13 @@ class TagTest(TestCase):
self.book1 = Edition.objects.create(title="Hyperion")
self.book2 = Edition.objects.create(title="Andymion")
self.movie1 = Edition.objects.create(title="Hyperion, The Movie")
self.user1 = User.objects.create(mastodon_site="site", username="name")
self.user2 = User.objects.create(mastodon_site="site2", username="name2")
self.user3 = User.objects.create(mastodon_site="site2", username="name3")
self.user1 = User.objects.create(mastodon_site="site", mastodon_username="name")
self.user2 = User.objects.create(
mastodon_site="site2", mastodon_username="name2"
)
self.user3 = User.objects.create(
mastodon_site="site2", mastodon_username="name3"
)
pass
def test_user_tag(self):
@ -120,7 +124,7 @@ class TagTest(TestCase):
class MarkTest(TestCase):
def setUp(self):
self.book1 = Edition.objects.create(title="Hyperion")
self.user1 = User.objects.create(mastodon_site="site", username="name")
self.user1 = User.objects.create(mastodon_site="site", mastodon_username="name")
pref = self.user1.get_preference()
pref.default_visibility = 2
pref.save()

View file

@ -127,9 +127,9 @@ def render_relogin(request):
"common/error.html",
{
"url": reverse("users:connect") + "?domain=" + request.user.mastodon_site,
"msg": _("信息已保存,但是未能分享到联邦网络"),
"msg": _("信息已保存,但是未能分享到联邦宇宙"),
"secondary_msg": _(
"可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼"
"可能是你在联邦宇宙(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦宇宙重新登录😼"
),
},
)

View file

@ -53,7 +53,7 @@ class MastodonApplicationModelAdmin(admin.ModelAdmin):
try:
response = create_app(request.POST.get("domain_name"))
except (Timeout, ConnectionError):
request.POST["domain_name"] = _("联邦网络请求超时。")
request.POST["domain_name"] = _("联邦宇宙请求超时。")
except Exception as e:
request.POST["domain_name"] = str(e)
else:

View file

@ -301,7 +301,7 @@ def get_mastodon_application(login_domain):
def get_mastodon_login_url(app, login_domain, request):
url = request.scheme + "://" + request.get_host() + reverse("users:OAuth2_login")
url = request.scheme + "://" + request.get_host() + "/users/OAuth2_login/"
if login_domain == TWITTER_DOMAIN:
return f"https://twitter.com/i/oauth2/authorize?response_type=code&client_id={app.client_id}&redirect_uri={quote(url)}&scope={quote(settings.TWITTER_CLIENT_SCOPE)}&state=state&code_challenge=challenge&code_challenge_method=plain"
version = app.server_version or ""
@ -326,9 +326,7 @@ def get_mastodon_login_url(app, login_domain, request):
def obtain_token(site, request, code):
"""Returns token if success else None."""
mast_app = MastodonApplication.objects.get(domain_name=site)
redirect_uri = (
request.scheme + "://" + request.get_host() + reverse("users:OAuth2_login")
)
redirect_uri = request.scheme + "://" + request.get_host() + "/users/OAuth2_login/"
payload = {
"client_id": mast_app.client_id,
"client_secret": mast_app.client_secret,

View file

@ -14,7 +14,7 @@ def mastodon_request_included(func):
return func(*args, **kwargs)
except (Timeout, ConnectionError):
return render(
args[0], "common/error.html", {"msg": _("联邦网络请求超时叻_(´ཀ`」 ∠)__ ")}
args[0], "common/error.html", {"msg": _("联邦宇宙请求超时叻_(´ཀ`」 ∠)__ ")}
)
return wrapper

View file

@ -21,6 +21,7 @@ django-tz-detect
django-bleach
django-redis
django-oauth-toolkit
django-anymail
easy-thumbnails
lxml
openpyxl

View file

@ -53,7 +53,7 @@
{% endif %}
</span>
<span>
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
</span>
</span>
<div class="spacing">

View file

@ -17,7 +17,7 @@
<a href="{{ activity.action_object.metadata.shared_link }}"
target="_blank"
rel="noopener"
title="打开联邦网络分享链接"><i class="fa-solid fa-circle-nodes"></i></a>
title="打开联邦宇宙分享链接"><i class="fa-solid fa-circle-nodes"></i></a>
</span>
{% endif %}
<span>

View file

@ -40,7 +40,7 @@
{% endif %}
</span>
<span>
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
</span>
</span>
<div class="spacing">

View file

@ -33,7 +33,7 @@
{% endif %}
</span>
<span>
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦宇宙分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
</span>
</span>
<div class="spacing">

View file

@ -22,6 +22,11 @@ from journal.models import remove_data_by_user
from django.db.models import Q
from django.core.cache import cache
from django.db.models import Count
from django import forms
from django.core.signing import TimestampSigner
from django.core.mail import send_mail
from loguru import logger
from django.core.validators import EmailValidator
# the 'login' page that user can see
@ -58,12 +63,37 @@ def login(request):
raise BadRequest()
# connect will redirect to mastodon server
# connect will send verification email or redirect to mastodon server
def connect(request):
if request.method == "POST" and request.POST.get("method") == "email":
login_email = request.POST.get("email", "")
try:
EmailValidator()(login_email)
except:
return render(
request,
"common/error.html",
{"msg": _("无效的电子邮件地址")},
)
user = User.objects.filter(email=login_email).first()
django_rq.get_queue("mastodon").enqueue(
send_verification_link,
user.pk if user else 0,
"login" if user else "register",
login_email,
)
return render(
request,
"common/info.html",
{
"msg": _("验证邮件已发送"),
"secondary_msg": _("请查阅收件箱"),
},
)
login_domain = (
request.session["swap_domain"]
if request.session.get("swap_login")
else request.GET.get("domain")
else (request.POST.get("domain") or request.GET.get("domain"))
)
if not login_domain:
return render(
@ -147,8 +177,9 @@ def OAuth2_login(request):
else: # newly registered user
code, user_data = verify_account(site, token)
if code != 200 or user_data is None:
return render(request, "common/error.html", {"msg": _("联邦网络访问失败😫")})
new_user = User(
return render(request, "common/error.html", {"msg": _("联邦宇宙访问失败😫")})
return register_new_user(
request,
username=None,
mastodon_username=user_data["username"],
mastodon_id=user_data["id"],
@ -157,11 +188,15 @@ def OAuth2_login(request):
mastodon_refresh_token=refresh_token,
mastodon_account=user_data,
)
new_user.save()
Preference.objects.create(user=new_user)
request.session["new_user"] = True
auth_login(request, new_user)
return redirect(reverse("users:register"))
def register_new_user(request, **param):
new_user = User(**param)
new_user.save()
Preference.objects.create(user=new_user)
request.session["new_user"] = True
auth_login(request, new_user)
return redirect(reverse("users:register"))
@mastodon_request_included
@ -188,13 +223,166 @@ def reconnect(request):
raise BadRequest()
@mastodon_request_included
def register(request):
if request.session.get("new_user"):
del request.session["new_user"]
return render(request, "users/register.html")
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
return username
def clean_email(self):
email = self.cleaned_data.get("email")
if (
email
and User.objects.filter(email=email)
.exclude(pk=self.instance.pk if self.instance else -1)
.exists()
):
raise forms.ValidationError(_("This email address is already in use."))
return email
def send_verification_link(user_id, action, email):
s = {"i": user_id, "e": email, "a": action}
v = TimestampSigner().sign_object(s) # type: ignore
site = settings.SITE_INFO["site_name"]
if action == "verify":
subject = f'{settings.SITE_INFO["site_name"]} - {_("验证电子邮件地址")}'
url = settings.SITE_INFO["site_url"] + "/account/verify_email?c=" + v
msg = f"你好,\n请点击以下链接验证你的电子邮件地址 {email}\n{url}\n\n如果你没有注册过本站,请忽略此邮件。"
elif action == "login":
subject = f'{settings.SITE_INFO["site_name"]} - {_("登录")}'
url = settings.SITE_INFO["site_url"] + "/account/login/email?c=" + v
msg = f"你好,\n请点击以下链接登录{email}账号\n{url}\n\n如果你没有请求登录本站,请忽略此邮件;如果你确信账号存在安全风险,请更改注册邮件地址或与我们联系。"
elif action == "register":
subject = f'{settings.SITE_INFO["site_name"]} - {_("注册新账号")}'
url = settings.SITE_INFO["site_url"] + "/account/register_email?c=" + v
msg = f"你好,\n{site}还没有与{email}关联的账号。你希望注册一个新账号吗?\n"
msg += f"如果你已经注册过{site}或联邦宇宙(长毛象),不必重新注册,只要用联邦宇宙身份登录{site},再关联这个电子邮件地址,未来就可以通过邮件登录。\n"
msg += f"\n如果你还没有联邦宇宙身份,可以访问这里选择实例并创建一个: https://joinmastodon.org/zh/servers\n"
if settings.ALLOW_EMAIL_ONLY_ACCOUNT:
msg += f"\n如果你不便使用联邦宇宙身份,可以点击以下链接注册新的{site}账号,以后再关联到联邦宇宙。\n{url}\n"
msg += f"\n如果你没有打算用此电子邮件地址注册或登录本站,请忽略此邮件。"
else:
return redirect(reverse("common:home"))
raise ValueError("Invalid action")
try:
send_mail(
subject=subject,
message=msg,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[email],
fail_silently=False,
)
except Exception as e:
logger.error(e)
def verify_email(request):
error = ""
try:
s = TimestampSigner().unsign_object(request.GET.get("c"), max_age=60 * 15) # type: ignore
email = s["e"]
action = s["a"]
if action == "verify":
user = User.objects.get(pk=s["i"])
if user.pending_email == email:
user.email = user.pending_email
user.pending_email = None
user.save(update_fields=["email", "pending_email"])
return render(
request, "users/verify_email.html", {"success": True, "user": user}
)
else:
error = _("电子邮件地址不匹配")
elif action == "login":
user = User.objects.get(pk=s["i"])
if user.email == email:
auth_login(request, user)
return redirect(reverse("common:home"))
else:
error = _("电子邮件地址不匹配")
elif action == "register":
user = User.objects.filter(email=email).first()
if user:
error = _("此电子邮件地址已被注册")
else:
return register_new_user(request, username=None, email=email)
except Exception as e:
error = _("链接已失效")
return render(
request, "users/verify_email.html", {"success": False, "error": error}
)
@login_required
def register(request):
form = None
if settings.MASTODON_ALLOW_ANY_SITE:
form = RegistrationForm(request.POST)
form.instance = (
User.objects.get(pk=request.user.pk)
if request.user.is_authenticated
else None
)
if request.method == "GET" or not form:
return render(request, "users/register.html", {"form": form})
elif request.method == "POST":
username_changed = False
email_cleared = False
if not form.is_valid():
return render(request, "users/register.html", {"form": form})
if request.user.username is None and form.cleaned_data["username"]:
if User.objects.filter(username=form.cleaned_data["username"]).exists():
return render(
request,
"users/register.html",
{
"form": form,
"error": _("用户名已被使用"),
},
)
request.user.username = form.cleaned_data["username"]
username_changed = True
if form.cleaned_data["email"]:
if form.cleaned_data["email"] != request.user.email:
if User.objects.filter(email=form.cleaned_data["email"]).exists():
return render(
request,
"users/register.html",
{
"form": form,
"error": _("电子邮件地址已被使用"),
},
)
request.user.pending_email = form.cleaned_data["email"]
else:
request.user.pending_email = None
elif request.user.email or request.user.pending_email:
request.user.pending_email = None
request.user.email = None
email_cleared = True
request.user.save()
if request.user.pending_email:
django_rq.get_queue("mastodon").enqueue(
send_verification_link,
request.user.id,
"verify",
request.user.pending_email,
)
messages.add_message(request, messages.INFO, _("已发送验证邮件,请查收。"))
if username_changed:
messages.add_message(request, messages.INFO, _("用户名已设置。"))
if email_cleared:
messages.add_message(request, messages.INFO, _("电子邮件地址已取消关联。"))
if request.session.get("new_user"):
del request.session["new_user"]
return redirect(request.GET.get("next", reverse("common:home")))
def swap_login(request, token, site, refresh_token):
@ -244,7 +432,7 @@ def swap_login(request, token, site, refresh_token):
request, messages.INFO, _(f"账号身份已更新为 {username}@{site}")
)
else:
messages.add_message(request, messages.ERROR, _("连接联邦网络获取身份信息失败。"))
messages.add_message(request, messages.ERROR, _("连接联邦宇宙获取身份信息失败。"))
return redirect(reverse("users:data"))
@ -263,15 +451,22 @@ def auth_logout(request):
auth.logout(request)
def clear_data_task(user_id):
user = User.objects.get(pk=user_id)
user_str = str(user)
remove_data_by_user(user)
user.clear()
user.save()
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":
if request.POST.get("verification") == request.user.mastodon_acct:
remove_data_by_user(request.user)
request.user.clear()
request.user.save()
django_rq.get_queue("mastodon").enqueue(clear_data_task, request.user.id)
auth_logout(request)
return redirect(reverse("users:login"))
else:

View file

@ -62,6 +62,18 @@ def data(request):
)
@mastodon_request_included
@login_required
def account_info(request):
return render(
request,
"users/account.html",
{
"allow_any_site": settings.MASTODON_ALLOW_ANY_SITE,
},
)
@login_required
def data_import_status(request):
return render(

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.19 on 2023-07-03 18:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0006_unique_email"),
]
operations = [
migrations.AddField(
model_name="user",
name="pending_email",
field=models.EmailField(
default=None,
max_length=254,
null=True,
verbose_name="email address pending verification",
),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.19 on 2023-07-04 02:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0007_user_pending_email"),
]
operations = [
migrations.AddConstraint(
model_name="user",
constraint=models.CheckConstraint(
check=models.Q(
("is_active", False),
("mastodon_username__isnull", False),
("email__isnull", False),
_connector="OR",
),
name="at_least_one_login_method",
),
),
]

View file

@ -1,9 +1,7 @@
import uuid
import re
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
import django.contrib.postgres.fields as postgres
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils import timezone
@ -14,17 +12,33 @@ from django.conf import settings
from management.models import Announcement
from mastodon.api import *
from django.urls import reverse
from django.db.models import Q
RESERVED_USERNAMES = [
"connect",
"oauth2_login",
"__",
"admin",
"api",
"me",
]
@deconstructible
class UsernameValidator(validators.RegexValidator):
regex = r"^[a-zA-Z0-9_]{2,50}$"
regex = r"^[a-zA-Z0-9_]{2,30}$"
message = _(
"Enter a valid username. This value may contain only unaccented lowercase a-z "
"and uppercase A-Z letters, numbers, and _ characters."
)
flags = re.ASCII
def __call__(self, value):
if value and value.lower() in RESERVED_USERNAMES:
raise ValidationError(self.message, code=self.code)
return super().__call__(value)
def report_image_path(instance, filename):
return GenerateDateUUIDMediaFilePath(
@ -46,6 +60,9 @@ class User(AbstractUser):
},
)
email = models.EmailField(_("email address"), unique=True, default=None, null=True)
pending_email = models.EmailField(
_("email address pending verification"), default=None, null=True
)
following = models.JSONField(default=list)
mastodon_id = models.CharField(max_length=100, default=None, null=True)
mastodon_username = models.CharField(max_length=100, default=None, null=True)
@ -74,6 +91,14 @@ class User(AbstractUser):
fields=["mastodon_id", "mastodon_site"],
name="unique_mastodon_id",
),
models.CheckConstraint(
check=(
Q(is_active=False)
| Q(mastodon_username__isnull=False)
| Q(email__isnull=False)
),
name="at_least_one_login_method",
),
]
# def save(self, *args, **kwargs):

View file

@ -0,0 +1,129 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="zh" class="classic-page">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - 账号信息</title>
{% include "common_libs.html" with jquery=0 v2=1 %}
</head>
<body>
{% include "_header.html" with current="data" %}
<main>
<div class="grid__main">
{% if allow_any_site %}
<article>
<details open>
<summary>{% trans '用户名、电子邮件与社交账号' %}</summary>
<form action="{% url 'users:register' %}?next={{ request.path }}"
method="post">
<small>{{ error }}</small>
<fieldset>
<label>
用户名
<input name="username"
_="on input remove [@disabled] from #save end"
placeholder="2-30个字符限英文字母数字下划线确认后不可更改"
required
{% if request.user.username %}value="{{ request.user.username }}" aria-invalid="false" readonly{% endif %}
pattern="^[a-zA-Z0-9_]{2,30}$" />
</label>
<label>
电子邮件地址
<input type="email"
name="email"
_="on input remove [@disabled] from #save then remove [@aria-invalid] end"
{% if request.user.email %}value="{{ request.user.email }}" aria-invalid="false"{% endif %}
placeholder="推荐,可作为备用登录方式"
autocomplete="email" />
{% if request.user.pending_email %}
<small>当前待确认的电子邮件地址为{{ request.user.pending_email }},请查收邮件并点击确认链接;如长时间未收到可重新输入并保存。</small>
{% endif %}
</label>
</fieldset>
{% csrf_token %}
<input type="submit" value="{% trans '保存' %}" disabled id="save">
</form>
<form action="{% url 'users:reconnect' %}" method="post">
{% csrf_token %}
<fieldset>
<label>
社交账号
<input type="input"
{% if request.user.mastodon_acct %}aria-invalid="false"{% endif %}
value="{{ request.user.mastodon_acct | default:'未关联' }}"
readonly>
</label>
<label>
如需关联到另一个社交账号,请输入新账号所在的实例域名
<input type="input"
name="domain"
value=""
placeholder="例如mastodon.online"
_="on input remove [@disabled] from #bind end">
</label>
<input type="submit" value="{% trans '登录并关联新账号' %}" disabled id="bind" />
</fieldset>
<div>替换关联后可使用新的联邦宇宙身份来登录{{ site_name }}和控制数据可见性,已有的标记评论收藏单等数据不受影响。</div>
</form>
</details>
</article>
{% endif %}
<article>
<details>
<summary>{% trans '更新社交关系数据' %}</summary>
<form action="{% url 'users:sync_mastodon' %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<input type="submit" value="{% trans '同步' %}" id="uploadBtn" />
上次更新时间 {{ user.mastodon_last_refresh }}
<div>
为了正确高效的展示短评和评论,{{ site_name }}会缓存你在联邦宇宙的关注、屏蔽和静音列表。如果你刚刚更新过帐户的上锁状态、增减过关注、静音或屏蔽,希望立即生效,可以点击这里立刻更新;这类信息也会每天自动同步。
</div>
</form>
</details>
</article>
<article>
<details>
<summary>{% trans '删除数据和账号信息' %}</summary>
<form action="{% url 'users:clear_data' %}"
method="post"
onsubmit="return confirm('账号数据一旦删除后将无法恢复。确认删除吗?');">
{% csrf_token %}
<div>
输入完整的 <code>用户名@实例名</code> 以确认删除
<input type="input"
name="verification"
_="on input remove [@disabled] from #delete end"
value=""
required
aria-invalid="true"
aria-describedby="invalid-helper"
placeholder="Gargron@mastodon.social">
<small id="invalid-helper">账号数据一旦删除后将无法恢复</small>
{% if import_status.douban_pending %}
<input type="submit" value="暂时无法删除,因为有导入任务正在进行" disabled />
{% else %}
<input type="submit"
value="{% trans '永久删除' %}"
class="contrast"
disabled
id="delete" />
{% endif %}
</div>
</form>
</details>
</article>
</div>
{% include "_sidebar.html" with show_profile=1 %}
</main>
{% include "_footer.html" %}
</body>
</html>

View file

@ -83,7 +83,7 @@
</article>
<article>
<details>
<summary>{% trans '导入Goodreads号或书单' %}</summary>
<summary>{% trans '导入Goodreads号或书单' %}</summary>
<form action="{% url 'users:import_goodreads' %}" method="post">
{% csrf_token %}
<div>
@ -176,59 +176,6 @@
</form>
</details>
</article>
<article>
<details>
<summary>{% trans '更新社交关系数据' %}</summary>
<form action="{% url 'users:sync_mastodon' %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<input type="submit" value="{% trans '同步' %}" id="uploadBtn" />
上次更新时间 {{ user.mastodon_last_refresh }}
<div>
为了正确高效的展示短评和评论,{{ site_name }}会缓存你在联邦网络的关注、屏蔽和静音列表。如果你刚刚更新过帐户的上锁状态、增减过关注、静音或屏蔽,希望立即生效,可以点击这里立刻更新;这类信息也会每天自动同步。
</div>
</form>
</details>
</article>
{% if allow_any_site %}
<article>
<details>
<summary>{% trans '替换社交账号' %}</summary>
<form action="{% url 'users:reconnect' %}" method="post">
{% csrf_token %}
<div>
输入新社交账号所在的实例域名
<input type="input" name="domain" value="" placeholder="例如mastodon.online">
<input type="submit" value="{% trans '登录新账号' %}" id="uploadBtn" />
</div>
<div>替换后可使用新的联邦网络身份来登录{{ site_name }}和控制数据可见性,已有的标记评论收藏单等数据不受影响。</div>
</form>
</details>
</article>
{% endif %}
<article>
<details>
<summary>{% trans '删除数据和帐号信息' %}</summary>
<form action="{% url 'users:clear_data' %}" method="post">
{% csrf_token %}
<div>
输入完整的 用户名@实例名 以确认删除
<input type="input"
name="verification"
value=""
required
placeholder="Gargron@mastodon.social">
{% if import_status.douban_pending %}
<input type="submit" value="暂时无法删除,因为有导入任务正在进行" disabled />
{% else %}
<input type="submit" value="{% trans '永久删除' %}" id="uploadBtn" />
{% endif %}
</div>
<div>删除将无法撤销</div>
</form>
</details>
</article>
</div>
{% include "_sidebar.html" with show_profile=1 %}
</main>

View file

@ -50,43 +50,82 @@
{% if user.is_authenticated %}
<a href="{% url 'common:home' %}" class="button">{% trans '前往首页' %}</a>
{% else %}
<form action="{% url 'users:connect' %}">
<form action="{% url 'users:connect' %}" method="post">
{% csrf_token %}
{% if allow_any_site %}
<input required
name="email"
id="email"
type="email"
placeholder="电子邮件地址"
disabled
autocomplete="email"
style="display:none" />
<input required
name="domain"
id="domain"
autofocus
pattern="(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,})"
pattern="(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,})"
title="实例域名(不含@和@之前的部分)如mastodon.social"
placeholder="实例域名(不含@和@之前的部分)如mastodon.social"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false" />
<input type='submit' value="{% trans '授权登录' %}" id="loginButton" disabled />
<div role="group" style="width:100%">
<select style="width:max-content"
required
onchange="switch_login()"
name="method">
<option disabled value="">选择登录方式</option>
<option selected value="fedi">通过联邦宇宙</option>
<option value="email">通过电子邮件</option>
</select>
<input style="width:100%"
type='submit'
value="{% trans '授权登录' %}"
id="loginButton"
disabled />
</div>
<script type="text/javascript">if (Cookies.get('mastodon_domain')) $('#domain').val(Cookies.get('mastodon_domain'));</script>
{{ sites|json_script:"sites-data" }}
<script>
const sites = JSON.parse(document.getElementById('sites-data').textContent);
const autoCompleteJS = new autoComplete({ placeHolder: "输入或选择实例域名(不含@和@之前的部分)",
selector: "#domain",
data: {
src: sites
},
submit: true,
resultsList: {
tabSelect: true,
maxResults: 10
},
events: {
input: {
selection: (event) => {
const selection = event.detail.selection.value;
autoCompleteJS.input.value = selection;
function switch_login(){
if ($('select').val() == 'email') {
$('#domain').prop("disabled", true);
$('#domain').hide();
$('#email').prop("disabled", false);
$('#email').show();
$('#email')[0].focus();
//$('#domain').val('');
} else {
$('#email').prop("disabled", true);
$('#email').hide();
$('#domain').prop("disabled", false);
$('#domain').show();
$('#domain')[0].focus();
//$('#email').val('');
}
}
}
});
const sites = JSON.parse(document.getElementById('sites-data').textContent);
const autoCompleteJS = new autoComplete({ placeHolder: "输入或选择实例域名(不含@和@之前的部分)",
selector: "#domain",
data: {
src: sites
},
submit: true,
resultsList: {
tabSelect: true,
maxResults: 10
},
events: {
input: {
selection: (event) => {
const selection = event.detail.selection.value;
autoCompleteJS.input.value = selection;
}
}
}
});
</script>
{% else %}
<select name="domain" placeholder="test">
@ -101,9 +140,10 @@
<div class="delayed">部分模块加载超时,请检查网络(翻墙)设置。</div>
</div>
</article>
{% comment %} </main> {% endcomment %}
<footer>
<small>本站使用cookie提供必要的功能继续访问视为同意。</small>
<small>如果你还没有注册过<em data-tooltip="联邦宇宙(Fediverse 亦称长毛象)是一种分布式社交网络">联邦宇宙</em>,可先<a href="https://joinmastodon.org/zh/servers" target="_blank">选择实例并注册</a></small>
<br>
<small>继续访问或注册视为同意本站<a href="{% url 'management:retrieve_slug' 'data-policy' %}">数据方针</a>及使用cookie提供必要功能</small>
</footer>
</body>
</html>

View file

@ -18,112 +18,104 @@
<main>
<div class="grid__main">
<article>
<form action="{% url 'users:preferences' %}" method="post">
<section>
<details>
<summary>{% trans '使用偏好设置' %}</summary>
{% csrf_token %}
<span>{% trans '新标记默认可见性:' %}</span>
<div>
<input type="radio"
name="default_visibility"
value="0"
required=""
id="id_visibility_0"
{% if request.user.preference.default_visibility == 0 %}checked{% endif %}>
<label for="id_visibility_0">公开</label>
<input type="radio"
name="default_visibility"
value="1"
required=""
id="id_visibility_1"
{% if request.user.preference.default_visibility == 1 %}checked{% endif %}>
<label for="id_visibility_1">仅关注者</label>
<input type="radio"
name="default_visibility"
value="2"
required=""
id="id_visibility_2"
{% if request.user.preference.default_visibility == 2 %}checked{% endif %}>
<label for="id_visibility_2">仅自己</label>
</div>
<br>
<span>{% trans '登录后显示:' %}</span>
<div>
<input type="radio"
name="classic_homepage"
value="0"
id="classic_homepage0"
{% if request.user.preference.classic_homepage == 0 %}checked{% endif %}>
<label for="classic_homepage0">内容发现</label>
<input type="radio"
name="classic_homepage"
value="2"
id="classic_homepage2"
{% if request.user.preference.classic_homepage == 2 %}checked{% endif %}>
<label for="classic_homepage2">好友动态</label>
<input type="radio"
name="classic_homepage"
value="1"
id="classic_homepage1"
{% if request.user.preference.classic_homepage == 1 %}checked{% endif %}>
<label for="classic_homepage1">个人主页</label>
</div>
<br style="margin-bottom:1.5em">
<div>
<details open>
<summary>
<b>{% trans '使用偏好' %}</b>
</summary>
<form action="{% url 'users:preferences' %}" method="post">
{% csrf_token %}
<fieldset>
<legend>{% trans '登录后显示:' %}</legend>
<input type="radio"
name="classic_homepage"
value="0"
id="classic_homepage0"
{% if request.user.preference.classic_homepage == 0 %}checked{% endif %}>
<label for="classic_homepage0">内容发现</label>
<input type="radio"
name="classic_homepage"
value="2"
id="classic_homepage2"
{% if request.user.preference.classic_homepage == 2 %}checked{% endif %}>
<label for="classic_homepage2">好友动态</label>
<input type="radio"
name="classic_homepage"
value="1"
id="classic_homepage1"
{% if request.user.preference.classic_homepage == 1 %}checked{% endif %}>
<label for="classic_homepage1">个人主页</label>
</fieldset>
<fieldset>
<legend>{% trans '新标记默认可见性:' %}</legend>
<input type="radio"
name="default_visibility"
value="0"
required=""
id="id_visibility_0"
{% if request.user.preference.default_visibility == 0 %}checked{% endif %}>
<label for="id_visibility_0">公开</label>
<input type="radio"
name="default_visibility"
value="1"
required=""
id="id_visibility_1"
{% if request.user.preference.default_visibility == 1 %}checked{% endif %}>
<label for="id_visibility_1">仅关注者</label>
<input type="radio"
name="default_visibility"
value="2"
required=""
id="id_visibility_2"
{% if request.user.preference.default_visibility == 2 %}checked{% endif %}>
<label for="id_visibility_2">仅自己</label>
</fieldset>
<fieldset>
<label>
<input type="checkbox"
name="no_anonymous_view"
id="no_anonymous_view"
{% if request.user.preference.no_anonymous_view %}checked{% endif %}>
<label for="no_anonymous_view">{% trans '不允许未登录用户访问你的个人主页' %}</label>
</div>
<br style="margin-bottom:1.5em">
<div>
{% trans '仅允许已登录用户查看你的个人主页' %}
</label>
</fieldset>
<fieldset>
<label>
<input type="checkbox"
name="show_last_edit"
id="show_last_edit"
{% if request.user.preference.show_last_edit %}checked{% endif %}>
<label for="show_last_edit">{% trans '显示你是某条目的最近编辑者' %}</label>
</div>
</details>
</section>
<section>
<details>
<summary>{% trans '社交网络分享相关设置' %}</summary>
<div>
{% trans '显示你是某条目的最近编辑者' %}
</label>
</fieldset>
<fieldset>
<label>
<input type="checkbox"
name="mastodon_publish_public"
id="mastodon_publish_public"
{% if request.user.preference.mastodon_publish_public %}checked{% endif %}>
<label for="mastodon_publish_public">
以公开方式分享的帖文发布到<em data-tooltip="选中时为public未选中时为unlisted">公共时间轴</em>
</label>
</div>
<br>
<br>
<span>{% trans '在联邦网络上分享帖文时在结尾附加标签:' %}</span>
<div>
<input name="mastodon_append_tag"
id="tag"
placeholder="#我的书影音"
value="{{ request.user.preference.mastodon_append_tag }}">
</div>
<br>
<div>
标记时以公开方式分享的帖文发布到<em data-tooltip="选中时为public未选中时为unlisted">公共时间轴</em>
</label>
</fieldset>
<fieldset>
<label>
<input type="checkbox"
name="default_no_share"
id="default_no_share"
{% if request.user.preference.default_no_share %}checked{% endif %}>
<label for="default_no_share">标记时默认不分享到联邦网络</label>
</div>
</details>
</section>
<input type="submit" value="{% trans '保存' %}">
</form>
<hr>
<section>
<details>
<summary>{% trans '当前设备设置' %}</summary>
标记时默认不分享到联邦宇宙
</label>
</fieldset>
<fieldset>
<label for="mastodon_append_tag">{% trans '在联邦宇宙分享帖文时在结尾附加标签:' %}</label>
<input name="mastodon_append_tag"
id="mastodon_append_tag"
placeholder="例如 #我的书影音"
value="{{ request.user.preference.mastodon_append_tag }}">
</fieldset>
<input type="submit" value="{% trans '保存' %}">
</form>
</details>
</article>
<article>
<details>
<summary>{% trans '当前设备设置' %}</summary>
<form onsubmit="return false;">
<h6>专注模式 (实验功能)</h6>
<p>
<input type="checkbox" id="solo_mode">
@ -132,9 +124,10 @@
<h6>自定义样式代码 (实验功能)</h6>
<textarea id="user_style"></textarea>
<br>
<button onclick="save_local();">保存</button>
</details>
<script>
<input type="button" onclick="save_local();" value="保存">
</form>
</details>
<script>
$("#user_style").val(localStorage.getItem("user_style")||"");
$("#solo_mode").prop("checked", localStorage.getItem("solo_mode")=="1");
function save_local() {
@ -142,17 +135,15 @@
localStorage.setItem("solo_mode", $("#solo_mode").prop("checked")?"1":"0");
alert("本地设置已保存");
}
</script>
</section>
<hr>
<section>
<details>
<summary>{% trans '应用管理' %}</summary>
<p>
<a href="{% url 'oauth2_provider:authorized-token-list' %}">查看已授权的应用程序</a>
</p>
</details>
</section>
</script>
</article>
<article>
<details>
<summary>{% trans '应用管理' %}</summary>
<p>
<a href="{% url 'oauth2_provider:authorized-token-list' %}">查看已授权的应用程序</a>
</p>
</details>
</article>
</div>
{% include "_sidebar.html" with show_profile=1 %}

View file

@ -1,34 +1,67 @@
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html lang="zh" class="login-page">
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {% trans '注册完成' %}</title>
<title>{{ site_name }} - {% trans '注册信息' %}</title>
{% include "common_libs.html" with jquery=0 v2=1 %}
</head>
<body>
<article>
<header style="text-align: center;">
<img src="{% static 'img/logo.svg' %}" class="logo" alt="logo">
</header>
<p>欢迎来到{{ site_name }}</p>
<p>
{{ site_name }}还在不断完善中,丰富的内容需要大家共同创造。
试图添加垃圾数据(如添加信息混乱或缺失的书籍、以推广为主要目的的评论)将会受到严肃处理。
{{ site_name }}继承了联邦宇宙的用户关系比如您在联邦宇宙屏蔽了某人那您将不会在书影音的公共区域看到TA的痕迹。
本站为非盈利站点cookie和其它数据保管使用原则请参阅<a href="{% url 'management:retrieve_slug' 'data-policy' %}">站内公告</a>
</p>
<p>
此外,{{ site_name }}现处于测试阶段,疏漏在所难免,请妥善备份您的数据。
使用过程中遇到的问题或者错误欢迎向<a href="{{ support_link }}">维护者</a>提出。感谢理解和支持!
</p>
<form action="{% url 'common:home' %}">
<input type="submit"
class="button"
value="{% trans 'Cut the sh*t and get me in!' %}">
</form>
</article>
<div class="container">
<article>
<header style="text-align: center;">
<img src="{% static 'img/logo.svg' %}" class="logo" alt="logo">
</header>
{% if request.session.new_user %}
<h4>欢迎来到{{ site_name }}{{ request.user.mastodon_acct }}</h4>
<p>
{{ site_name }}还在不断完善中。
丰富的内容需要大家共同创造,试图添加垃圾数据(如添加信息混乱或缺失的书籍、以推广为主要目的的评论)将会受到严肃处理。
本站为非盈利站点cookie和其它数据保管使用原则请参阅<a href="{% url 'management:retrieve_slug' 'data-policy' %}">站内公告</a>
本站提供API和导出功能请妥善备份您的数据使用过程中遇到的问题或者错误欢迎向<a href="{{ support_link }}">维护者</a>提出。感谢理解和支持!
</p>
{% endif %}
{% if form %}
<form action="{% url 'users:register' %}" method="post">
<small>{{ error }}</small>
<fieldset>
<label>
请输入你想在{{ site_name }}使用的用户名
<input name="username"
placeholder="2-30个字符限英文字母数字下划线确认后不可更改"
value="{{ form.username.value|default:request.user.username|default:'' }}"
required
_="on input remove [@aria-invalid] end"
{% if request.user.username and not form.username.errors %}aria-invalid="false" readonly{% endif %}
{% if form.username.errors %}aria-invalid="true"{% endif %}
pattern="^[a-zA-Z0-9_]{2,30}$" />
{% for error in form.username.errors %}<small>{{ error }}</small>{% endfor %}
</label>
<label>
以及作为备用登录方式的电子邮件地址(推荐)
<input type="email"
name="email"
{% if request.user.email and not request.user.mastodon_acct %}readonly{% endif %}
{% if request.user.email %}value="{{ request.user.email }}" aria-invalid="false"{% endif %}
placeholder="设置后请查收邮件点击其中的确认链接"
autocomplete="email" />
{% if request.user.pending_email %}
<small>当前待确认的电子邮件地址为{{ request.user.pending_email }},请查收邮件并点击确认链接;如长时间未收到可重新输入并保存。</small>
{% endif %}
{% for error in form.email.errors %}<small>{{ error }}</small>{% endfor %}
</label>
</fieldset>
{% csrf_token %}
<input type="submit" value="{% trans '确认并保存' %}">
</form>
{% else %}
<form action="{% url 'common:home' %}" method="get">
<input type="submit" value="{% trans 'Cut the sh*t and get me in!' %}">
</form>
{% endif %}
</article>
</div>
</body>
</html>

View file

@ -0,0 +1,30 @@
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {% trans '验证电子邮件' %}</title>
{% include "common_libs.html" with jquery=0 v2=1 %}
</head>
<body>
<div class="container">
<article>
<header style="text-align: center;">
<img src="{% static 'img/logo.svg' %}" class="logo" alt="logo">
</header>
<h4>验证电子邮件</h4>
{% if success %}
<p>
{{ request.user.email }} 验证成功,<a href="{% url 'common:home' %}">点击这里返回首页</a>
</p>
{% else %}
<p>
链接无效或已过期,<a href="{% url 'users:login' %}">点击这里重新登录</a>
</p>
{% endif %}
</article>
</div>
</body>
</html>

View file

@ -4,10 +4,15 @@ from .views import *
app_name = "users"
urlpatterns = [
path("login/", login, name="login"),
path("register/", register, name="register"),
path("connect/", connect, name="connect"),
path("reconnect/", reconnect, name="reconnect"),
path("data/", data, name="data"),
path("login/oauth", OAuth2_login, name="login_oauth"),
path("login/email", verify_email, name="login_email"),
path("verify_email", verify_email, name="verify_email"),
path("register_email", verify_email, name="register_email"),
path("register", register, name="register"),
path("connect", connect, name="connect"),
path("reconnect", reconnect, name="reconnect"),
path("data", data, name="data"),
path("info", account_info, name="info"),
path("data/import/status", data_import_status, name="import_status"),
path("data/import/goodreads", import_goodreads, name="import_goodreads"),
path("data/import/douban", import_douban, name="import_douban"),
@ -17,14 +22,13 @@ urlpatterns = [
path("data/sync_mastodon", sync_mastodon, name="sync_mastodon"),
path("data/reset_visibility", reset_visibility, name="reset_visibility"),
path("data/clear_data", clear_data, name="clear_data"),
path("preferences/", preferences, name="preferences"),
path("logout/", logout, name="logout"),
path("layout/", set_layout, name="set_layout"),
path("OAuth2_login/", OAuth2_login, name="OAuth2_login"),
path("<str:id>/followers/", followers, name="followers"),
path("<str:id>/following/", following, name="following"),
path("report/", report, name="report"),
path("manage_report/", manage_report, name="manage_report"),
path("preferences", preferences, name="preferences"),
path("logout", logout, name="logout"),
path("layout", set_layout, name="set_layout"),
path("<str:id>/followers", followers, name="followers"),
path("<str:id>/following", following, name="following"),
path("report", report, name="report"),
path("manage_report", manage_report, name="manage_report"),
path(
"mark_announcements_read/",
mark_announcements_read,