diff --git a/README.md b/README.md index ec03ca4..388086c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ Demo Django application which has HTTP endpoints to authorize a PowerSync enabled application to sync data between a client device and a Postgres database. This repo compliments the [PowerSync + Django React Native Demo: Todo List](https://github.com/powersync-ja/powersync-js/tree/main/demos/django-react-native-todolist) +## Note: +This demo backend uses ngrok to expose the JWKS endpoint to PowerSync Cloud. Ngrok has been progressively making their free version harder to use. Specifically, they now inject an interstitial warning page that requires a paid plan to remove. Therefore this demo app currently requires a paid ngrok plan. This demo will move to using the self-hosted PowerSync Open Edition in the future. + ## Requirements This app needs a Postgres instance that is hosted. For a free version for testing/demo purposes, visit [Supabase](https://supabase.com/). @@ -39,10 +42,12 @@ python manage.py makemigrations python manage.py migrate ``` +Note that one of the migrations creates a test user in the `auth_user` table - you can use it to log into your frontend app. Take note of the user's id and update the hard coded id in the `upload_data` endpoint of `api/views.py` to match this user's id. In production you'd typically want to authenticate the user on this endpoint (using whatever auth mechanism you already have in place) before signing a JWT for use with PowerSync. See an example [here](https://github.com/powersync-ja/powersync-jwks-example/blob/151adf17611bef8a60d9e6cc490827adc4612da9/supabase/functions/powersync-auth/index.ts#L22) + 6. Run the following SQL statement on your Postgres database: ```sql -create publication powersync for table api_list, api_todo; +create publication powersync for table lists, todos; ``` ## Start App @@ -86,7 +91,9 @@ Connections ttl opn rt1 rt5 p50 p90 1957 0 0.04 0.03 0.01 89.93 ``` -3. Open the PowerSync Dashboard and paste the `Forwarding` url starting with HTTPS into the Credentials tab of your PowerSync instance e.g. +3. Update ALLOWED_HOSTS in `todo_list_custom_backend/settings.py` to include your ngrok forwarding address e.g. `http://your_id.ngrok-free.app`, then restart your Django app. + +4. Open the PowerSync Dashboard and paste the `Forwarding` url starting with HTTPS into the Credentials tab of your PowerSync instance e.g. ``` JWKS URI @@ -95,4 +102,4 @@ https://your_id.ngrok-free.app/api/get_keys/ Pay special attention to the URL, it should include the `/api/get_keys/` path as this is used by the PowerSync server to validate tokens and the demo will not work without it. -4. Update the `AppConfig.ts` if you're using the [PowerSync + Django React Native Demo: Todo List](https://github.com/powersync-ja/powersync-js/tree/main/demos/django-react-native-todolist) and set the `djangoUrl` value. +5. Update the `AppConfig.ts` if you're using the [PowerSync + Django React Native Demo: Todo List](https://github.com/powersync-ja/powersync-js/tree/main/demos/django-react-native-todolist) and set the `djangoUrl` value. diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index a59a246..dec92ed 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.6 on 2023-10-10 17:13 +# Generated by Django 4.2.6 on 2024-06-28 06:17 from django.db import migrations, models @@ -12,25 +12,31 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='List', + name='Lists', fields=[ ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('name', models.CharField(max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('name', models.CharField(max_length=100, null=True)), ('owner_id', models.CharField(max_length=36)), ], + options={ + 'db_table': 'lists', + }, ), migrations.CreateModel( - name='Todo', + name='Todos', fields=[ ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), ('completed_at', models.DateTimeField(blank=True, null=True)), - ('description', models.TextField()), + ('description', models.TextField(null=True)), ('completed', models.BooleanField(default=False)), - ('created_by', models.CharField(max_length=36)), + ('created_by', models.CharField(max_length=36, null=True)), ('completed_by', models.CharField(blank=True, max_length=36, null=True)), - ('list_id', models.CharField(max_length=36)), + ('list_id', models.CharField(max_length=36, null=True)), ], + options={ + 'db_table': 'todos', + }, ), ] diff --git a/api/migrations/0002_alter_todo_description.py b/api/migrations/0002_alter_todo_description.py deleted file mode 100644 index 17d10c9..0000000 --- a/api/migrations/0002_alter_todo_description.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.6 on 2023-10-10 17:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='todo', - name='description', - field=models.TextField(null=True), - ), - ] diff --git a/api/migrations/0002_create_test_user.py b/api/migrations/0002_create_test_user.py new file mode 100644 index 0000000..9b3b7a3 --- /dev/null +++ b/api/migrations/0002_create_test_user.py @@ -0,0 +1,19 @@ +from django.db import migrations +from django.contrib.auth import get_user_model + +def create_test_user(apps, schema_editor): + User = get_user_model() + User.objects.create_user( + username='testuser', + password='testpassword' + ) + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_test_user), + ] \ No newline at end of file diff --git a/api/migrations/0003_alter_todo_created_by.py b/api/migrations/0003_alter_todo_created_by.py deleted file mode 100644 index dd5f034..0000000 --- a/api/migrations/0003_alter_todo_created_by.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.6 on 2023-10-10 17:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0002_alter_todo_description'), - ] - - operations = [ - migrations.AlterField( - model_name='todo', - name='created_by', - field=models.CharField(max_length=36, null=True), - ), - ] diff --git a/api/migrations/0004_alter_todo_list_id.py b/api/migrations/0004_alter_todo_list_id.py deleted file mode 100644 index 6e2d159..0000000 --- a/api/migrations/0004_alter_todo_list_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.6 on 2023-10-10 17:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0003_alter_todo_created_by'), - ] - - operations = [ - migrations.AlterField( - model_name='todo', - name='list_id', - field=models.CharField(max_length=36, null=True), - ), - ] diff --git a/api/migrations/0005_alter_list_created_at.py b/api/migrations/0005_alter_list_created_at.py deleted file mode 100644 index 3456813..0000000 --- a/api/migrations/0005_alter_list_created_at.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.6 on 2023-10-10 17:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0004_alter_todo_list_id'), - ] - - operations = [ - migrations.AlterField( - model_name='list', - name='created_at', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - ] diff --git a/api/migrations/0006_alter_list_name_alter_todo_created_at.py b/api/migrations/0006_alter_list_name_alter_todo_created_at.py deleted file mode 100644 index 9787f76..0000000 --- a/api/migrations/0006_alter_list_name_alter_todo_created_at.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.6 on 2023-10-10 18:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0005_alter_list_created_at'), - ] - - operations = [ - migrations.AlterField( - model_name='list', - name='name', - field=models.CharField(max_length=100, null=True), - ), - migrations.AlterField( - model_name='todo', - name='created_at', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - ] diff --git a/api/models.py b/api/models.py index 752b761..b229499 100644 --- a/api/models.py +++ b/api/models.py @@ -1,15 +1,17 @@ from django.db import models -class List(models.Model): +class Lists(models.Model): id = models.CharField(max_length=36, primary_key=True) # Use CharField for 'id' as text created_at = models.DateTimeField(auto_now_add=True, null=True) # Use DateTimeField for 'created_at' name = models.CharField(max_length=100, null=True) # Use CharField for 'name' as text owner_id = models.CharField(max_length=36) # Use CharField for 'owner_id' as text + class Meta: + db_table = 'lists' # Custom table name without app prefix def __str__(self): return self.name -class Todo(models.Model): +class Todos(models.Model): id = models.CharField(max_length=36, primary_key=True) # Use CharField for 'id' as text created_at = models.DateTimeField(auto_now_add=True, null=True) # Use DateTimeField for 'created_at' completed_at = models.DateTimeField(null=True, blank=True) # Use DateTimeField for 'completed_at' @@ -19,5 +21,7 @@ class Todo(models.Model): completed_by = models.CharField(max_length=36, null=True, blank=True) # Use CharField for 'completed_by' as text list_id = models.CharField(max_length=36, null=True) + class Meta: + db_table = 'todos' # Custom table name without app prefix def __str__(self): return self.description \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index a578572..0ab5563 100644 --- a/api/urls.py +++ b/api/urls.py @@ -2,7 +2,7 @@ from . import views urlpatterns = [ - path('get_token/', views.get_token, name='get_token'), + path('get_powersync_token/', views.get_powersync_token, name='get_powersync_token'), path('get_keys/', views.get_keys, name='get_keys'), path('get_session/', views.get_session, name='get_session'), path('auth/', views.auth, name='auth'), diff --git a/api/views.py b/api/views.py index eba51f0..29fa588 100644 --- a/api/views.py +++ b/api/views.py @@ -6,7 +6,7 @@ from rest_framework.decorators import api_view from rest_framework.response import Response import api.app_utils as app_utils -from .models import Todo, List +from .models import Todos, Lists @api_view(['GET']) def get_token(request): @@ -21,7 +21,6 @@ def get_token(request): }, status=200) except Exception as e: return JsonResponse({"error": str(e)}, status=500) - @api_view(['GET']) def get_keys(request): try: @@ -52,20 +51,29 @@ def auth(request): data = json.loads(request.body.decode('utf-8')) username = data.get('username') password = data.get('password') - user = User.objects.get(username=username, password=password) - if user is not None: - token = app_utils.create_jwt_token(user.id) - response = {'access_token': token} - return JsonResponse(response, status=200) - else: - return JsonResponse({'message': 'Authentication failed'}, status=401) + try: + user = authenticate(username=username, password=password) + if user is not None: + token = app_utils.create_jwt_token(user.id) + response = {'access_token': token} + return JsonResponse(response, status=200) + else: + logger.warning(f"Authentication failed for username: {username}") + return JsonResponse({'message': 'Authentication failed'}, status=401) + + except json.JSONDecodeError as e: + logger.error(f"JSON decode error: {e}") + return JsonResponse({'message': 'Invalid JSON'}, status=400) + except Exception as e: + logger.error(f"Unexpected error: {e}") + return JsonResponse({'message': 'Internal server error'}, status=500) @api_view(['PUT', 'PATCH', 'DELETE']) def upload_data(request): op = json.loads(request.body.decode('utf-8')) data = op.get('data') print(op) - if op.get('table') == 'api_todo': + if op.get('table') == 'todos': if request.method == 'PUT': upsertTodo(data) return Response({'message': 'Todo updated'}, status=200) @@ -74,12 +82,12 @@ def upload_data(request): return HttpResponse({'message': 'Todo updated'}, status=200) elif request.method == 'DELETE': try: - todo = Todo.objects.get(id=data.get('id')) + todo = Todos.objects.get(id=data.get('id')) todo.delete() return HttpResponse({'message': 'Todo deleted'}, status=200) - except Todo.DoesNotExist: + except Todos.DoesNotExist: return HttpResponse({'message': 'Todo does not exist'}, status=404) - elif op.get('table') == 'api_list': + elif op.get('table') == 'lists': if request.method == 'PUT': upsertList(data) return Response({'message': 'List created'}, status=200) @@ -88,45 +96,54 @@ def upload_data(request): return HttpResponse({'message': 'List updated'}, status=200) elif request.method == 'DELETE': try: - list = List.objects.get(id=data.get('id')) + list = Lists.objects.get(id=data.get('id')) list.delete() return HttpResponse({'message': 'List deleted'}, status=200) - except List.DoesNotExist: + except Lists.DoesNotExist: return HttpResponse({'message': 'List does not exist'}, status=404) def upsertTodo(data): try: - todo = Todo.objects.get(id=data.get('id')) + todo = Todos.objects.get(id=data.get('id')) todo.description = data.get('description') todo.created_by = data.get('created_by') todo.list_id = data.get('list_id') todo.save() - except Todo.DoesNotExist: - todo = Todo(id=data.get('id'), description=data.get('description'), created_by=data.get('created_by'), list_id=data.get('list_id')) + except Todos.DoesNotExist: + todo = Todos(id=data.get('id'), description=data.get('description'), created_by=data.get('created_by'), list_id=data.get('list_id')) todo.save() def updateTodo(data): - todo = Todo.objects.get(id=data.get('id')) + todo = Todos.objects.get(id=data.get('id')) if todo is not None: - todo.description = data.get('description') - todo.created_by = data.get('created_by') - todo.list_id = data.get('list_id') + if 'description' in data: + todo.description = data.get('description') + if 'created_by' in data: + todo.created_by = data.get('created_by') + if 'list_id' in data: + todo.list_id = data.get('list_id') + if 'completed' in data: + todo.completed = data.get('completed') + if 'completed_by' in data: + todo.completed_by = data.get('completed_by') + if 'completed_at' in data: + todo.completed_at = data.get('completed_at') todo.save() def upsertList(data): try: - list = List.objects.get(id=data.get('id')) + list = Lists.objects.get(id=data.get('id')) list.created_at = data.get('created_at') list.name = data.get('name') list.owner_id = data.get('owner_id') list.save() return Response({'message': 'List updated'}, status=200) - except List.DoesNotExist: - list = List(id=data.get('id'), created_at=data.get('created_at'), name=data.get('name'), owner_id=data.get('owner_id')) + except Lists.DoesNotExist: + list = Lists(id=data.get('id'), created_at=data.get('created_at'), name=data.get('name'), owner_id=data.get('owner_id')) list.save() def updateList(data): - list = List.objects.get(id=data.get('id')) + list = Lists.objects.get(id=data.get('id')) if list is not None: list.created_at = data.get('created_at') list.name = data.get('name')