forked from 00dani/lemoncurry
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', views.IndieView.as_view(), name='indie'),
|
||||||
path('indie/approve', views.indie_approve, name='indie_approve'),
|
path('indie/approve', views.indie_approve, name='indie_approve'),
|
||||||
path('token', views.TokenView.as_view(), name='token'),
|
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 .logout import logout
|
||||||
from .indie import IndieView, approve as indie_approve
|
from .indie import IndieView, approve as indie_approve
|
||||||
from .token import TokenView
|
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">
|
<ul class="navbar-nav">
|
||||||
{% if request.user.is_authenticated %}
|
{% 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">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url('admin:index') }}">
|
<a class="nav-link" href="{{ url('admin:index') }}">
|
||||||
<i class="fas fa-cog fa-fw" aria-hidden="true"></i>
|
<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>
|
<p>powered by <a rel="code-repository" href="{{ package.repository }}/src/tag/v{{ package.version }}">{{ package.name }} {{ package.version }}</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"
|
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"
|
||||||
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"></script>
|
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"
|
<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>
|
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"></script>
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" crossorigin="anonymous"
|
<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
|
border-color $base00
|
||||||
color $base07
|
color $base07
|
||||||
|
|
||||||
|
.list-group-item
|
||||||
|
background-color $base03
|
||||||
|
|
||||||
[class^="openwebicons-"], [class*=" openwebicons-"]
|
[class^="openwebicons-"], [class*=" openwebicons-"]
|
||||||
&::before
|
&::before
|
||||||
|
|
|
@ -31,6 +31,8 @@ PACKAGE = PackageJson()
|
||||||
|
|
||||||
def friendly_url(url):
|
def friendly_url(url):
|
||||||
(scheme, netloc, path, params, q, fragment) = urlparse(url)
|
(scheme, netloc, path, params, q, fragment) = urlparse(url)
|
||||||
|
if path == '/':
|
||||||
|
return netloc
|
||||||
return netloc + path
|
return netloc + path
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue