',
+ callback_url, unique_id, callback_url, unique_id, unique_id, unique_id
+ )
+ return formatted
+ return "-"
+ api_key_link.short_description = 'API Key URL'
+
+
# Register your models here.
admin.site.register(UploadedText, UploadedTextAdmin)
-
+admin.site.register(APIKey, APIKeyAdmin)
diff --git a/medcat-demo-app/webapp/demo/decorators.py b/medcat-demo-app/webapp/demo/decorators.py
new file mode 100644
index 000000000..5de46c3d7
--- /dev/null
+++ b/medcat-demo-app/webapp/demo/decorators.py
@@ -0,0 +1,53 @@
+from functools import wraps
+from django.http import JsonResponse
+from .models import APIKey
+
+
+def require_valid_api_key(view_func):
+ """
+ Decorator to protect endpoints with API key authentication
+
+ Usage:
+ @require_valid_api_key
+ def my_protected_view(request):
+ # Your view logic
+ pass
+ """
+ @wraps(view_func)
+ def wrapper(request, *args, **kwargs):
+ # Check for API key in header or query parameter
+ api_key = (
+ request.headers.get('X-API-Key') or
+ request.GET.get('api_key') or
+ request.POST.get('api_key')
+ )
+
+ if not api_key:
+ return JsonResponse({
+ 'error': 'API key required',
+ 'message': 'Please provide an API key via X-API-Key header or api_key parameter'
+ }, status=401)
+
+ if not APIKey.is_valid(api_key):
+ return JsonResponse({
+ 'error': 'Invalid or expired API key',
+ 'message': 'Please obtain a valid API key'
+ }, status=401)
+
+ # API key is valid, proceed with the view
+ return view_func(request, *args, **kwargs)
+
+ return wrapper
+
+
+# Example usage in views.py:
+"""
+from .decorators import require_valid_api_key
+
+@require_valid_api_key
+def protected_endpoint(request):
+ return JsonResponse({
+ 'message': 'You have access to this protected resource!',
+ 'data': {'example': 'data'}
+ })
+"""
diff --git a/medcat-demo-app/webapp/demo/migrations/0003_auto_20251120_2221.py b/medcat-demo-app/webapp/demo/migrations/0003_auto_20251120_2221.py
new file mode 100644
index 000000000..ddcfedc10
--- /dev/null
+++ b/medcat-demo-app/webapp/demo/migrations/0003_auto_20251120_2221.py
@@ -0,0 +1,34 @@
+# Generated by Django 3.2.25 on 2025-11-20 22:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('demo', '0002_downloader_medcatmodel'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='APIKey',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('key', models.CharField(db_index=True, max_length=64, unique=True)),
+ ('identifier', models.CharField(max_length=255)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('expires_at', models.DateTimeField()),
+ ('is_active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.AlterField(
+ model_name='downloader',
+ name='downloaded_file',
+ field=models.CharField(max_length=100),
+ ),
+ migrations.AlterField(
+ model_name='medcatmodel',
+ name='model_description',
+ field=models.TextField(max_length=200),
+ ),
+ ]
diff --git a/medcat-demo-app/webapp/demo/migrations/0004_alter_apikey_expires_at.py b/medcat-demo-app/webapp/demo/migrations/0004_alter_apikey_expires_at.py
new file mode 100644
index 000000000..45f8298c9
--- /dev/null
+++ b/medcat-demo-app/webapp/demo/migrations/0004_alter_apikey_expires_at.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.25 on 2025-11-24 16:21
+
+import demo.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('demo', '0003_auto_20251120_2221'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='apikey',
+ name='expires_at',
+ field=models.DateTimeField(default=demo.models._default_expiry),
+ ),
+ ]
diff --git a/medcat-demo-app/webapp/demo/models.py b/medcat-demo-app/webapp/demo/models.py
index da2c8aa5d..ee0793291 100644
--- a/medcat-demo-app/webapp/demo/models.py
+++ b/medcat-demo-app/webapp/demo/models.py
@@ -1,10 +1,18 @@
+import secrets
+
+from datetime import timedelta
+
from django.db import models
from django.core.files.storage import FileSystemStorage
+from django.utils import timezone
MODEL_FS = FileSystemStorage(location="/medcat_data")
+cooldown_days = 14
+
+
# Create your models here.
class UploadedText(models.Model):
text = models.TextField(default="", blank=True)
@@ -29,3 +37,42 @@ class MedcatModel(models.Model):
model_file = models.FileField(storage=MODEL_FS)
model_display_name = models.CharField(max_length=50)
model_description = models.TextField(max_length=200)
+
+
+def _default_expiry():
+ return timezone.now() + timedelta(days=cooldown_days)
+
+
+class APIKey(models.Model):
+ """Temporary API keys for successful completions"""
+ key = models.CharField(max_length=64, unique=True, db_index=True)
+ identifier = models.CharField(max_length=255)
+ created_at = models.DateTimeField(auto_now_add=True)
+ expires_at = models.DateTimeField(
+ default=_default_expiry)
+ is_active = models.BooleanField(default=True)
+
+ def save(self, *args, **kwargs):
+ if not self.key:
+ self.key = secrets.token_urlsafe(48)
+ if not self.expires_at:
+ self.expires_at = timezone.now() + timedelta(days=cooldown_days)
+ super().save(*args, **kwargs)
+
+ @classmethod
+ def is_valid(cls, key):
+ """Check if an API key is valid and not expired"""
+ try:
+ api_key = cls.objects.get(key=key, is_active=True)
+ if api_key.expires_at > timezone.now():
+ return True
+ else:
+ # Mark as inactive if expired
+ api_key.is_active = False
+ api_key.save()
+ return False
+ except cls.DoesNotExist:
+ return False
+
+ def __str__(self):
+ return f"{self.key[:10]}... (expires: {self.expires_at})"
diff --git a/medcat-demo-app/webapp/demo/templates/umls_api_key_entry.html b/medcat-demo-app/webapp/demo/templates/umls_api_key_entry.html
index 867790c71..1ea774b7d 100644
--- a/medcat-demo-app/webapp/demo/templates/umls_api_key_entry.html
+++ b/medcat-demo-app/webapp/demo/templates/umls_api_key_entry.html
@@ -46,4 +46,24 @@
Please enter your UMLS API key to verify your license:
You can find your API key by logging into your UMLS account and visiting your UMLS Profile.
+
+
+
+
+
+
+
+
+
+ If you are unable to obtain an API key due to your application for a UMLS license
+ being unanswered due to funding constraints in the NIH.
+ If that is the case, please email us at open-resources@cogstack.org with proof
+ of an ongoing application.
+ We will subsequently be able to provide you access to the models.
+