django-clerk-users 0.0.1__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/__init__.py +78 -7
- django_clerk_users/apps.py +20 -0
- django_clerk_users/authentication/__init__.py +24 -0
- django_clerk_users/authentication/backends.py +89 -0
- django_clerk_users/authentication/drf.py +111 -0
- django_clerk_users/authentication/utils.py +171 -0
- django_clerk_users/caching.py +161 -0
- django_clerk_users/checks.py +127 -0
- django_clerk_users/client.py +32 -0
- django_clerk_users/decorators.py +181 -0
- django_clerk_users/exceptions.py +51 -0
- django_clerk_users/management/__init__.py +0 -0
- django_clerk_users/management/commands/__init__.py +0 -0
- django_clerk_users/management/commands/migrate_users_to_clerk.py +223 -0
- django_clerk_users/management/commands/sync_clerk_organizations.py +191 -0
- django_clerk_users/management/commands/sync_clerk_users.py +114 -0
- django_clerk_users/managers.py +120 -0
- django_clerk_users/middleware/__init__.py +9 -0
- django_clerk_users/middleware/auth.py +230 -0
- django_clerk_users/migrations/0001_initial.py +174 -0
- django_clerk_users/migrations/0002_make_clerk_id_nullable.py +24 -0
- django_clerk_users/migrations/__init__.py +0 -0
- django_clerk_users/models.py +180 -0
- django_clerk_users/organizations/__init__.py +8 -0
- django_clerk_users/organizations/admin.py +81 -0
- django_clerk_users/organizations/apps.py +8 -0
- django_clerk_users/organizations/middleware.py +130 -0
- 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 +314 -0
- django_clerk_users/organizations/webhooks.py +417 -0
- django_clerk_users/settings.py +37 -0
- django_clerk_users/testing.py +381 -0
- django_clerk_users/utils.py +210 -0
- django_clerk_users/webhooks/__init__.py +26 -0
- django_clerk_users/webhooks/handlers.py +346 -0
- django_clerk_users/webhooks/security.py +108 -0
- django_clerk_users/webhooks/signals.py +42 -0
- django_clerk_users/webhooks/views.py +76 -0
- django_clerk_users-0.1.0.dist-info/METADATA +311 -0
- django_clerk_users-0.1.0.dist-info/RECORD +43 -0
- {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.1.0.dist-info}/WHEEL +1 -2
- django_clerk_users/main.py +0 -2
- django_clerk_users-0.0.1.dist-info/METADATA +0 -24
- django_clerk_users-0.0.1.dist-info/RECORD +0 -7
- django_clerk_users-0.0.1.dist-info/top_level.txt +0 -1
- {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Organization models for django-clerk-users.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from django.db import models
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from django_clerk_users.models import AbstractClerkUser
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Organization(models.Model):
|
|
19
|
+
"""
|
|
20
|
+
Represents a Clerk organization.
|
|
21
|
+
|
|
22
|
+
Organizations are synced from Clerk and stored locally for
|
|
23
|
+
efficient querying and relationships.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Public identifier (use in URLs and APIs)
|
|
27
|
+
uid = models.UUIDField(
|
|
28
|
+
default=uuid.uuid4,
|
|
29
|
+
editable=False,
|
|
30
|
+
db_index=True,
|
|
31
|
+
help_text="Public unique identifier for the organization.",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Clerk-specific fields
|
|
35
|
+
clerk_id = models.CharField(
|
|
36
|
+
max_length=255,
|
|
37
|
+
unique=True,
|
|
38
|
+
db_index=True,
|
|
39
|
+
help_text="Unique identifier from Clerk.",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Organization fields
|
|
43
|
+
name = models.CharField(
|
|
44
|
+
max_length=255,
|
|
45
|
+
help_text="Organization name.",
|
|
46
|
+
)
|
|
47
|
+
slug = models.SlugField(
|
|
48
|
+
max_length=255,
|
|
49
|
+
db_index=True,
|
|
50
|
+
help_text="URL-friendly organization identifier.",
|
|
51
|
+
)
|
|
52
|
+
image_url = models.URLField(
|
|
53
|
+
max_length=500,
|
|
54
|
+
blank=True,
|
|
55
|
+
default="",
|
|
56
|
+
help_text="URL to organization logo from Clerk.",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Metadata
|
|
60
|
+
public_metadata = models.JSONField(
|
|
61
|
+
default=dict,
|
|
62
|
+
blank=True,
|
|
63
|
+
help_text="Public metadata from Clerk.",
|
|
64
|
+
)
|
|
65
|
+
private_metadata = models.JSONField(
|
|
66
|
+
default=dict,
|
|
67
|
+
blank=True,
|
|
68
|
+
help_text="Private metadata from Clerk.",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Status
|
|
72
|
+
is_active = models.BooleanField(
|
|
73
|
+
default=True,
|
|
74
|
+
db_index=True,
|
|
75
|
+
help_text="Whether the organization is active.",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Stats (synced from Clerk)
|
|
79
|
+
members_count = models.PositiveIntegerField(
|
|
80
|
+
default=0,
|
|
81
|
+
help_text="Number of members in the organization.",
|
|
82
|
+
)
|
|
83
|
+
pending_invitations_count = models.PositiveIntegerField(
|
|
84
|
+
default=0,
|
|
85
|
+
help_text="Number of pending invitations.",
|
|
86
|
+
)
|
|
87
|
+
max_allowed_memberships = models.PositiveIntegerField(
|
|
88
|
+
default=0,
|
|
89
|
+
help_text="Maximum allowed memberships (0 = unlimited).",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Timestamps
|
|
93
|
+
created_at = models.DateTimeField(
|
|
94
|
+
default=timezone.now,
|
|
95
|
+
help_text="When the organization was created in Clerk.",
|
|
96
|
+
)
|
|
97
|
+
updated_at = models.DateTimeField(
|
|
98
|
+
auto_now=True,
|
|
99
|
+
help_text="When the organization was last updated.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
class Meta:
|
|
103
|
+
ordering = ["-created_at"]
|
|
104
|
+
indexes = [
|
|
105
|
+
models.Index(fields=["clerk_id"]),
|
|
106
|
+
models.Index(fields=["slug"]),
|
|
107
|
+
models.Index(fields=["is_active"]),
|
|
108
|
+
]
|
|
109
|
+
verbose_name = "Organization"
|
|
110
|
+
verbose_name_plural = "Organizations"
|
|
111
|
+
|
|
112
|
+
def __str__(self) -> str:
|
|
113
|
+
return self.name
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def public_id(self) -> str:
|
|
117
|
+
"""Return the public UUID as a string for API responses."""
|
|
118
|
+
return str(self.uid)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def handle(self) -> str:
|
|
122
|
+
"""Return the organization slug (alias for compatibility)."""
|
|
123
|
+
return self.slug
|
|
124
|
+
|
|
125
|
+
def get_member_count(self) -> int:
|
|
126
|
+
"""Get current member count from cached members."""
|
|
127
|
+
return self.cached_members.count()
|
|
128
|
+
|
|
129
|
+
def sync_from_clerk(self) -> tuple[bool, str]:
|
|
130
|
+
"""
|
|
131
|
+
Sync organization data from Clerk.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Tuple of (success, message)
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
from django_clerk_users.organizations.webhooks import (
|
|
138
|
+
update_or_create_organization,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
org, created = update_or_create_organization(self.clerk_id)
|
|
142
|
+
action = "created" if created else "updated"
|
|
143
|
+
return True, f"Organization {action} successfully"
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return False, str(e)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class OrganizationMember(models.Model):
|
|
149
|
+
"""
|
|
150
|
+
Represents a membership in a Clerk organization.
|
|
151
|
+
|
|
152
|
+
This is a cache of Clerk's organization memberships for efficient
|
|
153
|
+
local queries.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
# Clerk-specific fields
|
|
157
|
+
clerk_membership_id = models.CharField(
|
|
158
|
+
max_length=255,
|
|
159
|
+
unique=True,
|
|
160
|
+
db_index=True,
|
|
161
|
+
help_text="Unique membership identifier from Clerk.",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Relationships
|
|
165
|
+
organization = models.ForeignKey(
|
|
166
|
+
Organization,
|
|
167
|
+
on_delete=models.CASCADE,
|
|
168
|
+
related_name="cached_members",
|
|
169
|
+
help_text="The organization.",
|
|
170
|
+
)
|
|
171
|
+
user = models.ForeignKey(
|
|
172
|
+
settings.AUTH_USER_MODEL,
|
|
173
|
+
on_delete=models.CASCADE,
|
|
174
|
+
related_name="organization_memberships",
|
|
175
|
+
help_text="The user.",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Role
|
|
179
|
+
role = models.CharField(
|
|
180
|
+
max_length=100,
|
|
181
|
+
default="member",
|
|
182
|
+
help_text="User's role in the organization (e.g., 'admin', 'member').",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Metadata
|
|
186
|
+
public_metadata = models.JSONField(
|
|
187
|
+
default=dict,
|
|
188
|
+
blank=True,
|
|
189
|
+
help_text="Public metadata from Clerk.",
|
|
190
|
+
)
|
|
191
|
+
private_metadata = models.JSONField(
|
|
192
|
+
default=dict,
|
|
193
|
+
blank=True,
|
|
194
|
+
help_text="Private metadata from Clerk.",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Timestamps
|
|
198
|
+
joined_at = models.DateTimeField(
|
|
199
|
+
default=timezone.now,
|
|
200
|
+
help_text="When the user joined the organization.",
|
|
201
|
+
)
|
|
202
|
+
updated_at = models.DateTimeField(
|
|
203
|
+
auto_now=True,
|
|
204
|
+
help_text="When the membership was last updated.",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
class Meta:
|
|
208
|
+
ordering = ["-joined_at"]
|
|
209
|
+
unique_together = [("organization", "user")]
|
|
210
|
+
indexes = [
|
|
211
|
+
models.Index(fields=["clerk_membership_id"]),
|
|
212
|
+
models.Index(fields=["role"]),
|
|
213
|
+
]
|
|
214
|
+
verbose_name = "Organization Member"
|
|
215
|
+
verbose_name_plural = "Organization Members"
|
|
216
|
+
|
|
217
|
+
def __str__(self) -> str:
|
|
218
|
+
return f"{self.user.email} in {self.organization.name} ({self.role})"
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def is_admin(self) -> bool:
|
|
222
|
+
"""Check if this member has admin role."""
|
|
223
|
+
return self.role.lower() in ("admin", "org:admin", "owner")
|
|
224
|
+
|
|
225
|
+
def can_invite_members(self) -> bool:
|
|
226
|
+
"""Check if this member can invite others to the organization."""
|
|
227
|
+
return self.is_admin
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class OrganizationInvitation(models.Model):
|
|
231
|
+
"""
|
|
232
|
+
Represents a pending invitation to a Clerk organization.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
class Status(models.TextChoices):
|
|
236
|
+
PENDING = "pending", "Pending"
|
|
237
|
+
ACCEPTED = "accepted", "Accepted"
|
|
238
|
+
REVOKED = "revoked", "Revoked"
|
|
239
|
+
|
|
240
|
+
# Clerk-specific fields
|
|
241
|
+
clerk_invitation_id = models.CharField(
|
|
242
|
+
max_length=255,
|
|
243
|
+
unique=True,
|
|
244
|
+
db_index=True,
|
|
245
|
+
help_text="Unique invitation identifier from Clerk.",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Relationships
|
|
249
|
+
organization = models.ForeignKey(
|
|
250
|
+
Organization,
|
|
251
|
+
on_delete=models.CASCADE,
|
|
252
|
+
related_name="invitations",
|
|
253
|
+
help_text="The organization.",
|
|
254
|
+
)
|
|
255
|
+
inviter = models.ForeignKey(
|
|
256
|
+
settings.AUTH_USER_MODEL,
|
|
257
|
+
on_delete=models.SET_NULL,
|
|
258
|
+
null=True,
|
|
259
|
+
blank=True,
|
|
260
|
+
related_name="sent_invitations",
|
|
261
|
+
help_text="The user who sent the invitation.",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Invitation details
|
|
265
|
+
email_address = models.EmailField(
|
|
266
|
+
help_text="Email address of the invitee.",
|
|
267
|
+
)
|
|
268
|
+
role = models.CharField(
|
|
269
|
+
max_length=100,
|
|
270
|
+
default="member",
|
|
271
|
+
help_text="Role the user will have upon accepting.",
|
|
272
|
+
)
|
|
273
|
+
status = models.CharField(
|
|
274
|
+
max_length=20,
|
|
275
|
+
choices=Status.choices,
|
|
276
|
+
default=Status.PENDING,
|
|
277
|
+
db_index=True,
|
|
278
|
+
help_text="Invitation status.",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Metadata
|
|
282
|
+
public_metadata = models.JSONField(
|
|
283
|
+
default=dict,
|
|
284
|
+
blank=True,
|
|
285
|
+
help_text="Public metadata from Clerk.",
|
|
286
|
+
)
|
|
287
|
+
private_metadata = models.JSONField(
|
|
288
|
+
default=dict,
|
|
289
|
+
blank=True,
|
|
290
|
+
help_text="Private metadata from Clerk.",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Timestamps
|
|
294
|
+
created_at = models.DateTimeField(
|
|
295
|
+
default=timezone.now,
|
|
296
|
+
help_text="When the invitation was created.",
|
|
297
|
+
)
|
|
298
|
+
updated_at = models.DateTimeField(
|
|
299
|
+
auto_now=True,
|
|
300
|
+
help_text="When the invitation was last updated.",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
class Meta:
|
|
304
|
+
ordering = ["-created_at"]
|
|
305
|
+
indexes = [
|
|
306
|
+
models.Index(fields=["clerk_invitation_id"]),
|
|
307
|
+
models.Index(fields=["status"]),
|
|
308
|
+
models.Index(fields=["email_address"]),
|
|
309
|
+
]
|
|
310
|
+
verbose_name = "Organization Invitation"
|
|
311
|
+
verbose_name_plural = "Organization Invitations"
|
|
312
|
+
|
|
313
|
+
def __str__(self) -> str:
|
|
314
|
+
return f"Invitation to {self.email_address} for {self.organization.name}"
|