kopia lustrzana https://github.com/simonw/datasette
Initial prototype of create API token page, refs #1852
rodzic
f9ae92b377
commit
42f8b402e6
|
@ -33,6 +33,7 @@ from .views.special import (
|
|||
JsonDataView,
|
||||
PatternPortfolioView,
|
||||
AuthTokenView,
|
||||
CreateTokenView,
|
||||
LogoutView,
|
||||
AllowDebugView,
|
||||
PermissionsDebugView,
|
||||
|
@ -1212,6 +1213,10 @@ class Datasette:
|
|||
AuthTokenView.as_view(self),
|
||||
r"/-/auth-token$",
|
||||
)
|
||||
add_route(
|
||||
CreateTokenView.as_view(self),
|
||||
r"/-/create-token$",
|
||||
)
|
||||
add_route(
|
||||
LogoutView.as_view(self),
|
||||
r"/-/logout$",
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create an API token{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Create an API token</h1>
|
||||
|
||||
<p>This token will allow API access with the same abilities as your current user.</p>
|
||||
|
||||
{% if errors %}
|
||||
{% for error in errors %}
|
||||
<p class="message-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form action="{{ urls.path('-/create-token') }}" method="post">
|
||||
<div>
|
||||
<div class="select-wrapper" style="width: unset">
|
||||
<select name="expire_type">
|
||||
<option value="">Token never expires</option>
|
||||
<option value="minutes">Expires after X minutes</option>
|
||||
<option value="hours">Expires after X hours</option>
|
||||
<option value="days">Expires after X days</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="text" name="expire_duration" style="width: 10%">
|
||||
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
|
||||
<input type="submit" value="Create token">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if token %}
|
||||
<div>
|
||||
<h2>Your API token</h2>
|
||||
<form>
|
||||
<input type="text" class="copyable" style="width: 40%" value="{{ token }}">
|
||||
<span class="copy-link-wrapper"></span>
|
||||
</form>
|
||||
<!--- show token in a <details> -->
|
||||
<details style="margin-top: 1em">
|
||||
<summary>Token details</summary>
|
||||
<pre>{{ token_bits|tojson }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
var expireDuration = document.querySelector('input[name="expire_duration"]');
|
||||
expireDuration.style.display = 'none';
|
||||
var expireType = document.querySelector('select[name="expire_type"]');
|
||||
function showHideExpireDuration() {
|
||||
if (expireType.value) {
|
||||
expireDuration.style.display = 'inline';
|
||||
expireDuration.setAttribute("placeholder", expireType.value.replace("Expires after X ", ""));
|
||||
} else {
|
||||
expireDuration.style.display = 'none';
|
||||
}
|
||||
}
|
||||
showHideExpireDuration();
|
||||
expireType.addEventListener('change', showHideExpireDuration);
|
||||
var copyInput = document.querySelector(".copyable");
|
||||
if (copyInput) {
|
||||
var wrapper = document.querySelector(".copy-link-wrapper");
|
||||
var button = document.createElement("button");
|
||||
button.className = "copyable-copy-button";
|
||||
button.setAttribute("type", "button");
|
||||
button.innerHTML = "Copy to clipboard";
|
||||
button.onclick = (ev) => {
|
||||
ev.preventDefault();
|
||||
copyInput.select();
|
||||
document.execCommand("copy");
|
||||
button.innerHTML = "Copied!";
|
||||
setTimeout(() => {
|
||||
button.innerHTML = "Copy to clipboard";
|
||||
}, 1500);
|
||||
};
|
||||
wrapper.appendChild(button);
|
||||
wrapper.insertAdjacentElement("afterbegin", button);
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -3,6 +3,7 @@ from datasette.utils.asgi import Response, Forbidden
|
|||
from datasette.utils import actor_matches_allow, add_cors_headers
|
||||
from .base import BaseView
|
||||
import secrets
|
||||
import time
|
||||
|
||||
|
||||
class JsonDataView(BaseView):
|
||||
|
@ -163,3 +164,56 @@ class MessagesDebugView(BaseView):
|
|||
else:
|
||||
datasette.add_message(request, message, getattr(datasette, message_type))
|
||||
return Response.redirect(self.ds.urls.instance())
|
||||
|
||||
|
||||
class CreateTokenView(BaseView):
|
||||
name = "create_token"
|
||||
has_json_alternate = False
|
||||
|
||||
async def get(self, request):
|
||||
if not request.actor:
|
||||
raise Forbidden("You must be logged in to create a token")
|
||||
return await self.render(
|
||||
["create_token.html"],
|
||||
request,
|
||||
{"actor": request.actor},
|
||||
)
|
||||
|
||||
async def post(self, request):
|
||||
if not request.actor:
|
||||
raise Forbidden("You must be logged in to create a token")
|
||||
post = await request.post_vars()
|
||||
expires = None
|
||||
errors = []
|
||||
if post.get("expire_type"):
|
||||
duration = post.get("expire_duration")
|
||||
if not duration or not duration.isdigit() or not int(duration) > 0:
|
||||
errors.append("Invalid expire duration")
|
||||
else:
|
||||
unit = post["expire_type"]
|
||||
if unit == "minutes":
|
||||
expires = int(duration) * 60
|
||||
elif unit == "hours":
|
||||
expires = int(duration) * 60 * 60
|
||||
elif unit == "days":
|
||||
expires = int(duration) * 60 * 60 * 24
|
||||
else:
|
||||
errors.append("Invalid expire duration unit")
|
||||
token_bits = None
|
||||
token = None
|
||||
if not errors:
|
||||
token_bits = {
|
||||
"a": request.actor,
|
||||
"e": (int(time.time()) + expires) if expires else None,
|
||||
}
|
||||
token = self.ds.sign(token_bits, "token")
|
||||
return await self.render(
|
||||
["create_token.html"],
|
||||
request,
|
||||
{
|
||||
"actor": request.actor,
|
||||
"errors": errors,
|
||||
"token": token,
|
||||
"token_bits": token_bits,
|
||||
},
|
||||
)
|
||||
|
|
Ładowanie…
Reference in New Issue