Add a page that lists all authorised Micropub clients and allows a client's access to be revoked easily
This commit is contained in:
parent
bb91d3c6b6
commit
446029ce84
9 changed files with 137 additions and 2 deletions
68
lemonauth/jinja2/lemonauth/tokens.html
Normal file
68
lemonauth/jinja2/lemonauth/tokens.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{% extends 'lemoncurry/layout.html' %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
<div class="card-columns">
|
||||
{% for _, c in clients | dictsort %}
|
||||
<div class="h-x-app card">
|
||||
{% if c.app.logo %}
|
||||
<img class="u-photo card-img-top" src="{{ c.app.logo[0] }}" alt="{{ c.app.name[0] }}" />
|
||||
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<span class="p-name">{{ c.app.name[0] if c.app else (c.id | friendly_url) }}</span>
|
||||
<span class="badge badge-light">
|
||||
<span class="p-count">{{ c.count }}</span>
|
||||
{{ 'tokens' if c.count > 1 else 'token' }}
|
||||
</span>
|
||||
</h5>
|
||||
|
||||
<h6 class="card-subtitle mb-2">
|
||||
<a class="u-url" href="{{ c.id }}">{{ c.id }}</a>
|
||||
</h6>
|
||||
|
||||
<p class="card-text">this client has access to the scopes:</p>
|
||||
</div>
|
||||
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for scope in c.scopes %}
|
||||
<li class="p-scope list-group-item">{{ scope }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<form class="card-footer text-right" action="{{ url('lemonauth:tokens_revoke', kwargs={'client_id': c.id}) }}">
|
||||
{{ csrf_input }}
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-ban" aria-hidden="true"></i>
|
||||
revoke access
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block foot %}
|
||||
<script type="text/javascript">
|
||||
$('form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var $f = $(this);
|
||||
if ($f.data('deleting')) return;
|
||||
$f.data('deleting', true);
|
||||
$f.find('button').prop({disabled: true})
|
||||
.find('[data-fa-i2svg]').removeClass('fa-ban').addClass('fa-circle-notch fa-spin');
|
||||
$.ajax({
|
||||
headers: {'X-CSRFToken': $f.find('[name="csrfmiddlewaretoken"]').val()},
|
||||
method: 'DELETE',
|
||||
url: $f.attr('action'),
|
||||
}).then(function() {
|
||||
var $app = $f.parent('.h-x-app');
|
||||
return $app.hide(function() {
|
||||
$app.remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -8,4 +8,6 @@ urlpatterns = [
|
|||
path('indie', views.IndieView.as_view(), name='indie'),
|
||||
path('indie/approve', views.indie_approve, name='indie_approve'),
|
||||
path('token', views.TokenView.as_view(), name='token'),
|
||||
path('tokens', views.TokensListView.as_view(), name='tokens'),
|
||||
path('tokens/<path:client_id>', views.TokensRevokeView.as_view(), name='tokens_revoke'),
|
||||
]
|
||||
|
|
|
@ -2,3 +2,4 @@ from .login import login
|
|||
from .logout import logout
|
||||
from .indie import IndieView, approve as indie_approve
|
||||
from .token import TokenView
|
||||
from .tokens import TokensListView, TokensRevokeView
|
||||
|
|
2
lemonauth/views/tokens/__init__.py
Normal file
2
lemonauth/views/tokens/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .list import TokensListView
|
||||
from .revoke import TokensRevokeView
|
41
lemonauth/views/tokens/list.py
Normal file
41
lemonauth/views/tokens/list.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import TemplateView
|
||||
from typing import Dict, Optional, Set
|
||||
from lemoncurry.requests import mf2
|
||||
|
||||
|
||||
class ClientsDict(dict):
|
||||
def __missing__(self, client_id):
|
||||
self[client_id] = Client(client_id)
|
||||
return self[client_id]
|
||||
|
||||
|
||||
class Client:
|
||||
id: str
|
||||
count: int
|
||||
scopes: Set[str]
|
||||
app: Optional[Dict[str, str]]
|
||||
|
||||
def __init__(self, client_id):
|
||||
self.id = client_id
|
||||
self.count = 0
|
||||
self.scopes = set()
|
||||
apps = mf2(self.id).to_dict(filter_by_type='h-x-app')
|
||||
try:
|
||||
self.app = apps[0]['properties']
|
||||
except IndexError:
|
||||
self.app = None
|
||||
|
||||
|
||||
class TokensListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'lemonauth/tokens.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
clients = ClientsDict()
|
||||
for token in self.request.user.token_set.all():
|
||||
client = clients[token.client_id]
|
||||
client.count += 1
|
||||
client.scopes |= set(token.scope.split(' '))
|
||||
context.update({'clients': clients, 'title': 'tokens'})
|
||||
return context
|
11
lemonauth/views/tokens/revoke.py
Normal file
11
lemonauth/views/tokens/revoke.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.http import HttpResponse
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views import View
|
||||
|
||||
from ...models import Token
|
||||
|
||||
|
||||
class TokensRevokeView(LoginRequiredMixin, View):
|
||||
def delete(self, request, client_id: str):
|
||||
Token.objects.filter(client_id=client_id).delete()
|
||||
return HttpResponse(status=204)
|
|
@ -61,6 +61,12 @@
|
|||
|
||||
<ul class="navbar-nav">
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url('lemonauth:tokens') }}">
|
||||
<i class="fas fa-cookie-bite fa-fw" aria-hidden="true"></i>
|
||||
tokens
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url('admin:index') }}">
|
||||
<i class="fas fa-cog fa-fw" aria-hidden="true"></i>
|
||||
|
@ -133,8 +139,8 @@
|
|||
<p>powered by <a rel="code-repository" href="{{ package.repository }}/src/tag/v{{ package.version }}">{{ package.name }} {{ package.version }}</a></p>
|
||||
</footer>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"
|
||||
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"
|
||||
integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" crossorigin="anonymous"
|
||||
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" crossorigin="anonymous"
|
||||
|
|
|
@ -27,6 +27,8 @@ code, pre, .code, .pre
|
|||
border-color $base00
|
||||
color $base07
|
||||
|
||||
.list-group-item
|
||||
background-color $base03
|
||||
|
||||
[class^="openwebicons-"], [class*=" openwebicons-"]
|
||||
&::before
|
||||
|
|
|
@ -31,6 +31,8 @@ PACKAGE = PackageJson()
|
|||
|
||||
def friendly_url(url):
|
||||
(scheme, netloc, path, params, q, fragment) = urlparse(url)
|
||||
if path == '/':
|
||||
return netloc
|
||||
return netloc + path
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue