diff --git a/datasette/app.py b/datasette/app.py
index 9df16558..cab9d142 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -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$",
diff --git a/datasette/templates/create_token.html b/datasette/templates/create_token.html
new file mode 100644
index 00000000..a94881ed
--- /dev/null
+++ b/datasette/templates/create_token.html
@@ -0,0 +1,83 @@
+{% extends "base.html" %}
+
+{% block title %}Create an API token{% endblock %}
+
+{% block content %}
+
+
Create an API token
+
+This token will allow API access with the same abilities as your current user.
+
+{% if errors %}
+ {% for error in errors %}
+ {{ error }}
+ {% endfor %}
+{% endif %}
+
+
+
+{% if token %}
+
+
Your API token
+
+
+
+ Token details
+ {{ token_bits|tojson }}
+
+
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/datasette/views/special.py b/datasette/views/special.py
index dd834528..f2e69412 100644
--- a/datasette/views/special.py
+++ b/datasette/views/special.py
@@ -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,
+ },
+ )