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,
|
JsonDataView,
|
||||||
PatternPortfolioView,
|
PatternPortfolioView,
|
||||||
AuthTokenView,
|
AuthTokenView,
|
||||||
|
CreateTokenView,
|
||||||
LogoutView,
|
LogoutView,
|
||||||
AllowDebugView,
|
AllowDebugView,
|
||||||
PermissionsDebugView,
|
PermissionsDebugView,
|
||||||
|
@ -1212,6 +1213,10 @@ class Datasette:
|
||||||
AuthTokenView.as_view(self),
|
AuthTokenView.as_view(self),
|
||||||
r"/-/auth-token$",
|
r"/-/auth-token$",
|
||||||
)
|
)
|
||||||
|
add_route(
|
||||||
|
CreateTokenView.as_view(self),
|
||||||
|
r"/-/create-token$",
|
||||||
|
)
|
||||||
add_route(
|
add_route(
|
||||||
LogoutView.as_view(self),
|
LogoutView.as_view(self),
|
||||||
r"/-/logout$",
|
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 datasette.utils import actor_matches_allow, add_cors_headers
|
||||||
from .base import BaseView
|
from .base import BaseView
|
||||||
import secrets
|
import secrets
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
class JsonDataView(BaseView):
|
class JsonDataView(BaseView):
|
||||||
|
@ -163,3 +164,56 @@ class MessagesDebugView(BaseView):
|
||||||
else:
|
else:
|
||||||
datasette.add_message(request, message, getattr(datasette, message_type))
|
datasette.add_message(request, message, getattr(datasette, message_type))
|
||||||
return Response.redirect(self.ds.urls.instance())
|
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