Skip to content
Open
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
154 changes: 154 additions & 0 deletions gateway/sds_gateway/users/views/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Re-exports for backward compatibility
# This ensures all existing imports continue to work

# User profile views
# API key views
from .api_keys import MAX_API_KEY_COUNT
from .api_keys import GenerateAPIKeyFormView
from .api_keys import GenerateAPIKeyView
from .api_keys import NewAPIKeyView
from .api_keys import RevokeAPIKeyView
from .api_keys import generate_api_key_form_view
from .api_keys import get_active_api_key_count
from .api_keys import new_api_key_view
from .api_keys import revoke_api_key_view
from .api_keys import user_api_key_view
from .api_keys import validate_uuid

# Capture views
from .captures import API_CAPTURES_LIMIT
from .captures import CapturesAPIView
from .captures import KeywordAutocompleteAPIView
from .captures import ListCapturesView
from .captures import _get_captures_for_template # Exported for tests
from .captures import keyword_autocomplete_api_view
from .captures import user_capture_list_view
from .captures import user_captures_api_view

# Dataset views
from .datasets import DatasetDetailsView
from .datasets import DatasetVersioningView
from .datasets import GroupCapturesView
from .datasets import ListDatasetsView
from .datasets import SearchPublishedDatasetsView
from .datasets import user_dataset_details_view
from .datasets import user_dataset_list_view
from .datasets import user_dataset_versioning_view
from .datasets import user_group_captures_view
from .datasets import user_publish_dataset_view
from .datasets import user_search_datasets_view

# Download views
from .downloads import DownloadItemView
from .downloads import TemporaryZipDownloadView
from .downloads import user_download_item_view
from .downloads import user_temporary_zip_download_view

# File views
from .files import CheckFileExistsView
from .files import FileContentView
from .files import FileDetailView
from .files import FileDownloadView
from .files import FileH5InfoView
from .files import FilesView
from .files import ListFilesView
from .files import files_view
from .files import user_check_file_exists_view
from .files import user_file_detail_view
from .files import user_file_list_view

# Share group views
from .share_groups import ShareGroupListView
from .share_groups import user_share_group_list_view

# Sharing views
from .sharing import ShareItemView
from .sharing import ShareOperationError
from .sharing import user_share_item_view

# Special pages
from .special_pages import HomePageView
from .special_pages import SPXDACDatasetAltView
from .special_pages import home_page_view
from .special_pages import spx_dac_dataset_alt_view

# Upload views
from .uploads import UploadCaptureView
from .uploads import user_upload_capture_view
from .user_profile import UserDetailView
from .user_profile import UserRedirectView
from .user_profile import UserUpdateView
from .user_profile import user_detail_view
from .user_profile import user_redirect_view
from .user_profile import user_update_view

# Utility views
from .utilities import RenderHTMLFragmentView
from .utilities import render_html_fragment_view

__all__ = [
"API_CAPTURES_LIMIT",
"MAX_API_KEY_COUNT",
"CapturesAPIView",
"CheckFileExistsView",
"DatasetDetailsView",
"DatasetVersioningView",
"DownloadItemView",
"FileContentView",
"FileDetailView",
"FileDownloadView",
"FileH5InfoView",
"FilesView",
"GenerateAPIKeyFormView",
"GenerateAPIKeyView",
"GroupCapturesView",
"HomePageView",
"KeywordAutocompleteAPIView",
"ListCapturesView",
"ListDatasetsView",
"ListFilesView",
"NewAPIKeyView",
"RenderHTMLFragmentView",
"RevokeAPIKeyView",
"SPXDACDatasetAltView",
"SearchPublishedDatasetsView",
"ShareGroupListView",
"ShareItemView",
"ShareOperationError",
"TemporaryZipDownloadView",
"UploadCaptureView",
"UserDetailView",
"UserRedirectView",
"UserUpdateView",
"_get_captures_for_template",
"files_view",
"generate_api_key_form_view",
"get_active_api_key_count",
"home_page_view",
"keyword_autocomplete_api_view",
"new_api_key_view",
"render_html_fragment_view",
"revoke_api_key_view",
"spx_dac_dataset_alt_view",
"user_api_key_view",
"user_capture_list_view",
"user_captures_api_view",
"user_check_file_exists_view",
"user_dataset_details_view",
"user_dataset_list_view",
"user_dataset_versioning_view",
"user_detail_view",
"user_download_item_view",
"user_file_detail_view",
"user_file_list_view",
"user_group_captures_view",
"user_publish_dataset_view",
"user_redirect_view",
"user_search_datasets_view",
"user_share_group_list_view",
"user_share_item_view",
"user_temporary_zip_download_view",
"user_update_view",
"user_upload_capture_view",
"validate_uuid",
]
182 changes: 182 additions & 0 deletions gateway/sds_gateway/users/views/api_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import datetime
from typing import Any
from uuid import UUID

from django.contrib import messages
from django.http import HttpRequest
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import redirect
from django.shortcuts import render
from django.views import View

from sds_gateway.api_methods.models import KeySources
from sds_gateway.users.mixins import ApprovedUserRequiredMixin
from sds_gateway.users.mixins import Auth0LoginRequiredMixin
from sds_gateway.users.models import UserAPIKey

# Constants
MAX_API_KEY_COUNT = 10


def get_active_api_key_count(api_keys) -> int:
"""
Calculate the number of active (non-revoked and non-expired) API keys.

Args:
api_keys: QuerySet of UserAPIKey objects

Returns:
int: Number of active API keys
"""
now = datetime.datetime.now(datetime.UTC)
return sum(
1
for key in api_keys
if not key.revoked and (not key.expiry_date or key.expiry_date >= now)
)


def validate_uuid(uuid_string: str) -> bool:
"""Validate if a string is a valid UUID."""
try:
UUID(uuid_string)
except (ValueError, TypeError):
return False
else:
return True


class GenerateAPIKeyView(ApprovedUserRequiredMixin, Auth0LoginRequiredMixin, View):
template_name = "users/user_api_key.html"

def get(self, request, *args, **kwargs):
# Get all API keys for the user (except SVIBackend)
api_keys = (
UserAPIKey.objects.filter(user=request.user)
.exclude(source=KeySources.SVIBackend)
.order_by("revoked", "-created")
) # Active keys first, then by creation date (recent first)
now = datetime.datetime.now(datetime.UTC)
active_api_key_count = get_active_api_key_count(api_keys)
context = {
"api_key": False,
"expires_at": None,
"expired": False,
"current_api_keys": api_keys,
"now": now,
"active_api_key_count": active_api_key_count,
}
if not api_keys.exists():
return render(
request,
template_name=self.template_name,
context=context,
)

context.update(
{
"api_key": True, # return True if API key exists
"current_api_keys": api_keys,
"now": now,
"active_api_key_count": active_api_key_count,
}
)
return render(
request,
template_name=self.template_name,
context=context,
)

def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
"""
Creates a new API key for the authenticated user without deleting existing keys.
Enforces the maximum API key count (MAX_API_KEY_COUNT) per user.
"""
# Check if user has reached the maximum number of active API keys
api_keys = UserAPIKey.objects.filter(user=request.user).exclude(
source=KeySources.SVIBackend
)
active_api_key_count = get_active_api_key_count(api_keys)
if active_api_key_count >= MAX_API_KEY_COUNT:
messages.error(
request,
f"You have reached the maximum number of API keys "
f"({MAX_API_KEY_COUNT}). Please revoke an existing key "
"before creating a new one.",
)
return redirect("users:view_api_key")

# Get the name and description from the form
api_key_name = request.POST.get("api_key_name", "")
api_key_description = request.POST.get("api_key_description", "")
api_key_expiry_date_str = request.POST.get("api_key_expiry_date", "")

expiry_date = None
if api_key_expiry_date_str:
try:
expiry_date = datetime.datetime.strptime(
api_key_expiry_date_str, "%Y-%m-%d"
).replace(tzinfo=datetime.UTC)
except ValueError:
messages.error(request, "Invalid expiration date format.")
return redirect("users:view_api_key")

# create an API key for the user
_, raw_key = UserAPIKey.objects.create_key(
name=api_key_name,
description=api_key_description,
user=request.user,
source=KeySources.SDSWebUI,
expiry_date=expiry_date,
)
request.session["new_api_key"] = raw_key
return redirect("users:new_api_key")


user_api_key_view = GenerateAPIKeyView.as_view()


class NewAPIKeyView(ApprovedUserRequiredMixin, Auth0LoginRequiredMixin, View):
template_name = "users/new_api_key.html"

def get(self, request, *args, **kwargs):
api_key = request.session.pop("new_api_key", None)
return render(request, self.template_name, {"api_key": api_key})


new_api_key_view = NewAPIKeyView.as_view()


class RevokeAPIKeyView(ApprovedUserRequiredMixin, Auth0LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
key_id = request.POST.get("key_id")
api_key = get_object_or_404(UserAPIKey, id=key_id, user=request.user)
if not api_key.revoked:
api_key.revoked = True
api_key.save()
messages.success(request, "API key revoked successfully.")
else:
messages.info(request, "API key is already revoked.")
return redirect("users:view_api_key")


revoke_api_key_view = RevokeAPIKeyView.as_view()


class GenerateAPIKeyFormView(ApprovedUserRequiredMixin, Auth0LoginRequiredMixin, View):
template_name = "users/generate_api_key_form.html"

def get(self, request, *args, **kwargs):
api_keys = UserAPIKey.objects.filter(user=request.user).exclude(
source=KeySources.SVIBackend
)
active_api_key_count = get_active_api_key_count(api_keys)
is_allowed_to_generate_key = active_api_key_count < MAX_API_KEY_COUNT
context = {
"is_allowed_to_generate_key": is_allowed_to_generate_key,
}
return render(request, self.template_name, context)


generate_api_key_form_view = GenerateAPIKeyFormView.as_view()
Loading
Loading