django-supabase 1.0.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.
@@ -0,0 +1,4 @@
1
+ __version__ = '1.0.0'
2
+
3
+ default_app_config = 'django_supabase.apps.DjangoSupabaseConfig'
4
+
@@ -0,0 +1,13 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoSupabaseConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'django_supabase'
7
+ verbose_name = 'Supabase Integration'
8
+
9
+ def ready(self):
10
+ """Initialize Supabase integration on Django startup"""
11
+ from .conf import supabase_settings
12
+ # Validate settings on startup
13
+ supabase_settings.configure()
@@ -0,0 +1 @@
1
+ default_app_config = 'django_supabase.auth.apps.SupabaseAuthConfig'
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class SupabaseAuthConfig(AppConfig):
5
+ name = 'django_supabase.auth'
6
+ label = 'supabase_auth'
7
+ verbose_name = 'Supabase User'
8
+ verbose_name_plural = 'Supabase Users'
@@ -0,0 +1,121 @@
1
+ import logging
2
+
3
+ from django.conf import settings
4
+ from django.contrib.auth import get_user_model
5
+ from django.contrib.auth.backends import BaseBackend
6
+ from supabase import create_client
7
+
8
+ from .sync import sync_user_from_claims, sync_user_from_supabase
9
+ from .tokens import (
10
+ SupabaseTokenError,
11
+ token_algorithm,
12
+ verify_token_with_jwks,
13
+ verify_token_with_secret,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class SupabaseAuthBackend(BaseBackend):
20
+ """
21
+ Authenticates against Supabase and syncs with Django User model
22
+ """
23
+
24
+ def __init__(self):
25
+ self.supabase = create_client(
26
+ settings.SUPABASE_URL,
27
+ settings.SUPABASE_KEY
28
+ )
29
+
30
+ def authenticate(self, request, token=None, **credentials):
31
+ if token:
32
+ return self._authenticate_token(token)
33
+
34
+ # Email/password auth
35
+ email = credentials.get('email')
36
+ password = credentials.get('password')
37
+
38
+ if not email or not password:
39
+ return None
40
+
41
+ try:
42
+ response = self.supabase.auth.sign_in_with_password({
43
+ "email": email,
44
+ "password": password
45
+ })
46
+
47
+ if response.user:
48
+ user, created = self._sync_user(response.user)
49
+ return user
50
+
51
+ except Exception as exc:
52
+ logger.debug('Supabase email/password authentication failed: %s', exc)
53
+ return None
54
+
55
+ def _authenticate_token(self, token):
56
+ """Validate JWT token from Supabase"""
57
+ verification_mode = getattr(
58
+ settings,
59
+ 'SUPABASE_JWT_VERIFICATION',
60
+ 'auth_server',
61
+ ).lower()
62
+
63
+ if verification_mode == 'auth_server':
64
+ return self._authenticate_token_with_auth_server(token)
65
+
66
+ if verification_mode == 'jwks':
67
+ return self._authenticate_token_with_jwks(token)
68
+
69
+ if verification_mode == 'jwt_secret':
70
+ return self._authenticate_token_with_secret(token)
71
+
72
+ logger.error('Unsupported SUPABASE_JWT_VERIFICATION mode: %s', verification_mode)
73
+ return None
74
+
75
+ def _authenticate_token_with_auth_server(self, token):
76
+ try:
77
+ response = self.supabase.auth.get_user(token)
78
+ supabase_user = getattr(response, 'user', None)
79
+ if not supabase_user:
80
+ return None
81
+ user, _ = sync_user_from_supabase(supabase_user)
82
+ return user
83
+ except Exception as exc:
84
+ logger.debug('Supabase Auth server token validation failed: %s', exc)
85
+ return None
86
+
87
+ def _authenticate_token_with_jwks(self, token):
88
+ try:
89
+ if token_algorithm(token) == 'HS256':
90
+ logger.warning(
91
+ 'Received HS256 token while SUPABASE_JWT_VERIFICATION=jwks'
92
+ )
93
+ return None
94
+ payload = verify_token_with_jwks(token)
95
+ if getattr(settings, 'SUPABASE_SYNC_USER_FROM_AUTH_ON_TOKEN', False):
96
+ return self._authenticate_token_with_auth_server(token)
97
+ user, _ = sync_user_from_claims(payload)
98
+ return user
99
+ except SupabaseTokenError as exc:
100
+ logger.debug('Supabase JWKS token validation failed: %s', exc)
101
+ return None
102
+
103
+ def _authenticate_token_with_secret(self, token):
104
+ try:
105
+ payload = verify_token_with_secret(token)
106
+ user, _ = sync_user_from_claims(payload)
107
+ return user
108
+ except SupabaseTokenError as exc:
109
+ logger.debug('Supabase JWT secret token validation failed: %s', exc)
110
+ return None
111
+
112
+ def _sync_user(self, supabase_user):
113
+ """Sync Supabase user with Django User model"""
114
+ return sync_user_from_supabase(supabase_user)
115
+
116
+ def get_user(self, user_id):
117
+ User = get_user_model()
118
+ try:
119
+ return User.objects.get(pk=user_id)
120
+ except User.DoesNotExist:
121
+ return None
@@ -0,0 +1,33 @@
1
+ from django.contrib.auth import authenticate
2
+ from django.contrib.auth.middleware import get_user
3
+ from django.utils.functional import SimpleLazyObject
4
+
5
+
6
+ class SupabaseAuthMiddleware:
7
+ """
8
+ Middleware to authenticate requests using Supabase JWT tokens
9
+ """
10
+
11
+ def __init__(self, get_response):
12
+ self.get_response = get_response
13
+
14
+ def __call__(self, request):
15
+ # Extract token from Authorization header
16
+ auth_header = request.META.get('HTTP_AUTHORIZATION', '')
17
+
18
+ scheme, _, token = auth_header.partition(' ')
19
+
20
+ if scheme.lower() == 'bearer' and token:
21
+ request.user = SimpleLazyObject(
22
+ lambda: self._authenticate_token(request, token.strip())
23
+ )
24
+ else:
25
+ request.user = SimpleLazyObject(lambda: get_user(request))
26
+
27
+ response = self.get_response(request)
28
+ return response
29
+
30
+ def _authenticate_token(self, request, token):
31
+ """Authenticate using token"""
32
+ user = authenticate(request, token=token)
33
+ return user or get_user(request)
@@ -0,0 +1,63 @@
1
+ # Generated by Django 6.0.6 on 2026-06-11 12:57
2
+
3
+ import django.contrib.auth.models
4
+ import django.contrib.auth.validators
5
+ import django.utils.timezone
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ initial = True
12
+
13
+ dependencies = [
14
+ ('auth', '0012_alter_user_first_name_max_length'),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name='SupabaseUser',
20
+ fields=[
21
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
+ ('password', models.CharField(max_length=128, verbose_name='password')),
23
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
24
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
25
+ ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
26
+ ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
27
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
28
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
29
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
30
+ ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
31
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
32
+ ('supabase_id', models.UUIDField(blank=True, db_index=True, null=True, unique=True)),
33
+ ('phone', models.CharField(blank=True, max_length=32)),
34
+ ('display_name', models.CharField(blank=True, max_length=150)),
35
+ ('avatar_url', models.URLField(blank=True)),
36
+ ('aud', models.CharField(blank=True, max_length=64)),
37
+ ('role', models.CharField(blank=True, max_length=64)),
38
+ ('provider', models.CharField(blank=True, max_length=64)),
39
+ ('providers', models.JSONField(blank=True, default=list)),
40
+ ('app_metadata', models.JSONField(blank=True, default=dict)),
41
+ ('user_metadata', models.JSONField(blank=True, default=dict)),
42
+ ('metadata', models.JSONField(blank=True, default=dict)),
43
+ ('identities', models.JSONField(blank=True, default=list)),
44
+ ('email_confirmed_at', models.DateTimeField(blank=True, null=True)),
45
+ ('phone_confirmed_at', models.DateTimeField(blank=True, null=True)),
46
+ ('last_sign_in_at', models.DateTimeField(blank=True, null=True)),
47
+ ('supabase_created_at', models.DateTimeField(blank=True, null=True)),
48
+ ('supabase_updated_at', models.DateTimeField(blank=True, null=True)),
49
+ ('is_anonymous', models.BooleanField(default=False)),
50
+ ('is_sso_user', models.BooleanField(default=False)),
51
+ ('banned_until', models.DateTimeField(blank=True, null=True)),
52
+ ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
53
+ ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
54
+ ],
55
+ options={
56
+ 'db_table': 'auth_user',
57
+ 'swappable': 'AUTH_USER_MODEL',
58
+ },
59
+ managers=[
60
+ ('objects', django.contrib.auth.models.UserManager()),
61
+ ],
62
+ ),
63
+ ]
File without changes
@@ -0,0 +1,36 @@
1
+ from django.contrib.auth.models import AbstractUser
2
+ from django.db import models
3
+
4
+ class SupabaseUser(AbstractUser):
5
+ """
6
+ Extended user model to store Supabase-specific data
7
+ """
8
+ supabase_id = models.UUIDField(unique=True, null=True, blank=True, db_index=True)
9
+ phone = models.CharField(max_length=32, blank=True)
10
+ display_name = models.CharField(max_length=150, blank=True)
11
+ avatar_url = models.URLField(blank=True)
12
+ aud = models.CharField(max_length=64, blank=True)
13
+ role = models.CharField(max_length=64, blank=True)
14
+ provider = models.CharField(max_length=64, blank=True)
15
+ providers = models.JSONField(default=list, blank=True)
16
+ app_metadata = models.JSONField(default=dict, blank=True)
17
+ user_metadata = models.JSONField(default=dict, blank=True)
18
+ metadata = models.JSONField(default=dict, blank=True)
19
+ identities = models.JSONField(default=list, blank=True)
20
+ email_confirmed_at = models.DateTimeField(null=True, blank=True)
21
+ phone_confirmed_at = models.DateTimeField(null=True, blank=True)
22
+ last_sign_in_at = models.DateTimeField(null=True, blank=True)
23
+ supabase_created_at = models.DateTimeField(null=True, blank=True)
24
+ supabase_updated_at = models.DateTimeField(null=True, blank=True)
25
+ is_anonymous = models.BooleanField(default=False)
26
+ is_sso_user = models.BooleanField(default=False)
27
+ banned_until = models.DateTimeField(null=True, blank=True)
28
+
29
+ class Meta:
30
+ db_table = 'auth_user' # Keep Django's default table name
31
+ swappable = 'AUTH_USER_MODEL'
32
+
33
+ @property
34
+ def name(self):
35
+ return self.display_name or self.get_full_name() or self.username
36
+
@@ -0,0 +1,228 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ from typing import Any
5
+
6
+ from django.contrib.auth import get_user_model
7
+ from django.db import transaction
8
+ from django.utils import timezone
9
+ from django.utils.dateparse import parse_datetime
10
+
11
+
12
+ def sync_user_from_supabase(supabase_user: Any):
13
+ """Create or update a Django user from a Supabase Auth user object."""
14
+ app_metadata = _as_dict(_value(supabase_user, 'app_metadata', {}))
15
+ user_metadata = _as_dict(_value(supabase_user, 'user_metadata', {}))
16
+
17
+ return _sync_user(
18
+ supabase_id=_value(supabase_user, 'id'),
19
+ email=_value(supabase_user, 'email') or '',
20
+ phone=_value(supabase_user, 'phone') or '',
21
+ aud=_value(supabase_user, 'aud') or '',
22
+ role=_value(supabase_user, 'role') or '',
23
+ app_metadata=app_metadata,
24
+ user_metadata=user_metadata,
25
+ identities=_jsonable(_value(supabase_user, 'identities', [])) or [],
26
+ email_confirmed_at=_parse_datetime(_value(supabase_user, 'email_confirmed_at')),
27
+ phone_confirmed_at=_parse_datetime(_value(supabase_user, 'phone_confirmed_at')),
28
+ last_sign_in_at=_parse_datetime(_value(supabase_user, 'last_sign_in_at')),
29
+ supabase_created_at=_parse_datetime(_value(supabase_user, 'created_at')),
30
+ supabase_updated_at=_parse_datetime(_value(supabase_user, 'updated_at')),
31
+ is_anonymous=bool(_value(supabase_user, 'is_anonymous', False)),
32
+ is_sso_user=bool(_value(supabase_user, 'is_sso_user', False)),
33
+ banned_until=_parse_datetime(_value(supabase_user, 'banned_until')),
34
+ )
35
+
36
+
37
+ def sync_user_from_claims(claims: dict[str, Any]):
38
+ """Create or update a Django user from verified Supabase JWT claims."""
39
+ app_metadata = _as_dict(claims.get('app_metadata', {}))
40
+ user_metadata = _as_dict(claims.get('user_metadata', {}))
41
+
42
+ return _sync_user(
43
+ supabase_id=claims.get('sub'),
44
+ email=claims.get('email') or '',
45
+ phone=claims.get('phone') or '',
46
+ aud=_audience_to_string(claims.get('aud')),
47
+ role=claims.get('role') or '',
48
+ app_metadata=app_metadata,
49
+ user_metadata=user_metadata,
50
+ identities=[],
51
+ email_confirmed_at=None,
52
+ phone_confirmed_at=None,
53
+ last_sign_in_at=None,
54
+ supabase_created_at=None,
55
+ supabase_updated_at=None,
56
+ is_anonymous=bool(claims.get('is_anonymous', False)),
57
+ is_sso_user=False,
58
+ banned_until=None,
59
+ )
60
+
61
+
62
+ @transaction.atomic
63
+ def _sync_user(
64
+ *,
65
+ supabase_id: Any,
66
+ email: str,
67
+ phone: str,
68
+ aud: str,
69
+ role: str,
70
+ app_metadata: dict[str, Any],
71
+ user_metadata: dict[str, Any],
72
+ identities: list[Any],
73
+ email_confirmed_at: datetime | None,
74
+ phone_confirmed_at: datetime | None,
75
+ last_sign_in_at: datetime | None,
76
+ supabase_created_at: datetime | None,
77
+ supabase_updated_at: datetime | None,
78
+ is_anonymous: bool,
79
+ is_sso_user: bool,
80
+ banned_until: datetime | None,
81
+ ):
82
+ User = get_user_model()
83
+ field_names = {field.name for field in User._meta.fields}
84
+ supabase_id = str(supabase_id) if supabase_id else None
85
+
86
+ user = _find_existing_user(User, field_names, supabase_id, email)
87
+ created = user is None
88
+
89
+ if user is None:
90
+ user = User()
91
+ if 'password' in field_names:
92
+ user.set_unusable_password()
93
+
94
+ display_name = _display_name(user_metadata)
95
+ first_name, last_name = _name_parts(user_metadata, display_name)
96
+ avatar_url = (
97
+ user_metadata.get('avatar_url')
98
+ or user_metadata.get('picture')
99
+ or user_metadata.get('avatar')
100
+ or ''
101
+ )
102
+
103
+ defaults = {
104
+ 'supabase_id': supabase_id,
105
+ 'email': email,
106
+ 'username': email or (f'supabase_{supabase_id}' if supabase_id else ''),
107
+ 'phone': phone,
108
+ 'display_name': display_name,
109
+ 'first_name': first_name,
110
+ 'last_name': last_name,
111
+ 'avatar_url': avatar_url,
112
+ 'aud': aud,
113
+ 'role': role,
114
+ 'provider': app_metadata.get('provider') or '',
115
+ 'providers': app_metadata.get('providers') or [],
116
+ 'app_metadata': app_metadata,
117
+ 'user_metadata': user_metadata,
118
+ 'metadata': user_metadata,
119
+ 'identities': identities,
120
+ 'email_confirmed_at': email_confirmed_at,
121
+ 'phone_confirmed_at': phone_confirmed_at,
122
+ 'last_sign_in_at': last_sign_in_at,
123
+ 'supabase_created_at': supabase_created_at,
124
+ 'supabase_updated_at': supabase_updated_at,
125
+ 'is_anonymous': is_anonymous,
126
+ 'is_sso_user': is_sso_user,
127
+ 'banned_until': banned_until,
128
+ 'is_active': banned_until is None,
129
+ }
130
+
131
+ for field, value in defaults.items():
132
+ if field in field_names:
133
+ setattr(user, field, value)
134
+
135
+ user.save()
136
+ return user, created
137
+
138
+
139
+ def _find_existing_user(User, field_names: set[str], supabase_id: str | None, email: str):
140
+ if supabase_id and 'supabase_id' in field_names:
141
+ user = User.objects.filter(supabase_id=supabase_id).first()
142
+ if user:
143
+ return user
144
+
145
+ if email and 'email' in field_names:
146
+ return User.objects.filter(email__iexact=email).first()
147
+
148
+ return None
149
+
150
+
151
+ def _value(obj: Any, field: str, default: Any = None):
152
+ if isinstance(obj, dict):
153
+ return obj.get(field, default)
154
+ return getattr(obj, field, default)
155
+
156
+
157
+ def _as_dict(value: Any) -> dict[str, Any]:
158
+ if value is None:
159
+ return {}
160
+ if isinstance(value, dict):
161
+ return value
162
+ dumped = _jsonable(value)
163
+ return dumped if isinstance(dumped, dict) else {}
164
+
165
+
166
+ def _jsonable(value: Any):
167
+ if value is None:
168
+ return None
169
+ if hasattr(value, 'model_dump'):
170
+ return value.model_dump(mode='json')
171
+ if isinstance(value, list):
172
+ return [_jsonable(item) for item in value]
173
+ if isinstance(value, tuple):
174
+ return [_jsonable(item) for item in value]
175
+ if isinstance(value, dict):
176
+ return {key: _jsonable(item) for key, item in value.items()}
177
+ if hasattr(value, '__dict__'):
178
+ return {
179
+ key: _jsonable(item)
180
+ for key, item in vars(value).items()
181
+ if not key.startswith('_')
182
+ }
183
+ return value
184
+
185
+
186
+ def _parse_datetime(value: Any) -> datetime | None:
187
+ if not value:
188
+ return None
189
+
190
+ if isinstance(value, datetime):
191
+ parsed = value
192
+ elif isinstance(value, str):
193
+ parsed = parse_datetime(value)
194
+ else:
195
+ return None
196
+
197
+ if parsed and timezone.is_naive(parsed):
198
+ return timezone.make_aware(parsed, timezone=UTC)
199
+ return parsed
200
+
201
+
202
+ def _display_name(user_metadata: dict[str, Any]) -> str:
203
+ return (
204
+ user_metadata.get('display_name')
205
+ or user_metadata.get('full_name')
206
+ or user_metadata.get('name')
207
+ or ''
208
+ )
209
+
210
+
211
+ def _name_parts(user_metadata: dict[str, Any], display_name: str) -> tuple[str, str]:
212
+ first_name = user_metadata.get('first_name') or user_metadata.get('given_name')
213
+ last_name = user_metadata.get('last_name') or user_metadata.get('family_name')
214
+
215
+ if first_name or last_name:
216
+ return first_name or '', last_name or ''
217
+
218
+ if display_name and ' ' in display_name:
219
+ first_name, last_name = display_name.split(' ', 1)
220
+ return first_name, last_name
221
+
222
+ return display_name, ''
223
+
224
+
225
+ def _audience_to_string(audience: Any) -> str:
226
+ if isinstance(audience, list):
227
+ return ','.join(str(item) for item in audience)
228
+ return str(audience or '')
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import Any
5
+
6
+ import jwt
7
+ from django.conf import settings
8
+ from django.core.exceptions import ImproperlyConfigured
9
+ from jwt import PyJWKClient
10
+
11
+
12
+ class SupabaseTokenError(Exception):
13
+ """Raised when a Supabase access token cannot be verified."""
14
+
15
+
16
+ def verify_token_with_jwks(token: str) -> dict[str, Any]:
17
+ """Verify a Supabase JWT using the project's JWKS endpoint."""
18
+ try:
19
+ signing_key = _jwks_client().get_signing_key_from_jwt(token)
20
+ return jwt.decode(
21
+ token,
22
+ signing_key.key,
23
+ algorithms=_jwt_algorithms(),
24
+ audience=_jwt_audience(),
25
+ issuer=_jwt_issuer(),
26
+ )
27
+ except jwt.PyJWTError as exc:
28
+ raise SupabaseTokenError(str(exc)) from exc
29
+
30
+
31
+ def verify_token_with_secret(token: str) -> dict[str, Any]:
32
+ """Verify a Supabase JWT using the legacy shared JWT secret."""
33
+ secret = getattr(settings, 'SUPABASE_JWT_SECRET', None)
34
+ if not secret:
35
+ raise ImproperlyConfigured(
36
+ 'SUPABASE_JWT_SECRET is required when '
37
+ "SUPABASE_JWT_VERIFICATION='jwt_secret'"
38
+ )
39
+
40
+ try:
41
+ return jwt.decode(
42
+ token,
43
+ secret,
44
+ algorithms=['HS256'],
45
+ audience=_jwt_audience(),
46
+ issuer=_jwt_issuer(),
47
+ )
48
+ except jwt.PyJWTError as exc:
49
+ raise SupabaseTokenError(str(exc)) from exc
50
+
51
+
52
+ def token_algorithm(token: str) -> str:
53
+ """Return the token header algorithm without trusting the payload."""
54
+ try:
55
+ return jwt.get_unverified_header(token).get('alg', '')
56
+ except jwt.PyJWTError as exc:
57
+ raise SupabaseTokenError(str(exc)) from exc
58
+
59
+
60
+ @lru_cache(maxsize=1)
61
+ def _jwks_client() -> PyJWKClient:
62
+ return PyJWKClient(
63
+ _jwks_url(),
64
+ cache_keys=True,
65
+ cache_jwk_set=True,
66
+ lifespan=600,
67
+ timeout=getattr(settings, 'SUPABASE_TIMEOUT', 30),
68
+ )
69
+
70
+
71
+ def _jwt_issuer() -> str:
72
+ configured = getattr(settings, 'SUPABASE_JWT_ISSUER', None)
73
+ if configured:
74
+ return configured
75
+ return f"{settings.SUPABASE_URL.rstrip('/')}/auth/v1"
76
+
77
+
78
+ def _jwks_url() -> str:
79
+ configured = getattr(settings, 'SUPABASE_JWKS_URL', None)
80
+ if configured:
81
+ return configured
82
+ return f"{_jwt_issuer().rstrip('/')}/.well-known/jwks.json"
83
+
84
+
85
+ def _jwt_audience() -> str:
86
+ return getattr(settings, 'SUPABASE_JWT_AUDIENCE', 'authenticated')
87
+
88
+
89
+ def _jwt_algorithms() -> list[str]:
90
+ algorithms = getattr(settings, 'SUPABASE_JWT_ALGORITHMS', ['RS256', 'ES256'])
91
+ if isinstance(algorithms, str):
92
+ return [item.strip() for item in algorithms.split(',') if item.strip()]
93
+ return list(algorithms)
@@ -0,0 +1,70 @@
1
+ from django.conf import settings
2
+ from django.core.exceptions import ImproperlyConfigured
3
+
4
+
5
+ class SupabaseSettings:
6
+ """
7
+ Centralized settings management for Supabase integration
8
+ """
9
+
10
+ REQUIRED_SETTINGS = [
11
+ 'SUPABASE_URL',
12
+ 'SUPABASE_KEY',
13
+ ]
14
+
15
+ DEFAULTS = {
16
+ 'SUPABASE_JWT_SECRET': None,
17
+ 'SUPABASE_JWT_VERIFICATION': 'auth_server',
18
+ 'SUPABASE_JWT_AUDIENCE': 'authenticated',
19
+ 'SUPABASE_JWT_ALGORITHMS': ['RS256', 'ES256'],
20
+ 'SUPABASE_JWT_ISSUER': None,
21
+ 'SUPABASE_JWKS_URL': None,
22
+ 'SUPABASE_SECRET_KEY': None,
23
+ 'SUPABASE_SERVICE_KEY': None,
24
+ 'SUPABASE_STORAGE_BUCKET': 'media',
25
+ 'SUPABASE_MEDIA_BUCKET': 'media',
26
+ 'SUPABASE_STATIC_BUCKET': 'static',
27
+ 'SUPABASE_MEDIA_FOLDER': 'uploads',
28
+ 'SUPABASE_STATIC_FOLDER': 'static',
29
+ 'SUPABASE_STORAGE_PUBLIC': True,
30
+ 'SUPABASE_STORAGE_AUTO_CREATE_BUCKETS': False,
31
+ 'SUPABASE_AUTH_REQUIRED': False,
32
+ 'SUPABASE_SYNC_USER_ON_LOGIN': True,
33
+ 'SUPABASE_SYNC_USER_FROM_AUTH_ON_TOKEN': False,
34
+ 'SUPABASE_CONNECTION_POOL_SIZE': 20,
35
+ 'SUPABASE_TIMEOUT': 30,
36
+ }
37
+
38
+ def __init__(self):
39
+ self._configured = False
40
+
41
+ def configure(self):
42
+ """Validate required settings and apply defaults once Django is ready."""
43
+ if self._configured:
44
+ return
45
+ self._validate_settings()
46
+ self._set_defaults()
47
+ self._configured = True
48
+
49
+ def _validate_settings(self):
50
+ """Validate required settings are present"""
51
+ for setting in self.REQUIRED_SETTINGS:
52
+ if not getattr(settings, setting, None):
53
+ raise ImproperlyConfigured(
54
+ f"Setting {setting} is required for Supabase integration"
55
+ )
56
+
57
+ def _set_defaults(self):
58
+ """Set default values for optional settings"""
59
+ for key, value in self.DEFAULTS.items():
60
+ if not hasattr(settings, key):
61
+ setattr(settings, key, value)
62
+
63
+ @classmethod
64
+ def get(cls, key, default=None):
65
+ """Get a setting value"""
66
+ return getattr(settings, key, default)
67
+
68
+
69
+ # Initialize settings
70
+ supabase_settings = SupabaseSettings()
File without changes