API Tokens #134
@ -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 = {
|
||||||
|
@ -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
1
tokens/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'tokens.apps.TokensConfig'
|
6
tokens/apps.py
Normal file
6
tokens/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TokensConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'tokens'
|
28
tokens/migrations/0001_initial.py
Normal file
28
tokens/migrations/0001_initial.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
tokens/migrations/__init__.py
Normal file
0
tokens/migrations/__init__.py
Normal file
20
tokens/models.py
Normal file
20
tokens/models.py
Normal 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}"
|
30
tokens/templates/tokens/token_confirm_delete.html
Normal file
30
tokens/templates/tokens/token_confirm_delete.html
Normal 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 %}
|
37
tokens/templates/tokens/token_create.html
Normal file
37
tokens/templates/tokens/token_create.html
Normal 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 %}
|
43
tokens/templates/tokens/usertoken_list.html
Normal file
43
tokens/templates/tokens/usertoken_list.html
Normal 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
11
tokens/urls.py
Normal 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
69
tokens/views.py
Normal 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)
|
@ -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" %}
|
||||||
|
Loading…
Reference in New Issue
Block a user