mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-21 23:01:00 +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
|
||||
|
||||
|
||||
|
@ -17,3 +19,23 @@ class Application(models.Model):
|
|||
|
||||
created = models.DateTimeField(auto_now_add=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
|
||||
|
||||
|
||||
|
@ -37,6 +38,9 @@ class Token(models.Model):
|
|||
updated = models.DateTimeField(auto_now=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):
|
||||
"""
|
||||
Returns if this token has the given scope.
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import secrets
|
||||
|
||||
from hatchway import QueryOrBody, api_view
|
||||
|
||||
from .. import schemas
|
||||
|
@ -14,14 +12,10 @@ def add_app(
|
|||
scopes: QueryOrBody[None | str] = None,
|
||||
website: QueryOrBody[None | str] = None,
|
||||
) -> schemas.Application:
|
||||
client_id = "tk-" + secrets.token_urlsafe(16)
|
||||
client_secret = secrets.token_urlsafe(40)
|
||||
application = Application.objects.create(
|
||||
name=client_name,
|
||||
application = Application.create(
|
||||
client_name=client_name,
|
||||
website=website,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uris=redirect_uris,
|
||||
scopes=scopes or "read",
|
||||
scopes=scopes,
|
||||
)
|
||||
return schemas.Application.from_orm(application)
|
||||
|
|
|
@ -292,6 +292,6 @@ class Config(models.Model):
|
|||
class DomainOptions(pydantic.BaseModel):
|
||||
|
||||
site_name: str = ""
|
||||
site_icon: UploadedImage | None = None # type: ignore
|
||||
site_icon: UploadedImage | None = None
|
||||
hide_login: bool = False
|
||||
custom_css: str = ""
|
||||
|
|
|
@ -278,6 +278,7 @@ header menu a small {
|
|||
|
||||
nav {
|
||||
padding: 10px 0px 20px 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
nav hr {
|
||||
|
@ -377,7 +378,6 @@ nav .identity-banner a:hover {
|
|||
.settings nav {
|
||||
width: var(--width-sidebar-medium);
|
||||
background: var(--color-bg-menu);
|
||||
border-radius: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.settings nav h2 {
|
||||
|
@ -1095,6 +1095,25 @@ blockquote {
|
|||
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 */
|
||||
|
||||
|
@ -1301,9 +1320,10 @@ table.metadata td .emoji {
|
|||
min-width: 16px;
|
||||
}
|
||||
|
||||
/* Announcements */
|
||||
/* Announcements/Flash messages */
|
||||
|
||||
.announcement {
|
||||
.announcement,
|
||||
.message {
|
||||
background-color: var(--color-highlight);
|
||||
border-radius: 5px;
|
||||
margin: 10px 0 0 0;
|
||||
|
@ -1311,7 +1331,12 @@ table.metadata td .emoji {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.announcement .dismiss {
|
||||
.message {
|
||||
background-color: var(--color-bg-menu);
|
||||
}
|
||||
|
||||
.announcement .dismiss,
|
||||
.message .dismiss {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
|
|
|
@ -5,6 +5,7 @@ import signal
|
|||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
|
||||
from asgiref.sync import async_to_sync, sync_to_async
|
||||
from django.conf import settings
|
||||
|
@ -21,7 +22,7 @@ class LoopingTask:
|
|||
copy running at a time.
|
||||
"""
|
||||
|
||||
def __init__(self, callable):
|
||||
def __init__(self, callable: Callable):
|
||||
self.callable = callable
|
||||
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.urls import include, path, re_path
|
||||
|
||||
from activities.views import (
|
||||
compose,
|
||||
debug,
|
||||
posts,
|
||||
timelines,
|
||||
)
|
||||
from activities.views import compose, debug, posts, timelines
|
||||
from api.views import oauth
|
||||
from core import views as core
|
||||
from mediaproxy import views as mediaproxy
|
||||
from stator import views as stator
|
||||
from users.views import (
|
||||
activitypub,
|
||||
admin,
|
||||
announcements,
|
||||
auth,
|
||||
identity,
|
||||
settings,
|
||||
)
|
||||
from users.views.settings import follows
|
||||
from users.views import activitypub, admin, announcements, auth, identity, settings
|
||||
|
||||
urlpatterns = [
|
||||
path("", core.homepage),
|
||||
|
@ -78,6 +65,21 @@ urlpatterns = [
|
|||
settings.CsvFollowers.as_view(),
|
||||
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(
|
||||
"admin/",
|
||||
admin.AdminRoot.as_view(),
|
||||
|
|
|
@ -4,3 +4,13 @@
|
|||
{{ announcement.html }}
|
||||
</div>
|
||||
{% 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>
|
||||
<span>Import/Export</span>
|
||||
</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>
|
||||
<h3>Tools</h3>
|
||||
<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.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
|
||||
CsvFollowers,
|
||||
CsvFollowing,
|
||||
ImportExportPage,
|
||||
)
|
||||
from users.views.settings.interface import InterfacePage # noqa
|
||||
from users.views.settings.posting import PostingPage # noqa
|
||||
from users.views.settings.profile import ProfilePage # noqa
|
||||
from users.views.settings.security import SecurityPage # noqa
|
||||
from users.views.settings.settings_page import SettingsPage # noqa
|
||||
from users.views.settings.follows import FollowsPage # noqa
|
||||
from users.views.settings.interface import InterfacePage # noqa
|
||||
from users.views.settings.tokens import TokenCreate, TokenEdit, TokensRoot # noqa
|
||||
|
||||
|
||||
@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