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
2 changes: 1 addition & 1 deletion backend/backend/core/models/api_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class APIToken(DefaultOrganizationMixin, BaseModel):
last_used_at = models.DateTimeField(null=True, blank=True)

def save(self, *args, **kwargs):
if self.token and not self.token_hash:
if self.token:
self.token_hash = hashlib.sha256(self.token.encode()).hexdigest()
super().save(*args, **kwargs)

Expand Down
43 changes: 41 additions & 2 deletions backend/backend/core/routers/api_tokens/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ def regenerate_api_key(request: Request, key_id: str) -> Response:
token.signature = new_sig
token.is_disabled = False
token.expires_at = now() + timedelta(days=django_settings.API_KEY_EXPIRY_DAYS)
token.save(update_fields=["token", "signature", "is_disabled", "expires_at"])
# save() recalculates token_hash automatically
token.save()

logger.info(f"API key regenerated: id={token.id}, label={token.label}, user={request.user.email}")
log_api_key_event(
Expand All @@ -182,8 +183,46 @@ def regenerate_api_key(request: Request, key_id: str) -> Response:
@api_view([HTTPMethods.POST])
@handle_http_request
def generate_token(request: Request) -> Response:
"""Legacy token generation endpoint."""
"""Legacy token generation endpoint.

Now creates a proper APIToken record with vtk_ prefix, label, and expiry
to maintain consistency with the api-keys/create endpoint.
Uses upsert: updates existing auto-generated token or creates a new one.
"""
api_key = generate_api_key()
sig = generate_signature(api_key)
new_expiry = now() + timedelta(days=django_settings.API_KEY_EXPIRY_DAYS)

# Upsert: update existing auto-generated token if present, else create
existing = APIToken.objects.filter(
user=request.user, label="Default"
).order_by("-created_at").first()

if existing:
existing.token = api_key
existing.signature = sig
existing.is_disabled = False
existing.expires_at = new_expiry
# save() recalculates token_hash automatically
existing.save()
token = existing
audit_action = "regenerate"
else:
token = APIToken.objects.create(
user=request.user,
token=api_key,
signature=sig,
label="Default",
expires_at=new_expiry,
)
audit_action = "create"

logger.info(f"Legacy token generated: id={token.id}, user={request.user.email}")
log_api_key_event(
request, action=audit_action, key_id=token.id,
key_label="Default", key_masked=token.masked_token,
)

return Response({
"message": "Token generated successfully.",
"token": api_key,
Expand Down
10 changes: 7 additions & 3 deletions backend/backend/core/user.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import logging
import uuid
from datetime import timedelta
from typing import Any, Optional

from django.conf import settings as django_settings
from django.db import IntegrityError
from django.db import transaction
from django.utils.timezone import now

from backend.core.models.api_tokens import APIToken
from backend.core.models.organization_model import Organization
from backend.core.models.user_model import User
from backend.core.services.api_key_service import generate_api_key, generate_signature

Logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -87,11 +88,14 @@ def get_or_create_valid_token(self, user: User, organization: Organization):
token.delete()
token = None
if token is None:
api_key = generate_api_key()
token = APIToken.objects.create(
user=user,
organization=organization,
token=str(uuid.uuid4().hex),
expires_at=now() + timedelta(days=90),
token=api_key,
signature=generate_signature(api_key),
label="Default",
expires_at=now() + timedelta(days=django_settings.API_KEY_EXPIRY_DAYS),
)
logging.info(f"A new api token for user: {user} and tenant: {organization} is created")
except Exception as e:
Expand Down
46 changes: 33 additions & 13 deletions backend/backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
from backend.utils.tenant_context import get_current_tenant

from backend.core.models.api_tokens import APIToken
from backend.core.services.api_key_audit import log_api_key_event
from backend.core.services.api_key_service import generate_api_key, generate_signature
from django.conf import settings as django_settings
from django.utils.timezone import now
from datetime import timedelta

Expand All @@ -41,30 +44,47 @@ def update_user_profile(request: Request) -> Response:
user_service = UserService()
user = user_service.get_user_by_email(request.user.email)
user = user_service.update_user_display_names(user, request.data["first_name"], request.data["last_name"])
update_user_token(request, user)
return Response(
data={
'first_name': user.first_name,
'last_name': user.last_name,
},
status=status.HTTP_200_OK
)
updated_token = update_user_token(request, user)
data = {
'first_name': user.first_name,
'last_name': user.last_name,
}
if updated_token:
data['token'] = updated_token
return Response(data=data, status=status.HTTP_200_OK)


def update_user_token(request, user):
new_token = request.data.get("token")
existing_token: APIToken = APIToken.objects.filter(user=user).first()
# token_value is sent back by the frontend — used only to detect "unchanged"
token_value = request.data.get("token")
existing_token: APIToken = APIToken.objects.filter(user=user, label="Default").first()

if new_token:
if existing_token and existing_token.token == new_token:
if token_value:
# Skip regeneration if the token hasn't changed
if existing_token and existing_token.token == token_value:
return
had_existing = existing_token is not None
if existing_token:
existing_token.delete()

APIToken.objects.create(user=user, token=new_token, expires_at= now() + timedelta(days=90))
api_key = generate_api_key()
token = APIToken.objects.create(
user=user,
token=api_key,
signature=generate_signature(api_key),
label="Default",
expires_at=now() + timedelta(days=django_settings.API_KEY_EXPIRY_DAYS),
)
log_api_key_event(
request, action="regenerate" if had_existing else "create",
key_id=token.id,
key_label="Default", key_masked=token.masked_token,
)
return api_key
else:
if existing_token:
existing_token.delete()
return None


@api_view([HTTPMethods.GET])
Expand Down
Loading