API Tokens #134

Merged
Dalai Felinto merged 6 commits from tokens into main 2024-05-27 12:53:31 +02:00
17 changed files with 89 additions and 60 deletions
Showing only changes of commit f8663bdb78 - Show all commits

1
apitokens/__init__.py Normal file
View File

@ -0,0 +1 @@
default_app_config = 'apitokens.apps.TokensConfig'

View File

@ -3,4 +3,4 @@ from django.apps import AppConfig
class TokensConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tokens'
name = 'apitokens'

View File

@ -1,6 +1,10 @@
import datetime
import hashlib
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from .models import UserToken
from utils import clean_ip_address
class UserTokenAuthentication(BaseAuthentication):
@ -18,8 +22,12 @@ class UserTokenAuthentication(BaseAuthentication):
return None
try:
token = UserToken.objects.get(token=token_key)
token_hash = hashlib.sha256(token_key.encode()).hexdigest()
token = UserToken.objects.get(token_hash=token_hash)
except UserToken.DoesNotExist:
raise AuthenticationFailed('Invalid token')
token.ip_address_last_access = clean_ip_address(request)
token.date_last_access = datetime.datetime.now()
return (token.user, token)

View File

@ -1,9 +1,8 @@
# Generated by Django 4.2.11 on 2024-05-17 13:15
# Generated by Django 4.2.11 on 2024-05-21 21:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
@ -19,9 +18,12 @@ class Migration(migrations.Migration):
name='UserToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('name', models.CharField(max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True)),
('token_begin', models.CharField(editable=False, max_length=5)),
('token_hash', models.CharField(editable=False, max_length=64, unique=True)),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_last_access', models.DateTimeField(blank=True, editable=False, null=True)),
('ip_address_last_access', models.GenericIPAddressField(null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to=settings.AUTH_USER_MODEL)),
],
),

20
apitokens/models.py Normal file
View File

@ -0,0 +1,20 @@
from django.db import models
from django.urls import reverse
from users.models import User
class UserToken(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tokens')
name = models.CharField(max_length=255)
token_begin = models.CharField(max_length=5, editable=False)
token_hash = models.CharField(max_length=64, editable=False, unique=True)
date_created = models.DateTimeField(auto_now_add=True)
date_last_access = models.DateTimeField(null=True, blank=True, editable=False)
ip_address_last_access = models.GenericIPAddressField(protocol='both', null=True)
def get_delete_url(self):
return reverse('apitokens:delete', kwargs={'pk': self.pk})
def __str__(self):
return f"{self.user.username} - {self.token_begin} - {self.name}"

View File

@ -8,11 +8,11 @@
Delete API Token?
</h2>
<p>
This will delete the token <b>{{ object.name }}</b> created at {{ object.created_at }}<br />
This will delete the token <b>{{ object.name }}</b> created at {{ object.date_created }}<br />
Any application which were relying on this token will require a new token.
</p>
<div class="btn-row-fluid">
<a href="{% url 'tokens:list' %}" class="btn">
<a href="{% url 'apitokens:list' %}" class="btn">
<i class="i-cancel"></i>
<span>{% trans 'Cancel' %}</span>
</a>

View File

@ -14,20 +14,20 @@
<form method="post">
{% csrf_token %}
<div>
<label for="id_name">Token Name:</label>
<label for="id_name">API Token Name:</label>
<input type="text" id="id_name" name="name" class="form-control" required>
{% if form.errors.name %}
<div class="warning">{{ form.errors.name }}</div>
{% endif %}
</div>
<div class="btn-row-fluid">
<a href="{% url 'tokens:list' %}" class="btn">
<a href="{% url 'apitokens:list' %}" class="btn">
<i class="i-cancel"></i>
<span>{% trans 'Cancel' %}</span>
</a>
<button type="submit" class="btn btn-block btn-success">
<i class="i-lock"></i>
<span>{% trans 'Create Token' %}</span>
<span>{% trans 'Create API Token' %}</span>
</button>
</div>
</form>

View File

@ -15,19 +15,29 @@
Name
</th>
<th>
Token
Token Begin
</th>
<th>
Created At
</th>
<th>
Last Access
</th>
</tr>
</thead>
<tbody>
{% for token in tokens %}
<tr>
<td>{{token.name}}</td>
<td>{{token.token}}</td>
<td>{{token.created_at}}</td>
<td>{{token.token_begin}}...</td>
<td>{{token.date_created}}</td>
<td>
{% if token.date_last_access %}
{{token.date_last_access}}
{% else %}
-
{% endif %}
</td>
<td><a href="{{ token.get_delete_url }} " class="btn btn-danger i-trash"></a></td>
</tr>
{% endfor %}
@ -36,7 +46,7 @@
{% endif %}
<br/>
<a href="{% url 'tokens:create' %}" class="btn btn-primary">Create Token</a>
<a href="{% url 'apitokens:create' %}" class="btn btn-primary">Create Token</a>
</div>
</div>

11
apitokens/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path
import apitokens.views
app_name = 'apitokens'
urlpatterns = [
path('settings/tokens/', apitokens.views.TokensView.as_view(), name='list'),
path('tokens/create/', apitokens.views.CreateTokenView.as_view(), name='create'),
path('tokens/delete/<int:pk>/', apitokens.views.DeleteTokenView.as_view(), name='delete'),
]

View File

@ -1,8 +1,11 @@
import hashlib
import logging
import secrets
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib import messages
from django.views.generic.edit import CreateView
from django.views.generic import ListView, DeleteView
from django.shortcuts import reverse
@ -43,11 +46,13 @@ class UserTokenForm(forms.ModelForm):
class CreateTokenView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = UserToken
form_class = UserTokenForm
template_name = 'tokens/token_create.html'
success_message = "Token created successfully"
template_name = 'apitokens/apitoken_create.html'
success_message = (
"Your new token has been generated. Copy it now as it will not be shown again."
)
def get_success_url(self):
return reverse('tokens:list')
return reverse('apitokens:list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@ -56,13 +61,21 @@ class CreateTokenView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
def form_valid(self, form):
form.instance.user = self.request.user
token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(token.encode()).hexdigest()
form.instance.token_hash = token_hash
form.instance.token_begin = token[:5]
messages.info(self.request, f'Your new token: {token}')
return super().form_valid(form)
class DeleteTokenView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = UserToken
template_name = 'tokens/token_confirm_delete.html'
success_url = reverse_lazy('tokens:list')
template_name = 'apitokens/apitoken_confirm_delete.html'
success_url = reverse_lazy('apitokens:list')
success_message = "Token deleted successfully"
def get_queryset(self):

View File

@ -60,8 +60,8 @@ INSTALLED_APPS = [
'rangefilter',
'reviewers',
'stats',
'apitokens',
'taggit',
'tokens',
'drf_spectacular',
'drf_spectacular_sidecar',
'rest_framework',
@ -274,11 +274,7 @@ ACTSTREAM_SETTINGS = {
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
'tokens.authentication.UserTokenAuthentication',
),
'DEFAULT_AUTHENTICATION_CLASSES': ('apitokens.authentication.UserTokenAuthentication',),
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
}

View File

@ -40,7 +40,7 @@ urlpatterns = [
path('', include('teams.urls')),
path('', include('reviewers.urls')),
path('', include('notifications.urls')),
path('', include('tokens.urls')),
path('', include('apitokens.urls')),
path('api/swagger/', RedirectView.as_view(url='/api/v1/swagger/')),
path('api/v1/', SpectacularAPIView.as_view(), name='schema_v1'),
path('api/v1/swagger/', SpectacularSwaggerView.as_view(url_name='schema_v1'), name='swagger'),

View File

@ -1 +0,0 @@
default_app_config = 'tokens.apps.TokensConfig'

View File

@ -1,20 +0,0 @@
import uuid
from django.db import models
from django.urls import reverse
from users.models import User
class UserToken(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tokens')
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
def get_delete_url(self):
return reverse('tokens:delete', kwargs={'pk': self.pk})
def __str__(self):
return f"{self.user.username} - {self.token} - {self.name}"

View File

@ -1,11 +0,0 @@
from django.urls import path
import tokens.views
app_name = 'tokens'
urlpatterns = [
path('settings/tokens/', tokens.views.TokensView.as_view(), name='list'),
path('tokens/create/', tokens.views.CreateTokenView.as_view(), name='create'),
path('tokens/delete/<int:pk>/', tokens.views.DeleteTokenView.as_view(), name='delete'),
]

View File

@ -4,7 +4,7 @@
{% include "common/components/nav_link.html" with name="teams:list" title="Teams" classes="i-users py-2" %}
{% include "common/components/nav_link.html" with name="tokens:list" title="Tokens" classes="i-lock py-2" %}
{% include "common/components/nav_link.html" with name="apitokens:list" title="Tokens" classes="i-lock py-2" %}
<div class="nav-pills-divider"></div>