mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 12:16:49 +00:00
The REST API took only the login session, so a script could not use it without a browser. A user now makes API tokens on the profile page (API Tokens tab): a name, and an expiry of 30, 90 or 365 days or none. The token is shown once, on a page that is not cached, and then kept only as a SHA-512 digest; the list shows its name, first characters, creation and expiry, and a token is deleted there. Tools send it as "Authorization: Token wvc_...", and act as its user, with that user's permissions. Making and deleting a token is logged, without the token. A password alone (HTTP Basic) still does not reach the API, as it would skip the OTP second factor: a token is made in a session that passed the login and its OTP. The token class comes first, so a refused request gets 401 with its challenge instead of 403. django-rest-knox 5.1.0 provides the tokens (accounts.ApiToken adds the name); settings.py takes "knox", KNOX_TOKEN_MODEL and REST_KNOX from the template.
147 lines
No EOL
6.6 KiB
HTML
147 lines
No EOL
6.6 KiB
HTML
{% extends "base.html" %}
|
|
{% load i18n %}
|
|
{% load django_bootstrap5 %}
|
|
{% load bootstrap_icons %}
|
|
{% load tags_fingerprint %}
|
|
|
|
{% block title %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock %}
|
|
|
|
{% block page_heading %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock page_heading %}
|
|
|
|
{% block content %}
|
|
<ul class="nav nav-tabs">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#editprofile" type="button" role="tab" aria-controls="editprofile" aria-selected="true">
|
|
{% trans "Edit Profile" %}
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#sshkeys" type="button" role="tab" aria-controls="sshkeys" aria-selected="false">
|
|
{% trans "SSH Keys" %}
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#apitokens" type="button" role="tab" aria-controls="apitokens" aria-selected="false">
|
|
{% trans "API Tokens" %}
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content">
|
|
<div class="tab-pane tab-pane-bordered active" id="editprofile">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<form method="post" action="" role="form" aria-label="Edit user info form">
|
|
{% csrf_token %}
|
|
{% bootstrap_form profile_form layout='horizontal' %}
|
|
{% if perms.accounts.change_password %}
|
|
<a href="{% url 'accounts:change_password' %}" class="btn btn-primary">
|
|
{% bs_icon 'lock' %} {% trans "Change Password" %}
|
|
</a>
|
|
{% endif %}
|
|
<div class="mb-0 float-end">
|
|
<button type="submit" class="btn btn-primary">
|
|
{% bs_icon 'pencil' %} {% trans "Update" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane tab-pane-bordered fade" id="sshkeys">
|
|
{% if publickeys %}
|
|
<div class="col-lg-12">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<tbody class="text-center">
|
|
{% for key in publickeys %}
|
|
<tr>
|
|
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
|
|
<td>
|
|
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
|
|
{% bs_icon 'trash' %}
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
{%trans "Add SSH Key" %}
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="post" action="{% url 'accounts:ssh_key_create' %}" role="form" aria-label="Add key to user form">
|
|
{% csrf_token %}
|
|
{% bootstrap_form ssh_key_form layout='horizontal' %}
|
|
<div class="mb-0 float-end">
|
|
<button type="submit" class="btn btn-primary">
|
|
{% bs_icon 'plus-circle-fill' %} {% trans "Add" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane tab-pane-bordered fade" id="apitokens">
|
|
<p class="text-body-secondary">{% blocktrans %}A token lets a script or a tool use the REST API as you, without your password or OTP: send it as <code>Authorization: Token <token></code>. A password change does not delete tokens: delete a token you no longer use, or one that may have leaked.{% endblocktrans %}</p>
|
|
{% if api_tokens %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">{% trans "Name" %}</th>
|
|
<th scope="col">{% trans "Starts with" %}</th>
|
|
<th scope="col">{% trans "Created" %}</th>
|
|
<th scope="col">{% trans "Expires" %}</th>
|
|
<th scope="col"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for token in api_tokens %}
|
|
<tr>
|
|
<td>{{ token.name }}</td>
|
|
<td><code>{{ token.token_key }}</code></td>
|
|
<td>{{ token.created|date:"Y-m-d H:i" }}</td>
|
|
<td>{% if token.expiry %}{{ token.expiry|date:"Y-m-d H:i" }}{% else %}{% trans "never" %}{% endif %}</td>
|
|
<td class="text-end">
|
|
<a href="{% url 'accounts:api_token_delete' token.token_key %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
|
|
{% bs_icon 'trash' %}
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
{% trans "Create an API token" %}
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="post" action="{% url 'accounts:api_token_create' %}" role="form" aria-label="{% trans "Create an API token" %}">
|
|
{% csrf_token %}
|
|
{% bootstrap_form api_token_form layout='horizontal' %}
|
|
<div class="mb-0 float-end">
|
|
<button type="submit" class="btn btn-primary">
|
|
{% bs_icon 'plus-circle-fill' %} {% trans "Create" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block script %}
|
|
<script>
|
|
// a link to a tab (#apitokens) opens it
|
|
var linked = location.hash.length > 1 && document.querySelector('[data-bs-target="#' + CSS.escape(location.hash.slice(1)) + '"]');
|
|
if (linked) {
|
|
bootstrap.Tab.getOrCreateInstance(linked).show();
|
|
}
|
|
</script>
|
|
{% endblock %} |