Skip to content

Commit

Permalink
Merge pull request #760 from basedosdados/feat/api_keys
Browse files Browse the repository at this point in the history
feat: working data_api app with views
  • Loading branch information
rdahis authored Feb 12, 2025
2 parents a166de9 + f972e91 commit 1364009
Show file tree
Hide file tree
Showing 20 changed files with 1,461 additions and 153 deletions.
74 changes: 3 additions & 71 deletions backend/apps/account/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-

from django import forms
from django.contrib import admin, messages
from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.contrib.auth.admin import UserAdmin as BaseAccountAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
Expand All @@ -15,12 +15,12 @@
BDGroup,
BDRole,
Career,
DataAPIKey,
Role,
Subscription,
Team,
)
from backend.apps.account.tasks import sync_subscription_task
from backend.apps.data_api.admin import KeyInline


def sync_subscription(modeladmin: ModelAdmin, request: HttpRequest, queryset: QuerySet):
Expand Down Expand Up @@ -201,30 +201,6 @@ def queryset(self, request, queryset):
return queryset.filter(subscription__status=self.value())


class DataAPIKeyInline(admin.TabularInline):
model = DataAPIKey
extra = 0
readonly_fields = (
"id",
"name",
"prefix",
"is_active",
"expires_at",
"last_used_at",
"created_at",
"updated_at",
)
fields = readonly_fields
can_delete = False
show_change_link = True

def has_add_permission(self, request, obj=None):
return False

def has_change_permission(self, request, obj=None):
return False


class AccountAdmin(BaseAccountAdmin):
form = AccountChangeForm
add_form = AccountCreationForm
Expand Down Expand Up @@ -324,7 +300,7 @@ class AccountAdmin(BaseAccountAdmin):
)
search_fields = ("email", "full_name")
ordering = ["-created_at"]
inlines = (CareerInline, SubscriptionInline, DataAPIKeyInline)
inlines = (CareerInline, SubscriptionInline, KeyInline)
filter_horizontal = ()

def is_subscriber(self, instance):
Expand Down Expand Up @@ -411,54 +387,10 @@ def has_delete_permission(self, request, obj=None):
return False


class DataAPIKeyAdmin(admin.ModelAdmin):
list_display = (
"name",
"account",
"prefix",
"is_active",
"expires_at",
"last_used_at",
"created_at",
)
list_filter = ("is_active",)
search_fields = ("name", "prefix", "account__email", "account__full_name")
readonly_fields = ("id", "prefix", "hash", "last_used_at", "created_at", "updated_at")
fieldsets = (
(
None,
{
"fields": (
"account",
"name",
"is_active",
"expires_at",
)
},
),
)
ordering = ["-created_at"]

def has_add_permission(self, request):
return True

def save_model(self, request, obj, form, change):
if not change: # Only when creating new object
obj, key = DataAPIKey.create_key(**form.cleaned_data)
messages.success(
request,
f"API Key generated successfully. "
f"Please copy this key now as it won't be shown again: {key}",
)
else:
super().save_model(request, obj, form, change)


admin.site.register(Account, AccountAdmin)
admin.site.register(Career, CareerAdmin)
admin.site.register(Role, RoleAdmin)
admin.site.register(Subscription, SubscriptionAdmin)
admin.site.register(Team, TeamAdmin)
admin.site.register(DataAPIKey, DataAPIKeyAdmin)
admin.site.register(BDGroup)
admin.site.register(BDRole)
16 changes: 16 additions & 0 deletions backend/apps/account/migrations/0026_delete_dataapikey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 4.2.18 on 2025-02-12 07:02

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("account", "0025_rename_hashed_key_hash"),
]

operations = [
migrations.DeleteModel(
name="DataAPIKey",
),
]
41 changes: 0 additions & 41 deletions backend/apps/account/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from hashlib import sha256
from typing import Tuple
from uuid import uuid4

Expand Down Expand Up @@ -610,46 +609,6 @@ def next_billing_cycle(self):
return None


class DataAPIKey(BaseModel):
id = models.UUIDField(primary_key=True, default=uuid4)
account = models.ForeignKey(Account, on_delete=models.DO_NOTHING, related_name="data_api_keys")
name = models.CharField(
max_length=100, null=True, blank=True, help_text="A friendly name to identify this API key"
)
hash = models.CharField(
max_length=64, unique=True, null=True, blank=True, help_text="The hashed API key"
)
prefix = models.CharField(
max_length=8,
unique=True,
null=True,
blank=True,
help_text="First 8 characters of the API key",
)
is_active = models.BooleanField(default=True)
expires_at = models.DateTimeField(null=True, blank=True, help_text="Optional expiration date")
last_used_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
verbose_name = "Data API Key"
verbose_name_plural = "Data API Keys"
ordering = ["created_at"]

def __str__(self):
return f"{self.name} ({self.prefix}...)"

@classmethod
def create_key(cls, **kwargs):
key = str(uuid4())
obj = cls(**kwargs)
obj.prefix = key[:8]
obj.hash = sha256(key.encode()).hexdigest()
obj.save()
return obj, key


def split_password(password: str) -> Tuple[str, str, str, str]:
"""Split a password into four parts: algorithm, iterations, salt, and hash"""
algorithm, iterations, salt, hash = password.split("$", 3)
Expand Down
6 changes: 0 additions & 6 deletions backend/apps/account/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from backend.apps.account.views import (
AccountActivateConfirmView,
AccountActivateView,
DataAPIKeyValidateView,
PasswordResetConfirmView,
PasswordResetView,
)
Expand All @@ -30,9 +29,4 @@
PasswordResetConfirmView.as_view(),
name="password_reset_confirm",
),
path(
"account/validate_data_api_key",
DataAPIKeyValidateView.as_view(),
name="validate_data_api_key",
),
]
35 changes: 0 additions & 35 deletions backend/apps/account/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from hashlib import sha256
from json import loads
from typing import Any

Expand All @@ -11,7 +10,6 @@
from django.http import JsonResponse
from django.template.loader import render_to_string
from django.urls import reverse_lazy as r
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
Expand All @@ -23,8 +21,6 @@
from backend.apps.account.token import token_generator
from backend.custom.environment import get_frontend_url

from .models import DataAPIKey


class AccountActivateView(View):
@method_decorator(csrf_exempt, name="dispatch")
Expand Down Expand Up @@ -143,34 +139,3 @@ def dispatch(self, request, uidb64, token):
return JsonResponse({}, status=200)
else:
return JsonResponse({}, status=422)


class DataAPIKeyValidateView(View):
def get(self, request):
api_key = request.GET.get("id")
if not api_key:
return JsonResponse({"error": "API key not provided", "success": False}, status=400)

# Hash the API key
hashed_key = sha256(api_key.encode()).hexdigest()

try:
key = DataAPIKey.objects.get(hash=hashed_key)

# Check if key is expired
is_expired = False
if key.expires_at and key.expires_at < timezone.now():
is_expired = True

return JsonResponse(
{
"success": True,
"resource": {
"isActive": key.is_active and not is_expired,
"createdAt": key.created_at,
"expiresAt": key.expires_at,
},
}
)
except DataAPIKey.DoesNotExist:
return JsonResponse({"error": "API key not found", "success": False}, status=404)
Empty file.
Loading

0 comments on commit 1364009

Please sign in to comment.