Coverage for sm/views_api_keys.py: 100%
60 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-24 19:06 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-24 19:06 +0000
1from datetime import timedelta
3from django import forms
4from django.contrib import messages
5from django.contrib.auth.mixins import LoginRequiredMixin
6from django.shortcuts import get_object_or_404, redirect
7from django.urls import reverse_lazy
8from django.utils import timezone
9from django.utils.translation import gettext as _
10from django.views import View
11from django.views.generic import FormView
12from typing import Any
14from .models import ApiKey
17class CreateApiKeyForm(forms.Form):
18 name = forms.CharField(
19 label=_("Name (optional)"),
20 max_length=100,
21 required=False,
22 help_text=_("A label to remember this key by."),
23 )
24 expires_in_days = forms.IntegerField(
25 label=_("Expire after (days)"),
26 required=False,
27 min_value=1,
28 max_value=3650,
29 initial=90,
30 help_text=_("Optional. The key will be rejected after this many days."),
31 )
33 def clean_expires_in_days(self) -> int:
34 value = self.cleaned_data.get("expires_in_days")
35 return value or 0
38class ApiKeyListView(LoginRequiredMixin, FormView):
39 template_name = "api_keys/list.html"
40 form_class = CreateApiKeyForm
41 success_url = reverse_lazy("api_keys")
43 def get_context_data(self, **kwargs: Any) -> Any:
44 context = super().get_context_data(**kwargs)
45 context["api_keys"] = self.request.user.api_keys.all()
46 return context
48 def form_valid(self, form: Any) -> Any:
49 name = form.cleaned_data["name"].strip()
50 expires_in_days = form.cleaned_data.get("expires_in_days") or 0
51 expires_at = None
52 if expires_in_days:
53 expires_at = timezone.now() + timedelta(days=expires_in_days)
54 key, secret = ApiKey.create_for_user(
55 self.request.user, name, expires_at=expires_at
56 )
58 context = self.get_context_data(form=form)
59 context["new_key"] = key
60 context["new_secret"] = secret
61 messages.success(
62 self.request,
63 _("API key created. Copy the secret now - it will not be shown again."),
64 )
65 return self.render_to_response(context)
68class RevokeApiKeyView(LoginRequiredMixin, View):
69 def post(self, request: Any, pk: int, *args: Any, **kwargs: Any) -> Any:
70 key = get_object_or_404(ApiKey, pk=pk, user=request.user)
71 key.is_active = False
72 key.revoked_at = timezone.now()
73 key.save(update_fields=["is_active", "revoked_at"])
74 messages.success(
75 request,
76 _("API key %(key)s has been revoked.") % {"key": key},
77 )
78 return redirect("api_keys")
81class RotateApiKeyView(LoginRequiredMixin, FormView):
82 form_class = CreateApiKeyForm
83 template_name = "api_keys/list.html"
84 success_url = reverse_lazy("api_keys")
86 def post(self, request: Any, *args: Any, **kwargs: Any) -> Any:
87 pk = kwargs.get("pk")
88 key = get_object_or_404(ApiKey, pk=pk, user=request.user)
89 new_key, new_secret = key.rotate(name=key.name)
91 context = self.get_context_data()
92 context["api_keys"] = request.user.api_keys.all()
93 context["new_key"] = new_key
94 context["new_secret"] = new_secret
95 messages.success(
96 request,
97 _("API key %(key)s has been rotated. Copy the new secret now.")
98 % {"key": key},
99 )
100 return self.render_to_response(context)