Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 76 additions & 5 deletions backend/apps/account/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.utils.translation import gettext_lazy
from faker import Faker

from backend.apps.account.models import Account, BDGroup, BDRole, Team, Role, Career, Subscription
from backend.apps.account.models import Account, BDGroup, BDRole, Team, Role, Career, Subscription, DataAPIKey
from backend.apps.account.tasks import sync_subscription_task


Expand Down Expand Up @@ -192,6 +192,30 @@ 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 @@ -291,7 +315,7 @@ class AccountAdmin(BaseAccountAdmin):
)
search_fields = ("email", "full_name")
ordering = ["-created_at"]
inlines = (CareerInline, SubscriptionInline)
inlines = (CareerInline, SubscriptionInline, DataAPIKeyInline)
filter_horizontal = ()

def is_subscriber(self, instance):
Expand Down Expand Up @@ -378,10 +402,57 @@ 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",
"hashed_key",
"last_used_at",
"created_at",
"updated_at"
)
fieldsets = (
(None, {
"fields": (
"id",
"account",
"name",
"prefix",
"hashed_key",
"is_active",
)
}),
("Timing", {
"fields": (
"expires_at",
"last_used_at",
"created_at",
"updated_at"
)
}),
)
ordering = ["-created_at"]

def has_add_permission(self, request):
return False # API keys should be created through the API, not admin


admin.site.register(Account, AccountAdmin)
admin.site.register(Team, TeamAdmin)
admin.site.register(Role, RoleAdmin)
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)
admin.site.register(BDRole)
60 changes: 60 additions & 0 deletions backend/apps/account/migrations/0024_dataapikey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Generated by Django 4.2.18 on 2025-02-10 04:13

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [
('account', '0023_alter_career_role_old_alter_career_team_old'),
]

operations = [
migrations.CreateModel(
name='DataAPIKey',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='data_api_keys', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Data API Key',
'verbose_name_plural': 'Data API Keys',
'ordering': ['created_at'],
},
),
migrations.AddField(
model_name='dataapikey',
name='expires_at',
field=models.DateTimeField(blank=True, help_text='Optional expiration date', null=True),
),
migrations.AddField(
model_name='dataapikey',
name='hashed_key',
field=models.CharField(blank=True, help_text='The hashed API key', max_length=64, null=True, unique=True),
),
migrations.AddField(
model_name='dataapikey',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='dataapikey',
name='last_used_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='dataapikey',
name='name',
field=models.CharField(blank=True, help_text='A friendly name to identify this API key', max_length=100, null=True),
),
migrations.AddField(
model_name='dataapikey',
name='prefix',
field=models.CharField(blank=True, help_text='First 8 characters of the API key', max_length=8, null=True, unique=True),
),
]
20 changes: 20 additions & 0 deletions backend/apps/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,26 @@ def next_billing_cycle(self):
return self.subscription.current_period_end.isoformat()
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")
hashed_key = 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}...)"


def split_password(password: str) -> Tuple[str, str, str, str]:
"""Split a password into four parts: algorithm, iterations, salt, and hash"""
Expand Down
Loading