simplify developer module
This commit is contained in:
parent
ed389b9085
commit
8bcec254dc
36 changed files with 329 additions and 633 deletions
|
@ -305,7 +305,6 @@ INSTALLED_APPS += [
|
||||||
"catalog.apps.CatalogConfig",
|
"catalog.apps.CatalogConfig",
|
||||||
"journal.apps.JournalConfig",
|
"journal.apps.JournalConfig",
|
||||||
"social.apps.SocialConfig",
|
"social.apps.SocialConfig",
|
||||||
"developer.apps.DeveloperConfig",
|
|
||||||
"takahe.apps.TakaheConfig",
|
"takahe.apps.TakaheConfig",
|
||||||
"legacy.apps.LegacyConfig",
|
"legacy.apps.LegacyConfig",
|
||||||
]
|
]
|
||||||
|
@ -313,9 +312,6 @@ INSTALLED_APPS += [
|
||||||
for app in env("NEODB_EXTRA_APPS"):
|
for app in env("NEODB_EXTRA_APPS"):
|
||||||
INSTALLED_APPS.append(app)
|
INSTALLED_APPS.append(app)
|
||||||
|
|
||||||
INSTALLED_APPS += [ # we may override templates in these 3rd party apps
|
|
||||||
"oauth2_provider",
|
|
||||||
]
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
@ -325,7 +321,6 @@ MIDDLEWARE = [
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"oauth2_provider.middleware.OAuth2TokenMiddleware",
|
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
"hijack.middleware.HijackUserMiddleware",
|
"hijack.middleware.HijackUserMiddleware",
|
||||||
|
@ -363,7 +358,6 @@ SESSION_COOKIE_AGE = 90 * 24 * 60 * 60 # 90 days
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
"mastodon.auth.OAuth2Backend",
|
"mastodon.auth.OAuth2Backend",
|
||||||
"oauth2_provider.backends.OAuth2Backend",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
LOG_LEVEL = env("NEODB_LOG_LEVEL", default="DEBUG" if DEBUG else "INFO") # type:ignore
|
LOG_LEVEL = env("NEODB_LOG_LEVEL", default="DEBUG" if DEBUG else "INFO") # type:ignore
|
||||||
|
|
|
@ -45,7 +45,6 @@ urlpatterns = [
|
||||||
path("hijack/", include("hijack.urls")),
|
path("hijack/", include("hijack.urls")),
|
||||||
path("", include("common.urls")),
|
path("", include("common.urls")),
|
||||||
path("", include("legacy.urls")),
|
path("", include("legacy.urls")),
|
||||||
path("", include("developer.urls")),
|
|
||||||
path("", include("takahe.urls")),
|
path("", include("takahe.urls")),
|
||||||
path("tz_detect/", include("tz_detect.urls")),
|
path("tz_detect/", include("tz_detect.urls")),
|
||||||
path(settings.ADMIN_URL + "/", admin.site.urls),
|
path(settings.ADMIN_URL + "/", admin.site.urls),
|
||||||
|
|
|
@ -13,6 +13,7 @@ from common.models import BaseJob, JobManager
|
||||||
from journal.models import (
|
from journal.models import (
|
||||||
Collection,
|
Collection,
|
||||||
Comment,
|
Comment,
|
||||||
|
Review,
|
||||||
ShelfMember,
|
ShelfMember,
|
||||||
TagManager,
|
TagManager,
|
||||||
q_item_in_category,
|
q_item_in_category,
|
||||||
|
@ -138,21 +139,24 @@ class DiscoverGenerator(BaseJob):
|
||||||
.values_list("pk", flat=True)[:40]
|
.values_list("pk", flat=True)[:40]
|
||||||
)
|
)
|
||||||
tags = TagManager.popular_tags(days=14)[:40]
|
tags = TagManager.popular_tags(days=14)[:40]
|
||||||
post_ids = set(
|
post_ids = (
|
||||||
Takahe.get_popular_posts(7, settings.MIN_MARKS_FOR_DISCOVER).values_list(
|
set(
|
||||||
"pk", flat=True
|
Takahe.get_popular_posts(
|
||||||
)[:10]
|
28, settings.MIN_MARKS_FOR_DISCOVER
|
||||||
) | set(
|
).values_list("pk", flat=True)[:20]
|
||||||
Takahe.get_popular_posts(28, settings.MIN_MARKS_FOR_DISCOVER).values_list(
|
|
||||||
"pk", flat=True
|
|
||||||
)[:20]
|
|
||||||
)
|
|
||||||
if len(post_ids) < 30:
|
|
||||||
post_ids |= set(
|
|
||||||
Comment.objects.filter(visibility=0)
|
|
||||||
.order_by("-created_time")
|
|
||||||
.values_list("posts", flat=True)[:2]
|
|
||||||
)
|
)
|
||||||
|
| set(
|
||||||
|
Takahe.get_popular_posts(
|
||||||
|
7, settings.MIN_MARKS_FOR_DISCOVER
|
||||||
|
).values_list("pk", flat=True)[:10]
|
||||||
|
)
|
||||||
|
| set(Takahe.get_popular_posts(2, 0).values_list("pk", flat=True)[:3])
|
||||||
|
| set(
|
||||||
|
Review.objects.filter(visibility=0)
|
||||||
|
.order_by("-created_time")
|
||||||
|
.values_list("posts", flat=True)[:3]
|
||||||
|
)
|
||||||
|
)
|
||||||
cache.set("public_gallery", gallery_list, timeout=None)
|
cache.set("public_gallery", gallery_list, timeout=None)
|
||||||
cache.set("trends_links", trends, timeout=None)
|
cache.set("trends_links", trends, timeout=None)
|
||||||
cache.set("featured_collections", collection_ids, timeout=None)
|
cache.set("featured_collections", collection_ids, timeout=None)
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<div>{% include '_people.html' with people=item.genre role='genre' max=5 %}</div>
|
<div>{% include '_people.html' with people=item.genre role='genre' max=5 %}</div>
|
||||||
<div>
|
<div>
|
||||||
{% if item.barcode %}
|
{% if item.barcode %}
|
||||||
{% trans 'barcode' %}{{ item.barcode }}
|
{% trans 'barcode' %}: {{ item.barcode }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<div>{% include '_people.html' with people=item.publisher role='publisher' max=2 %}</div>
|
<div>{% include '_people.html' with people=item.publisher role='publisher' max=2 %}</div>
|
||||||
<div>
|
<div>
|
||||||
{% if item.official_site %}
|
{% if item.official_site %}
|
||||||
{% trans 'website' %}{{ item.official_site|urlizetrunc:24 }}
|
{% trans 'website' %}: {{ item.official_site|urlizetrunc:24 }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -6,9 +6,9 @@ from loguru import logger
|
||||||
from ninja import NinjaAPI, Schema
|
from ninja import NinjaAPI, Schema
|
||||||
from ninja.pagination import PageNumberPagination as NinjaPageNumberPagination
|
from ninja.pagination import PageNumberPagination as NinjaPageNumberPagination
|
||||||
from ninja.security import HttpBearer
|
from ninja.security import HttpBearer
|
||||||
from oauth2_provider.oauth2_backends import OAuthLibCore
|
|
||||||
from oauth2_provider.oauth2_validators import OAuth2Validator
|
from takahe.utils import Takahe
|
||||||
from oauthlib.oauth2 import Server
|
from users.models.apidentity import APIdentity
|
||||||
|
|
||||||
PERMITTED_WRITE_METHODS = ["PUT", "POST", "DELETE", "PATCH"]
|
PERMITTED_WRITE_METHODS = ["PUT", "POST", "DELETE", "PATCH"]
|
||||||
PERMITTED_READ_METHODS = ["GET", "HEAD", "OPTIONS"]
|
PERMITTED_READ_METHODS = ["GET", "HEAD", "OPTIONS"]
|
||||||
|
@ -16,23 +16,38 @@ PERMITTED_READ_METHODS = ["GET", "HEAD", "OPTIONS"]
|
||||||
|
|
||||||
class OAuthAccessTokenAuth(HttpBearer):
|
class OAuthAccessTokenAuth(HttpBearer):
|
||||||
def authenticate(self, request, token) -> bool:
|
def authenticate(self, request, token) -> bool:
|
||||||
if not token or not request.user.is_authenticated:
|
if not token:
|
||||||
logger.debug("API auth: no access token or user not authenticated")
|
logger.debug("API auth: no access token provided")
|
||||||
return False
|
return False
|
||||||
request_scopes = []
|
tk = Takahe.get_token(token)
|
||||||
|
if not tk:
|
||||||
|
logger.debug("API auth: access token not found")
|
||||||
|
return False
|
||||||
|
request_scope = ""
|
||||||
request_method = request.method
|
request_method = request.method
|
||||||
if request_method in PERMITTED_READ_METHODS:
|
if request_method in PERMITTED_READ_METHODS:
|
||||||
request_scopes = ["read"]
|
request_scope = "read"
|
||||||
elif request_method in PERMITTED_WRITE_METHODS:
|
elif request_method in PERMITTED_WRITE_METHODS:
|
||||||
request_scopes = ["write"]
|
request_scope = "write"
|
||||||
else:
|
else:
|
||||||
|
logger.debug("API auth: unsupported HTTP method")
|
||||||
return False
|
return False
|
||||||
validator = OAuth2Validator()
|
if request_scope not in tk.scopes:
|
||||||
core = OAuthLibCore(Server(validator))
|
logger.debug("API auth: scope not allowed")
|
||||||
valid, oauthlib_req = core.verify_request(request, scopes=request_scopes)
|
return False
|
||||||
if not valid:
|
identity = APIdentity.objects.filter(pk=tk.identity_id).first()
|
||||||
logger.debug(f"API auth: request scope {request_scopes} not verified")
|
if not identity:
|
||||||
return valid
|
logger.debug("API auth: identity not found")
|
||||||
|
return False
|
||||||
|
if identity.deleted:
|
||||||
|
logger.debug("API auth: identity deleted")
|
||||||
|
return False
|
||||||
|
user = identity.user
|
||||||
|
if not user:
|
||||||
|
logger.debug("API auth: user not found")
|
||||||
|
return False
|
||||||
|
request.user = user
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class EmptyResult(Schema):
|
class EmptyResult(Schema):
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<!-- <a href="/pages/rules/">{% trans 'Rules' %}</a> -->
|
<!-- <a href="/pages/rules/">{% trans 'Rules' %}</a> -->
|
||||||
<!-- <a href="/pages/terms/">{% trans 'Terms' %}</a> -->
|
<!-- <a href="/pages/terms/">{% trans 'Terms' %}</a> -->
|
||||||
<a href="{% url 'users:announcements' %}">{% trans 'Announcements' %}</a>
|
<a href="{% url 'users:announcements' %}">{% trans 'Announcements' %}</a>
|
||||||
<a href="{% url 'oauth2_provider:developer' %}">{% trans 'Developer' %}</a>
|
<a href="{% url 'common:developer' %}">{% trans 'Developer' %}</a>
|
||||||
<a title="{{ neodb_version }}"
|
<a title="{{ neodb_version }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
|
|
|
@ -33,11 +33,14 @@
|
||||||
<body>
|
<body>
|
||||||
{% include "_header.html" %}
|
{% include "_header.html" %}
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<h3>
|
<h3>Developer Console</h3>
|
||||||
Developer Console | <a href="{% url 'oauth2_provider:list' %}">{% trans "Your applications" %}</a>
|
<p>
|
||||||
</h3>
|
<li class="empty">
|
||||||
<p class="empty">
|
By using our APIs, you agree to our <a href="/pages/terms">Terms of Service</a>.
|
||||||
By using our APIs, you agree to our <a href="/pages/terms">Terms of Service</a>.
|
</li>
|
||||||
|
<li class="empty">
|
||||||
|
In additional to APIs below, {{ site_name }} also supports a subset of <a href="https://docs.joinmastodon.org/client/intro/">Mastodon APIs</a>.
|
||||||
|
</li>
|
||||||
</p>
|
</p>
|
||||||
<details {% if token %}open{% endif %}>
|
<details {% if token %}open{% endif %}>
|
||||||
<summary>
|
<summary>
|
||||||
|
@ -48,7 +51,8 @@
|
||||||
<input type="text"
|
<input type="text"
|
||||||
readonly
|
readonly
|
||||||
value="{{ token | default:'Once generated, token will only be shown once here, previous tokens will be revoked.' }}">
|
value="{{ token | default:'Once generated, token will only be shown once here, previous tokens will be revoked.' }}">
|
||||||
<input type="submit" value="Generate" />
|
<input type="submit"
|
||||||
|
{% if request.user.is_authenticated %}value="Generate"{% else %}value="Login to enerate" disabled{% endif %} />
|
||||||
</form>
|
</form>
|
||||||
<p>
|
<p>
|
||||||
Click <code>Authorize</code> button below, input your token there to invoke APIs with your account, which is required for APIs like <code>/api/me</code>
|
Click <code>Authorize</code> button below, input your token there to invoke APIs with your account, which is required for APIs like <code>/api/me</code>
|
||||||
|
@ -62,32 +66,7 @@
|
||||||
<summary>
|
<summary>
|
||||||
<a>How to authorize</a>
|
<a>How to authorize</a>
|
||||||
</summary>
|
</summary>
|
||||||
0. Create an application (you must have at least one URL included in the Redirect URIs field, e.g. <code>https://example.org/callback</code>)
|
Please check <a href="https://neodb.net/api/">NeoDB documentation</a> for details.
|
||||||
<br>
|
|
||||||
1. Guide your user to open this URL
|
|
||||||
<input type="text"
|
|
||||||
value="{{ site_url }}/auth/oauth/authorize/?response_type=code&client_id=CLIENT_ID&redirect_uri=https://example.org/callback"
|
|
||||||
readonly>
|
|
||||||
2. Once authorizated by user, it will redirect to <code>https://example.org/callback</code> with a <code>code</code> parameter:
|
|
||||||
<input type="text"
|
|
||||||
value="https://example.org/callback?code=AUTH_CODE"
|
|
||||||
readonly>
|
|
||||||
3. Obtain access token with the following POST request:
|
|
||||||
<textarea readonly rows="7">
|
|
||||||
curl -X POST {{ site_url }}/auth/oauth/token/ \
|
|
||||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
||||||
-d "client_id=CLIENT_ID" \
|
|
||||||
-d "client_secret=CLIENT_SECRET" \
|
|
||||||
-d "code=AUTH_CODE" \
|
|
||||||
-d "redirect_uri=https://example.org/callback" \
|
|
||||||
-d "grant_type=authorization_code"</textarea>
|
|
||||||
and access token will be returned in the response:
|
|
||||||
<textarea readonly rows="1">{"access_token": "ACCESS_TOKEN", "expires_in": 31536000, "token_type": "Bearer", "scope": "read write", "refresh_token": "REFRESH_TOKEN"}</textarea>
|
|
||||||
4. Use the access token to access protected endpoints like <code>/api/me</code>
|
|
||||||
<textarea readonly rows="1">curl -H "Authorization: Bearer ACCESS_TOKEN" -X GET http://localhost:8000/api/me</textarea>
|
|
||||||
and response will be returned accordingly:
|
|
||||||
<textarea readonly rows="1">{"url": "https://neodb.social/users/xxx@yyy.zzz/", "external_acct": "xxx@yyy.zzz", "display_name": "XYZ", "avatar": "https://yyy.zzz/xxx.gif"}</textarea>
|
|
||||||
more endpoints can be found in API Documentation below.
|
|
||||||
</details>
|
</details>
|
||||||
<div id="swagger-ui" data-theme="light"></div>
|
<div id="swagger-ui" data-theme="light"></div>
|
||||||
<script src="{{ cdn_url }}/npm/swagger-ui-dist@5.13.0/swagger-ui-bundle.min.js"></script>
|
<script src="{{ cdn_url }}/npm/swagger-ui-dist@5.13.0/swagger-ui-bundle.min.js"></script>
|
|
@ -8,5 +8,6 @@ urlpatterns = [
|
||||||
path("home/", home, name="home"),
|
path("home/", home, name="home"),
|
||||||
path("me/", me, name="me"),
|
path("me/", me, name="me"),
|
||||||
path("nodeinfo/2.0/", nodeinfo2),
|
path("nodeinfo/2.0/", nodeinfo2),
|
||||||
|
path("developer/", console, name="developer"),
|
||||||
re_path("^~neodb~(?P<uri>.+)", ap_redirect),
|
re_path("^~neodb~(?P<uri>.+)", ap_redirect),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import connection
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from boofilsic import __version__
|
from boofilsic import __version__
|
||||||
from users.models import User
|
from takahe.utils import Takahe
|
||||||
|
|
||||||
|
from .api import api
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -75,3 +75,24 @@ def error_404(request, exception=None):
|
||||||
|
|
||||||
def error_500(request, exception=None):
|
def error_500(request, exception=None):
|
||||||
return render(request, "500.html", status=500, context={"exception": exception})
|
return render(request, "500.html", status=500, context={"exception": exception})
|
||||||
|
|
||||||
|
|
||||||
|
def console(request):
|
||||||
|
token = None
|
||||||
|
if request.method == "POST":
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return redirect(reverse("users:login"))
|
||||||
|
app = Takahe.get_or_create_app(
|
||||||
|
"Dev Console",
|
||||||
|
settings.SITE_INFO["site_url"],
|
||||||
|
"",
|
||||||
|
owner_pk=0,
|
||||||
|
client_id="app-00000000000-dev",
|
||||||
|
)
|
||||||
|
token = Takahe.refresh_token(app, request.user.identity.pk, request.user.pk)
|
||||||
|
context = {
|
||||||
|
"api": api,
|
||||||
|
"token": token,
|
||||||
|
"openapi_json_url": reverse(f"{api.urls_namespace}:openapi-json"),
|
||||||
|
}
|
||||||
|
return render(request, "console.html", context)
|
||||||
|
|
|
@ -61,6 +61,7 @@ x-shared:
|
||||||
TAKAHE_MEDIA_BACKEND: local://www/media/
|
TAKAHE_MEDIA_BACKEND: local://www/media/
|
||||||
TAKAHE_MEDIA_ROOT: /www/media
|
TAKAHE_MEDIA_ROOT: /www/media
|
||||||
TAKAHE_USE_PROXY_HEADERS: true
|
TAKAHE_USE_PROXY_HEADERS: true
|
||||||
|
TAKAHE_ALLOW_USER_MIGRATION: true
|
||||||
TAKAHE_STATOR_CONCURRENCY: ${TAKAHE_STATOR_CONCURRENCY:-4}
|
TAKAHE_STATOR_CONCURRENCY: ${TAKAHE_STATOR_CONCURRENCY:-4}
|
||||||
TAKAHE_STATOR_CONCURRENCY_PER_MODEL: ${TAKAHE_STATOR_CONCURRENCY_PER_MODEL:-2}
|
TAKAHE_STATOR_CONCURRENCY_PER_MODEL: ${TAKAHE_STATOR_CONCURRENCY_PER_MODEL:-2}
|
||||||
TAKAHE_VAPID_PUBLIC_KEY:
|
TAKAHE_VAPID_PUBLIC_KEY:
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class DeveloperConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "developer"
|
|
|
@ -1,128 +0,0 @@
|
||||||
# Generated by Django 3.2.19 on 2023-06-28 05:09
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
import django.db.models.deletion
|
|
||||||
import markdownx.models
|
|
||||||
import oauth2_provider.generators
|
|
||||||
import oauth2_provider.models
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, 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,
|
|
||||||
validators=[
|
|
||||||
django.core.validators.RegexValidator(
|
|
||||||
message="minimum two characters, words and -_. only, no special characters",
|
|
||||||
regex="^\\w[\\w_\\-. ]*\\w$",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"description",
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Generated by Django 4.2.3 on 2023-07-06 22:53
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
("developer", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="application",
|
|
||||||
name="user",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="%(app_label)s_%(class)s",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 4.2.3 on 2023-07-13 04:02
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("developer", "0002_alter_application_user"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="application",
|
|
||||||
name="redirect_uris",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="Allowed URIs list, space separated, at least one URI is required"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,33 +0,0 @@
|
||||||
from django.core.validators import RegexValidator
|
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from markdownx.models import MarkdownxField
|
|
||||||
from oauth2_provider.models import AbstractApplication
|
|
||||||
|
|
||||||
from journal.models.renderers import render_md
|
|
||||||
|
|
||||||
|
|
||||||
class Application(AbstractApplication):
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
blank=False,
|
|
||||||
validators=[
|
|
||||||
RegexValidator(
|
|
||||||
regex=r"^\w[\w_\-. ]*\w$",
|
|
||||||
message=_(
|
|
||||||
"minimum two characters, words and -_. only, no special characters"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
description = MarkdownxField(default="", blank=True)
|
|
||||||
url = models.URLField(null=True, blank=True)
|
|
||||||
is_official = models.BooleanField(default=False)
|
|
||||||
unique_together = [["user", "name"]]
|
|
||||||
redirect_uris = models.TextField(
|
|
||||||
blank=False,
|
|
||||||
help_text=_("Allowed URIs list, space separated, at least one URI is required"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def description_html(self):
|
|
||||||
return render_md(self.description)
|
|
|
@ -1,45 +0,0 @@
|
||||||
{% extends "oauth2_provider/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="block-center">
|
|
||||||
<h3 class="block-center-heading">{{ application.name }}</h3>
|
|
||||||
<ul class="unstyled">
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
<b>{% trans "Client ID" %}</b>
|
|
||||||
</p>
|
|
||||||
<input class="input-block-level"
|
|
||||||
type="text"
|
|
||||||
value="{{ application.client_id }}"
|
|
||||||
readonly>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
<b>{% trans "URL" %}</b>
|
|
||||||
</p>
|
|
||||||
<p>{{ application.url | default:"" | urlize }}</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
<b>{% trans "Description" %}</b>
|
|
||||||
</p>
|
|
||||||
<p>{{ application.description_html|safe }}</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
<b>{% trans "Redirect Uris" %}</b>
|
|
||||||
{% if not application.redirect_uris %}WARNING: no redirect uris have been set, authorization may not work.{% endif %}
|
|
||||||
</p>
|
|
||||||
<textarea readonly>{{ application.redirect_uris }}</textarea>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
<div class="btn-toolbar">
|
|
||||||
<a class="btn" href="{% url "oauth2_provider:list" %}">{% trans "Go Back" %}</a>
|
|
||||||
<a class="btn btn-primary"
|
|
||||||
href="{% url "oauth2_provider:update" application.id %}">{% trans "Edit" %}</a>
|
|
||||||
<a class="btn btn-danger"
|
|
||||||
href="{% url "oauth2_provider:delete" application.id %}">{% trans "Delete" %}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,39 +0,0 @@
|
||||||
{% extends "oauth2_provider/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block content %}
|
|
||||||
<form method="post"
|
|
||||||
action="{% block app-form-action-url %}{% url 'oauth2_provider:update' application.id %}{% endblock app-form-action-url %}">
|
|
||||||
<h3>
|
|
||||||
{% block app-form-title %}
|
|
||||||
{% trans "Edit application" %} {{ application.name }}
|
|
||||||
{% endblock app-form-title %}
|
|
||||||
</h3>
|
|
||||||
{% csrf_token %}
|
|
||||||
<fieldset>
|
|
||||||
{% for field in form %}
|
|
||||||
<label {% if field.errors %}aria-invalid="true"{% endif %}>
|
|
||||||
{{ field.label }}
|
|
||||||
{% if field.name == 'client_secret' %}
|
|
||||||
<small><b>(please save it properly as it will NOT be editable or shown again)</b></small>
|
|
||||||
{% elif field.name == 'description' %}
|
|
||||||
<small>(markdown syntax supported)</small>
|
|
||||||
{% endif %}
|
|
||||||
{{ field }}
|
|
||||||
{% for error in field.errors %}<small>{{ error }}</small>{% endfor %}
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
|
||||||
</fieldset>
|
|
||||||
<div class="control-group {% if form.non_field_errors %}error{% endif %}">
|
|
||||||
{% for error in form.non_field_errors %}<span class="help-inline">{{ error }}</span>{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="control-group">
|
|
||||||
<div class="controls">
|
|
||||||
<a class="btn"
|
|
||||||
href="{% block app-form-back-url %}{% url "oauth2_provider:detail" application.id %}{% endblock app-form-back-url %}">
|
|
||||||
{% trans "Go Back" %}
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
|
@ -1,27 +0,0 @@
|
||||||
{% 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 not request.user.mastodon_acct %}
|
|
||||||
<p>
|
|
||||||
Please <a href="{% url 'users:info' %}">connect to a Fediverse identity</a> before creating an application.
|
|
||||||
</p>
|
|
||||||
{% elif 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 %}
|
|
|
@ -1,41 +0,0 @@
|
||||||
{% 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="{{ application.user.identity.url }}">@{{ application.user.identity.handle }}</a> 创建和维护的应用程序。
|
|
||||||
{{ site_name }}无法保证其安全性和有效性,请自行验证确认后再授权。
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if application.url %}应用网址: {{ application.url | urlize }}{% endif %}
|
|
||||||
<p>{{ application.description_html|safe }}</p>
|
|
||||||
{% 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 %}
|
|
|
@ -1,25 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
<!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" %}
|
|
||||||
<style>
|
|
||||||
.controls input {
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% include "_header.html" %}
|
|
||||||
<main class="container">
|
|
||||||
{% block content %}
|
|
||||||
{% endblock content %}
|
|
||||||
</main>
|
|
||||||
{% include "_footer.html" %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
|
@ -1,69 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth import views as auth_views
|
|
||||||
from django.urls import include, path, re_path
|
|
||||||
from oauth2_provider import views as oauth2_views
|
|
||||||
from oauth2_provider.views import oidc as oidc_views
|
|
||||||
|
|
||||||
from .views import *
|
|
||||||
|
|
||||||
_urlpatterns = [
|
|
||||||
re_path(
|
|
||||||
r"^auth/oauth/authorize/$",
|
|
||||||
oauth2_views.AuthorizationView.as_view(),
|
|
||||||
name="authorize",
|
|
||||||
),
|
|
||||||
re_path(r"^oauth/token/$", oauth2_views.TokenView.as_view(), name="token"),
|
|
||||||
re_path(
|
|
||||||
r"^auth/oauth/revoke_token/$",
|
|
||||||
oauth2_views.RevokeTokenView.as_view(),
|
|
||||||
name="revoke-token",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^auth/oauth/introspect/$",
|
|
||||||
oauth2_views.IntrospectTokenView.as_view(),
|
|
||||||
name="introspect",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^auth/oauth/authorized_tokens/$",
|
|
||||||
oauth2_views.AuthorizedTokensListView.as_view(),
|
|
||||||
name="authorized-token-list",
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r"^auth/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/$",
|
|
||||||
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/$",
|
|
||||||
ApplicationUpdate.as_view(),
|
|
||||||
name="update",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("", include((_urlpatterns, "oauth2_provider"))),
|
|
||||||
]
|
|
|
@ -1,90 +0,0 @@
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.forms.models import modelform_factory
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
|
||||||
from loguru import logger
|
|
||||||
from oauth2_provider.forms import AllowForm
|
|
||||||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
|
||||||
from oauth2_provider.models import AccessToken, RefreshToken, get_application_model
|
|
||||||
from oauth2_provider.settings import oauth2_settings
|
|
||||||
from oauth2_provider.views import ApplicationRegistration as BaseApplicationRegistration
|
|
||||||
from oauth2_provider.views import ApplicationUpdate as BaseApplicationUpdate
|
|
||||||
from oauth2_provider.views.base import AuthorizationView as BaseAuthorizationView
|
|
||||||
from oauthlib.common import generate_token
|
|
||||||
|
|
||||||
from common.api import api
|
|
||||||
|
|
||||||
from .models import Application
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationRegistration(BaseApplicationRegistration):
|
|
||||||
def get_form_class(self):
|
|
||||||
return modelform_factory(
|
|
||||||
Application,
|
|
||||||
fields=(
|
|
||||||
"name",
|
|
||||||
"url",
|
|
||||||
"description",
|
|
||||||
"client_secret",
|
|
||||||
"redirect_uris",
|
|
||||||
# "post_logout_redirect_uris",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
form.instance.user = self.request.user
|
|
||||||
if not form.instance.id:
|
|
||||||
form.instance.client_id = generate_client_id()
|
|
||||||
# form.instance.client_secret = generate_client_secret()
|
|
||||||
form.instance.client_type = Application.CLIENT_CONFIDENTIAL
|
|
||||||
form.instance.authorization_grant_type = (
|
|
||||||
Application.GRANT_AUTHORIZATION_CODE
|
|
||||||
)
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationUpdate(BaseApplicationUpdate):
|
|
||||||
def get_form_class(self):
|
|
||||||
return modelform_factory(
|
|
||||||
get_application_model(), # type:ignore
|
|
||||||
fields=(
|
|
||||||
"name",
|
|
||||||
"url",
|
|
||||||
"description",
|
|
||||||
# "client_secret",
|
|
||||||
"redirect_uris",
|
|
||||||
# "post_logout_redirect_uris",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
79
docs/api.md
Normal file
79
docs/api.md
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
# API
|
||||||
|
|
||||||
|
NeoDB has a set of API endpoints mapping to its functions like marking a book or listing collections, they can be found in swagger based API documentation at `/developer/` of your running instance, [a version of it](https://neodb.social/developer/) is available on our flagship instance.
|
||||||
|
|
||||||
|
NeoDB also supports a subset of Mastodon API, details can be found in [Mastodon API documentation](https://docs.joinmastodon.org/api/).
|
||||||
|
|
||||||
|
Both set of APIs can be accessed by the same access token.
|
||||||
|
|
||||||
|
## How to authorize
|
||||||
|
|
||||||
|
0. Create an application
|
||||||
|
|
||||||
|
you must have at least one URL included in the Redirect URIs field, e.g. `https://example.org/callback`, or use `urn:ietf:wg:oauth:2.0:oob` if you don't have a callback URL.
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://neodb.social/api/v1/apps \
|
||||||
|
-d client_name=MyApp \
|
||||||
|
-d redirect_uris=https://example.org/callback \
|
||||||
|
-d website=https://my.site
|
||||||
|
```
|
||||||
|
|
||||||
|
and save of the `client_id` and `client_secret` returned in the response:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"client_id": "CLIENT_ID",
|
||||||
|
"client_secret": "CLIENT_SECRET",
|
||||||
|
"name": "MyApp",
|
||||||
|
"redirect_uri": "https://example.org/callback",
|
||||||
|
"vapid_key": "PUSH_KEY",
|
||||||
|
"website": "https://my.site"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
1. Guide your user to open this URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://neodb.social/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https://example.org/callback&scope=read+write
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Once authorizated by user, it will redirect to `https://example.org/callback` with a `code` parameter:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://example.org/callback?code=AUTH_CODE
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Obtain access token with the following POST request:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://neodb.social/oauth/token \
|
||||||
|
-d "client_id=CLIENT_ID" \
|
||||||
|
-d "client_secret=CLIENT_SECRET" \
|
||||||
|
-d "code=AUTH_CODE" \
|
||||||
|
-d "redirect_uri=https://example.org/callback" \
|
||||||
|
-d "grant_type=authorization_code"
|
||||||
|
```
|
||||||
|
|
||||||
|
and access token will be returned in the response:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"access_token": "ACCESS_TOKEN",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"scope": "read write"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Use the access token to access protected endpoints like `/api/me`
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -H "Authorization: Bearer ACCESS_TOKEN" -X GET https://neodb.social/api/me
|
||||||
|
```
|
||||||
|
|
||||||
|
and response will be returned accordingly:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"url": "https://neodb.social/users/xxx/", "external_acct": "xxx@yyy.zzz", "display_name": "XYZ", "avatar": "https://yyy.zzz/xxx.gif"}
|
||||||
|
```
|
|
@ -22,8 +22,7 @@ All instances interact with each other in the Fediverse via ActivityPub, allowin
|
||||||
|
|
||||||
## API and Development
|
## API and Development
|
||||||
|
|
||||||
- NeoDB offers an API to manage user collections, with swagger-based documentation available in the [NeoDB API Developer Console](https://neodb.social/developer/).
|
- NeoDB offers [APIs to manage user collections](api.md), and [Mastodon client compatible API](https://docs.joinmastodon.org/client/) to manage user posts.
|
||||||
- [Mastodon client compatible API](https://docs.joinmastodon.org/client/) is also available
|
|
||||||
- For those interested in developing for NeoDB, please refer to the [development](development.md) section for basic instructions to get started.
|
- For those interested in developing for NeoDB, please refer to the [development](development.md) section for basic instructions to get started.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden"
|
overflow: hidden"
|
||||||
{% if k == "wishlist" or k == "dropped" %} _="on click add .outline to .status-selector then remove .outline from me then set value of #mark-status to '{{ k }}' then add .hidden to .rating-editor then halt" {% else %} _="on click add .outline to .status-selector then remove .outline from me then set value of #mark-status to '{{ k }}' then remove .hidden from .rating-editor then halt" {% endif %}
|
{% if k == "wishlist" %} _="on click add .outline to .status-selector then remove .outline from me then set value of #mark-status to '{{ k }}' then add .hidden to .rating-editor then halt" {% else %} _="on click add .outline to .status-selector then remove .outline from me then set value of #mark-status to '{{ k }}' then remove .hidden from .rating-editor then halt" {% endif %}
|
||||||
{% if shelf_type == k %}class="status-selector"{% else %}class="status-selector outline"{% endif %}>
|
{% if shelf_type == k %}class="status-selector"{% else %}class="status-selector outline"{% endif %}>
|
||||||
{{ v }}
|
{{ v }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -11,6 +11,7 @@ nav:
|
||||||
- configuration.md
|
- configuration.md
|
||||||
- troubleshooting.md
|
- troubleshooting.md
|
||||||
- development.md
|
- development.md
|
||||||
|
- api.md
|
||||||
- origin.md
|
- origin.md
|
||||||
theme:
|
theme:
|
||||||
logo: assets/logo.svg
|
logo: assets/logo.svg
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1522364d4fd399ba2560656e7e714ee7cea7a905
|
Subproject commit 01cacb7dce19f7fadcda7e629b78b2752d74eece
|
110
takahe/models.py
110
takahe/models.py
|
@ -2103,3 +2103,113 @@ class Announcement(models.Model):
|
||||||
from journal.models import render_md
|
from journal.models import render_md
|
||||||
|
|
||||||
return mark_safe(render_md(self.text))
|
return mark_safe(render_md(self.text))
|
||||||
|
|
||||||
|
|
||||||
|
class Application(models.Model):
|
||||||
|
"""
|
||||||
|
OAuth applications
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "api_application"
|
||||||
|
|
||||||
|
client_id = models.CharField(max_length=500)
|
||||||
|
client_secret = models.CharField(max_length=500)
|
||||||
|
|
||||||
|
redirect_uris = models.TextField()
|
||||||
|
scopes = models.TextField()
|
||||||
|
|
||||||
|
name = models.CharField(max_length=500)
|
||||||
|
website = models.CharField(max_length=500, blank=True, null=True)
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Authorization(models.Model):
|
||||||
|
"""
|
||||||
|
An authorization code as part of the OAuth flow
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "api_authorization"
|
||||||
|
|
||||||
|
application = models.ForeignKey(
|
||||||
|
"takahe.Application",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="authorizations",
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"takahe.User",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="authorizations",
|
||||||
|
)
|
||||||
|
|
||||||
|
identity = models.ForeignKey(
|
||||||
|
"takahe.Identity",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="authorizations",
|
||||||
|
)
|
||||||
|
|
||||||
|
code = models.CharField(max_length=128, blank=True, null=True, unique=True)
|
||||||
|
token = models.OneToOneField(
|
||||||
|
"takahe.Token",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
scopes = models.JSONField()
|
||||||
|
redirect_uri = models.TextField(blank=True, null=True)
|
||||||
|
valid_for_seconds = models.IntegerField(default=60)
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Token(models.Model):
|
||||||
|
"""
|
||||||
|
An (access) token to call the API with.
|
||||||
|
|
||||||
|
Can be either tied to a user, or app-level only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "api_token"
|
||||||
|
|
||||||
|
identity_id: int | None
|
||||||
|
application = models.ForeignKey(
|
||||||
|
"takahe.Application",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="tokens",
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"takahe.User",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="tokens",
|
||||||
|
)
|
||||||
|
|
||||||
|
identity = models.ForeignKey(
|
||||||
|
"takahe.Identity",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="tokens",
|
||||||
|
)
|
||||||
|
|
||||||
|
token = models.CharField(max_length=500, unique=True)
|
||||||
|
scopes = models.JSONField()
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
revoked = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
# push_subscription: "PushSubscription"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import blurhash
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.files.images import ImageFile
|
from django.core.files.images import ImageFile
|
||||||
|
from django.core.signing import b62_encode
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
@ -1010,3 +1011,50 @@ class Takahe:
|
||||||
if featured:
|
if featured:
|
||||||
identity.fanout("tag_unfeatured", subject_hashtag_id=hashtag)
|
identity.fanout("tag_unfeatured", subject_hashtag_id=hashtag)
|
||||||
featured.delete()
|
featured.delete()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_or_create_app(
|
||||||
|
name: str,
|
||||||
|
website: str,
|
||||||
|
redirect_uris: str,
|
||||||
|
owner_pk: int,
|
||||||
|
scopes: str = "read write follow",
|
||||||
|
client_id: str | None = None,
|
||||||
|
):
|
||||||
|
client_id = client_id or (
|
||||||
|
"app-" + b62_encode(owner_pk).zfill(11) + "-" + secrets.token_urlsafe(16)
|
||||||
|
)
|
||||||
|
client_secret = secrets.token_urlsafe(40)
|
||||||
|
return Application.objects.get_or_create(
|
||||||
|
client_id=client_id,
|
||||||
|
defaults={
|
||||||
|
"name": name,
|
||||||
|
"website": website,
|
||||||
|
"client_secret": client_secret,
|
||||||
|
"redirect_uris": redirect_uris,
|
||||||
|
"scopes": scopes,
|
||||||
|
},
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_apps(owner_pk: int):
|
||||||
|
return Application.objects.filter(
|
||||||
|
name__startswith="app-" + b62_encode(owner_pk).zfill(11)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def refresh_token(app: Application, owner_pk: int, user_pk) -> str:
|
||||||
|
tk = Token.objects.filter(application=app, identity_id=owner_pk).first()
|
||||||
|
if tk:
|
||||||
|
tk.delete()
|
||||||
|
return Token.objects.create(
|
||||||
|
application=app,
|
||||||
|
identity_id=owner_pk,
|
||||||
|
user_id=user_pk,
|
||||||
|
scopes=["read", "write"],
|
||||||
|
token=secrets.token_urlsafe(43),
|
||||||
|
).token
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_token(token: str) -> Token | None:
|
||||||
|
return Token.objects.filter(token=token).first()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
from ninja.security import django_auth
|
from ninja.security import django_auth
|
||||||
from oauth2_provider.decorators import protected_resource
|
|
||||||
|
|
||||||
from common.api import *
|
from common.api import *
|
||||||
|
|
||||||
|
|
|
@ -214,7 +214,7 @@
|
||||||
<details>
|
<details>
|
||||||
<summary>{% trans 'Applications' %}</summary>
|
<summary>{% trans 'Applications' %}</summary>
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'oauth2_provider:authorized-token-list' %}">{% trans "View authorized applications" %}</a>
|
<a href="/@{{ request.user.identity.handle }}/settings/tokens/">{% trans "View authorized applications" %}</a>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
</article>
|
</article>
|
||||||
|
|
Loading…
Add table
Reference in a new issue