django-clerk-users 0.0.2__py3-none-any.whl → 0.1.0__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.
- django_clerk_users/managers.py +14 -15
- django_clerk_users/middleware/auth.py +36 -7
- django_clerk_users/migrations/0002_make_clerk_id_nullable.py +24 -0
- django_clerk_users/models.py +8 -2
- django_clerk_users/organizations/migrations/0001_initial.py +349 -0
- django_clerk_users/organizations/migrations/__init__.py +0 -0
- django_clerk_users/organizations/models.py +1 -3
- {django_clerk_users-0.0.2.dist-info → django_clerk_users-0.1.0.dist-info}/METADATA +92 -9
- {django_clerk_users-0.0.2.dist-info → django_clerk_users-0.1.0.dist-info}/RECORD +11 -9
- {django_clerk_users-0.0.2.dist-info → django_clerk_users-0.1.0.dist-info}/WHEEL +1 -2
- django_clerk_users-0.0.2.dist-info/top_level.txt +0 -1
- {django_clerk_users-0.0.2.dist-info → django_clerk_users-0.1.0.dist-info}/licenses/LICENSE +0 -0
django_clerk_users/managers.py
CHANGED
|
@@ -21,29 +21,26 @@ class ClerkUserManager(BaseUserManager["AbstractClerkUser"]):
|
|
|
21
21
|
|
|
22
22
|
def create_user(
|
|
23
23
|
self,
|
|
24
|
-
clerk_id: str,
|
|
25
24
|
email: str,
|
|
25
|
+
clerk_id: str | None = None,
|
|
26
26
|
password: str | None = None,
|
|
27
27
|
**extra_fields: Any,
|
|
28
28
|
) -> "AbstractClerkUser":
|
|
29
29
|
"""
|
|
30
|
-
Create and save a user with the given
|
|
30
|
+
Create and save a user with the given email and optional clerk_id.
|
|
31
31
|
|
|
32
32
|
Args:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
password: Optional password (
|
|
36
|
-
for Django admin compatibility).
|
|
33
|
+
email: The user's email address (required).
|
|
34
|
+
clerk_id: The Clerk user ID (optional for Django admin users).
|
|
35
|
+
password: Optional password (required for Django admin users).
|
|
37
36
|
**extra_fields: Additional fields for the user model.
|
|
38
37
|
|
|
39
38
|
Returns:
|
|
40
39
|
The created user instance.
|
|
41
40
|
|
|
42
41
|
Raises:
|
|
43
|
-
ValueError: If
|
|
42
|
+
ValueError: If email is not provided.
|
|
44
43
|
"""
|
|
45
|
-
if not clerk_id:
|
|
46
|
-
raise ValueError("The clerk_id must be set")
|
|
47
44
|
if not email:
|
|
48
45
|
raise ValueError("The email must be set")
|
|
49
46
|
|
|
@@ -62,18 +59,18 @@ class ClerkUserManager(BaseUserManager["AbstractClerkUser"]):
|
|
|
62
59
|
|
|
63
60
|
def create_superuser(
|
|
64
61
|
self,
|
|
65
|
-
clerk_id: str,
|
|
66
62
|
email: str,
|
|
67
63
|
password: str | None = None,
|
|
64
|
+
clerk_id: str | None = None,
|
|
68
65
|
**extra_fields: Any,
|
|
69
66
|
) -> "AbstractClerkUser":
|
|
70
67
|
"""
|
|
71
|
-
Create and save a superuser with the given
|
|
68
|
+
Create and save a superuser with the given email.
|
|
72
69
|
|
|
73
70
|
Args:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
email: The user's email address (required).
|
|
72
|
+
password: Password for the superuser (required for Django admin access).
|
|
73
|
+
clerk_id: The Clerk user ID (optional).
|
|
77
74
|
**extra_fields: Additional fields for the user model.
|
|
78
75
|
|
|
79
76
|
Returns:
|
|
@@ -88,7 +85,9 @@ class ClerkUserManager(BaseUserManager["AbstractClerkUser"]):
|
|
|
88
85
|
if extra_fields.get("is_superuser") is not True:
|
|
89
86
|
raise ValueError("Superuser must have is_superuser=True.")
|
|
90
87
|
|
|
91
|
-
return self.create_user(
|
|
88
|
+
return self.create_user(
|
|
89
|
+
email=email, clerk_id=clerk_id, password=password, **extra_fields
|
|
90
|
+
)
|
|
92
91
|
|
|
93
92
|
def get_by_clerk_id(self, clerk_id: str) -> "AbstractClerkUser | None":
|
|
94
93
|
"""
|
|
@@ -70,18 +70,35 @@ class ClerkAuthMiddleware:
|
|
|
70
70
|
|
|
71
71
|
Sets request.user, request.clerk_user, request.clerk_payload,
|
|
72
72
|
and request.org based on the authentication result.
|
|
73
|
+
|
|
74
|
+
Supports hybrid authentication:
|
|
75
|
+
- If user is authenticated via Django session (e.g., admin login),
|
|
76
|
+
respects that authentication and skips Clerk JWT validation
|
|
77
|
+
- Otherwise, attempts Clerk JWT authentication
|
|
73
78
|
"""
|
|
74
79
|
# Initialize attributes
|
|
75
80
|
request.clerk_user = None # type: ignore
|
|
76
81
|
request.clerk_payload = None # type: ignore
|
|
77
82
|
request.org = None # type: ignore
|
|
78
83
|
|
|
79
|
-
# Check if user is already authenticated via
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
# Check if user is already authenticated via Django's standard auth
|
|
85
|
+
# (e.g., admin login with username/password)
|
|
86
|
+
if hasattr(request, "user") and request.user.is_authenticated:
|
|
87
|
+
# Check if this is a Clerk session or a traditional Django session
|
|
88
|
+
if self._is_clerk_session(request):
|
|
89
|
+
# This is a Clerk session, validate it
|
|
90
|
+
if self._is_session_valid(request):
|
|
91
|
+
# Clerk session is still valid
|
|
92
|
+
request.clerk_user = request.user # type: ignore
|
|
93
|
+
request.org = request.session.get("clerk_org_id") # type: ignore
|
|
94
|
+
return
|
|
95
|
+
# Clerk session expired, clear it and try JWT auth below
|
|
96
|
+
self._clear_session(request)
|
|
97
|
+
else:
|
|
98
|
+
# This is a traditional Django session (e.g., admin login)
|
|
99
|
+
# Don't interfere with it - just skip Clerk authentication
|
|
100
|
+
logger.debug("Using existing Django session (non-Clerk)")
|
|
101
|
+
return
|
|
85
102
|
|
|
86
103
|
# Try to authenticate via JWT token
|
|
87
104
|
try:
|
|
@@ -128,14 +145,26 @@ class ClerkAuthMiddleware:
|
|
|
128
145
|
if created:
|
|
129
146
|
logger.info(f"Created new user: {user.email} ({user.clerk_id})")
|
|
130
147
|
|
|
148
|
+
def _is_clerk_session(self, request: "HttpRequest") -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Check if the current session is a Clerk-authenticated session.
|
|
151
|
+
|
|
152
|
+
Returns True if this session was created by Clerk authentication,
|
|
153
|
+
False if it's a traditional Django session (e.g., admin login).
|
|
154
|
+
"""
|
|
155
|
+
# Clerk sessions have the last_clerk_check timestamp
|
|
156
|
+
return "last_clerk_check" in request.session
|
|
157
|
+
|
|
131
158
|
def _is_session_valid(self, request: "HttpRequest") -> bool:
|
|
132
159
|
"""
|
|
133
|
-
Check if the current session is valid and doesn't need revalidation.
|
|
160
|
+
Check if the current Clerk session is valid and doesn't need revalidation.
|
|
134
161
|
|
|
135
162
|
Returns True if:
|
|
136
163
|
1. User is authenticated in session
|
|
137
164
|
2. The session hasn't expired
|
|
138
165
|
3. The re-validation interval hasn't passed
|
|
166
|
+
|
|
167
|
+
Note: This method should only be called for Clerk sessions.
|
|
139
168
|
"""
|
|
140
169
|
if not hasattr(request, "user") or not request.user.is_authenticated:
|
|
141
170
|
return False
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated migration for hybrid authentication support
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("django_clerk_users", "0001_initial"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AlterField(
|
|
13
|
+
model_name="clerkuser",
|
|
14
|
+
name="clerk_id",
|
|
15
|
+
field=models.CharField(
|
|
16
|
+
blank=True,
|
|
17
|
+
db_index=True,
|
|
18
|
+
help_text="Unique identifier from Clerk. Can be null for Django admin users.",
|
|
19
|
+
max_length=255,
|
|
20
|
+
null=True,
|
|
21
|
+
unique=True,
|
|
22
|
+
),
|
|
23
|
+
),
|
|
24
|
+
]
|
django_clerk_users/models.py
CHANGED
|
@@ -43,9 +43,15 @@ class AbstractClerkUser(AbstractBaseUser, PermissionsMixin):
|
|
|
43
43
|
max_length=255,
|
|
44
44
|
unique=True,
|
|
45
45
|
db_index=True,
|
|
46
|
-
|
|
46
|
+
null=True,
|
|
47
|
+
blank=True,
|
|
48
|
+
help_text="Unique identifier from Clerk. Can be null for Django admin users.",
|
|
47
49
|
)
|
|
48
50
|
|
|
51
|
+
# Password field for Django admin compatibility
|
|
52
|
+
# Inherited from AbstractBaseUser, but we make it explicit that it's optional
|
|
53
|
+
# for Clerk users (who authenticate via JWT) but required for admin users
|
|
54
|
+
|
|
49
55
|
# Standard user fields
|
|
50
56
|
email = models.EmailField(
|
|
51
57
|
unique=True,
|
|
@@ -104,7 +110,7 @@ class AbstractClerkUser(AbstractBaseUser, PermissionsMixin):
|
|
|
104
110
|
objects = ClerkUserManager()
|
|
105
111
|
|
|
106
112
|
USERNAME_FIELD = "email"
|
|
107
|
-
REQUIRED_FIELDS = [
|
|
113
|
+
REQUIRED_FIELDS = [] # Changed to empty - clerk_id is optional for admin users
|
|
108
114
|
|
|
109
115
|
class Meta:
|
|
110
116
|
abstract = True
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# Generated by Django 5.2.8 on 2026-01-13 18:24
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django.utils.timezone
|
|
5
|
+
import uuid
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.db import migrations, models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name="Organization",
|
|
20
|
+
fields=[
|
|
21
|
+
(
|
|
22
|
+
"id",
|
|
23
|
+
models.BigAutoField(
|
|
24
|
+
auto_created=True,
|
|
25
|
+
primary_key=True,
|
|
26
|
+
serialize=False,
|
|
27
|
+
verbose_name="ID",
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
(
|
|
31
|
+
"uid",
|
|
32
|
+
models.UUIDField(
|
|
33
|
+
db_index=True,
|
|
34
|
+
default=uuid.uuid4,
|
|
35
|
+
editable=False,
|
|
36
|
+
help_text="Public unique identifier for the organization.",
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
(
|
|
40
|
+
"clerk_id",
|
|
41
|
+
models.CharField(
|
|
42
|
+
db_index=True,
|
|
43
|
+
help_text="Unique identifier from Clerk.",
|
|
44
|
+
max_length=255,
|
|
45
|
+
unique=True,
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
"name",
|
|
50
|
+
models.CharField(help_text="Organization name.", max_length=255),
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
"slug",
|
|
54
|
+
models.SlugField(
|
|
55
|
+
help_text="URL-friendly organization identifier.",
|
|
56
|
+
max_length=255,
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
(
|
|
60
|
+
"image_url",
|
|
61
|
+
models.URLField(
|
|
62
|
+
blank=True,
|
|
63
|
+
default="",
|
|
64
|
+
help_text="URL to organization logo from Clerk.",
|
|
65
|
+
max_length=500,
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
(
|
|
69
|
+
"public_metadata",
|
|
70
|
+
models.JSONField(
|
|
71
|
+
blank=True,
|
|
72
|
+
default=dict,
|
|
73
|
+
help_text="Public metadata from Clerk.",
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"private_metadata",
|
|
78
|
+
models.JSONField(
|
|
79
|
+
blank=True,
|
|
80
|
+
default=dict,
|
|
81
|
+
help_text="Private metadata from Clerk.",
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
(
|
|
85
|
+
"is_active",
|
|
86
|
+
models.BooleanField(
|
|
87
|
+
db_index=True,
|
|
88
|
+
default=True,
|
|
89
|
+
help_text="Whether the organization is active.",
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
(
|
|
93
|
+
"members_count",
|
|
94
|
+
models.PositiveIntegerField(
|
|
95
|
+
default=0, help_text="Number of members in the organization."
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
(
|
|
99
|
+
"pending_invitations_count",
|
|
100
|
+
models.PositiveIntegerField(
|
|
101
|
+
default=0, help_text="Number of pending invitations."
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
(
|
|
105
|
+
"max_allowed_memberships",
|
|
106
|
+
models.PositiveIntegerField(
|
|
107
|
+
default=0,
|
|
108
|
+
help_text="Maximum allowed memberships (0 = unlimited).",
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
"created_at",
|
|
113
|
+
models.DateTimeField(
|
|
114
|
+
default=django.utils.timezone.now,
|
|
115
|
+
help_text="When the organization was created in Clerk.",
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
(
|
|
119
|
+
"updated_at",
|
|
120
|
+
models.DateTimeField(
|
|
121
|
+
auto_now=True,
|
|
122
|
+
help_text="When the organization was last updated.",
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
options={
|
|
127
|
+
"verbose_name": "Organization",
|
|
128
|
+
"verbose_name_plural": "Organizations",
|
|
129
|
+
"ordering": ["-created_at"],
|
|
130
|
+
"indexes": [
|
|
131
|
+
models.Index(
|
|
132
|
+
fields=["clerk_id"], name="clerk_organ_clerk_i_b2b811_idx"
|
|
133
|
+
),
|
|
134
|
+
models.Index(fields=["slug"], name="clerk_organ_slug_6c2124_idx"),
|
|
135
|
+
models.Index(
|
|
136
|
+
fields=["is_active"], name="clerk_organ_is_acti_67b48f_idx"
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
migrations.CreateModel(
|
|
142
|
+
name="OrganizationInvitation",
|
|
143
|
+
fields=[
|
|
144
|
+
(
|
|
145
|
+
"id",
|
|
146
|
+
models.BigAutoField(
|
|
147
|
+
auto_created=True,
|
|
148
|
+
primary_key=True,
|
|
149
|
+
serialize=False,
|
|
150
|
+
verbose_name="ID",
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
(
|
|
154
|
+
"clerk_invitation_id",
|
|
155
|
+
models.CharField(
|
|
156
|
+
db_index=True,
|
|
157
|
+
help_text="Unique invitation identifier from Clerk.",
|
|
158
|
+
max_length=255,
|
|
159
|
+
unique=True,
|
|
160
|
+
),
|
|
161
|
+
),
|
|
162
|
+
(
|
|
163
|
+
"email_address",
|
|
164
|
+
models.EmailField(
|
|
165
|
+
help_text="Email address of the invitee.", max_length=254
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
(
|
|
169
|
+
"role",
|
|
170
|
+
models.CharField(
|
|
171
|
+
default="member",
|
|
172
|
+
help_text="Role the user will have upon accepting.",
|
|
173
|
+
max_length=100,
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
(
|
|
177
|
+
"status",
|
|
178
|
+
models.CharField(
|
|
179
|
+
choices=[
|
|
180
|
+
("pending", "Pending"),
|
|
181
|
+
("accepted", "Accepted"),
|
|
182
|
+
("revoked", "Revoked"),
|
|
183
|
+
],
|
|
184
|
+
db_index=True,
|
|
185
|
+
default="pending",
|
|
186
|
+
help_text="Invitation status.",
|
|
187
|
+
max_length=20,
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
(
|
|
191
|
+
"public_metadata",
|
|
192
|
+
models.JSONField(
|
|
193
|
+
blank=True,
|
|
194
|
+
default=dict,
|
|
195
|
+
help_text="Public metadata from Clerk.",
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
(
|
|
199
|
+
"private_metadata",
|
|
200
|
+
models.JSONField(
|
|
201
|
+
blank=True,
|
|
202
|
+
default=dict,
|
|
203
|
+
help_text="Private metadata from Clerk.",
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
(
|
|
207
|
+
"created_at",
|
|
208
|
+
models.DateTimeField(
|
|
209
|
+
default=django.utils.timezone.now,
|
|
210
|
+
help_text="When the invitation was created.",
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
(
|
|
214
|
+
"updated_at",
|
|
215
|
+
models.DateTimeField(
|
|
216
|
+
auto_now=True, help_text="When the invitation was last updated."
|
|
217
|
+
),
|
|
218
|
+
),
|
|
219
|
+
(
|
|
220
|
+
"inviter",
|
|
221
|
+
models.ForeignKey(
|
|
222
|
+
blank=True,
|
|
223
|
+
help_text="The user who sent the invitation.",
|
|
224
|
+
null=True,
|
|
225
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
226
|
+
related_name="sent_invitations",
|
|
227
|
+
to=settings.AUTH_USER_MODEL,
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
(
|
|
231
|
+
"organization",
|
|
232
|
+
models.ForeignKey(
|
|
233
|
+
help_text="The organization.",
|
|
234
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
235
|
+
related_name="invitations",
|
|
236
|
+
to="clerk_organizations.organization",
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
],
|
|
240
|
+
options={
|
|
241
|
+
"verbose_name": "Organization Invitation",
|
|
242
|
+
"verbose_name_plural": "Organization Invitations",
|
|
243
|
+
"ordering": ["-created_at"],
|
|
244
|
+
"indexes": [
|
|
245
|
+
models.Index(
|
|
246
|
+
fields=["clerk_invitation_id"],
|
|
247
|
+
name="clerk_organ_clerk_i_53d078_idx",
|
|
248
|
+
),
|
|
249
|
+
models.Index(
|
|
250
|
+
fields=["status"], name="clerk_organ_status_bef241_idx"
|
|
251
|
+
),
|
|
252
|
+
models.Index(
|
|
253
|
+
fields=["email_address"], name="clerk_organ_email_a_b3359c_idx"
|
|
254
|
+
),
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
),
|
|
258
|
+
migrations.CreateModel(
|
|
259
|
+
name="OrganizationMember",
|
|
260
|
+
fields=[
|
|
261
|
+
(
|
|
262
|
+
"id",
|
|
263
|
+
models.BigAutoField(
|
|
264
|
+
auto_created=True,
|
|
265
|
+
primary_key=True,
|
|
266
|
+
serialize=False,
|
|
267
|
+
verbose_name="ID",
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
(
|
|
271
|
+
"clerk_membership_id",
|
|
272
|
+
models.CharField(
|
|
273
|
+
db_index=True,
|
|
274
|
+
help_text="Unique membership identifier from Clerk.",
|
|
275
|
+
max_length=255,
|
|
276
|
+
unique=True,
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
(
|
|
280
|
+
"role",
|
|
281
|
+
models.CharField(
|
|
282
|
+
default="member",
|
|
283
|
+
help_text="User's role in the organization (e.g., 'admin', 'member').",
|
|
284
|
+
max_length=100,
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
(
|
|
288
|
+
"public_metadata",
|
|
289
|
+
models.JSONField(
|
|
290
|
+
blank=True,
|
|
291
|
+
default=dict,
|
|
292
|
+
help_text="Public metadata from Clerk.",
|
|
293
|
+
),
|
|
294
|
+
),
|
|
295
|
+
(
|
|
296
|
+
"private_metadata",
|
|
297
|
+
models.JSONField(
|
|
298
|
+
blank=True,
|
|
299
|
+
default=dict,
|
|
300
|
+
help_text="Private metadata from Clerk.",
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
(
|
|
304
|
+
"joined_at",
|
|
305
|
+
models.DateTimeField(
|
|
306
|
+
default=django.utils.timezone.now,
|
|
307
|
+
help_text="When the user joined the organization.",
|
|
308
|
+
),
|
|
309
|
+
),
|
|
310
|
+
(
|
|
311
|
+
"updated_at",
|
|
312
|
+
models.DateTimeField(
|
|
313
|
+
auto_now=True, help_text="When the membership was last updated."
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
(
|
|
317
|
+
"organization",
|
|
318
|
+
models.ForeignKey(
|
|
319
|
+
help_text="The organization.",
|
|
320
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
321
|
+
related_name="cached_members",
|
|
322
|
+
to="clerk_organizations.organization",
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
(
|
|
326
|
+
"user",
|
|
327
|
+
models.ForeignKey(
|
|
328
|
+
help_text="The user.",
|
|
329
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
330
|
+
related_name="organization_memberships",
|
|
331
|
+
to=settings.AUTH_USER_MODEL,
|
|
332
|
+
),
|
|
333
|
+
),
|
|
334
|
+
],
|
|
335
|
+
options={
|
|
336
|
+
"verbose_name": "Organization Member",
|
|
337
|
+
"verbose_name_plural": "Organization Members",
|
|
338
|
+
"ordering": ["-joined_at"],
|
|
339
|
+
"indexes": [
|
|
340
|
+
models.Index(
|
|
341
|
+
fields=["clerk_membership_id"],
|
|
342
|
+
name="clerk_organ_clerk_m_10f2c5_idx",
|
|
343
|
+
),
|
|
344
|
+
models.Index(fields=["role"], name="clerk_organ_role_645ce1_idx"),
|
|
345
|
+
],
|
|
346
|
+
"unique_together": {("organization", "user")},
|
|
347
|
+
},
|
|
348
|
+
),
|
|
349
|
+
]
|
|
File without changes
|
|
@@ -133,10 +133,8 @@ class Organization(models.Model):
|
|
|
133
133
|
Returns:
|
|
134
134
|
Tuple of (success, message)
|
|
135
135
|
"""
|
|
136
|
-
from django_clerk_users.utils import update_or_create_clerk_user
|
|
137
|
-
|
|
138
136
|
try:
|
|
139
|
-
from django_clerk_users.organizations.
|
|
137
|
+
from django_clerk_users.organizations.webhooks import (
|
|
140
138
|
update_or_create_organization,
|
|
141
139
|
)
|
|
142
140
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-clerk-users
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Integrate Clerk with Django
|
|
5
5
|
Project-URL: Changelog, https://github.com/jmitchel3/django-clerk-users
|
|
6
6
|
Project-URL: Documentation, https://github.com/jmitchel3/django-clerk-users
|
|
7
7
|
Project-URL: Funding, https://github.com/jmitchel3/django-clerk-users
|
|
8
8
|
Project-URL: Repository, https://github.com/jmitchel3/django-clerk-users
|
|
9
|
+
License-File: LICENSE
|
|
9
10
|
Classifier: Development Status :: 4 - Beta
|
|
10
11
|
Classifier: Framework :: Django
|
|
11
12
|
Classifier: Framework :: Django :: 4.2
|
|
@@ -14,14 +15,12 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
17
|
Requires-Python: >=3.12
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Requires-Dist: django>=4.2
|
|
20
18
|
Requires-Dist: clerk-backend-api>=1.0.0
|
|
19
|
+
Requires-Dist: django>=4.2
|
|
21
20
|
Requires-Dist: svix>=1.0.0
|
|
22
21
|
Provides-Extra: drf
|
|
23
|
-
Requires-Dist: djangorestframework>=3.14; extra ==
|
|
24
|
-
|
|
22
|
+
Requires-Dist: djangorestframework>=3.14; extra == 'drf'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
25
24
|
|
|
26
25
|
# Django Clerk Users
|
|
27
26
|
|
|
@@ -114,12 +113,30 @@ MIDDLEWARE = [
|
|
|
114
113
|
|
|
115
114
|
### 5. Add authentication backend
|
|
116
115
|
|
|
116
|
+
**For Clerk-only authentication:**
|
|
117
|
+
|
|
117
118
|
```python
|
|
118
119
|
AUTHENTICATION_BACKENDS = [
|
|
119
120
|
"django_clerk_users.authentication.ClerkBackend",
|
|
120
121
|
]
|
|
121
122
|
```
|
|
122
123
|
|
|
124
|
+
**For hybrid authentication (Clerk + Django admin):**
|
|
125
|
+
|
|
126
|
+
If you want to support both Clerk authentication (JWT) and traditional Django admin login (username/password), use both backends:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
AUTHENTICATION_BACKENDS = [
|
|
130
|
+
"django.contrib.auth.backends.ModelBackend", # For Django admin
|
|
131
|
+
"django_clerk_users.authentication.ClerkBackend", # For Clerk JWT
|
|
132
|
+
]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This allows:
|
|
136
|
+
- Admin users to log in via Django admin with username/password
|
|
137
|
+
- Frontend users to authenticate via Clerk JWT tokens
|
|
138
|
+
- The middleware automatically detects which authentication method was used
|
|
139
|
+
|
|
123
140
|
### 6. Run migrations
|
|
124
141
|
|
|
125
142
|
```bash
|
|
@@ -141,6 +158,21 @@ urlpatterns = [
|
|
|
141
158
|
|
|
142
159
|
Then configure your Clerk Dashboard to send webhooks to `https://your-app.com/webhooks/clerk/`.
|
|
143
160
|
|
|
161
|
+
### 8. Create admin users (for hybrid authentication)
|
|
162
|
+
|
|
163
|
+
If you're using hybrid authentication, create an admin user for Django admin access:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
python manage.py createsuperuser
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This creates a user with:
|
|
170
|
+
- Username/password authentication (for Django admin)
|
|
171
|
+
- No `clerk_id` (since they're not Clerk users)
|
|
172
|
+
- Access to Django admin panel
|
|
173
|
+
|
|
174
|
+
Note: Regular Clerk users are created automatically via webhooks when they sign up through your frontend.
|
|
175
|
+
|
|
144
176
|
## Usage
|
|
145
177
|
|
|
146
178
|
### Accessing the user in views
|
|
@@ -179,6 +211,55 @@ REST_FRAMEWORK = {
|
|
|
179
211
|
}
|
|
180
212
|
```
|
|
181
213
|
|
|
214
|
+
## Hybrid Authentication (Clerk + Django Admin)
|
|
215
|
+
|
|
216
|
+
The package supports hybrid authentication, allowing you to use both Clerk (JWT-based) authentication for your frontend users and traditional Django admin authentication for internal staff.
|
|
217
|
+
|
|
218
|
+
### How it works
|
|
219
|
+
|
|
220
|
+
1. **Frontend users**: Authenticate via Clerk JWT tokens (handled by `ClerkAuthMiddleware`)
|
|
221
|
+
2. **Admin users**: Authenticate via username/password (handled by Django's `ModelBackend`)
|
|
222
|
+
3. The middleware automatically detects which authentication method was used and respects existing sessions
|
|
223
|
+
|
|
224
|
+
### Configuration
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# settings.py
|
|
228
|
+
AUTHENTICATION_BACKENDS = [
|
|
229
|
+
"django.contrib.auth.backends.ModelBackend", # For Django admin
|
|
230
|
+
"django_clerk_users.authentication.ClerkBackend", # For Clerk JWT
|
|
231
|
+
]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Creating admin users
|
|
235
|
+
|
|
236
|
+
Admin users don't need a `clerk_id` (it's optional in hybrid mode):
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
python manage.py createsuperuser
|
|
240
|
+
# Email: admin@example.com
|
|
241
|
+
# Password: ********
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
This creates a user with:
|
|
245
|
+
- Username/password authentication (no Clerk integration)
|
|
246
|
+
- Access to Django admin panel at `/admin/`
|
|
247
|
+
- Standard Django permissions (is_staff, is_superuser)
|
|
248
|
+
|
|
249
|
+
### Session handling
|
|
250
|
+
|
|
251
|
+
- **Django admin sessions**: Traditional session cookies (set by Django's auth system)
|
|
252
|
+
- **Clerk sessions**: JWT validated once, then cached in session with `last_clerk_check` marker
|
|
253
|
+
- The middleware checks for `last_clerk_check` to distinguish between the two types
|
|
254
|
+
|
|
255
|
+
### Use cases
|
|
256
|
+
|
|
257
|
+
This is particularly useful when:
|
|
258
|
+
- Your admin panel is on a different domain than your frontend
|
|
259
|
+
- You want internal staff to access Django admin without Clerk accounts
|
|
260
|
+
- You need traditional Django auth features (permissions, groups, etc.)
|
|
261
|
+
- You're migrating from Django auth to Clerk gradually
|
|
262
|
+
|
|
182
263
|
## Organizations (Optional)
|
|
183
264
|
|
|
184
265
|
For Clerk organization support:
|
|
@@ -215,9 +296,11 @@ python manage.py sync_clerk_organizations
|
|
|
215
296
|
| `CLERK_SECRET_KEY` | Yes | - | Your Clerk secret key |
|
|
216
297
|
| `CLERK_WEBHOOK_SIGNING_KEY` | Yes* | - | Webhook signing secret (*required for webhooks) |
|
|
217
298
|
| `CLERK_FRONTEND_HOSTS` | Yes | `[]` | Authorized frontend URLs |
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
299
|
+
| `CLERK_AUTH_PARTIES` | No | `[]` | Alias for `CLERK_FRONTEND_HOSTS` |
|
|
300
|
+
| `CLERK_SESSION_REVALIDATION_SECONDS` | No | `300` | JWT revalidation interval (seconds) |
|
|
301
|
+
| `CLERK_CACHE_TIMEOUT` | No | `300` | User cache timeout (seconds) |
|
|
302
|
+
| `CLERK_ORG_CACHE_TIMEOUT` | No | `900` | Organization cache timeout (seconds) |
|
|
303
|
+
| `CLERK_WEBHOOK_DEDUP_TIMEOUT` | No | `45` | Webhook deduplication cache timeout (seconds) |
|
|
221
304
|
|
|
222
305
|
## License
|
|
223
306
|
|
|
@@ -5,8 +5,8 @@ django_clerk_users/checks.py,sha256=gnHccAyXixtGToGhgWl4gfCY-qPB5ckimpDVadOP3E4,
|
|
|
5
5
|
django_clerk_users/client.py,sha256=-nBXsPOibVwD7zXQ-Z-qTBb7NyPuUZpvlDcAlDVFUBA,815
|
|
6
6
|
django_clerk_users/decorators.py,sha256=Hm86XIxNdSiuDmqT8tFRrz6sQR9IOxB7zfxfO8MeJLg,5011
|
|
7
7
|
django_clerk_users/exceptions.py,sha256=nVTJR1d5PxuMqC8js1Sj-MRHJHkI4KUwphjuCEN4fiM,890
|
|
8
|
-
django_clerk_users/managers.py,sha256=
|
|
9
|
-
django_clerk_users/models.py,sha256=
|
|
8
|
+
django_clerk_users/managers.py,sha256=HvSkGeiQhjA6EFXrW4GTlxXtlRDtlT54BbwbvnNuOWg,3610
|
|
9
|
+
django_clerk_users/models.py,sha256=DMcEnGNioGX5xsP4U47ich1L_UoZNAFEDb-vZVVV8Tk,5210
|
|
10
10
|
django_clerk_users/settings.py,sha256=pRyt_kSPWOT8CkTvyce3sf0RxfIoG5DEwnGUT8bIDi8,1305
|
|
11
11
|
django_clerk_users/testing.py,sha256=ZoYi7dG_HjSp19_c1AvILOoCfHlofs7LN-5ALS_UhDw,12160
|
|
12
12
|
django_clerk_users/utils.py,sha256=bQWfPUKfVvXi69Ctny1T1Kxzk-qMDpXQBRVa6URH53I,5886
|
|
@@ -20,22 +20,24 @@ django_clerk_users/management/commands/migrate_users_to_clerk.py,sha256=qjw4Q6pU
|
|
|
20
20
|
django_clerk_users/management/commands/sync_clerk_organizations.py,sha256=G1kasAPvvwM-PPb5xXxKNlex_-gWyKDP2HLS5g8hVkk,6656
|
|
21
21
|
django_clerk_users/management/commands/sync_clerk_users.py,sha256=hvWrvcqAkZHvjqQAOj3-e6pKx5TKtArmlsHDUottRMM,3755
|
|
22
22
|
django_clerk_users/middleware/__init__.py,sha256=tnr4eBer0KGVBAZBgBQZVpcL7jf8t2_CBLsMnau5JW4,153
|
|
23
|
-
django_clerk_users/middleware/auth.py,sha256=
|
|
23
|
+
django_clerk_users/middleware/auth.py,sha256=FLo4DHkiL3QnEPZsvJDGFIahfH1HUcqkBEzYXEx0fN0,8889
|
|
24
24
|
django_clerk_users/migrations/0001_initial.py,sha256=tnPvGlLnWrItuhYS0s5mr3TJ8__2e_yNKJS4nsFuWhk,6246
|
|
25
|
+
django_clerk_users/migrations/0002_make_clerk_id_nullable.py,sha256=OXad63KPFTMLn2lkwkuNkr8ZbM6BiNlsw6z72fhY-eU,640
|
|
25
26
|
django_clerk_users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
27
|
django_clerk_users/organizations/__init__.py,sha256=NZU2C8F4Trm63_qaUf78jROKz6XFnu36uREkw7g_yho,270
|
|
27
28
|
django_clerk_users/organizations/admin.py,sha256=u7qHMgb11akHiJ2FCzqbSLQ3_TsTiejf0wH10f5_PPQ,1838
|
|
28
29
|
django_clerk_users/organizations/apps.py,sha256=IgKNl5REkAgLDHC06GFI-a3x3Mi2HWN4L-El0Ef6CE8,252
|
|
29
30
|
django_clerk_users/organizations/middleware.py,sha256=5Gvu28SCEqpuIjr2Fah5O85m0wnjMiDp4vxdvVzJh68,4129
|
|
30
|
-
django_clerk_users/organizations/models.py,sha256=
|
|
31
|
+
django_clerk_users/organizations/models.py,sha256=rGy3udfo2xO6XBMPHS7eTTk9meyxx_3GI9zL9lH-ScY,8602
|
|
31
32
|
django_clerk_users/organizations/webhooks.py,sha256=zp16_vL5j7DEaVC5ZxgYwrE8zPpUsRIq8TGBd8SYhoo,14321
|
|
33
|
+
django_clerk_users/organizations/migrations/0001_initial.py,sha256=NXZ63PLqkX0ebavvLk6aHOffiFDq0pZamEelb6PG4Yg,12625
|
|
34
|
+
django_clerk_users/organizations/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
35
|
django_clerk_users/webhooks/__init__.py,sha256=XlpsPmc_lHQkZVMeuXce_2hsq7WZzXOXNy36kiQA5Lk,558
|
|
33
36
|
django_clerk_users/webhooks/handlers.py,sha256=GnotJNhN809DsrbfZjtJD0KurThKyAXZS982a41ljwg,9457
|
|
34
37
|
django_clerk_users/webhooks/security.py,sha256=Ig2ZxF8SxX5o-4bNRehFhip4hVvcQxoGsTj3sTY3WSU,3461
|
|
35
38
|
django_clerk_users/webhooks/signals.py,sha256=bytshg7IMDnlvnCZ0_TGjUXZZLRNxtn2RSx97qacZ-w,1668
|
|
36
39
|
django_clerk_users/webhooks/views.py,sha256=0-ilzzO7tBfc-pENMy0ZSSkQ4uPqH2QAt249EK2wQKA,2287
|
|
37
|
-
django_clerk_users-0.0.
|
|
38
|
-
django_clerk_users-0.0.
|
|
39
|
-
django_clerk_users-0.0.
|
|
40
|
-
django_clerk_users-0.0.
|
|
41
|
-
django_clerk_users-0.0.2.dist-info/RECORD,,
|
|
40
|
+
django_clerk_users-0.1.0.dist-info/METADATA,sha256=JSE1da_-Xl_lXQimkztPbpz1bY4MF45wkQY2K49U75E,8687
|
|
41
|
+
django_clerk_users-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
42
|
+
django_clerk_users-0.1.0.dist-info/licenses/LICENSE,sha256=X4PZDRQG4RmPhHU5c0G21Ki9LXWDCuLQ8W4mnED5RDU,1071
|
|
43
|
+
django_clerk_users-0.1.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
django_clerk_users
|
|
File without changes
|