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

1from datetime import timedelta 

2 

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 

13 

14from .models import ApiKey 

15 

16 

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 ) 

32 

33 def clean_expires_in_days(self) -> int: 

34 value = self.cleaned_data.get("expires_in_days") 

35 return value or 0 

36 

37 

38class ApiKeyListView(LoginRequiredMixin, FormView): 

39 template_name = "api_keys/list.html" 

40 form_class = CreateApiKeyForm 

41 success_url = reverse_lazy("api_keys") 

42 

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 

47 

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 ) 

57 

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) 

66 

67 

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") 

79 

80 

81class RotateApiKeyView(LoginRequiredMixin, FormView): 

82 form_class = CreateApiKeyForm 

83 template_name = "api_keys/list.html" 

84 success_url = reverse_lazy("api_keys") 

85 

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) 

90 

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)