mirror of
https://github.com/jointakahe/takahe.git
synced 2025-02-16 15:45:14 +00:00
Add apps settings pages
This commit is contained in:
parent
55de31e3de
commit
32e74a620d
14 changed files with 279 additions and 34 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
import secrets
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,3 +19,23 @@ class Application(models.Model):
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
updated = models.DateTimeField(auto_now=True)
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
client_name: str,
|
||||||
|
redirect_uris: str,
|
||||||
|
website: str | None,
|
||||||
|
scopes: str | None = None,
|
||||||
|
):
|
||||||
|
client_id = "tk-" + secrets.token_urlsafe(16)
|
||||||
|
client_secret = secrets.token_urlsafe(40)
|
||||||
|
|
||||||
|
return cls.objects.create(
|
||||||
|
name=client_name,
|
||||||
|
website=website,
|
||||||
|
client_id=client_id,
|
||||||
|
client_secret=client_secret,
|
||||||
|
redirect_uris=redirect_uris,
|
||||||
|
scopes=scopes or "read",
|
||||||
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import urlman
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +38,9 @@ class Token(models.Model):
|
||||||
updated = models.DateTimeField(auto_now=True)
|
updated = models.DateTimeField(auto_now=True)
|
||||||
revoked = models.DateTimeField(blank=True, null=True)
|
revoked = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
class urls(urlman.Urls):
|
||||||
|
edit = "/@{self.identity.handle}/settings/tokens/{self.id}/"
|
||||||
|
|
||||||
def has_scope(self, scope: str):
|
def has_scope(self, scope: str):
|
||||||
"""
|
"""
|
||||||
Returns if this token has the given scope.
|
Returns if this token has the given scope.
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import secrets
|
|
||||||
|
|
||||||
from hatchway import QueryOrBody, api_view
|
from hatchway import QueryOrBody, api_view
|
||||||
|
|
||||||
from .. import schemas
|
from .. import schemas
|
||||||
|
@ -14,14 +12,10 @@ def add_app(
|
||||||
scopes: QueryOrBody[None | str] = None,
|
scopes: QueryOrBody[None | str] = None,
|
||||||
website: QueryOrBody[None | str] = None,
|
website: QueryOrBody[None | str] = None,
|
||||||
) -> schemas.Application:
|
) -> schemas.Application:
|
||||||
client_id = "tk-" + secrets.token_urlsafe(16)
|
application = Application.create(
|
||||||
client_secret = secrets.token_urlsafe(40)
|
client_name=client_name,
|
||||||
application = Application.objects.create(
|
|
||||||
name=client_name,
|
|
||||||
website=website,
|
website=website,
|
||||||
client_id=client_id,
|
|
||||||
client_secret=client_secret,
|
|
||||||
redirect_uris=redirect_uris,
|
redirect_uris=redirect_uris,
|
||||||
scopes=scopes or "read",
|
scopes=scopes,
|
||||||
)
|
)
|
||||||
return schemas.Application.from_orm(application)
|
return schemas.Application.from_orm(application)
|
||||||
|
|
|
@ -292,6 +292,6 @@ class Config(models.Model):
|
||||||
class DomainOptions(pydantic.BaseModel):
|
class DomainOptions(pydantic.BaseModel):
|
||||||
|
|
||||||
site_name: str = ""
|
site_name: str = ""
|
||||||
site_icon: UploadedImage | None = None # type: ignore
|
site_icon: UploadedImage | None = None
|
||||||
hide_login: bool = False
|
hide_login: bool = False
|
||||||
custom_css: str = ""
|
custom_css: str = ""
|
||||||
|
|
|
@ -278,6 +278,7 @@ header menu a small {
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
padding: 10px 0px 20px 5px;
|
padding: 10px 0px 20px 5px;
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav hr {
|
nav hr {
|
||||||
|
@ -377,7 +378,6 @@ nav .identity-banner a:hover {
|
||||||
.settings nav {
|
.settings nav {
|
||||||
width: var(--width-sidebar-medium);
|
width: var(--width-sidebar-medium);
|
||||||
background: var(--color-bg-menu);
|
background: var(--color-bg-menu);
|
||||||
border-radius: 0 0 5px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings nav h2 {
|
.settings nav h2 {
|
||||||
|
@ -1095,6 +1095,25 @@ blockquote {
|
||||||
border-left: 2px solid var(--color-bg-menu);
|
border-left: 2px solid var(--color-bg-menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secret .label {
|
||||||
|
background-color: var(--color-bg-menu);
|
||||||
|
padding: 3px 7px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret.visible .label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret .value {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret.visible .value {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Logged out homepage */
|
/* Logged out homepage */
|
||||||
|
|
||||||
|
@ -1301,9 +1320,10 @@ table.metadata td .emoji {
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Announcements */
|
/* Announcements/Flash messages */
|
||||||
|
|
||||||
.announcement {
|
.announcement,
|
||||||
|
.message {
|
||||||
background-color: var(--color-highlight);
|
background-color: var(--color-highlight);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 10px 0 0 0;
|
margin: 10px 0 0 0;
|
||||||
|
@ -1311,7 +1331,12 @@ table.metadata td .emoji {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.announcement .dismiss {
|
.message {
|
||||||
|
background-color: var(--color-bg-menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement .dismiss,
|
||||||
|
.message .dismiss {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import signal
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync, sync_to_async
|
from asgiref.sync import async_to_sync, sync_to_async
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -21,7 +22,7 @@ class LoopingTask:
|
||||||
copy running at a time.
|
copy running at a time.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, callable):
|
def __init__(self, callable: Callable):
|
||||||
self.callable = callable
|
self.callable = callable
|
||||||
self.task: asyncio.Task | None = None
|
self.task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
|
|
@ -2,25 +2,12 @@ from django.conf import settings as djsettings
|
||||||
from django.contrib import admin as djadmin
|
from django.contrib import admin as djadmin
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
|
|
||||||
from activities.views import (
|
from activities.views import compose, debug, posts, timelines
|
||||||
compose,
|
|
||||||
debug,
|
|
||||||
posts,
|
|
||||||
timelines,
|
|
||||||
)
|
|
||||||
from api.views import oauth
|
from api.views import oauth
|
||||||
from core import views as core
|
from core import views as core
|
||||||
from mediaproxy import views as mediaproxy
|
from mediaproxy import views as mediaproxy
|
||||||
from stator import views as stator
|
from stator import views as stator
|
||||||
from users.views import (
|
from users.views import activitypub, admin, announcements, auth, identity, settings
|
||||||
activitypub,
|
|
||||||
admin,
|
|
||||||
announcements,
|
|
||||||
auth,
|
|
||||||
identity,
|
|
||||||
settings,
|
|
||||||
)
|
|
||||||
from users.views.settings import follows
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", core.homepage),
|
path("", core.homepage),
|
||||||
|
@ -78,6 +65,21 @@ urlpatterns = [
|
||||||
settings.CsvFollowers.as_view(),
|
settings.CsvFollowers.as_view(),
|
||||||
name="settings_export_followers_csv",
|
name="settings_export_followers_csv",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"@<handle>/settings/tokens/",
|
||||||
|
settings.TokensRoot.as_view(),
|
||||||
|
name="settings_tokens",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"@<handle>/settings/tokens/create/",
|
||||||
|
settings.TokenCreate.as_view(),
|
||||||
|
name="settings_token_create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"@<handle>/settings/tokens/<pk>/",
|
||||||
|
settings.TokenEdit.as_view(),
|
||||||
|
name="settings_token_edit",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"admin/",
|
"admin/",
|
||||||
admin.AdminRoot.as_view(),
|
admin.AdminRoot.as_view(),
|
||||||
|
|
|
@ -4,3 +4,13 @@
|
||||||
{{ announcement.html }}
|
{{ announcement.html }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul class="messages">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="message {{ message.tags }}">
|
||||||
|
<a class="dismiss" title="Dismiss" _="on click remove closest <div/>"><i class="fa-solid fa-xmark"></i></a>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||||
<span>Import/Export</span>
|
<span>Import/Export</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url "settings_tokens" handle=identity.handle %}" {% if section == "tokens" %}class="selected"{% endif %} title="Interface">
|
||||||
|
<i class="fa-solid fa-window-restore"></i>
|
||||||
|
<span>Authorized Apps</span>
|
||||||
|
</a>
|
||||||
<hr>
|
<hr>
|
||||||
<h3>Tools</h3>
|
<h3>Tools</h3>
|
||||||
<a href="{% url "settings_follows" handle=identity.handle %}" {% if section == "follows" %}class="selected"{% endif %} title="Follows">
|
<a href="{% url "settings_follows" handle=identity.handle %}" {% if section == "follows" %}class="selected"{% endif %} title="Follows">
|
||||||
|
|
27
templates/settings/token_create.html
Normal file
27
templates/settings/token_create.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "settings/base.html" %}
|
||||||
|
|
||||||
|
{% block subtitle %}Add App{% endblock %}
|
||||||
|
|
||||||
|
{% block settings_content %}
|
||||||
|
<form action="." method="POST">
|
||||||
|
<h1>Create Personal Token</h1>
|
||||||
|
<p>
|
||||||
|
This lets you create a personal application token that you can
|
||||||
|
use to talk to the API.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>Do not create a token if someone else tells you to - it can let
|
||||||
|
them take over your account!</b>
|
||||||
|
</p>
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>App Details</legend>
|
||||||
|
{% include "forms/_field.html" with field=form.name %}
|
||||||
|
{% include "forms/_field.html" with field=form.scope %}
|
||||||
|
</fieldset>
|
||||||
|
<div class="buttons">
|
||||||
|
<a href="{% url "settings_tokens" handle=identity.handle %}" class="button secondary left">Back</a>
|
||||||
|
<button>Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
49
templates/settings/token_edit.html
Normal file
49
templates/settings/token_edit.html
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{% extends "settings/base.html" %}
|
||||||
|
|
||||||
|
{% block subtitle %}Token{% endblock %}
|
||||||
|
|
||||||
|
{% block settings_content %}
|
||||||
|
<h1>{{ token.application.name }}</h1>
|
||||||
|
<form action="." method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>Application</legend>
|
||||||
|
<table class="metadata">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<td>{{ token.application.name }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if token.application.website %}
|
||||||
|
<tr>
|
||||||
|
<th>Website</th>
|
||||||
|
<td>{{ token.application.website }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th>Authorized</th>
|
||||||
|
<td>{{ token.created }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Token</legend>
|
||||||
|
<table class="metadata">
|
||||||
|
<tr>
|
||||||
|
<th>Access Token</th>
|
||||||
|
<td>
|
||||||
|
<span class="secret" _="on click add .visible to me">
|
||||||
|
<span class="label">Click to reveal</span>
|
||||||
|
<tt class="value">{{ token.token }}</tt>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<a href="{% url "settings_tokens" handle=identity.handle %}" class="button secondary left">Back</a>
|
||||||
|
<button class="danger">Remove Access</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
30
templates/settings/tokens.html
Normal file
30
templates/settings/tokens.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends "settings/base.html" %}
|
||||||
|
{% load activity_tags %}
|
||||||
|
|
||||||
|
{% block subtitle %}Apps{% endblock %}
|
||||||
|
|
||||||
|
{% block settings_content %}
|
||||||
|
<div class="view-options">
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<a href="{% url "settings_token_create" handle=identity.handle %}" class="button"><i class="fa-solid fa-plus"></i> Create Personal Token</a>
|
||||||
|
</div>
|
||||||
|
<table class="items">
|
||||||
|
{% for token in tokens %}
|
||||||
|
<tr>
|
||||||
|
<td class="icon">
|
||||||
|
<a href="{{ token.urls.edit }}" class="overlay"></a>
|
||||||
|
<i class="fa-solid fa-window-restore"></i>
|
||||||
|
</td>
|
||||||
|
<td class="name">
|
||||||
|
<a href="{{ token.urls.edit }}" class="overlay"></a>
|
||||||
|
{{ token.application.name }}
|
||||||
|
</td>
|
||||||
|
<td class="stat">
|
||||||
|
Created {{ token.created|timedeltashort }} ago
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr class="empty"><td>You have no apps authorized against this identity.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
|
@ -1,19 +1,20 @@
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import redirect
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.shortcuts import redirect
|
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from users.views.settings.follows import FollowsPage # noqa
|
||||||
from users.views.settings.import_export import ( # noqa
|
from users.views.settings.import_export import ( # noqa
|
||||||
CsvFollowers,
|
CsvFollowers,
|
||||||
CsvFollowing,
|
CsvFollowing,
|
||||||
ImportExportPage,
|
ImportExportPage,
|
||||||
)
|
)
|
||||||
|
from users.views.settings.interface import InterfacePage # noqa
|
||||||
from users.views.settings.posting import PostingPage # noqa
|
from users.views.settings.posting import PostingPage # noqa
|
||||||
from users.views.settings.profile import ProfilePage # noqa
|
from users.views.settings.profile import ProfilePage # noqa
|
||||||
from users.views.settings.security import SecurityPage # noqa
|
from users.views.settings.security import SecurityPage # noqa
|
||||||
from users.views.settings.settings_page import SettingsPage # noqa
|
from users.views.settings.settings_page import SettingsPage # noqa
|
||||||
from users.views.settings.follows import FollowsPage # noqa
|
from users.views.settings.tokens import TokenCreate, TokenEdit, TokensRoot # noqa
|
||||||
from users.views.settings.interface import InterfacePage # noqa
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(login_required, name="dispatch")
|
@method_decorator(login_required, name="dispatch")
|
||||||
|
|
76
users/views/settings/tokens.py
Normal file
76
users/views/settings/tokens.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.views.generic import DetailView, FormView
|
||||||
|
from django.views.generic.list import ListView
|
||||||
|
|
||||||
|
from api.models.application import Application
|
||||||
|
from api.models.token import Token
|
||||||
|
from users.views.base import IdentityViewMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TokensRoot(IdentityViewMixin, ListView):
|
||||||
|
"""
|
||||||
|
Shows a listing of tokens the user has authorized
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = "settings/tokens.html"
|
||||||
|
extra_context = {"section": "tokens"}
|
||||||
|
context_object_name = "tokens"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Token.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
identity=self.identity,
|
||||||
|
).prefetch_related("application")
|
||||||
|
|
||||||
|
|
||||||
|
class TokenCreate(IdentityViewMixin, FormView):
|
||||||
|
"""
|
||||||
|
Allows the user to create a new app and token just for themselves.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = "settings/token_create.html"
|
||||||
|
|
||||||
|
class form_class(forms.Form):
|
||||||
|
name = forms.CharField(help_text="Identifies this app in your app list")
|
||||||
|
scope = forms.ChoiceField(
|
||||||
|
choices=(("read", "Read-only access"), ("write", "Full access")),
|
||||||
|
help_text="What should this app be able to do with your account?",
|
||||||
|
)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
scopes = "read write push" if form.cleaned_data["scope"] == "write" else "read"
|
||||||
|
application = Application.create(
|
||||||
|
client_name=form.cleaned_data["name"],
|
||||||
|
website=None,
|
||||||
|
redirect_uris="urn:ietf:wg:oauth:2.0:oob",
|
||||||
|
scopes=scopes,
|
||||||
|
)
|
||||||
|
token = Token.objects.create(
|
||||||
|
application=application,
|
||||||
|
user=self.request.user,
|
||||||
|
identity=self.identity,
|
||||||
|
token=secrets.token_urlsafe(43),
|
||||||
|
scopes=scopes,
|
||||||
|
)
|
||||||
|
return redirect("settings_token_edit", handle=self.identity.handle, pk=token.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenEdit(IdentityViewMixin, DetailView):
|
||||||
|
|
||||||
|
template_name = "settings/token_edit.html"
|
||||||
|
extra_context = {"section": "tokens"}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.identity.tokens
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
messages.success(
|
||||||
|
request, f"{self.object.application.name}'s access has been removed."
|
||||||
|
)
|
||||||
|
self.object.delete()
|
||||||
|
return redirect("settings_tokens", handle=self.identity.handle)
|
Loading…
Reference in a new issue