token and app mgmt
This commit is contained in:
parent
2c68ff8831
commit
8ba8c4490e
19 changed files with 302 additions and 42 deletions
|
@ -52,7 +52,6 @@ INSTALLED_APPS = [
|
|||
"django_rq",
|
||||
"django_bleach",
|
||||
"django_jsonform",
|
||||
"oauth2_provider",
|
||||
"tz_detect",
|
||||
"sass_processor",
|
||||
"auditlog",
|
||||
|
@ -75,6 +74,10 @@ INSTALLED_APPS += [
|
|||
"legacy.apps.LegacyConfig",
|
||||
]
|
||||
|
||||
INSTALLED_APPS += [ # we may override templates in these 3rd party apps
|
||||
"oauth2_provider",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
|
@ -423,7 +426,10 @@ MAINTENANCE_MODE_IGNORE_URLS = (r"^/users/connect/", r"^/users/OAuth2_login/")
|
|||
DISCORD_WEBHOOKS = {}
|
||||
|
||||
NINJA_PAGINATION_PER_PAGE = 20
|
||||
OAUTH2_PROVIDER = {"ACCESS_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 365}
|
||||
OAUTH2_PROVIDER = {
|
||||
"ACCESS_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 365,
|
||||
"PKCE_REQUIRED": False,
|
||||
}
|
||||
OAUTH2_PROVIDER_APPLICATION_MODEL = "developer.Application"
|
||||
|
||||
DEVELOPER_CONSOLE_APPLICATION_CLIENT_ID = "NEODB_DEVELOPER_CONSOLE"
|
||||
|
|
|
@ -31,6 +31,7 @@ urlpatterns = [
|
|||
path("hijack/", include("hijack.urls")),
|
||||
path("", include("common.urls")),
|
||||
path("", include("legacy.urls")),
|
||||
path("", include("developer.urls")),
|
||||
# path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
||||
path("tz_detect/", include("tz_detect.urls")),
|
||||
path(settings.ADMIN_URL + "/", admin.site.urls),
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
href="{{ donation_link }}">捐助本站</a>
|
||||
{% endif %}
|
||||
<a class="footer__link" href="{% url 'management:list' %}">公告栏</a>
|
||||
<a class="footer__link" href="{% url 'common:developer' %}">API</a>
|
||||
<a class="footer__link" href="{% url 'oauth2_provider:developer' %}">应用开发</a>
|
||||
<a class="footer__link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
|
|
|
@ -4,7 +4,6 @@ from .views import *
|
|||
app_name = "common"
|
||||
urlpatterns = [
|
||||
path("", home),
|
||||
path("developer/", developer, name="developer"),
|
||||
path("home/", home, name="home"),
|
||||
path("me/", me, name="me"),
|
||||
]
|
||||
|
|
|
@ -2,13 +2,6 @@ from django.contrib import messages
|
|||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .api import api
|
||||
from oauthlib.common import generate_token
|
||||
from oauth2_provider.models import AccessToken, Application
|
||||
from django.utils import timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from oauth2_provider.models import RefreshToken
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -52,32 +45,3 @@ def error_404(request, exception=None):
|
|||
|
||||
def error_500(request, exception=None):
|
||||
return render(request, "500.html", status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
def developer(request):
|
||||
token = None
|
||||
if request.method == "POST":
|
||||
user = request.user
|
||||
app = Application.objects.filter(
|
||||
client_id=settings.DEVELOPER_CONSOLE_APPLICATION_CLIENT_ID
|
||||
).first()
|
||||
if app:
|
||||
for token in AccessToken.objects.filter(user=user, application=app):
|
||||
token.revoke()
|
||||
token = generate_token()
|
||||
AccessToken.objects.create(
|
||||
user=user,
|
||||
application=app,
|
||||
scope="read write",
|
||||
expires=timezone.now() + relativedelta(days=365),
|
||||
token=token,
|
||||
)
|
||||
else:
|
||||
token = "Configuration error, contact admin"
|
||||
context = {
|
||||
"api": api,
|
||||
"token": token,
|
||||
"openapi_json_url": reverse(f"{api.urls_namespace}:openapi-json"),
|
||||
}
|
||||
return render(request, "developer.html", context)
|
||||
|
|
0
developer/__init__.py
Normal file
0
developer/__init__.py
Normal file
3
developer/admin.py
Normal file
3
developer/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
developer/apps.py
Normal file
6
developer/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DeveloperConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "developer"
|
45
developer/migrations/0001_initial.py
Normal file
45
developer/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Generated by Django 3.2.19 on 2023-06-28 02:44
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import markdownx.models
|
||||
import oauth2_provider.generators
|
||||
import oauth2_provider.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)),
|
||||
('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated')),
|
||||
('post_logout_redirect_uris', models.TextField(blank=True, help_text='Allowed Post Logout URIs list, space separated')),
|
||||
('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
|
||||
('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32)),
|
||||
('client_secret', oauth2_provider.models.ClientSecretField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, help_text='Hashed on Save. Copy it now if this is a new secret.', max_length=255)),
|
||||
('skip_authorization', models.BooleanField(default=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('algorithm', models.CharField(blank=True, choices=[('', 'No OIDC support'), ('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='', max_length=5)),
|
||||
('name', models.CharField(max_length=255, unique=True, validators=[django.core.validators.RegexValidator(message='至少两个字,不可包含普通文字和-_.以外的字符', regex='^\\w[\\w_\\-. ]*\\w$')])),
|
||||
('descrpition', markdownx.models.MarkdownxField(blank=True, default='')),
|
||||
('url', models.URLField(blank=True, null=True)),
|
||||
('is_official', models.BooleanField(default=False)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='developer_application', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
0
developer/migrations/__init__.py
Normal file
0
developer/migrations/__init__.py
Normal file
21
developer/models.py
Normal file
21
developer/models.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from django.db import models
|
||||
from django.core.validators import RegexValidator
|
||||
from oauth2_provider.models import AbstractApplication
|
||||
from markdownx.models import MarkdownxField
|
||||
|
||||
|
||||
class Application(AbstractApplication):
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
blank=False,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r"^\w[\w_\-. ]*\w$",
|
||||
message="至少两个字,不可包含普通文字和-_.以外的字符",
|
||||
),
|
||||
],
|
||||
unique=True,
|
||||
)
|
||||
descrpition = MarkdownxField(default="", blank=True)
|
||||
url = models.URLField(null=True, blank=True)
|
||||
is_official = models.BooleanField(default=False)
|
|
@ -1,3 +1,4 @@
|
|||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
|
@ -25,10 +26,15 @@
|
|||
<body>
|
||||
{% include "_header.html" %}
|
||||
<main class="container">
|
||||
<h5>Developer Console</h5>
|
||||
<h3>
|
||||
Developer Console | <a href="{% url 'oauth2_provider:list' %}">{% trans "Your applications" %}</a>
|
||||
</h3>
|
||||
<p class="empty">
|
||||
By using our APIs, you agree to our <a href="{% url 'management:retrieve_slug' 'developer-term' %}">term</a> and <a href="{% url 'management:retrieve_slug' 'data-policy' %}">data policy</a>.
|
||||
</p>
|
||||
<details {% if token %}open{% endif %}>
|
||||
<summary>
|
||||
<b>Access Token Management</b>
|
||||
<b>Test Access Token</b>
|
||||
</summary>
|
||||
<form method="post" role="group">
|
||||
{% csrf_token %}
|
23
developer/templates/oauth2_provider/application_list.html
Normal file
23
developer/templates/oauth2_provider/application_list.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends "oauth2_provider/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<h3>
|
||||
<a href="{% url 'oauth2_provider:developer' %}">Developer Console</a> | {% trans "Your applications" %}
|
||||
</h3>
|
||||
<div class="block-center">
|
||||
{% if applications %}
|
||||
<ul>
|
||||
{% for application in applications %}
|
||||
<li>
|
||||
<a href="{{ application.get_absolute_url }}">{{ application.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a class="btn btn-success" href="{% url "oauth2_provider:register" %}">{% trans "New Application" %}</a>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans "No applications defined" %}. <a href="{% url 'oauth2_provider:register' %}">{% trans "Click here" %}</a> {% trans "if you want to register a new one" %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
39
developer/templates/oauth2_provider/authorize.html
Normal file
39
developer/templates/oauth2_provider/authorize.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% extends "oauth2_provider/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<article>
|
||||
{% if not error %}
|
||||
<header>
|
||||
<h4>授权 {{ application.name }} 访问你的帐户吗?</h4>
|
||||
</header>
|
||||
<form id="authorizationForm" method="post">
|
||||
{% csrf_token %}
|
||||
{% if not application.is_official %}
|
||||
<p>
|
||||
<b>{{ application.name }}</b> 是 <a href="{% url 'journal:user_profile' application.user.mastodon_username %}">{{ application.user.mastodon_username }}</a> 创建和维护的应用程序, 并非来自{{ site_name }}官方。
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if application.url %}应用网址: {{ application.url | urlize }}{% endif %}
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}{{ field }}{% endif %}
|
||||
{% endfor %}
|
||||
<p>授权后这个应用将能读写你帐户的全部数据。</p>
|
||||
{% comment %} <p>{% trans "Application requires the following permissions" %}</p>
|
||||
<ul>
|
||||
{% for scope in scopes_descriptions %}<li>{{ scope }}</li>{% endfor %}
|
||||
</ul> {% endcomment %}
|
||||
{{ form.errors }}
|
||||
{{ form.non_field_errors }}
|
||||
<footer>
|
||||
<div class="grid">
|
||||
<input type="submit" class="primary" name="allow" value="确认授权" />
|
||||
<input type="submit" class="secondary" value="取消" />
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
{% else %}
|
||||
<h2>Error: {{ error.error }}</h2>
|
||||
<p>{{ error.description }}</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endblock %}
|
24
developer/templates/oauth2_provider/base.html
Normal file
24
developer/templates/oauth2_provider/base.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% block title %}
|
||||
{% endblock title %}
|
||||
<title>{{ site_name }}</title>
|
||||
{% include "common_libs.html" with jquery=0 v2=1 %}
|
||||
<style>
|
||||
.controls input {
|
||||
width: unset;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% include "_header.html" %}
|
||||
<main class="container">
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</main>
|
||||
{% include "_footer.html" %}
|
||||
</body>
|
||||
</html>
|
3
developer/tests.py
Normal file
3
developer/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
69
developer/urls.py
Normal file
69
developer/urls.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from django.urls import path, re_path, include
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import views as auth_views
|
||||
from oauth2_provider import views as oauth2_views
|
||||
from oauth2_provider.views import oidc as oidc_views
|
||||
from .views import *
|
||||
|
||||
|
||||
_urlpatterns = [
|
||||
re_path(
|
||||
r"^oauth/authorize/$",
|
||||
oauth2_views.AuthorizationView.as_view(),
|
||||
name="authorize",
|
||||
),
|
||||
re_path(r"^oauth/token/$", oauth2_views.TokenView.as_view(), name="token"),
|
||||
re_path(
|
||||
r"^oauth/revoke_token/$",
|
||||
oauth2_views.RevokeTokenView.as_view(),
|
||||
name="revoke-token",
|
||||
),
|
||||
re_path(
|
||||
r"^oauth/introspect/$",
|
||||
oauth2_views.IntrospectTokenView.as_view(),
|
||||
name="introspect",
|
||||
),
|
||||
re_path(
|
||||
r"^oauth/authorized_tokens/$",
|
||||
oauth2_views.AuthorizedTokensListView.as_view(),
|
||||
name="authorized-token-list",
|
||||
),
|
||||
re_path(
|
||||
r"^oauth/authorized_tokens/(?P<pk>[\w-]+)/delete/$",
|
||||
oauth2_views.AuthorizedTokenDeleteView.as_view(),
|
||||
name="authorized-token-delete",
|
||||
),
|
||||
]
|
||||
|
||||
_urlpatterns += [
|
||||
path("developer/", console, name="developer"),
|
||||
re_path(
|
||||
r"^developer/applications/$",
|
||||
oauth2_views.ApplicationList.as_view(),
|
||||
name="list",
|
||||
),
|
||||
re_path(
|
||||
r"^developer/applications/register/$",
|
||||
oauth2_views.ApplicationRegistration.as_view(),
|
||||
name="register",
|
||||
),
|
||||
re_path(
|
||||
r"^developer/applications/(?P<pk>[\w-]+)/$",
|
||||
oauth2_views.ApplicationDetail.as_view(),
|
||||
name="detail",
|
||||
),
|
||||
re_path(
|
||||
r"^developer/applications/(?P<pk>[\w-]+)/delete/$",
|
||||
oauth2_views.ApplicationDelete.as_view(),
|
||||
name="delete",
|
||||
),
|
||||
re_path(
|
||||
r"^developer/applications/(?P<pk>[\w-]+)/update/$",
|
||||
oauth2_views.ApplicationUpdate.as_view(),
|
||||
name="update",
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
path("", include((_urlpatterns, "oauth2_provider"))),
|
||||
]
|
50
developer/views.py
Normal file
50
developer/views.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
from django.shortcuts import render
|
||||
from loguru import logger
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
from oauth2_provider.forms import AllowForm
|
||||
from oauth2_provider.models import get_application_model
|
||||
from oauth2_provider.views import ProtectedResourceView
|
||||
from oauth2_provider.views.base import AuthorizationView as BaseAuthorizationView
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
from common.api import api
|
||||
from oauthlib.common import generate_token
|
||||
from oauth2_provider.models import AccessToken
|
||||
from django.utils import timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from oauth2_provider.models import RefreshToken
|
||||
from django.conf import settings
|
||||
from .models import Application
|
||||
|
||||
|
||||
class AuthorizationView(BaseAuthorizationView):
|
||||
pass
|
||||
|
||||
|
||||
@login_required
|
||||
def console(request):
|
||||
token = None
|
||||
if request.method == "POST":
|
||||
user = request.user
|
||||
app = Application.objects.filter(
|
||||
client_id=settings.DEVELOPER_CONSOLE_APPLICATION_CLIENT_ID
|
||||
).first()
|
||||
if app:
|
||||
for token in AccessToken.objects.filter(user=user, application=app):
|
||||
token.revoke()
|
||||
token = generate_token()
|
||||
AccessToken.objects.create(
|
||||
user=user,
|
||||
application=app,
|
||||
scope="read write",
|
||||
expires=timezone.now() + relativedelta(days=365),
|
||||
token=token,
|
||||
)
|
||||
else:
|
||||
token = "Configuration error, contact admin"
|
||||
context = {
|
||||
"api": api,
|
||||
"token": token,
|
||||
"openapi_json_url": reverse(f"{api.urls_namespace}:openapi-json"),
|
||||
}
|
||||
return render(request, "console.html", context)
|
|
@ -38,3 +38,4 @@ podcastparser
|
|||
listparser
|
||||
fontawesomefree
|
||||
discord.py
|
||||
loguru
|
||||
|
|
Loading…
Add table
Reference in a new issue