Add a page that lists all authorised Micropub clients and allows a client's access to be revoked easily

This commit is contained in:
Danielle McLean 2018-06-25 22:31:42 +10:00
parent bb91d3c6b6
commit 446029ce84
Signed by untrusted user: 00dani
GPG key ID: 8EB789DDF3ABD240
9 changed files with 137 additions and 2 deletions

View 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 %}

View file

@ -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'),
]

View file

@ -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

View file

@ -0,0 +1,2 @@
from .list import TokensListView
from .revoke import TokensRevokeView

View 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

View 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)

View file

@ -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"

View file

@ -27,6 +27,8 @@ code, pre, .code, .pre
border-color $base00
color $base07
.list-group-item
background-color $base03
[class^="openwebicons-"], [class*=" openwebicons-"]
&::before

View file

@ -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