django-clerk-users 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. django_clerk_users/__init__.py +78 -7
  2. django_clerk_users/apps.py +20 -0
  3. django_clerk_users/authentication/__init__.py +24 -0
  4. django_clerk_users/authentication/backends.py +89 -0
  5. django_clerk_users/authentication/drf.py +111 -0
  6. django_clerk_users/authentication/utils.py +171 -0
  7. django_clerk_users/caching.py +161 -0
  8. django_clerk_users/checks.py +127 -0
  9. django_clerk_users/client.py +32 -0
  10. django_clerk_users/decorators.py +181 -0
  11. django_clerk_users/exceptions.py +51 -0
  12. django_clerk_users/management/__init__.py +0 -0
  13. django_clerk_users/management/commands/__init__.py +0 -0
  14. django_clerk_users/management/commands/migrate_users_to_clerk.py +223 -0
  15. django_clerk_users/management/commands/sync_clerk_organizations.py +191 -0
  16. django_clerk_users/management/commands/sync_clerk_users.py +114 -0
  17. django_clerk_users/managers.py +121 -0
  18. django_clerk_users/middleware/__init__.py +9 -0
  19. django_clerk_users/middleware/auth.py +201 -0
  20. django_clerk_users/migrations/0001_initial.py +174 -0
  21. django_clerk_users/migrations/__init__.py +0 -0
  22. django_clerk_users/models.py +174 -0
  23. django_clerk_users/organizations/__init__.py +8 -0
  24. django_clerk_users/organizations/admin.py +81 -0
  25. django_clerk_users/organizations/apps.py +8 -0
  26. django_clerk_users/organizations/middleware.py +130 -0
  27. django_clerk_users/organizations/models.py +316 -0
  28. django_clerk_users/organizations/webhooks.py +417 -0
  29. django_clerk_users/settings.py +37 -0
  30. django_clerk_users/testing.py +381 -0
  31. django_clerk_users/utils.py +210 -0
  32. django_clerk_users/webhooks/__init__.py +26 -0
  33. django_clerk_users/webhooks/handlers.py +346 -0
  34. django_clerk_users/webhooks/security.py +108 -0
  35. django_clerk_users/webhooks/signals.py +42 -0
  36. django_clerk_users/webhooks/views.py +76 -0
  37. django_clerk_users-0.0.2.dist-info/METADATA +228 -0
  38. django_clerk_users-0.0.2.dist-info/RECORD +41 -0
  39. django_clerk_users/main.py +0 -2
  40. django_clerk_users-0.0.1.dist-info/METADATA +0 -24
  41. django_clerk_users-0.0.1.dist-info/RECORD +0 -7
  42. {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.0.2.dist-info}/WHEEL +0 -0
  43. {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.0.2.dist-info}/licenses/LICENSE +0 -0
  44. {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,127 @@
1
+ """
2
+ Django system checks for django-clerk-users configuration.
3
+ """
4
+
5
+ from django.conf import settings
6
+ from django.core.checks import Error, Warning, register
7
+
8
+
9
+ @register()
10
+ def check_clerk_secret_key(app_configs, **kwargs):
11
+ """Check that CLERK_SECRET_KEY is configured."""
12
+ errors = []
13
+ if not getattr(settings, "CLERK_SECRET_KEY", None):
14
+ errors.append(
15
+ Error(
16
+ "CLERK_SECRET_KEY is not configured.",
17
+ hint="Set CLERK_SECRET_KEY in your Django settings.",
18
+ id="django_clerk_users.E001",
19
+ )
20
+ )
21
+ return errors
22
+
23
+
24
+ @register()
25
+ def check_clerk_webhook_signing_key(app_configs, **kwargs):
26
+ """Check that CLERK_WEBHOOK_SIGNING_KEY is configured."""
27
+ warnings = []
28
+ if not getattr(settings, "CLERK_WEBHOOK_SIGNING_KEY", None):
29
+ warnings.append(
30
+ Warning(
31
+ "CLERK_WEBHOOK_SIGNING_KEY is not configured.",
32
+ hint=(
33
+ "Set CLERK_WEBHOOK_SIGNING_KEY in your Django settings "
34
+ "if you plan to use Clerk webhooks."
35
+ ),
36
+ id="django_clerk_users.W001",
37
+ )
38
+ )
39
+ return warnings
40
+
41
+
42
+ @register()
43
+ def check_auth_user_model(app_configs, **kwargs):
44
+ """Check that AUTH_USER_MODEL is configured for Clerk."""
45
+ warnings = []
46
+ auth_user_model = getattr(settings, "AUTH_USER_MODEL", "auth.User")
47
+
48
+ # Check if using a Clerk-compatible user model
49
+ if not (
50
+ auth_user_model.startswith("django_clerk_users.")
51
+ or "clerk" in auth_user_model.lower()
52
+ ):
53
+ warnings.append(
54
+ Warning(
55
+ f"AUTH_USER_MODEL is set to '{auth_user_model}'.",
56
+ hint=(
57
+ "Consider using 'django_clerk_users.ClerkUser' or a custom model "
58
+ "that extends AbstractClerkUser for full Clerk integration."
59
+ ),
60
+ id="django_clerk_users.W002",
61
+ )
62
+ )
63
+ return warnings
64
+
65
+
66
+ @register()
67
+ def check_middleware_installed(app_configs, **kwargs):
68
+ """Check that ClerkAuthMiddleware is installed."""
69
+ warnings = []
70
+ middleware = getattr(settings, "MIDDLEWARE", [])
71
+
72
+ clerk_middleware = "django_clerk_users.middleware.ClerkAuthMiddleware"
73
+ if clerk_middleware not in middleware:
74
+ warnings.append(
75
+ Warning(
76
+ "ClerkAuthMiddleware is not in MIDDLEWARE.",
77
+ hint=(
78
+ f"Add '{clerk_middleware}' to MIDDLEWARE in your Django settings "
79
+ "for automatic Clerk authentication."
80
+ ),
81
+ id="django_clerk_users.W003",
82
+ )
83
+ )
84
+ return warnings
85
+
86
+
87
+ @register()
88
+ def check_authentication_backend(app_configs, **kwargs):
89
+ """Check that ClerkBackend is in AUTHENTICATION_BACKENDS."""
90
+ warnings = []
91
+ backends = getattr(settings, "AUTHENTICATION_BACKENDS", [])
92
+
93
+ clerk_backend = "django_clerk_users.authentication.ClerkBackend"
94
+ if clerk_backend not in backends:
95
+ warnings.append(
96
+ Warning(
97
+ "ClerkBackend is not in AUTHENTICATION_BACKENDS.",
98
+ hint=(
99
+ f"Add '{clerk_backend}' to AUTHENTICATION_BACKENDS in your "
100
+ "Django settings."
101
+ ),
102
+ id="django_clerk_users.W004",
103
+ )
104
+ )
105
+ return warnings
106
+
107
+
108
+ @register()
109
+ def check_frontend_hosts(app_configs, **kwargs):
110
+ """Check that CLERK_FRONTEND_HOSTS is configured."""
111
+ warnings = []
112
+ frontend_hosts = getattr(settings, "CLERK_FRONTEND_HOSTS", [])
113
+ auth_parties = getattr(settings, "CLERK_AUTH_PARTIES", [])
114
+
115
+ if not frontend_hosts and not auth_parties:
116
+ warnings.append(
117
+ Warning(
118
+ "CLERK_FRONTEND_HOSTS is not configured.",
119
+ hint=(
120
+ "Set CLERK_FRONTEND_HOSTS in your Django settings to the list of "
121
+ "frontend URLs that will be sending authenticated requests "
122
+ "(e.g., ['https://myapp.com', 'http://localhost:3000'])."
123
+ ),
124
+ id="django_clerk_users.W005",
125
+ )
126
+ )
127
+ return warnings
@@ -0,0 +1,32 @@
1
+ """
2
+ Clerk SDK client singleton.
3
+ """
4
+
5
+ from functools import lru_cache
6
+
7
+ from clerk_backend_api import Clerk
8
+
9
+ from django_clerk_users.exceptions import ClerkConfigurationError
10
+ from django_clerk_users.settings import CLERK_SECRET_KEY
11
+
12
+
13
+ @lru_cache(maxsize=1)
14
+ def get_clerk_client() -> Clerk:
15
+ """
16
+ Get the Clerk SDK client instance.
17
+
18
+ Returns a cached singleton instance of the Clerk client.
19
+
20
+ Raises:
21
+ ClerkConfigurationError: If CLERK_SECRET_KEY is not set.
22
+ """
23
+ if not CLERK_SECRET_KEY:
24
+ raise ClerkConfigurationError(
25
+ "CLERK_SECRET_KEY is not set. Please configure it in your Django settings."
26
+ )
27
+ return Clerk(bearer_auth=CLERK_SECRET_KEY)
28
+
29
+
30
+ def get_clerk_sdk() -> Clerk:
31
+ """Alias for get_clerk_client() for compatibility."""
32
+ return get_clerk_client()
@@ -0,0 +1,181 @@
1
+ """
2
+ View decorators for django-clerk-users.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import functools
8
+ from typing import TYPE_CHECKING, Callable
9
+
10
+ from django.http import JsonResponse
11
+
12
+ if TYPE_CHECKING:
13
+ from django.http import HttpRequest, HttpResponse
14
+
15
+
16
+ def clerk_user_required(view_func: Callable) -> Callable:
17
+ """
18
+ Decorator that requires a Clerk-authenticated user.
19
+
20
+ Use this decorator on views that require authentication.
21
+ Returns a 401 response if the user is not authenticated.
22
+
23
+ Example:
24
+ from django_clerk_users.decorators import clerk_user_required
25
+
26
+ @clerk_user_required
27
+ def my_protected_view(request):
28
+ user = request.clerk_user
29
+ return JsonResponse({"email": user.email})
30
+
31
+ Args:
32
+ view_func: The view function to wrap.
33
+
34
+ Returns:
35
+ The wrapped view function.
36
+ """
37
+
38
+ @functools.wraps(view_func)
39
+ def wrapper(request: "HttpRequest", *args, **kwargs) -> "HttpResponse":
40
+ clerk_user = getattr(request, "clerk_user", None)
41
+
42
+ if not clerk_user or not clerk_user.is_authenticated:
43
+ return JsonResponse(
44
+ {"error": "Authentication required"},
45
+ status=401,
46
+ )
47
+
48
+ return view_func(request, *args, **kwargs)
49
+
50
+ return wrapper
51
+
52
+
53
+ def clerk_org_required(view_func: Callable) -> Callable:
54
+ """
55
+ Decorator that requires an organization context.
56
+
57
+ Use this decorator on views that require both authentication
58
+ and an organization context. Returns a 401 response if not
59
+ authenticated, or a 403 response if no organization is set.
60
+
61
+ Example:
62
+ from django_clerk_users.decorators import clerk_org_required
63
+
64
+ @clerk_org_required
65
+ def my_org_view(request):
66
+ org_id = request.org
67
+ return JsonResponse({"org_id": org_id})
68
+
69
+ Args:
70
+ view_func: The view function to wrap.
71
+
72
+ Returns:
73
+ The wrapped view function.
74
+ """
75
+
76
+ @functools.wraps(view_func)
77
+ def wrapper(request: "HttpRequest", *args, **kwargs) -> "HttpResponse":
78
+ clerk_user = getattr(request, "clerk_user", None)
79
+
80
+ if not clerk_user or not clerk_user.is_authenticated:
81
+ return JsonResponse(
82
+ {"error": "Authentication required"},
83
+ status=401,
84
+ )
85
+
86
+ org = getattr(request, "org", None)
87
+ if not org:
88
+ return JsonResponse(
89
+ {"error": "Organization context required"},
90
+ status=403,
91
+ )
92
+
93
+ return view_func(request, *args, **kwargs)
94
+
95
+ return wrapper
96
+
97
+
98
+ def clerk_staff_required(view_func: Callable) -> Callable:
99
+ """
100
+ Decorator that requires a staff user.
101
+
102
+ Use this decorator on views that require staff access.
103
+ Returns a 401 response if not authenticated, or a 403 response
104
+ if the user is not staff.
105
+
106
+ Example:
107
+ from django_clerk_users.decorators import clerk_staff_required
108
+
109
+ @clerk_staff_required
110
+ def admin_view(request):
111
+ return JsonResponse({"message": "Staff access granted"})
112
+
113
+ Args:
114
+ view_func: The view function to wrap.
115
+
116
+ Returns:
117
+ The wrapped view function.
118
+ """
119
+
120
+ @functools.wraps(view_func)
121
+ def wrapper(request: "HttpRequest", *args, **kwargs) -> "HttpResponse":
122
+ clerk_user = getattr(request, "clerk_user", None)
123
+
124
+ if not clerk_user or not clerk_user.is_authenticated:
125
+ return JsonResponse(
126
+ {"error": "Authentication required"},
127
+ status=401,
128
+ )
129
+
130
+ if not clerk_user.is_staff:
131
+ return JsonResponse(
132
+ {"error": "Staff access required"},
133
+ status=403,
134
+ )
135
+
136
+ return view_func(request, *args, **kwargs)
137
+
138
+ return wrapper
139
+
140
+
141
+ def clerk_superuser_required(view_func: Callable) -> Callable:
142
+ """
143
+ Decorator that requires a superuser.
144
+
145
+ Use this decorator on views that require superuser access.
146
+ Returns a 401 response if not authenticated, or a 403 response
147
+ if the user is not a superuser.
148
+
149
+ Example:
150
+ from django_clerk_users.decorators import clerk_superuser_required
151
+
152
+ @clerk_superuser_required
153
+ def superuser_view(request):
154
+ return JsonResponse({"message": "Superuser access granted"})
155
+
156
+ Args:
157
+ view_func: The view function to wrap.
158
+
159
+ Returns:
160
+ The wrapped view function.
161
+ """
162
+
163
+ @functools.wraps(view_func)
164
+ def wrapper(request: "HttpRequest", *args, **kwargs) -> "HttpResponse":
165
+ clerk_user = getattr(request, "clerk_user", None)
166
+
167
+ if not clerk_user or not clerk_user.is_authenticated:
168
+ return JsonResponse(
169
+ {"error": "Authentication required"},
170
+ status=401,
171
+ )
172
+
173
+ if not clerk_user.is_superuser:
174
+ return JsonResponse(
175
+ {"error": "Superuser access required"},
176
+ status=403,
177
+ )
178
+
179
+ return view_func(request, *args, **kwargs)
180
+
181
+ return wrapper
@@ -0,0 +1,51 @@
1
+ """
2
+ Custom exceptions for django-clerk-users.
3
+ """
4
+
5
+
6
+ class ClerkError(Exception):
7
+ """Base exception for all Clerk-related errors."""
8
+
9
+ pass
10
+
11
+
12
+ class ClerkConfigurationError(ClerkError):
13
+ """Raised when Clerk is not properly configured."""
14
+
15
+ pass
16
+
17
+
18
+ class ClerkAuthenticationError(ClerkError):
19
+ """Raised when authentication fails."""
20
+
21
+ pass
22
+
23
+
24
+ class ClerkTokenError(ClerkAuthenticationError):
25
+ """Raised when JWT token validation fails."""
26
+
27
+ pass
28
+
29
+
30
+ class ClerkWebhookError(ClerkError):
31
+ """Raised when webhook verification fails."""
32
+
33
+ pass
34
+
35
+
36
+ class ClerkAPIError(ClerkError):
37
+ """Raised when Clerk API returns an error."""
38
+
39
+ pass
40
+
41
+
42
+ class ClerkUserNotFoundError(ClerkError):
43
+ """Raised when a Clerk user cannot be found."""
44
+
45
+ pass
46
+
47
+
48
+ class ClerkOrganizationNotFoundError(ClerkError):
49
+ """Raised when a Clerk organization cannot be found."""
50
+
51
+ pass
File without changes
File without changes
@@ -0,0 +1,223 @@
1
+ """
2
+ Management command to migrate existing Django users to Clerk.
3
+
4
+ This command creates users in Clerk from your existing Django user database,
5
+ allowing you to migrate to Clerk authentication without losing user data.
6
+
7
+ Note: Passwords cannot be migrated. Users will need to reset their password
8
+ or use OAuth/social login.
9
+ """
10
+
11
+ from datetime import datetime
12
+
13
+ from django.apps import apps
14
+ from django.core.management.base import BaseCommand, CommandError
15
+
16
+ from django_clerk_users.client import get_clerk_client
17
+
18
+
19
+ class Command(BaseCommand):
20
+ help = "Migrate existing Django users to Clerk"
21
+
22
+ def add_arguments(self, parser):
23
+ parser.add_argument(
24
+ "--source-model",
25
+ type=str,
26
+ default="auth.User",
27
+ help="Source user model in app.Model format (default: auth.User)",
28
+ )
29
+ parser.add_argument(
30
+ "--email",
31
+ type=str,
32
+ help="Migrate a specific user by email address",
33
+ )
34
+ parser.add_argument(
35
+ "--all",
36
+ action="store_true",
37
+ help="Migrate all users",
38
+ )
39
+ parser.add_argument(
40
+ "--created-before",
41
+ type=str,
42
+ help="Migrate users created before this date (YYYY-MM-DD)",
43
+ )
44
+ parser.add_argument(
45
+ "--skip-existing",
46
+ action="store_true",
47
+ help="Skip users that already exist in Clerk (by email)",
48
+ )
49
+ parser.add_argument(
50
+ "--skip-password-email",
51
+ action="store_true",
52
+ default=True,
53
+ help="Don't trigger password reset emails (default: True)",
54
+ )
55
+ parser.add_argument(
56
+ "--dry-run",
57
+ action="store_true",
58
+ help="Show what would be migrated without making changes",
59
+ )
60
+ parser.add_argument(
61
+ "--limit",
62
+ type=int,
63
+ default=100,
64
+ help="Limit number of users to migrate (default: 100)",
65
+ )
66
+
67
+ def handle(self, *args, **options):
68
+ # Get the source model
69
+ source_model_path = options["source_model"]
70
+ try:
71
+ app_label, model_name = source_model_path.split(".")
72
+ SourceUser = apps.get_model(app_label, model_name)
73
+ except (ValueError, LookupError) as e:
74
+ raise CommandError(f"Invalid source model '{source_model_path}': {e}")
75
+
76
+ email = options["email"]
77
+ migrate_all = options["all"]
78
+ created_before = options["created_before"]
79
+ skip_existing = options["skip_existing"]
80
+ skip_password_email = options["skip_password_email"]
81
+ dry_run = options["dry_run"]
82
+ limit = options["limit"]
83
+
84
+ if not email and not migrate_all and not created_before:
85
+ raise CommandError(
86
+ "You must specify --email, --all, or --created-before"
87
+ )
88
+
89
+ clerk = get_clerk_client()
90
+
91
+ # Build queryset
92
+ queryset = SourceUser.objects.all()
93
+
94
+ if email:
95
+ queryset = queryset.filter(email=email)
96
+ elif created_before:
97
+ try:
98
+ before_date = datetime.strptime(created_before, "%Y-%m-%d")
99
+ queryset = queryset.filter(date_joined__lt=before_date)
100
+ except ValueError:
101
+ raise CommandError(
102
+ "Invalid date format. Use YYYY-MM-DD"
103
+ )
104
+
105
+ queryset = queryset[:limit]
106
+
107
+ created_count = 0
108
+ skipped_count = 0
109
+ error_count = 0
110
+ linked_count = 0
111
+ total_count = 0
112
+
113
+ self.stdout.write(f"Migrating users from {source_model_path}...")
114
+ self.stdout.write(f"Found {queryset.count()} users to process")
115
+
116
+ if dry_run:
117
+ self.stdout.write(self.style.WARNING("DRY RUN - No changes will be made"))
118
+
119
+ for source_user in queryset:
120
+ total_count += 1
121
+ user_email = getattr(source_user, "email", None)
122
+
123
+ if not user_email:
124
+ self.stderr.write(
125
+ self.style.WARNING(f" Skipping user {source_user.pk}: no email")
126
+ )
127
+ skipped_count += 1
128
+ continue
129
+
130
+ first_name = getattr(source_user, "first_name", "") or ""
131
+ last_name = getattr(source_user, "last_name", "") or ""
132
+
133
+ # Check if user exists in Clerk
134
+ if skip_existing:
135
+ try:
136
+ existing_users = clerk.users.list(email_address=[user_email])
137
+ users_data = existing_users.data if hasattr(existing_users, "data") else existing_users
138
+ if users_data:
139
+ if dry_run:
140
+ self.stdout.write(
141
+ f" Would skip (exists in Clerk): {user_email}"
142
+ )
143
+ else:
144
+ self.stdout.write(f" Skipping (exists in Clerk): {user_email}")
145
+ # Try to link the user
146
+ clerk_user = users_data[0]
147
+ self._link_user(source_user, clerk_user)
148
+ linked_count += 1
149
+ skipped_count += 1
150
+ continue
151
+ except Exception as e:
152
+ self.stderr.write(
153
+ self.style.WARNING(f" Error checking if {user_email} exists: {e}")
154
+ )
155
+
156
+ if dry_run:
157
+ self.stdout.write(f" Would create: {user_email}")
158
+ continue
159
+
160
+ # Create user in Clerk
161
+ try:
162
+ clerk_user = clerk.users.create(
163
+ email_address=[user_email],
164
+ first_name=first_name if first_name else None,
165
+ last_name=last_name if last_name else None,
166
+ skip_password_requirement=True,
167
+ skip_password_checks=True,
168
+ )
169
+
170
+ # Link Django user to Clerk
171
+ self._link_user(source_user, clerk_user)
172
+
173
+ created_count += 1
174
+ self.stdout.write(
175
+ self.style.SUCCESS(f" Created: {user_email}")
176
+ )
177
+
178
+ except Exception as e:
179
+ error_str = str(e)
180
+ if "email_address" in error_str.lower() and "taken" in error_str.lower():
181
+ self.stdout.write(
182
+ self.style.WARNING(f" Email already exists in Clerk: {user_email}")
183
+ )
184
+ skipped_count += 1
185
+ else:
186
+ error_count += 1
187
+ self.stderr.write(
188
+ self.style.ERROR(f" Failed to create {user_email}: {e}")
189
+ )
190
+
191
+ self.stdout.write("")
192
+ self.stdout.write(self.style.SUCCESS("Migration complete!"))
193
+ self.stdout.write(f" Total processed: {total_count}")
194
+ if not dry_run:
195
+ self.stdout.write(f" Created in Clerk: {created_count}")
196
+ self.stdout.write(f" Linked existing: {linked_count}")
197
+ self.stdout.write(f" Skipped: {skipped_count}")
198
+ self.stdout.write(f" Errors: {error_count}")
199
+
200
+ if not dry_run and created_count > 0:
201
+ self.stdout.write("")
202
+ self.stdout.write(
203
+ self.style.WARNING(
204
+ "Note: Migrated users will need to reset their password "
205
+ "or use OAuth to sign in."
206
+ )
207
+ )
208
+
209
+ def _link_user(self, source_user, clerk_user):
210
+ """
211
+ Link a Django user to their Clerk user.
212
+
213
+ If the source user has a clerk_id field, update it.
214
+ """
215
+ clerk_id = getattr(clerk_user, "id", None)
216
+ if not clerk_id:
217
+ return
218
+
219
+ # Check if source user has clerk_id field
220
+ if hasattr(source_user, "clerk_id"):
221
+ source_user.clerk_id = clerk_id
222
+ source_user.save(update_fields=["clerk_id"])
223
+ self.stdout.write(f" Linked clerk_id: {clerk_id}")