django-supabase 1.0.0__tar.gz
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_supabase-1.0.0/PKG-INFO +184 -0
- django_supabase-1.0.0/README.md +164 -0
- django_supabase-1.0.0/pyproject.toml +26 -0
- django_supabase-1.0.0/src/django_supabase/__init__.py +4 -0
- django_supabase-1.0.0/src/django_supabase/apps.py +13 -0
- django_supabase-1.0.0/src/django_supabase/auth/__init__.py +1 -0
- django_supabase-1.0.0/src/django_supabase/auth/apps.py +8 -0
- django_supabase-1.0.0/src/django_supabase/auth/backends.py +121 -0
- django_supabase-1.0.0/src/django_supabase/auth/middleware.py +33 -0
- django_supabase-1.0.0/src/django_supabase/auth/migrations/0001_initial.py +63 -0
- django_supabase-1.0.0/src/django_supabase/auth/migrations/__init__.py +0 -0
- django_supabase-1.0.0/src/django_supabase/auth/models.py +36 -0
- django_supabase-1.0.0/src/django_supabase/auth/sync.py +228 -0
- django_supabase-1.0.0/src/django_supabase/auth/tokens.py +93 -0
- django_supabase-1.0.0/src/django_supabase/conf.py +70 -0
- django_supabase-1.0.0/src/django_supabase/db/__init__.py +0 -0
- django_supabase-1.0.0/src/django_supabase/db/client.py +79 -0
- django_supabase-1.0.0/src/django_supabase/db/engine.py +66 -0
- django_supabase-1.0.0/src/django_supabase/defaults.py +79 -0
- django_supabase-1.0.0/src/django_supabase/env.py +46 -0
- django_supabase-1.0.0/src/django_supabase/management/__init__.py +0 -0
- django_supabase-1.0.0/src/django_supabase/management/commands/__init__.py +0 -0
- django_supabase-1.0.0/src/django_supabase/management/commands/create_storage_buckets.py +30 -0
- django_supabase-1.0.0/src/django_supabase/management/commands/sync_users.py +72 -0
- django_supabase-1.0.0/src/django_supabase/storage/__init__.py +0 -0
- django_supabase-1.0.0/src/django_supabase/storage/backends.py +252 -0
- django_supabase-1.0.0/src/django_supabase/storage/utils.py +53 -0
- django_supabase-1.0.0/src/django_supabase/utils.py +40 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-supabase
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight django package designed to connect Django applications to Supabase Auth, Database Access, and Storage
|
|
5
|
+
Author: Lakan
|
|
6
|
+
Author-email: Lakan <leydotpy.dev@gmail.com>
|
|
7
|
+
Requires-Dist: celery>=5.6.3
|
|
8
|
+
Requires-Dist: django>=6.0.6
|
|
9
|
+
Requires-Dist: django-celery-beat>=2.9.0
|
|
10
|
+
Requires-Dist: django-celery-results>=2.6.0
|
|
11
|
+
Requires-Dist: django-guardian>=3.3.2
|
|
12
|
+
Requires-Dist: django-redis>=7.0.0
|
|
13
|
+
Requires-Dist: djangorestframework>=3.17.1
|
|
14
|
+
Requires-Dist: gunicorn>=26.0.0
|
|
15
|
+
Requires-Dist: pyjwt[crypto]>=2.10.0
|
|
16
|
+
Requires-Dist: redis>=8.0.0
|
|
17
|
+
Requires-Dist: supabase>=2.31.0
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Django Supabase
|
|
22
|
+
|
|
23
|
+
Professional, production-ready Supabase integration for Django.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- 🔐 **Authentication**: Seamless Supabase Auth + Django User model sync
|
|
28
|
+
- 💾 **Database**: Direct PostgreSQL connection with Django ORM
|
|
29
|
+
- 📦 **Storage**: Django storage backends for Supabase Storage
|
|
30
|
+
- 🚀 **Production-Ready**: Connection pooling, caching, error handling
|
|
31
|
+
- 🔧 **Django-Native**: Uses Django's ecosystem and patterns
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install django-supabase
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Setup
|
|
40
|
+
### Add to INSTALLED_APPS:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
INSTALLED_APPS = [
|
|
44
|
+
# ...
|
|
45
|
+
'django_supabase',
|
|
46
|
+
'django_supabase.auth',
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configure Settings
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
SUPABASE_URL = 'https://your-project.supabase.co'
|
|
54
|
+
SUPABASE_KEY = 'your-publishable-or-anon-key'
|
|
55
|
+
SUPABASE_SECRET_KEY = 'your-secret-or-service-role-key' # server-side only
|
|
56
|
+
|
|
57
|
+
# Recommended default: ask Supabase Auth to validate bearer tokens.
|
|
58
|
+
SUPABASE_JWT_VERIFICATION = 'auth_server'
|
|
59
|
+
|
|
60
|
+
# Optional for projects using asymmetric JWT signing keys:
|
|
61
|
+
# SUPABASE_JWT_VERIFICATION = 'jwks'
|
|
62
|
+
# SUPABASE_JWT_ALGORITHMS = ['RS256', 'ES256']
|
|
63
|
+
|
|
64
|
+
# Legacy/local-only fallback for HS256 shared-secret verification:
|
|
65
|
+
# SUPABASE_JWT_VERIFICATION = 'jwt_secret'
|
|
66
|
+
# SUPABASE_JWT_SECRET = 'your-jwt-secret'
|
|
67
|
+
|
|
68
|
+
AUTH_USER_MODEL = 'supabase_auth.SupabaseUser'
|
|
69
|
+
|
|
70
|
+
AUTHENTICATION_BACKENDS = [
|
|
71
|
+
'django_supabase.auth.backends.SupabaseAuthBackend',
|
|
72
|
+
'django.contrib.auth.backends.ModelBackend',
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
DATABASES = {
|
|
76
|
+
'default': {
|
|
77
|
+
'ENGINE': 'django_supabase.db.engine.SupabasePostgreSQLEngine',
|
|
78
|
+
'NAME': 'postgres',
|
|
79
|
+
'USER': 'postgres',
|
|
80
|
+
'PASSWORD': 'your-password',
|
|
81
|
+
'HOST': 'db.your-project.supabase.co',
|
|
82
|
+
'PORT': 5432,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
STORAGES = {
|
|
87
|
+
'default': {
|
|
88
|
+
'BACKEND': 'django_supabase.storage.backends.SupabaseMediaStorage',
|
|
89
|
+
},
|
|
90
|
+
'staticfiles': {
|
|
91
|
+
'BACKEND': 'django_supabase.storage.backends.SupabaseStaticStorage',
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Run Migrations
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python manage.py migrate
|
|
100
|
+
python manage.py create_storage_buckets
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
# Usage Example
|
|
104
|
+
## Authentication
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from django.contrib.auth import authenticate
|
|
108
|
+
|
|
109
|
+
# Email/Password login
|
|
110
|
+
user = authenticate(email='user@example.com', password='password')
|
|
111
|
+
|
|
112
|
+
# JWT Token authentication (in views)
|
|
113
|
+
# Token automatically extracted from Authorization: Bearer <token>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Frontend OAuth
|
|
117
|
+
|
|
118
|
+
Use Supabase Auth in the frontend with a publishable key. After OAuth login,
|
|
119
|
+
send the Supabase session access token to Django:
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
123
|
+
provider: 'google',
|
|
124
|
+
options: { redirectTo: 'https://your-frontend.example.com/auth/callback' },
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const { data: sessionData } = await supabase.auth.getSession()
|
|
128
|
+
|
|
129
|
+
await fetch('/api/me/', {
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer ${sessionData.session.access_token}`,
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Django validates the bearer token through `SupabaseAuthMiddleware` and syncs the
|
|
137
|
+
local user by Supabase `sub` / `user.id`, not by email alone.
|
|
138
|
+
|
|
139
|
+
## File Upload
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from django_supabase.storage.utils import upload_file_to_supabase
|
|
143
|
+
|
|
144
|
+
file = ...
|
|
145
|
+
|
|
146
|
+
url = upload_file_to_supabase(file, 'media', path='uploads/')
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Direct Supabase Queries
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from django_supabase.db.client import get_supabase_client
|
|
154
|
+
|
|
155
|
+
supabase = get_supabase_client()
|
|
156
|
+
response = supabase.table('posts').select('*').execute()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Test
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# supabase_integration/tests.py
|
|
163
|
+
from django.test import TestCase
|
|
164
|
+
from django.contrib.auth import get_user_model
|
|
165
|
+
from django_supabase.db.client import get_supabase_client
|
|
166
|
+
|
|
167
|
+
User = get_user_model()
|
|
168
|
+
|
|
169
|
+
class SupabaseIntegrationTestCase(TestCase):
|
|
170
|
+
"""Base test case with Supabase utilities"""
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def setUpClass(cls):
|
|
174
|
+
super().setUpClass()
|
|
175
|
+
cls.supabase = get_supabase_client()
|
|
176
|
+
|
|
177
|
+
def create_test_user(self, email='test@example.com'):
|
|
178
|
+
"""Helper to create test users"""
|
|
179
|
+
return User.objects.create_user(
|
|
180
|
+
username=email,
|
|
181
|
+
email=email,
|
|
182
|
+
password='testpass123'
|
|
183
|
+
)
|
|
184
|
+
```
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Django Supabase
|
|
2
|
+
|
|
3
|
+
Professional, production-ready Supabase integration for Django.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 **Authentication**: Seamless Supabase Auth + Django User model sync
|
|
8
|
+
- 💾 **Database**: Direct PostgreSQL connection with Django ORM
|
|
9
|
+
- 📦 **Storage**: Django storage backends for Supabase Storage
|
|
10
|
+
- 🚀 **Production-Ready**: Connection pooling, caching, error handling
|
|
11
|
+
- 🔧 **Django-Native**: Uses Django's ecosystem and patterns
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install django-supabase
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Setup
|
|
20
|
+
### Add to INSTALLED_APPS:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
INSTALLED_APPS = [
|
|
24
|
+
# ...
|
|
25
|
+
'django_supabase',
|
|
26
|
+
'django_supabase.auth',
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configure Settings
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
SUPABASE_URL = 'https://your-project.supabase.co'
|
|
34
|
+
SUPABASE_KEY = 'your-publishable-or-anon-key'
|
|
35
|
+
SUPABASE_SECRET_KEY = 'your-secret-or-service-role-key' # server-side only
|
|
36
|
+
|
|
37
|
+
# Recommended default: ask Supabase Auth to validate bearer tokens.
|
|
38
|
+
SUPABASE_JWT_VERIFICATION = 'auth_server'
|
|
39
|
+
|
|
40
|
+
# Optional for projects using asymmetric JWT signing keys:
|
|
41
|
+
# SUPABASE_JWT_VERIFICATION = 'jwks'
|
|
42
|
+
# SUPABASE_JWT_ALGORITHMS = ['RS256', 'ES256']
|
|
43
|
+
|
|
44
|
+
# Legacy/local-only fallback for HS256 shared-secret verification:
|
|
45
|
+
# SUPABASE_JWT_VERIFICATION = 'jwt_secret'
|
|
46
|
+
# SUPABASE_JWT_SECRET = 'your-jwt-secret'
|
|
47
|
+
|
|
48
|
+
AUTH_USER_MODEL = 'supabase_auth.SupabaseUser'
|
|
49
|
+
|
|
50
|
+
AUTHENTICATION_BACKENDS = [
|
|
51
|
+
'django_supabase.auth.backends.SupabaseAuthBackend',
|
|
52
|
+
'django.contrib.auth.backends.ModelBackend',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
DATABASES = {
|
|
56
|
+
'default': {
|
|
57
|
+
'ENGINE': 'django_supabase.db.engine.SupabasePostgreSQLEngine',
|
|
58
|
+
'NAME': 'postgres',
|
|
59
|
+
'USER': 'postgres',
|
|
60
|
+
'PASSWORD': 'your-password',
|
|
61
|
+
'HOST': 'db.your-project.supabase.co',
|
|
62
|
+
'PORT': 5432,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
STORAGES = {
|
|
67
|
+
'default': {
|
|
68
|
+
'BACKEND': 'django_supabase.storage.backends.SupabaseMediaStorage',
|
|
69
|
+
},
|
|
70
|
+
'staticfiles': {
|
|
71
|
+
'BACKEND': 'django_supabase.storage.backends.SupabaseStaticStorage',
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Run Migrations
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python manage.py migrate
|
|
80
|
+
python manage.py create_storage_buckets
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
# Usage Example
|
|
84
|
+
## Authentication
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from django.contrib.auth import authenticate
|
|
88
|
+
|
|
89
|
+
# Email/Password login
|
|
90
|
+
user = authenticate(email='user@example.com', password='password')
|
|
91
|
+
|
|
92
|
+
# JWT Token authentication (in views)
|
|
93
|
+
# Token automatically extracted from Authorization: Bearer <token>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Frontend OAuth
|
|
97
|
+
|
|
98
|
+
Use Supabase Auth in the frontend with a publishable key. After OAuth login,
|
|
99
|
+
send the Supabase session access token to Django:
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
103
|
+
provider: 'google',
|
|
104
|
+
options: { redirectTo: 'https://your-frontend.example.com/auth/callback' },
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const { data: sessionData } = await supabase.auth.getSession()
|
|
108
|
+
|
|
109
|
+
await fetch('/api/me/', {
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${sessionData.session.access_token}`,
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Django validates the bearer token through `SupabaseAuthMiddleware` and syncs the
|
|
117
|
+
local user by Supabase `sub` / `user.id`, not by email alone.
|
|
118
|
+
|
|
119
|
+
## File Upload
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from django_supabase.storage.utils import upload_file_to_supabase
|
|
123
|
+
|
|
124
|
+
file = ...
|
|
125
|
+
|
|
126
|
+
url = upload_file_to_supabase(file, 'media', path='uploads/')
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Direct Supabase Queries
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from django_supabase.db.client import get_supabase_client
|
|
134
|
+
|
|
135
|
+
supabase = get_supabase_client()
|
|
136
|
+
response = supabase.table('posts').select('*').execute()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Test
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# supabase_integration/tests.py
|
|
143
|
+
from django.test import TestCase
|
|
144
|
+
from django.contrib.auth import get_user_model
|
|
145
|
+
from django_supabase.db.client import get_supabase_client
|
|
146
|
+
|
|
147
|
+
User = get_user_model()
|
|
148
|
+
|
|
149
|
+
class SupabaseIntegrationTestCase(TestCase):
|
|
150
|
+
"""Base test case with Supabase utilities"""
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def setUpClass(cls):
|
|
154
|
+
super().setUpClass()
|
|
155
|
+
cls.supabase = get_supabase_client()
|
|
156
|
+
|
|
157
|
+
def create_test_user(self, email='test@example.com'):
|
|
158
|
+
"""Helper to create test users"""
|
|
159
|
+
return User.objects.create_user(
|
|
160
|
+
username=email,
|
|
161
|
+
email=email,
|
|
162
|
+
password='testpass123'
|
|
163
|
+
)
|
|
164
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-supabase"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "A lightweight django package designed to connect Django applications to Supabase Auth, Database Access, and Storage"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Lakan", email = "leydotpy.dev@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"celery>=5.6.3",
|
|
12
|
+
"django>=6.0.6",
|
|
13
|
+
"django-celery-beat>=2.9.0",
|
|
14
|
+
"django-celery-results>=2.6.0",
|
|
15
|
+
"django-guardian>=3.3.2",
|
|
16
|
+
"django-redis>=7.0.0",
|
|
17
|
+
"djangorestframework>=3.17.1",
|
|
18
|
+
"gunicorn>=26.0.0",
|
|
19
|
+
"pyjwt[crypto]>=2.10.0",
|
|
20
|
+
"redis>=8.0.0",
|
|
21
|
+
"supabase>=2.31.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["uv_build>=0.9.7,<0.10.0"]
|
|
26
|
+
build-backend = "uv_build"
|
|
@@ -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,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
|
+
|