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.
- 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 +121 -0
- django_clerk_users/middleware/__init__.py +9 -0
- django_clerk_users/middleware/auth.py +201 -0
- django_clerk_users/migrations/0001_initial.py +174 -0
- django_clerk_users/migrations/__init__.py +0 -0
- django_clerk_users/models.py +174 -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/models.py +316 -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.0.2.dist-info/METADATA +228 -0
- django_clerk_users-0.0.2.dist-info/RECORD +41 -0
- 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 → django_clerk_users-0.0.2.dist-info}/WHEEL +0 -0
- {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.0.2.dist-info}/licenses/LICENSE +0 -0
- {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Generated by Django 5.2.8 on 2026-01-13 00:44
|
|
2
|
+
|
|
3
|
+
import django.utils.timezone
|
|
4
|
+
import uuid
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("auth", "0012_alter_user_first_name_max_length"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name="ClerkUser",
|
|
18
|
+
fields=[
|
|
19
|
+
(
|
|
20
|
+
"id",
|
|
21
|
+
models.BigAutoField(
|
|
22
|
+
auto_created=True,
|
|
23
|
+
primary_key=True,
|
|
24
|
+
serialize=False,
|
|
25
|
+
verbose_name="ID",
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
("password", models.CharField(max_length=128, verbose_name="password")),
|
|
29
|
+
(
|
|
30
|
+
"is_superuser",
|
|
31
|
+
models.BooleanField(
|
|
32
|
+
default=False,
|
|
33
|
+
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
|
34
|
+
verbose_name="superuser status",
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
(
|
|
38
|
+
"uid",
|
|
39
|
+
models.UUIDField(
|
|
40
|
+
db_index=True,
|
|
41
|
+
default=uuid.uuid4,
|
|
42
|
+
editable=False,
|
|
43
|
+
help_text="Public unique identifier for the user.",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"clerk_id",
|
|
48
|
+
models.CharField(
|
|
49
|
+
db_index=True,
|
|
50
|
+
help_text="Unique identifier from Clerk.",
|
|
51
|
+
max_length=255,
|
|
52
|
+
unique=True,
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"email",
|
|
57
|
+
models.EmailField(
|
|
58
|
+
db_index=True,
|
|
59
|
+
help_text="User's email address.",
|
|
60
|
+
max_length=254,
|
|
61
|
+
unique=True,
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"first_name",
|
|
66
|
+
models.CharField(
|
|
67
|
+
blank=True,
|
|
68
|
+
default="",
|
|
69
|
+
help_text="User's first name.",
|
|
70
|
+
max_length=255,
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
"last_name",
|
|
75
|
+
models.CharField(
|
|
76
|
+
blank=True,
|
|
77
|
+
default="",
|
|
78
|
+
help_text="User's last name.",
|
|
79
|
+
max_length=255,
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
(
|
|
83
|
+
"image_url",
|
|
84
|
+
models.URLField(
|
|
85
|
+
blank=True,
|
|
86
|
+
default="",
|
|
87
|
+
help_text="URL to user's profile image from Clerk.",
|
|
88
|
+
max_length=500,
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
(
|
|
92
|
+
"is_active",
|
|
93
|
+
models.BooleanField(
|
|
94
|
+
default=True, help_text="Whether the user account is active."
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
(
|
|
98
|
+
"is_staff",
|
|
99
|
+
models.BooleanField(
|
|
100
|
+
default=False,
|
|
101
|
+
help_text="Whether the user can access the admin site.",
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
(
|
|
105
|
+
"created_at",
|
|
106
|
+
models.DateTimeField(
|
|
107
|
+
default=django.utils.timezone.now,
|
|
108
|
+
help_text="When the user was created.",
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
"updated_at",
|
|
113
|
+
models.DateTimeField(
|
|
114
|
+
auto_now=True, help_text="When the user was last updated."
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
(
|
|
118
|
+
"last_login",
|
|
119
|
+
models.DateTimeField(
|
|
120
|
+
blank=True,
|
|
121
|
+
help_text="Last login timestamp (managed by Clerk).",
|
|
122
|
+
null=True,
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
(
|
|
126
|
+
"last_logout",
|
|
127
|
+
models.DateTimeField(
|
|
128
|
+
blank=True,
|
|
129
|
+
help_text="Last logout timestamp (managed by Clerk).",
|
|
130
|
+
null=True,
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
(
|
|
134
|
+
"groups",
|
|
135
|
+
models.ManyToManyField(
|
|
136
|
+
blank=True,
|
|
137
|
+
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
|
138
|
+
related_name="user_set",
|
|
139
|
+
related_query_name="user",
|
|
140
|
+
to="auth.group",
|
|
141
|
+
verbose_name="groups",
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
(
|
|
145
|
+
"user_permissions",
|
|
146
|
+
models.ManyToManyField(
|
|
147
|
+
blank=True,
|
|
148
|
+
help_text="Specific permissions for this user.",
|
|
149
|
+
related_name="user_set",
|
|
150
|
+
related_query_name="user",
|
|
151
|
+
to="auth.permission",
|
|
152
|
+
verbose_name="user permissions",
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
],
|
|
156
|
+
options={
|
|
157
|
+
"verbose_name": "Clerk User",
|
|
158
|
+
"verbose_name_plural": "Clerk Users",
|
|
159
|
+
"ordering": ["-created_at"],
|
|
160
|
+
"abstract": False,
|
|
161
|
+
"swappable": "AUTH_USER_MODEL",
|
|
162
|
+
"indexes": [
|
|
163
|
+
models.Index(
|
|
164
|
+
fields=["clerk_id"], name="django_cler_clerk_i_d591b6_idx"
|
|
165
|
+
),
|
|
166
|
+
models.Index(fields=["email"], name="django_cler_email_f1e649_idx"),
|
|
167
|
+
models.Index(fields=["uid"], name="django_cler_uid_9f6a95_idx"),
|
|
168
|
+
models.Index(
|
|
169
|
+
fields=["is_active"], name="django_cler_is_acti_ceff9c_idx"
|
|
170
|
+
),
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User models for django-clerk-users.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
|
11
|
+
from django.db import models
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
|
|
14
|
+
from django_clerk_users.managers import ClerkUserManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AbstractClerkUser(AbstractBaseUser, PermissionsMixin):
|
|
18
|
+
"""
|
|
19
|
+
Abstract base class for Clerk-authenticated users.
|
|
20
|
+
|
|
21
|
+
Extend this class to create a custom user model with additional fields
|
|
22
|
+
while maintaining Clerk integration.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
class CustomUser(AbstractClerkUser):
|
|
26
|
+
company = models.CharField(max_length=255, blank=True)
|
|
27
|
+
phone = models.CharField(max_length=20, blank=True)
|
|
28
|
+
|
|
29
|
+
class Meta(AbstractClerkUser.Meta):
|
|
30
|
+
swappable = "AUTH_USER_MODEL"
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Public identifier (use this in URLs and APIs instead of pk)
|
|
34
|
+
uid = models.UUIDField(
|
|
35
|
+
default=uuid.uuid4,
|
|
36
|
+
editable=False,
|
|
37
|
+
db_index=True,
|
|
38
|
+
help_text="Public unique identifier for the user.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Clerk-specific fields
|
|
42
|
+
clerk_id = models.CharField(
|
|
43
|
+
max_length=255,
|
|
44
|
+
unique=True,
|
|
45
|
+
db_index=True,
|
|
46
|
+
help_text="Unique identifier from Clerk.",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Standard user fields
|
|
50
|
+
email = models.EmailField(
|
|
51
|
+
unique=True,
|
|
52
|
+
db_index=True,
|
|
53
|
+
help_text="User's email address.",
|
|
54
|
+
)
|
|
55
|
+
first_name = models.CharField(
|
|
56
|
+
max_length=255,
|
|
57
|
+
blank=True,
|
|
58
|
+
default="",
|
|
59
|
+
help_text="User's first name.",
|
|
60
|
+
)
|
|
61
|
+
last_name = models.CharField(
|
|
62
|
+
max_length=255,
|
|
63
|
+
blank=True,
|
|
64
|
+
default="",
|
|
65
|
+
help_text="User's last name.",
|
|
66
|
+
)
|
|
67
|
+
image_url = models.URLField(
|
|
68
|
+
max_length=500,
|
|
69
|
+
blank=True,
|
|
70
|
+
default="",
|
|
71
|
+
help_text="URL to user's profile image from Clerk.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Status fields
|
|
75
|
+
is_active = models.BooleanField(
|
|
76
|
+
default=True,
|
|
77
|
+
help_text="Whether the user account is active.",
|
|
78
|
+
)
|
|
79
|
+
is_staff = models.BooleanField(
|
|
80
|
+
default=False,
|
|
81
|
+
help_text="Whether the user can access the admin site.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Timestamps
|
|
85
|
+
created_at = models.DateTimeField(
|
|
86
|
+
default=timezone.now,
|
|
87
|
+
help_text="When the user was created.",
|
|
88
|
+
)
|
|
89
|
+
updated_at = models.DateTimeField(
|
|
90
|
+
auto_now=True,
|
|
91
|
+
help_text="When the user was last updated.",
|
|
92
|
+
)
|
|
93
|
+
last_login = models.DateTimeField(
|
|
94
|
+
null=True,
|
|
95
|
+
blank=True,
|
|
96
|
+
help_text="Last login timestamp (managed by Clerk).",
|
|
97
|
+
)
|
|
98
|
+
last_logout = models.DateTimeField(
|
|
99
|
+
null=True,
|
|
100
|
+
blank=True,
|
|
101
|
+
help_text="Last logout timestamp (managed by Clerk).",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
objects = ClerkUserManager()
|
|
105
|
+
|
|
106
|
+
USERNAME_FIELD = "email"
|
|
107
|
+
REQUIRED_FIELDS = ["clerk_id"]
|
|
108
|
+
|
|
109
|
+
class Meta:
|
|
110
|
+
abstract = True
|
|
111
|
+
ordering = ["-created_at"]
|
|
112
|
+
indexes = [
|
|
113
|
+
models.Index(fields=["clerk_id"]),
|
|
114
|
+
models.Index(fields=["email"]),
|
|
115
|
+
models.Index(fields=["uid"]),
|
|
116
|
+
models.Index(fields=["is_active"]),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
def __str__(self) -> str:
|
|
120
|
+
return self.email
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def public_id(self) -> str:
|
|
124
|
+
"""Return the public UUID as a string for API responses."""
|
|
125
|
+
return str(self.uid)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def full_name(self) -> str:
|
|
129
|
+
"""Return the user's full name."""
|
|
130
|
+
return f"{self.first_name} {self.last_name}".strip()
|
|
131
|
+
|
|
132
|
+
def get_full_name(self) -> str:
|
|
133
|
+
"""Return the user's full name (Django compatibility)."""
|
|
134
|
+
return self.full_name
|
|
135
|
+
|
|
136
|
+
def get_short_name(self) -> str:
|
|
137
|
+
"""Return the user's first name (Django compatibility)."""
|
|
138
|
+
return self.first_name or self.email.split("@")[0]
|
|
139
|
+
|
|
140
|
+
def has_perm(self, perm: str, obj: Any = None) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Return True if the user has the specified permission.
|
|
143
|
+
|
|
144
|
+
For Clerk users, permissions are typically managed through
|
|
145
|
+
Clerk's organization roles or custom metadata.
|
|
146
|
+
Superusers have all permissions.
|
|
147
|
+
"""
|
|
148
|
+
if self.is_superuser:
|
|
149
|
+
return True
|
|
150
|
+
return super().has_perm(perm, obj)
|
|
151
|
+
|
|
152
|
+
def has_module_perms(self, app_label: str) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Return True if the user has any permissions in the given app.
|
|
155
|
+
|
|
156
|
+
Superusers have all permissions.
|
|
157
|
+
"""
|
|
158
|
+
if self.is_superuser:
|
|
159
|
+
return True
|
|
160
|
+
return super().has_module_perms(app_label)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ClerkUser(AbstractClerkUser):
|
|
164
|
+
"""
|
|
165
|
+
Concrete user model for Clerk authentication.
|
|
166
|
+
|
|
167
|
+
Use this model directly by setting AUTH_USER_MODEL = "django_clerk_users.ClerkUser"
|
|
168
|
+
in your Django settings, or extend AbstractClerkUser for custom fields.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
class Meta(AbstractClerkUser.Meta):
|
|
172
|
+
swappable = "AUTH_USER_MODEL"
|
|
173
|
+
verbose_name = "Clerk User"
|
|
174
|
+
verbose_name_plural = "Clerk Users"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Organizations sub-app for django-clerk-users.
|
|
3
|
+
|
|
4
|
+
This is an optional sub-app that provides organization support.
|
|
5
|
+
To use it, add 'django_clerk_users.organizations' to INSTALLED_APPS.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
default_app_config = "django_clerk_users.organizations.apps.OrganizationsConfig"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django admin configuration for organization models.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib import admin
|
|
6
|
+
|
|
7
|
+
from django_clerk_users.organizations.models import (
|
|
8
|
+
Organization,
|
|
9
|
+
OrganizationInvitation,
|
|
10
|
+
OrganizationMember,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@admin.register(Organization)
|
|
15
|
+
class OrganizationAdmin(admin.ModelAdmin):
|
|
16
|
+
list_display = [
|
|
17
|
+
"name",
|
|
18
|
+
"slug",
|
|
19
|
+
"clerk_id",
|
|
20
|
+
"members_count",
|
|
21
|
+
"is_active",
|
|
22
|
+
"created_at",
|
|
23
|
+
]
|
|
24
|
+
list_filter = ["is_active", "created_at"]
|
|
25
|
+
search_fields = ["name", "slug", "clerk_id"]
|
|
26
|
+
readonly_fields = [
|
|
27
|
+
"uid",
|
|
28
|
+
"clerk_id",
|
|
29
|
+
"created_at",
|
|
30
|
+
"updated_at",
|
|
31
|
+
"members_count",
|
|
32
|
+
"pending_invitations_count",
|
|
33
|
+
]
|
|
34
|
+
ordering = ["-created_at"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@admin.register(OrganizationMember)
|
|
38
|
+
class OrganizationMemberAdmin(admin.ModelAdmin):
|
|
39
|
+
list_display = [
|
|
40
|
+
"user",
|
|
41
|
+
"organization",
|
|
42
|
+
"role",
|
|
43
|
+
"joined_at",
|
|
44
|
+
]
|
|
45
|
+
list_filter = ["role", "joined_at"]
|
|
46
|
+
search_fields = [
|
|
47
|
+
"user__email",
|
|
48
|
+
"organization__name",
|
|
49
|
+
"clerk_membership_id",
|
|
50
|
+
]
|
|
51
|
+
readonly_fields = [
|
|
52
|
+
"clerk_membership_id",
|
|
53
|
+
"joined_at",
|
|
54
|
+
"updated_at",
|
|
55
|
+
]
|
|
56
|
+
raw_id_fields = ["user", "organization"]
|
|
57
|
+
ordering = ["-joined_at"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@admin.register(OrganizationInvitation)
|
|
61
|
+
class OrganizationInvitationAdmin(admin.ModelAdmin):
|
|
62
|
+
list_display = [
|
|
63
|
+
"email_address",
|
|
64
|
+
"organization",
|
|
65
|
+
"role",
|
|
66
|
+
"status",
|
|
67
|
+
"created_at",
|
|
68
|
+
]
|
|
69
|
+
list_filter = ["status", "role", "created_at"]
|
|
70
|
+
search_fields = [
|
|
71
|
+
"email_address",
|
|
72
|
+
"organization__name",
|
|
73
|
+
"clerk_invitation_id",
|
|
74
|
+
]
|
|
75
|
+
readonly_fields = [
|
|
76
|
+
"clerk_invitation_id",
|
|
77
|
+
"created_at",
|
|
78
|
+
"updated_at",
|
|
79
|
+
]
|
|
80
|
+
raw_id_fields = ["organization", "inviter"]
|
|
81
|
+
ordering = ["-created_at"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Organization middleware for django-clerk-users.
|
|
3
|
+
|
|
4
|
+
This middleware resolves Clerk organization IDs to Organization model instances.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Callable
|
|
11
|
+
|
|
12
|
+
from django_clerk_users.caching import (
|
|
13
|
+
get_cached_organization,
|
|
14
|
+
set_cached_organization,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from django.http import HttpRequest, HttpResponse
|
|
19
|
+
|
|
20
|
+
from django_clerk_users.organizations.models import Organization
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClerkOrganizationMiddleware:
|
|
26
|
+
"""
|
|
27
|
+
Middleware that resolves organization context.
|
|
28
|
+
|
|
29
|
+
This middleware runs after ClerkAuthMiddleware and resolves the
|
|
30
|
+
organization ID (from request.org) to an Organization model instance.
|
|
31
|
+
|
|
32
|
+
After processing, the middleware sets:
|
|
33
|
+
- request.organization: The Organization model instance (or None)
|
|
34
|
+
|
|
35
|
+
The organization ID can come from:
|
|
36
|
+
1. request.org (set by ClerkAuthMiddleware from JWT payload)
|
|
37
|
+
2. X-Organization-Id header (for explicit org switching)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, get_response: Callable[["HttpRequest"], "HttpResponse"]):
|
|
41
|
+
self.get_response = get_response
|
|
42
|
+
|
|
43
|
+
def __call__(self, request: "HttpRequest") -> "HttpResponse":
|
|
44
|
+
# Process organization before the view
|
|
45
|
+
self.process_request(request)
|
|
46
|
+
|
|
47
|
+
# Call the next middleware/view
|
|
48
|
+
response = self.get_response(request)
|
|
49
|
+
|
|
50
|
+
return response
|
|
51
|
+
|
|
52
|
+
def process_request(self, request: "HttpRequest") -> None:
|
|
53
|
+
"""
|
|
54
|
+
Resolve the organization context.
|
|
55
|
+
|
|
56
|
+
Sets request.organization to the Organization model instance
|
|
57
|
+
if an organization ID is present.
|
|
58
|
+
"""
|
|
59
|
+
request.organization = None # type: ignore
|
|
60
|
+
|
|
61
|
+
# Get organization ID from request
|
|
62
|
+
# Priority: request.org (from JWT) > X-Organization-Id header
|
|
63
|
+
org_id = getattr(request, "org", None)
|
|
64
|
+
if not org_id:
|
|
65
|
+
org_id = request.headers.get("X-Organization-Id")
|
|
66
|
+
|
|
67
|
+
if not org_id:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Resolve to Organization model
|
|
71
|
+
organization = self._get_organization(org_id)
|
|
72
|
+
if organization:
|
|
73
|
+
if not self._is_member(request, organization):
|
|
74
|
+
logger.debug("User is not a member of org %s", org_id)
|
|
75
|
+
return
|
|
76
|
+
request.organization = organization # type: ignore
|
|
77
|
+
# Update request.org in case it came from header
|
|
78
|
+
request.org = org_id # type: ignore
|
|
79
|
+
|
|
80
|
+
def _is_member(self, request: "HttpRequest", organization: "Organization") -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Check whether the current user belongs to the organization.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
request: The current HTTP request.
|
|
86
|
+
organization: The organization to check.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if the user is authenticated and a member.
|
|
90
|
+
"""
|
|
91
|
+
if not hasattr(request, "user") or not request.user.is_authenticated:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
from django_clerk_users.organizations.models import OrganizationMember
|
|
95
|
+
|
|
96
|
+
return OrganizationMember.objects.filter(
|
|
97
|
+
organization=organization,
|
|
98
|
+
user=request.user,
|
|
99
|
+
).exists()
|
|
100
|
+
|
|
101
|
+
def _get_organization(self, clerk_id: str) -> "Organization | None":
|
|
102
|
+
"""
|
|
103
|
+
Get an Organization by Clerk ID, using cache.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
clerk_id: The Clerk organization ID.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The Organization instance or None if not found.
|
|
110
|
+
"""
|
|
111
|
+
from django_clerk_users.organizations.models import Organization
|
|
112
|
+
|
|
113
|
+
# Check cache first
|
|
114
|
+
cached = get_cached_organization(clerk_id)
|
|
115
|
+
if cached is not None:
|
|
116
|
+
if cached is False:
|
|
117
|
+
return None # Cached as "not found"
|
|
118
|
+
return cached
|
|
119
|
+
|
|
120
|
+
# Query database
|
|
121
|
+
try:
|
|
122
|
+
organization = Organization.objects.get(
|
|
123
|
+
clerk_id=clerk_id,
|
|
124
|
+
is_active=True,
|
|
125
|
+
)
|
|
126
|
+
set_cached_organization(clerk_id, organization)
|
|
127
|
+
return organization
|
|
128
|
+
except Organization.DoesNotExist:
|
|
129
|
+
set_cached_organization(clerk_id, None)
|
|
130
|
+
return None
|