diff --git a/Watcher/Watcher/accounts/admin.py b/Watcher/Watcher/accounts/admin.py
index b6695a8..9d96271 100644
--- a/Watcher/Watcher/accounts/admin.py
+++ b/Watcher/Watcher/accounts/admin.py
@@ -6,6 +6,13 @@
from django.urls import reverse, NoReverseMatch
from django.contrib.auth.models import User
from django.utils.safestring import mark_safe
+from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
+from .models import APIKey
+from .api import generate_api_key
+from django.contrib import messages
+from django import forms
+from django.utils import timezone
+from datetime import timedelta
"""
Log Entries Snippet
@@ -127,3 +134,203 @@ def action_description(self, obj):
admin.site.register(LogEntry, LogEntryAdmin)
+
+
+class UserAdmin(BaseUserAdmin):
+ actions = ['generate_api_key']
+
+ def generate_api_key(self, request, queryset):
+ for user in queryset:
+ raw_key, hashed_key = generate_api_key(user)
+ if raw_key:
+ self.message_user(request, f"API Key generated for {user.username}: {raw_key[:10]}...")
+ else:
+ self.message_user(request, f"Failed to generate API Key for {user.username}", level='ERROR')
+
+ generate_api_key.short_description = "Generate API Key"
+
+admin.site.unregister(User)
+admin.site.register(User, UserAdmin)
+
+
+class ReadOnlyTextInput(forms.TextInput):
+ def render(self, name, value, attrs=None, renderer=None):
+ if value:
+ truncated_value = value[:5] + '*' * 59
+ return f'{truncated_value}'
+ return super().render(name, value, attrs, renderer)
+
+
+class APIKeyForm(forms.ModelForm):
+ EXPIRATION_CHOICES = (
+ (1, '1 day'),
+ (7, '7 days'),
+ (30, '30 days'),
+ (60, '60 days'),
+ (90, '90 days'),
+ (365, '1 year'),
+ (730, '2 years'),
+ )
+ expiration = forms.ChoiceField(choices=EXPIRATION_CHOICES, label='Expiration', required=True)
+
+ class Meta:
+ model = APIKey
+ fields = ['user', 'key', 'expiration', 'expiry_at']
+
+ def __init__(self, *args, **kwargs):
+ self.request = kwargs.pop('request', None)
+ super().__init__(*args, **kwargs)
+ instance = kwargs.get('instance')
+ if instance and instance.pk:
+ if 'key' in self.fields:
+ self.fields['key'].widget.attrs['readonly'] = True
+ self.fields['key'].widget = ReadOnlyTextInput()
+ if 'user' in self.fields:
+ self.fields['user'].widget.attrs['readonly'] = True
+ if 'expiry_at' in self.fields:
+ self.fields['expiry_at'].widget.attrs['readonly'] = True
+ if 'expiration' in self.fields:
+ if not self.request.user.is_superuser:
+ self.fields['expiration'].widget.attrs['disabled'] = True
+ else:
+ self.fields['expiration'].widget.attrs['readonly'] = True
+ else:
+ if 'key' in self.fields:
+ self.fields['key'].widget = forms.HiddenInput()
+ if 'expiry_at' in self.fields:
+ self.fields['expiry_at'].widget = forms.HiddenInput()
+
+ if self.request and not self.request.user.is_superuser:
+ self.fields['user'].queryset = User.objects.filter(id=self.request.user.id)
+ self.fields['user'].initial = self.request.user
+ else:
+ self.fields['user'].queryset = User.objects.all()
+
+ def clean_key(self):
+ instance = getattr(self, 'instance', None)
+ if instance and instance.pk:
+ return instance.key
+ return self.cleaned_data.get('key', '')
+
+ def clean_expiration(self):
+ expiration = self.cleaned_data.get('expiration')
+ if expiration:
+ try:
+ expiration = int(expiration)
+ if expiration not in [choice[0] for choice in self.EXPIRATION_CHOICES]:
+ raise forms.ValidationError('Invalid expiration value.')
+ except ValueError:
+ raise forms.ValidationError('Invalid expiration value.')
+ return expiration
+
+ def save(self, commit=True):
+ instance = super().save(commit=False)
+ expiration = self.cleaned_data.get('expiration')
+
+ if expiration:
+ instance.expiry_at = timezone.now() + timedelta(days=int(expiration))
+
+ if commit:
+ instance.save()
+
+ return instance
+
+
+class APIKeyAdmin(admin.ModelAdmin):
+ list_display = ('user', 'shortened_key', 'created_at', 'expiry_at_display')
+ form = APIKeyForm
+ readonly_fields = ('key_details',)
+
+ def get_queryset(self, request):
+ if request.user.is_superuser:
+ return APIKey.objects.all()
+ else:
+ return APIKey.objects.filter(user=request.user)
+
+ def has_add_permission(self, request):
+ return True
+
+ def get_form(self, request, obj=None, **kwargs):
+ kwargs['form'] = self.form
+ form = super().get_form(request, obj, **kwargs)
+ if 'key' in form.base_fields:
+ form.base_fields['key'].widget = ReadOnlyTextInput()
+
+ class CustomAPIKeyForm(form):
+ def __init__(self, *args, **kwargs):
+ kwargs['request'] = request
+ super().__init__(*args, **kwargs)
+
+ return CustomAPIKeyForm
+
+ def save_model(self, request, obj, form, change):
+ if not obj.key:
+ user = request.user
+ expiration_days = int(form.cleaned_data.get('expiration', 30))
+ raw_key, hashed_key = generate_api_key(user, expiration_days)
+ obj.key = hashed_key
+ obj.expiry_at = timezone.now() + timedelta(days=expiration_days)
+ hash_parts = hashed_key.split('$')
+ obj.key_details = (
+ f"algorithm: pbkdf2_sha256 \n "
+ f"iterations: {hash_parts[1]}\n "
+ f"salt: {hash_parts[2][:8]}{'*' * (len(hash_parts[2]) - 8)}\n "
+ f"hash: {hash_parts[3][:8]}{'*' * (len(hash_parts[3]) - 8)}\n\n"
+ f"Raw API keys are not stored, so there is no way to see this user’s API key."
+ )
+ copy_button = f'''
+
+
+ '''
+ messages.success(request, mark_safe(f"The API Key for {user.username} was added successfully: {raw_key}. {copy_button} Make sure to copy this personal token now. You won't be able to see it again!"), extra_tags='safe', fail_silently=True)
+ super().save_model(request, obj, form, change)
+
+ def shortened_key(self, obj):
+ if obj.key:
+ hash_parts = obj.key.split('$')
+ if len(hash_parts) >= 4:
+ hash_start = hash_parts[3][:5] + '*' * (len(hash_parts[3]) - 5)
+ return hash_start
+ else:
+ return 'Invalid Key'
+ else:
+ return ''
+
+ shortened_key.short_description = 'Api-Key'
+
+ def expiry_at_display(self, obj):
+ return obj.expiry_at.strftime("%b %d, %Y, %-I:%M %p").replace('AM', 'a.m.').replace('PM', 'p.m.') if obj.expiry_at else '-'
+
+ def get_readonly_fields(self, request, obj=None):
+ readonly_fields = []
+ if obj:
+ readonly_fields.extend(['user', 'expiry_at', 'key_details'])
+ return readonly_fields
+
+ def get_exclude(self, request, obj=None):
+ if not obj:
+ return ['key', 'expiry_at']
+ else:
+ return ['key']
+
+ def has_view_permission(self, request, obj=None):
+ if request.user.is_superuser:
+ return True
+ if obj is None:
+ return True
+ return obj.user == request.user
+
+admin.site.register(APIKey, APIKeyAdmin)
\ No newline at end of file
diff --git a/Watcher/Watcher/accounts/api.py b/Watcher/Watcher/accounts/api.py
index fb0abe4..5548bb9 100644
--- a/Watcher/Watcher/accounts/api.py
+++ b/Watcher/Watcher/accounts/api.py
@@ -2,6 +2,10 @@
from rest_framework.response import Response
from knox.models import AuthToken
from .serializers import UserSerializer, LoginSerializer, UserPasswordChangeSerializer
+from django.utils import timezone
+from django.contrib.auth.models import User
+from hashlib import sha256
+from django.contrib.auth.hashers import make_password, check_password
# Login API
@@ -12,6 +16,7 @@ def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data
+ raw_key, _ = generate_api_key(user)
return Response({
"user": UserSerializer(user, context=self.get_serializer_context()).data,
"token": AuthToken.objects.create(user)[1]
@@ -35,3 +40,19 @@ class PasswordChangeViewSet(viewsets.ModelViewSet):
permissions.IsAuthenticated,
]
serializer_class = UserPasswordChangeSerializer
+
+
+# Generate Api Key
+def generate_api_key(user, expiration_days=30):
+ expiry = timezone.timedelta(days=expiration_days)
+ token_instance, raw_key = AuthToken.objects.create(user, expiry=expiry)
+
+ # Generate hash using pbkdf2_sha256
+ hashed_key = make_password(raw_key, salt=None, hasher='pbkdf2_sha256')
+
+ if raw_key:
+ print(f"API Key generated for user {user.username}: {raw_key}")
+ return raw_key, hashed_key
+ else:
+ print(f"Failed to generate API Key for user {user.username}")
+ return None, None
\ No newline at end of file
diff --git a/Watcher/Watcher/accounts/models.py b/Watcher/Watcher/accounts/models.py
index 38aff13..bb7f5d5 100644
--- a/Watcher/Watcher/accounts/models.py
+++ b/Watcher/Watcher/accounts/models.py
@@ -2,6 +2,22 @@
from django_auth_ldap.backend import populate_user
from django.contrib.auth.models import User
+class APIKey(models.Model):
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ key = models.CharField(max_length=100, unique=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ expiration = models.IntegerField(default=30)
+ expiry_at = models.DateTimeField(null=True, blank=True)
+ key_details = models.TextField(null=True, blank=True) # Ajout de ce champ
+
+ def __str__(self):
+ return f"API Key for {self.user.username}"
+
+ class Meta:
+ verbose_name = "API Key"
+ verbose_name_plural = "API Keys"
+ app_label = 'auth'
+
def make_inactive(sender, user, **kwargs):
if not User.objects.filter(username=user.username):