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,316 @@
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
+ from django_clerk_users.utils import update_or_create_clerk_user
137
+
138
+ try:
139
+ from django_clerk_users.organizations.utils import (
140
+ update_or_create_organization,
141
+ )
142
+
143
+ org, created = update_or_create_organization(self.clerk_id)
144
+ action = "created" if created else "updated"
145
+ return True, f"Organization {action} successfully"
146
+ except Exception as e:
147
+ return False, str(e)
148
+
149
+
150
+ class OrganizationMember(models.Model):
151
+ """
152
+ Represents a membership in a Clerk organization.
153
+
154
+ This is a cache of Clerk's organization memberships for efficient
155
+ local queries.
156
+ """
157
+
158
+ # Clerk-specific fields
159
+ clerk_membership_id = models.CharField(
160
+ max_length=255,
161
+ unique=True,
162
+ db_index=True,
163
+ help_text="Unique membership identifier from Clerk.",
164
+ )
165
+
166
+ # Relationships
167
+ organization = models.ForeignKey(
168
+ Organization,
169
+ on_delete=models.CASCADE,
170
+ related_name="cached_members",
171
+ help_text="The organization.",
172
+ )
173
+ user = models.ForeignKey(
174
+ settings.AUTH_USER_MODEL,
175
+ on_delete=models.CASCADE,
176
+ related_name="organization_memberships",
177
+ help_text="The user.",
178
+ )
179
+
180
+ # Role
181
+ role = models.CharField(
182
+ max_length=100,
183
+ default="member",
184
+ help_text="User's role in the organization (e.g., 'admin', 'member').",
185
+ )
186
+
187
+ # Metadata
188
+ public_metadata = models.JSONField(
189
+ default=dict,
190
+ blank=True,
191
+ help_text="Public metadata from Clerk.",
192
+ )
193
+ private_metadata = models.JSONField(
194
+ default=dict,
195
+ blank=True,
196
+ help_text="Private metadata from Clerk.",
197
+ )
198
+
199
+ # Timestamps
200
+ joined_at = models.DateTimeField(
201
+ default=timezone.now,
202
+ help_text="When the user joined the organization.",
203
+ )
204
+ updated_at = models.DateTimeField(
205
+ auto_now=True,
206
+ help_text="When the membership was last updated.",
207
+ )
208
+
209
+ class Meta:
210
+ ordering = ["-joined_at"]
211
+ unique_together = [("organization", "user")]
212
+ indexes = [
213
+ models.Index(fields=["clerk_membership_id"]),
214
+ models.Index(fields=["role"]),
215
+ ]
216
+ verbose_name = "Organization Member"
217
+ verbose_name_plural = "Organization Members"
218
+
219
+ def __str__(self) -> str:
220
+ return f"{self.user.email} in {self.organization.name} ({self.role})"
221
+
222
+ @property
223
+ def is_admin(self) -> bool:
224
+ """Check if this member has admin role."""
225
+ return self.role.lower() in ("admin", "org:admin", "owner")
226
+
227
+ def can_invite_members(self) -> bool:
228
+ """Check if this member can invite others to the organization."""
229
+ return self.is_admin
230
+
231
+
232
+ class OrganizationInvitation(models.Model):
233
+ """
234
+ Represents a pending invitation to a Clerk organization.
235
+ """
236
+
237
+ class Status(models.TextChoices):
238
+ PENDING = "pending", "Pending"
239
+ ACCEPTED = "accepted", "Accepted"
240
+ REVOKED = "revoked", "Revoked"
241
+
242
+ # Clerk-specific fields
243
+ clerk_invitation_id = models.CharField(
244
+ max_length=255,
245
+ unique=True,
246
+ db_index=True,
247
+ help_text="Unique invitation identifier from Clerk.",
248
+ )
249
+
250
+ # Relationships
251
+ organization = models.ForeignKey(
252
+ Organization,
253
+ on_delete=models.CASCADE,
254
+ related_name="invitations",
255
+ help_text="The organization.",
256
+ )
257
+ inviter = models.ForeignKey(
258
+ settings.AUTH_USER_MODEL,
259
+ on_delete=models.SET_NULL,
260
+ null=True,
261
+ blank=True,
262
+ related_name="sent_invitations",
263
+ help_text="The user who sent the invitation.",
264
+ )
265
+
266
+ # Invitation details
267
+ email_address = models.EmailField(
268
+ help_text="Email address of the invitee.",
269
+ )
270
+ role = models.CharField(
271
+ max_length=100,
272
+ default="member",
273
+ help_text="Role the user will have upon accepting.",
274
+ )
275
+ status = models.CharField(
276
+ max_length=20,
277
+ choices=Status.choices,
278
+ default=Status.PENDING,
279
+ db_index=True,
280
+ help_text="Invitation status.",
281
+ )
282
+
283
+ # Metadata
284
+ public_metadata = models.JSONField(
285
+ default=dict,
286
+ blank=True,
287
+ help_text="Public metadata from Clerk.",
288
+ )
289
+ private_metadata = models.JSONField(
290
+ default=dict,
291
+ blank=True,
292
+ help_text="Private metadata from Clerk.",
293
+ )
294
+
295
+ # Timestamps
296
+ created_at = models.DateTimeField(
297
+ default=timezone.now,
298
+ help_text="When the invitation was created.",
299
+ )
300
+ updated_at = models.DateTimeField(
301
+ auto_now=True,
302
+ help_text="When the invitation was last updated.",
303
+ )
304
+
305
+ class Meta:
306
+ ordering = ["-created_at"]
307
+ indexes = [
308
+ models.Index(fields=["clerk_invitation_id"]),
309
+ models.Index(fields=["status"]),
310
+ models.Index(fields=["email_address"]),
311
+ ]
312
+ verbose_name = "Organization Invitation"
313
+ verbose_name_plural = "Organization Invitations"
314
+
315
+ def __str__(self) -> str:
316
+ return f"Invitation to {self.email_address} for {self.organization.name}"