API Tokens #134

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

View File

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

View File

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

1
tokens/__init__.py Normal file
View File

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

6
tokens/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TokensConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tokens'

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.11 on 2024-05-17 13:15
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
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)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

20
tokens/models.py Normal file
View File

@ -0,0 +1,20 @@
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

@ -0,0 +1,30 @@
{% extends "common/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-8 mx-auto my-4">
<div class="box">
<h2>
Delete API Token?
</h2>
<p>
This will delete the token <b>{{ object.name }}</b> created at {{ object.created_at }}<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">
<i class="i-cancel"></i>
<span>{% trans 'Cancel' %}</span>
</a>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-block btn-danger">
<i class="i-trash"></i>
<span>{% trans 'Confirm Deletion' %}</span>
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,37 @@
{% extends "common/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-8 mx-auto my-4">
<div class="box">
<h2>
User API Token
</h2>
<p>
Create a new user token to be used with the <a href="{% url 'swagger' %}">API</a>.
</p>
<form method="post">
{% csrf_token %}
<div>
<label for="id_name">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">
<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>
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,43 @@
{% extends 'users/settings/base.html' %}
{% block settings %}
<h1 class="mb-3">Tokens</h1>
<div class="row">
<div class="col">
<div>
List of user tokens to automate tasks via the <a href="{% url 'swagger' %}">API</a>.
</div>
{% if tokens %}
<table class="table table-hover">
<thead>
<tr>
<th>
Name
</th>
<th>
Token
</th>
<th>
Created At
</th>
</tr>
</thead>
<tbody>
{% for token in tokens %}
<tr>
<td>{{token.name}}</td>
<td>{{token.token}}</td>
<td>{{token.created_at}}</td>
<td><a href="{{ token.get_delete_url }} " class="btn btn-danger i-trash"></a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br/>
<a href="{% url 'tokens:create' %}" class="btn btn-primary">Create Token</a>
</div>
</div>
{% endblock settings %}

11
tokens/urls.py Normal file
View File

@ -0,0 +1,11 @@
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'),
]

69
tokens/views.py Normal file
View File

@ -0,0 +1,69 @@
import logging
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.edit import CreateView
from django.views.generic import ListView, DeleteView
from django.shortcuts import reverse
from django.utils.safestring import mark_safe
from django.urls import reverse_lazy
from .models import UserToken
logger = logging.getLogger(__name__)
class TokensView(LoginRequiredMixin, ListView):
model = UserToken
context_object_name = 'tokens'
def get_queryset(self):
return self.request.user.tokens.all()
class UserTokenForm(forms.ModelForm):
class Meta:
model = UserToken
fields = ('name',)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None) # Get user information from kwargs
super().__init__(*args, **kwargs)
def clean_name(self):
name = self.cleaned_data.get('name')
if UserToken.objects.filter(user=self.user, name=name).exists():
error_message = mark_safe(f'A token with this name already exists: <b>{name}</b>.')
raise forms.ValidationError(error_message)
return name
class CreateTokenView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = UserToken
form_class = UserTokenForm
template_name = 'tokens/token_create.html'
success_message = "Token created successfully"
def get_success_url(self):
return reverse('tokens:list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
form.instance.user = self.request.user
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')
success_message = "Token deleted successfully"
def get_queryset(self):
return super().get_queryset().filter(user=self.request.user)

View File

@ -4,6 +4,8 @@
{% 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="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" %}
<div class="nav-pills-divider"></div> <div class="nav-pills-divider"></div>
{% include "common/components/nav_link.html" with name="users:my-profile-delete" title="Delete account" classes="i-trash py-2" %} {% include "common/components/nav_link.html" with name="users:my-profile-delete" title="Delete account" classes="i-trash py-2" %}