Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POWERSYNC prototype: add migration to create postgresql publication #1747

Draft
wants to merge 17 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions extras/docker/development/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# Installs some additional packages needed for development
#
# Note that this dockerfile is built from the corresponding docker-compose file
#

FROM wger/server:latest

USER root

WORKDIR /home/wger/src
RUN apt-get update && \
apt-get install -y \
git \
vim \
yarnpkg \
sassc

COPY ../../../requirements.txt /tmp/requirements.txt
COPY ../../../requirements_dev.txt /tmp/requirements_dev.txt

RUN ln -s /usr/bin/yarnpkg /usr/bin/yarn \
&& ln -s /usr/bin/sassc /usr/bin/sass

USER wger
RUN pip3 install --break-system-packages --user -r /tmp/requirements.txt \
&& pip3 install --break-system-packages --user -r /tmp/requirements_dev.txt
7 changes: 7 additions & 0 deletions extras/docker/production/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,10 @@
#
EXPOSE_PROMETHEUS_METRICS = env.bool('EXPOSE_PROMETHEUS_METRICS', False)
PROMETHEUS_URL_PATH = env.str('PROMETHEUS_URL_PATH', 'super-secret-path')

#
# PowerSync configuration
#
POWERSYNC_JWKS_PUBLIC_KEY = env.str('POWERSYNC_JWKS_PUBLIC_KEY', '')
POWERSYNC_JWKS_PRIVATE_KEY = env.str('POWERSYNC_JWKS_PRIVATE_KEY', '')
POWERSYNC_URL = env.str('POWERSYNC_URL', 'http://powersync:8080')
Empty file modified manage.py
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ tzdata==2024.2
django-cors-headers==4.4.0
django-filter==24.3
djangorestframework==3.15.2
djangorestframework-simplejwt[crypto]==5.3.1
djangorestframework-simplejwt[crypto,python-jose]==5.3.1

# Not used anymore, but needed because some modules are imported in DB migration
# files
Expand Down
79 changes: 77 additions & 2 deletions wger/core/api/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-

# This file is part of wger Workout Manager.
#
# wger Workout Manager is free software: you can redistribute it and/or modify
Expand All @@ -16,11 +15,18 @@
# along with Workout Manager. If not, see <http://www.gnu.org/licenses/>.

# Standard Library
import json
import logging
import time
from base64 import urlsafe_b64decode

# Django
from django.conf import settings
from django.contrib.auth.models import User
from django.http import (
HttpResponseForbidden,
JsonResponse,
)
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

Expand All @@ -33,11 +39,17 @@
extend_schema,
inline_serializer,
)
from jose.constants import ALGORITHMS
from jose.exceptions import JWKError
from jose.jwt import encode
from rest_framework import (
status,
viewsets,
)
from rest_framework.decorators import action
from rest_framework.decorators import (
action,
api_view,
)
from rest_framework.fields import (
BooleanField,
CharField,
Expand All @@ -47,6 +59,7 @@
IsAuthenticated,
)
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import AccessToken

# wger
from wger import (
Expand Down Expand Up @@ -403,3 +416,65 @@
serializer_class = RoutineWeightUnitSerializer
ordering_fields = '__all__'
filterset_fields = ('name',)


def create_jwt_token(user_id):
power_sync_private_key_bytes = urlsafe_b64decode(settings.POWERSYNC_JWKS_PRIVATE_KEY)
power_sync_private_key_json = json.loads(power_sync_private_key_bytes.decode('utf-8'))

try:
jwt_header = {
'alg': power_sync_private_key_json['alg'],
'kid': power_sync_private_key_json['kid'],
}

jwt_payload = {
'sub': user_id,
'iat': time.time(),
'aud': 'powersync',
'exp': int(time.time()) + 300, # 5 minutes expiration
}

token = encode(
jwt_payload, power_sync_private_key_json, algorithm=ALGORITHMS.RS256, headers=jwt_header
)

return token

except (JWKError, ValueError, KeyError) as e:
raise Exception(f'Error creating JWT token: {str(e)}')


@api_view()
def get_powersync_token(request):
if not request.user.is_authenticated:
return HttpResponseForbidden()

token = create_jwt_token(request.user.id)

try:
return JsonResponse({'token': token, 'powersync_url': settings.POWERSYNC_URL}, status=200)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix AI 7 days ago

To fix the problem, we need to ensure that detailed exception messages are not exposed to the end user. Instead, we should log the detailed error message on the server and return a generic error message to the client. This can be achieved by modifying the exception handling in the get_powersync_token function.

  1. Import the logging module if not already imported.
  2. Replace the line that returns the detailed error message with a line that logs the error and returns a generic error message.
Suggested changeset 1
wger/core/api/views.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/wger/core/api/views.py b/wger/core/api/views.py
--- a/wger/core/api/views.py
+++ b/wger/core/api/views.py
@@ -457,3 +457,4 @@
     except Exception as e:
-        return JsonResponse({'error': str(e)}, status=500)
+        logging.error(f'Error in get_powersync_token: {str(e)}')
+        return JsonResponse({'error': 'An internal error has occurred.'}, status=500)
 
EOF
@@ -457,3 +457,4 @@
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
logging.error(f'Error in get_powersync_token: {str(e)}')
return JsonResponse({'error': 'An internal error has occurred.'}, status=500)

Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options


@api_view()
def get_powersync_keys(request):
power_sync_public_key_bytes = urlsafe_b64decode(settings.POWERSYNC_JWKS_PUBLIC_KEY)

return JsonResponse(
{'keys': [json.loads(power_sync_public_key_bytes.decode('utf-8'))]},
status=200,
)


@api_view()
def upload_powersync_data(request):
if not request.user.is_authenticated:
return HttpResponseForbidden()

logger.debug(request.POST)
return JsonResponse(
{'ok!'},
status=200,
)
48 changes: 48 additions & 0 deletions wger/core/migrations/0018_create_publication_add_ivm_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 4.2.13 on 2024-09-19 13:43

from django.db import migrations

from wger.utils.db import postgres_only


@postgres_only
def add_publication(apps, schema_editor):
# Note that "FOR ALL TABLES" applies for all tables created in the future as well
schema_editor.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_publication WHERE pubname = 'powersync'
) THEN
CREATE PUBLICATION powersync FOR ALL TABLES;
END IF;
END $$;
"""
)


@postgres_only
def remove_publication(apps, schema_editor):
schema_editor.execute('DROP PUBLICATION IF EXISTS powersync;')


@postgres_only
def add_ivm_extension(apps, schema_editor):
schema_editor.execute('CREATE EXTENSION IF NOT EXISTS pg_ivm;')


@postgres_only
def remove_ivm_extension(apps, schema_editor):
schema_editor.execute('DROP EXTENSION IF EXISTS pg_ivm;')


class Migration(migrations.Migration):
dependencies = [
('core', '0017_language_full_name_en'),
]

operations = [
migrations.RunPython(add_publication, reverse_code=remove_publication),
migrations.RunPython(add_ivm_extension, reverse_code=remove_ivm_extension),
]
122 changes: 122 additions & 0 deletions wger/nutrition/migrations/0025_add_uuids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Generated by Django 4.2.16 on 2024-10-19 21:04

from django.db import migrations, models
import uuid


def gen_uuids(apps, schema_editor):
NutritionPlan = apps.get_model('nutrition', 'NutritionPlan')
Meal = apps.get_model('nutrition', 'Meal')
MealItem = apps.get_model('nutrition', 'MealItem')
LogItem = apps.get_model('nutrition', 'LogItem')

for item in LogItem.objects.all():
item.uuid = uuid.uuid4()
item.save(update_fields=['uuid'])

for item in MealItem.objects.all():
item.uuid = uuid.uuid4()
item.save(update_fields=['uuid'])

for meal in Meal.objects.all():
meal.uuid = uuid.uuid4()
meal.save(update_fields=['uuid'])

for plan in NutritionPlan.objects.all():
plan.uuid = uuid.uuid4()
plan.save(update_fields=['uuid'])


class Migration(migrations.Migration):
dependencies = [
('nutrition', '0024_remove_ingredient_status'),
]

operations = [
migrations.AddField(
model_name='nutritionplan',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
null=True,
unique=False,
),
),
migrations.AddField(
model_name='meal',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
null=True,
unique=False,
),
),
migrations.AddField(
model_name='mealitem',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
null=True,
unique=False,
),
),
migrations.AddField(
model_name='logitem',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
null=True,
unique=False,
),
),
# Generate UUIDs
migrations.RunPython(
gen_uuids,
reverse_code=migrations.RunPython.noop,
),
# Set uuid fields to non-nullable
migrations.AlterField(
model_name='nutritionplan',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
null=False,
unique=True,
),
),
migrations.AlterField(
model_name='meal',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
null=False,
unique=True,
),
),
migrations.AlterField(
model_name='mealitem',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
null=False,
unique=True,
),
),
migrations.AlterField(
model_name='logitem',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
null=False,
unique=True,
),
),
]
Loading
Loading