diff --git a/backend/backend/core/models/api_tokens.py b/backend/backend/core/models/api_tokens.py index 369ffed..a73bbe4 100644 --- a/backend/backend/core/models/api_tokens.py +++ b/backend/backend/core/models/api_tokens.py @@ -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) diff --git a/backend/backend/core/routers/api_tokens/views.py b/backend/backend/core/routers/api_tokens/views.py index b2bb9e8..be19d0f 100644 --- a/backend/backend/core/routers/api_tokens/views.py +++ b/backend/backend/core/routers/api_tokens/views.py @@ -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( @@ -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, diff --git a/backend/backend/core/user.py b/backend/backend/core/user.py index 4c4c060..e2e2bc3 100644 --- a/backend/backend/core/user.py +++ b/backend/backend/core/user.py @@ -1,8 +1,8 @@ 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 @@ -10,6 +10,7 @@ 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__) @@ -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: diff --git a/backend/backend/core/views.py b/backend/backend/core/views.py index 888a71e..d687482 100644 --- a/backend/backend/core/views.py +++ b/backend/backend/core/views.py @@ -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 @@ -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])