From b43a2ffbdce28a53592c1e19be57c3f3ec95001a Mon Sep 17 00:00:00 2001 From: Julian Lobbes Date: Fri, 28 Jul 2023 23:27:12 +0200 Subject: [PATCH] feat: finish user registration flow --- .dockerignore | 2 + README.md | 1 + app/authentication/forms.py | 0 .../authentication/register-continue.html | 7 +- app/authentication/views.py | 71 ++++++++++++++++--- app/core/settings.py | 3 +- app/gotify/api.py | 58 +++++++++++++++ app/medwings/forms.py | 12 ++++ app/withings/api.py | 9 +-- development.docker-compose.yml | 4 ++ 10 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 .dockerignore delete mode 100644 app/authentication/forms.py create mode 100644 app/gotify/api.py create mode 100644 app/medwings/forms.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..827d37b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +**/.venv/ +**/__pycache__/ diff --git a/README.md b/README.md index 708c201..bac1c6b 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ PG_HOST=medwings-postgres PG_PORT=5432 GOTIFY_USER= GOTIFY_PASSWORD= +GOTIFY_HOST=medwings-gotify WITHINGS_CLIENT_ID= WITHINGS_CLIENT_SECRET= ``` diff --git a/app/authentication/forms.py b/app/authentication/forms.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/authentication/templates/authentication/register-continue.html b/app/authentication/templates/authentication/register-continue.html index 129883f..6d31bd1 100644 --- a/app/authentication/templates/authentication/register-continue.html +++ b/app/authentication/templates/authentication/register-continue.html @@ -7,6 +7,11 @@ {% block content %}

Register

-

{{ auth_code }}

+
+ {% csrf_token %} + {{ user_form }} + {{ profile_form }} + +
{% endblock content %} diff --git a/app/authentication/views.py b/app/authentication/views.py index 02275df..ed5c777 100644 --- a/app/authentication/views.py +++ b/app/authentication/views.py @@ -1,13 +1,19 @@ from urllib.parse import urlencode from uuid import uuid4 -from django.shortcuts import render +from django.shortcuts import redirect, render from django.conf import settings from django.urls import reverse from django.core.exceptions import PermissionDenied from django.http import HttpResponseBadRequest +from django.contrib.auth.forms import UserCreationForm +from django.utils.dateparse import parse_datetime import withings.api +import withings.models +import gotify.api +import gotify.models +from medwings.forms import ProfileForm def register_init(request): @@ -36,15 +42,15 @@ def register_init(request): def register_continue(request): - # Parse GET request parameters authorization_code = request.GET.get('code') authorization_state = request.GET.get('state') if not authorization_code: return HttpResponseBadRequest() if not authorization_state: return HttpResponseBadRequest() - if not request.session.get('spoof_protection_token', None) == authorization_state: - return HttpResponseBadRequest() + # TODO enable this when not mocking + # if not request.session.get('spoof_protection_token', None) == authorization_state: + # return HttpResponseBadRequest() # Fetch access and refresh tokens and save them to session storage redirect_uri = request.build_absolute_uri(reverse('register-continue')) @@ -54,13 +60,62 @@ def register_continue(request): return HttpResponseBadRequest() withings.api.save_tokens_to_session(request, response_data) - # TODO add user registration form + if request.method == 'POST': + user_form = UserCreationForm(request.POST) + profile_form = ProfileForm(request.POST) - # TODO once user registration form is valid, make gotify API calls + if user_form.is_valid() and profile_form.is_valid(): + user = user_form.save(commit=False) + profile = profile_form.save(commit=False) + profile.user = user - # TODO once gotify is set up, create and save database objects + user_password = request.POST.get('password1') + gotify_user_info = gotify.api.create_user(user.username, user_password) + gotify_app_info = gotify.api.create_application(user.username, user_password) + gotify_user = gotify.models.GotifyUser( + user=user, + id=gotify_user_info['id'] + ) + gotify_app = gotify.models.GotifyApplication( + user=gotify_user, + id=gotify_app_info['id'], + token=gotify_app_info['token'] + ) - context = {} + withings_api_account = withings.models.ApiAccount( + user=user, + userid=request.session.get('withings_userid') + ) + withings_access_token = withings.models.AccessToken( + account=withings_api_account, + value=request.session.get('withings_access_token'), + expires=parse_datetime(request.session.get('withings_access_token_expiry')) + ) + withings_refresh_token = withings.models.RefreshToken( + account=withings_api_account, + value=request.session.get('withings_refresh_token'), + expires=parse_datetime(request.session.get('withings_refresh_token_expiry')) + ) + + for instance in [ + user, profile, + gotify_user, gotify_app, + withings_api_account, withings_access_token, withings_refresh_token + ]: + instance.save() + + # TODO sync withings health data + # TODO redirect user to some other page and ask them to log in + return redirect('dashboard') + + else: + user_form = UserCreationForm() + profile_form = ProfileForm() + + context = { + 'user_form': user_form, + 'profile_form': profile_form, + } return render(request, 'authentication/register-continue.html', context) diff --git a/app/core/settings.py b/app/core/settings.py index eddc481..b688f75 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -127,9 +127,10 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' WITHINGS_CONFIG = { 'CLIENT_ID': getenv('WITHINGS_CLIENT_ID'), - 'CLIENT_SECRET': getenv('WITHINGS_CLIENT_SECRET'), + 'CLIENT_SECRET': getenv('WITHINGS_CLIENT_SECRET') } GOTIFY_CONFIG = { 'USERNAME': getenv('GOTIFY_USER'), 'PASSWORD': getenv('GOTIFY_PASSWORD'), + 'HOST': getenv('GOTIFY_HOST') } diff --git a/app/gotify/api.py b/app/gotify/api.py new file mode 100644 index 0000000..766e9fa --- /dev/null +++ b/app/gotify/api.py @@ -0,0 +1,58 @@ +import requests +from requests.auth import HTTPBasicAuth + +from django.conf import settings +from .models import GotifyUser + + +def create_user(username: str, password: str) -> dict: + """Creates a user on the Gotify instance. + + :param username: The name of the user to be created. + :param password: The password of the user to be created. + """ + + data = { + 'admin': False, + 'name': username, + 'pass': password + } + + response = requests.post( + url=f"http://{settings.GOTIFY_CONFIG['HOST']}/user", + auth=HTTPBasicAuth( + settings.GOTIFY_CONFIG['USERNAME'], + settings.GOTIFY_CONFIG['PASSWORD'], + ), + json=data + ) + + if response is not None: + response.raise_for_status() + + return response.json() + + +def create_application(username: str, password: str) -> dict: + """Creates an application on the Gotify instance. + + :param username: The user for whom an application will be created. + :param password: The user's password. + """ + + data = { + 'defaultPriority': 6, + 'description': 'A remote patient health monitoring system.', + 'name': 'Medwings' + } + + response = requests.post( + url=f"http://{settings.GOTIFY_CONFIG['HOST']}/application", + auth=HTTPBasicAuth(username, password), + json=data + ) + + if response is not None: + response.raise_for_status() + + return response.json() diff --git a/app/medwings/forms.py b/app/medwings/forms.py new file mode 100644 index 0000000..9f18171 --- /dev/null +++ b/app/medwings/forms.py @@ -0,0 +1,12 @@ +from django import forms + +from medwings.models import Profile + + +class ProfileForm(forms.ModelForm): + class Meta: + model = Profile + fields = ['date_of_birth', 'sex'] + widgets = { + 'date_of_birth': forms.DateInput(attrs={'type': 'date'}), + } diff --git a/app/withings/api.py b/app/withings/api.py index b2b16d0..6e95e12 100644 --- a/app/withings/api.py +++ b/app/withings/api.py @@ -1,8 +1,9 @@ -from datetime import datetime, timedelta +from datetime import timedelta from random import randint import requests from django.conf import settings +from django.utils import timezone from urllib.parse import urlencode def fetch_withings_tokens(authorization_code, redirect_uri): @@ -43,6 +44,6 @@ def save_tokens_to_session(request, response_data): request.session['withings_access_token'] = response_data['body']['access_token'] request.session['withings_refresh_token'] = response_data['body']['refresh_token'] - now = datetime.now() - request.session['withings_access_token_expiry'] = now + timedelta(seconds=response_data['body']['expires_in']) - request.session['withings_refresh_token_expiry'] = now + timedelta(days=365) + now = timezone.now() + request.session['withings_access_token_expiry'] = (now + timedelta(seconds=response_data['body']['expires_in'])).isoformat() + request.session['withings_refresh_token_expiry'] = (now + timedelta(days=365)).isoformat() diff --git a/development.docker-compose.yml b/development.docker-compose.yml index fd4dfa6..87ff978 100644 --- a/development.docker-compose.yml +++ b/development.docker-compose.yml @@ -52,6 +52,7 @@ services: WITHINGS_CLIENT_SECRET: ${WITHINGS_CLIENT_SECRET} GOTIFY_USER: ${GOTIFY_USER} GOTIFY_PASSWORD: ${GOTIFY_PASSWORD} + GOTIFY_HOST: ${GOTIFY_HOST} medwings-postgres: image: postgres:alpine container_name: ${PG_HOST} @@ -80,6 +81,9 @@ services: GOTIFY_SERVER_SSL_REDIRECTTOHTTPS: false GOTIFY_DEFAULTUSER_NAME: ${GOTIFY_USER} GOTIFY_DEFAULTUSER_PASS: ${GOTIFY_PASSWORD} + GOTIFY_SERVER_CORS_ALLOWORIGINS: "- \"localhost:8000\"\n- \"medwings.lobbes.dev\"" + GOTIFY_SERVER_CORS_ALLOWMETHODS: "- \"GET\"\n- \"POST\"" + GOTIFY_SERVER_CORS_ALLOWHEADERS: "- \"Authorization\"\n- \"content-type\"" medwings-pgweb: image: sosedoff/pgweb container_name: medwings-pgweb