feat: begin work on signup flow
This commit is contained in:
parent
4dad4cf068
commit
9fe2365831
8
.env
8
.env
@ -1,8 +0,0 @@
|
||||
TIMEZONE=Europe/Berlin
|
||||
PG_NAME=medwings
|
||||
PG_USER=medwings
|
||||
PG_PASSWORD=medwings
|
||||
PG_HOST=medwings-postgres
|
||||
PG_PORT=5432
|
||||
GOTIFY_USER=gotify
|
||||
GOTIFY_PASSWORD=gotify
|
27
README.md
27
README.md
@ -91,10 +91,31 @@ Steps to create a new user's channel on gotify:
|
||||
|
||||
# Deployment
|
||||
|
||||
This section is incomplete.
|
||||
|
||||
1. Build the asset bundle:
|
||||
Build the asset bundle:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
In the root directory, create a file named `.env` and fill it with environment variables containing your access and connection credentials:
|
||||
|
||||
```env
|
||||
TIMEZONE=Europe/Berlin
|
||||
PG_NAME=medwings
|
||||
PG_USER=medwings
|
||||
PG_PASSWORD=<secret>
|
||||
PG_HOST=medwings-postgres
|
||||
PG_PORT=5432
|
||||
GOTIFY_USER=<secret>
|
||||
GOTIFY_PASSWORD=<secret>
|
||||
WITHINGS_CLIENT_ID=<secret>
|
||||
WITHINGS_CLIENT_SECRET=<secret>
|
||||
```
|
||||
|
||||
Substitute each `<secret>` with your information as follows:
|
||||
|
||||
- `PG_PASSWORD`: A random string, at least 32 characters
|
||||
- `GOTIFY_USER`: Can be a username of your choice, for the Gotify server admin user
|
||||
- `GOTIFY_password`: A random string, at least 8 characters
|
||||
- `WITHINGS_CLIENT_ID`: Your Withings Developer API Client ID
|
||||
- `WITHINGS_CLIENT_SECRET`: Your Withings Developer API Client Secret
|
||||
|
@ -0,0 +1,12 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load static %}
|
||||
{% block title %}
|
||||
Medwings | Sign Up
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
|
||||
<h2>Register</h2>
|
||||
<p>{{ auth_code }}</p>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,12 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load static %}
|
||||
{% block title %}
|
||||
Medwings | Sign Up
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
|
||||
<h2>Register</h2>
|
||||
<p>Nothing to see here.</p>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -0,0 +1,21 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load static %}
|
||||
{% block title %}
|
||||
Medwings | Sign Up
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
|
||||
<h2>Register</h2>
|
||||
<p>
|
||||
Something something glad you're signing up.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 items-center call-to-action-box">
|
||||
<p class="font-semibold">To get started, please allow us to access your health data</p>
|
||||
<a class="btn text-lg" href="{{ auth_url }}">Link Withings Account</a>
|
||||
</div>
|
||||
<p>
|
||||
Something something why this is necessary.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -1,8 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("login/", auth_views.LoginView.as_view(template_name="authentication/login.html"), name="login"),
|
||||
path("logout/", auth_views.LogoutView.as_view(template_name="authentication/logout.html"), name="logout"),
|
||||
path("register/init/", views.register_init, name="register-init"),
|
||||
path("register/continue/", views.register_continue, name="register-continue"),
|
||||
path("register/finalize/", views.register_finalize, name="register-finalize"),
|
||||
]
|
||||
|
@ -1,3 +1,71 @@
|
||||
from django.shortcuts import render
|
||||
from urllib.parse import urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
# Create your views here.
|
||||
from django.shortcuts import render
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseBadRequest
|
||||
|
||||
import withings.api
|
||||
|
||||
|
||||
def register_init(request):
|
||||
if request.user.is_authenticated:
|
||||
raise PermissionDenied('You are already registered and logged in.')
|
||||
|
||||
# Generate a unique token and save it for later
|
||||
spoof_protection_token = str(uuid4())
|
||||
request.session['spoof_protection_token'] = spoof_protection_token
|
||||
|
||||
auth_url_base = 'https://account.withings.com/oauth2_user/authorize2'
|
||||
auth_url_params = {
|
||||
'response_type': 'code',
|
||||
'client_id': settings.WITHINGS_CONFIG['CLIENT_ID'],
|
||||
'scope': 'user.metrics,user.activity',
|
||||
'redirect_uri': request.build_absolute_uri(reverse('register-continue')),
|
||||
'state': spoof_protection_token
|
||||
}
|
||||
auth_url = f"{auth_url_base}?{urlencode(auth_url_params)}"
|
||||
|
||||
context = {
|
||||
"auth_url": auth_url
|
||||
}
|
||||
|
||||
return render(request, 'authentication/register-init.html', context)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
# Fetch access and refresh tokens and save them to session storage
|
||||
redirect_uri = request.build_absolute_uri(reverse('register-continue'))
|
||||
# DEBUG use an API mock
|
||||
response_data = withings.api.mock_fetch_withings_tokens(authorization_code, redirect_uri)
|
||||
if response_data['status'] != 0:
|
||||
return HttpResponseBadRequest()
|
||||
withings.api.save_tokens_to_session(request, response_data)
|
||||
|
||||
# TODO add user registration form
|
||||
|
||||
# TODO once user registration form is valid, make gotify API calls
|
||||
|
||||
# TODO once gotify is set up, create and save database objects
|
||||
|
||||
context = {}
|
||||
|
||||
return render(request, 'authentication/register-continue.html', context)
|
||||
|
||||
|
||||
def register_finalize(request):
|
||||
# TODO implement
|
||||
|
||||
return render(request, 'authentication/register-finalize.html')
|
||||
|
@ -13,24 +13,21 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
from pathlib import Path
|
||||
from os import getenv
|
||||
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-s^q)z%f-7=1h5b00ctki2*-w=#3!k@p-#sq%=eajw)x2axx-e5'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'core',
|
||||
'authentication',
|
||||
@ -44,7 +41,6 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
@ -54,9 +50,7 @@ MIDDLEWARE = [
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'core.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
@ -72,13 +66,11 @@ TEMPLATES = [
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'core.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
@ -93,7 +85,6 @@ DATABASES = {
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
@ -115,25 +106,30 @@ LOGOUT_REDIRECT_URL = 'home'
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = getenv('TZ', 'Europe/Berlin')
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'static',
|
||||
]
|
||||
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
WITHINGS_CONFIG = {
|
||||
'CLIENT_ID': getenv('WITHINGS_CLIENT_ID'),
|
||||
'CLIENT_SECRET': getenv('WITHINGS_CLIENT_SECRET'),
|
||||
}
|
||||
GOTIFY_CONFIG = {
|
||||
'USERNAME': getenv('GOTIFY_USER'),
|
||||
'PASSWORD': getenv('GOTIFY_PASSWORD'),
|
||||
}
|
||||
|
12
app/medwings/templates/medwings/dashboard.html
Normal file
12
app/medwings/templates/medwings/dashboard.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load static %}
|
||||
{% block title %}
|
||||
Medwings | Dashboard
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
|
||||
<h1>Dashboard</h1>
|
||||
<p1>There is nothing here yet.</p1>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -5,7 +5,47 @@
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Welcome to Medwings</h1>
|
||||
<p1>There ain't much 'ere yet...</p1>
|
||||
{% comment %}<img src="{% static 'medwings/images/devices/withings-thermo.webp' %}" alt="A withings thermometer.">{% endcomment %}
|
||||
<div class="flex flex-col justify-center items-center gap-2 py-4 mx-4 max-w-4xl">
|
||||
<h1>Welcome to Medwings</h1>
|
||||
<p1>Your personal health guardian</p1>
|
||||
{% comment %}<img src="{% static 'medwings/images/devices/withings-thermo.webp' %}" alt="A withings thermometer.">{% endcomment %}
|
||||
<p>
|
||||
We understand that after receiving medical care, you may still have concerns about your health, particularly if you're at
|
||||
risk of sudden health changes.
|
||||
That's where we come in.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 items-center call-to-action-box">
|
||||
{% if not request.user.is_authenticated %}
|
||||
<p class="font-semibold">To use the platform, please log in:</p>
|
||||
<a class="btn max-w-fit" href="{% url 'login' %}">Log In</a>
|
||||
<p class="font-semibold">If you do not have an account yet, please register:</p>
|
||||
<a class="btn max-w-fit" href="{% url 'register-init' %}">Create An Account</a>
|
||||
{% else %}
|
||||
<p class="font-semibold">View your latest health data to stay up to date:</p>
|
||||
<a class="btn text-lg" href="{% url 'dashboard' %}">Go to your personal dashboard</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p>
|
||||
Our platform leverages smart medical sensor devices to keep track of your vital signs - such as heart rate,
|
||||
blood pressure, and body temperature - providing you and your healthcare team with a detailed and continuous
|
||||
picture of your health status.
|
||||
</p>
|
||||
<p>
|
||||
Our unique feature is the ability to calculate your Modified Early Warning Score (MEWS) from your vitals data.
|
||||
This system is used widely in healthcare settings to detect early signs of deterioration.
|
||||
Now, it is available for you, right in the comfort of your home or on the go.
|
||||
</p>
|
||||
<p>
|
||||
Prompted by periodic reminders, you'll be asked to take measurements which will be sent automatically to our platform.
|
||||
Here, we calculate your MEWS and generate alerts if we detect an increased risk of health deterioration.
|
||||
</p>
|
||||
<p>
|
||||
While we take care of your monitoring needs, you can enjoy your daily activities with peace of mind, knowing that a
|
||||
dedicated team has your health in their sights.
|
||||
Stay in control of your health with us, your personal health guardian.
|
||||
</p>
|
||||
<p>
|
||||
Welcome aboard!
|
||||
</p>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -4,4 +4,5 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="home"),
|
||||
path("dashboard/", views.dashboard, name="dashboard"),
|
||||
]
|
||||
|
@ -3,3 +3,6 @@ from django.shortcuts import render
|
||||
|
||||
def index(request):
|
||||
return render(request, 'medwings/index.html')
|
||||
|
||||
def dashboard(request):
|
||||
return render(request, 'medwings/dashboard.html')
|
||||
|
@ -1,6 +1,11 @@
|
||||
asgiref==3.7.2
|
||||
certifi==2023.7.22
|
||||
charset-normalizer==3.2.0
|
||||
Django==4.2.3
|
||||
idna==3.4
|
||||
psycopg==3.1.9
|
||||
psycopg-binary==3.1.9
|
||||
requests==2.31.0
|
||||
sqlparse==0.4.4
|
||||
typing_extensions==4.7.1
|
||||
urllib3==2.0.4
|
||||
|
48
app/withings/api.py
Normal file
48
app/withings/api.py
Normal file
@ -0,0 +1,48 @@
|
||||
from datetime import datetime, timedelta
|
||||
from random import randint
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from urllib.parse import urlencode
|
||||
|
||||
def fetch_withings_tokens(authorization_code, redirect_uri):
|
||||
token_url_base = "https://wbsapi.withings.net/v2/oauth2"
|
||||
token_url_params = {
|
||||
'action': 'requesttoken',
|
||||
'client_id': settings.WITHINGS_CONFIG['CLIENT_ID'],
|
||||
'client_secret': settings.WITHINGS_CONFIG['CLIENT_SECRET'],
|
||||
'grant_type': 'authorization_code',
|
||||
'code': authorization_code,
|
||||
'redirect_uri': redirect_uri
|
||||
}
|
||||
token_url = f"{token_url_base}?{urlencode(token_url_params)}"
|
||||
response = requests.get(token_url)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def mock_fetch_withings_tokens(authorization_code, redirect_uri):
|
||||
response = {
|
||||
"status": 0,
|
||||
"body": {
|
||||
"userid": f"{randint(1, 5000)}",
|
||||
"access_token": "a075f8c14fb8df40b08ebc8508533dc332a6910a",
|
||||
"refresh_token": "f631236f02b991810feb774765b6ae8e6c6839ca",
|
||||
"expires_in": 10800,
|
||||
"scope": "user.info,user.metrics",
|
||||
"csrf_token": "PACnnxwHTaBQOzF7bQqwFUUotIuvtzSM",
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
def save_tokens_to_session(request, response_data):
|
||||
request.session['withings_userid'] = response_data['body']['userid']
|
||||
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)
|
@ -7,6 +7,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
font-family: Kanit;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@ -47,6 +48,7 @@ input[type="submit"] {
|
||||
}
|
||||
|
||||
a.btn {
|
||||
text-align: center;
|
||||
@apply rounded rounded-lg drop-shadow-md px-4 py-2;
|
||||
@apply bg-accent-600;
|
||||
@apply font-semibold;
|
||||
@ -54,6 +56,7 @@ a.btn {
|
||||
}
|
||||
|
||||
a.btn-outline {
|
||||
text-align: center;
|
||||
@apply rounded rounded-lg drop-shadow-md px-4 py-2;
|
||||
@apply text-accent-600 bg-accent-600/10;
|
||||
@apply border-2 border-accent-600;
|
||||
@ -73,7 +76,11 @@ header.global, main.global, footer.global {
|
||||
}
|
||||
|
||||
main.global {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.status-message {
|
||||
@ -88,3 +95,8 @@ div.status-message {
|
||||
div.status-message.error {
|
||||
@apply bg-failure/50;
|
||||
}
|
||||
|
||||
div.call-to-action-box {
|
||||
@apply bg-gradient-to-r from-secondary-300/75 to-secondary-500/75;
|
||||
@apply rounded-md py-4 px-6;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import './css/styles.css';
|
||||
import 'htmx.org';
|
||||
//import 'htmx.org';
|
||||
import './ts/index.ts';
|
||||
|
||||
window.htmx = require('htmx.org');
|
||||
//window.htmx = require('htmx.org');
|
||||
|
@ -33,19 +33,25 @@ services:
|
||||
expose:
|
||||
- "8000"
|
||||
volumes:
|
||||
- ./app/manage.py:/app/manage.py:ro
|
||||
- ./app/requirements.txt:/app/requirements.txt:ro
|
||||
- ./app/core/:/app/core:ro
|
||||
- ./app/authentication/:/app/authentication:ro
|
||||
- ./app/core/:/app/core:ro
|
||||
- ./app/gotify/:/app/gotify:ro
|
||||
- ./app/manage.py:/app/manage.py:ro
|
||||
- ./app/medwings/:/app/medwings:ro
|
||||
- ./app/requirements.txt:/app/requirements.txt:ro
|
||||
- ./app/static/:/app/static:ro
|
||||
- ./app/withings/:/app/withings:ro
|
||||
environment:
|
||||
TZ: ${TIMEZONE}
|
||||
PG_NAME: ${PG_NAME}
|
||||
PG_USER: ${PG_USER}
|
||||
PG_PASSWORD: ${PG_PASSWORD}
|
||||
PG_HOST: ${PG_HOST}
|
||||
PG_PORT: ${PG_PORT}
|
||||
TZ: ${TIMEZONE}
|
||||
WITHINGS_CLIENT_ID: ${WITHINGS_CLIENT_ID}
|
||||
WITHINGS_CLIENT_SECRET: ${WITHINGS_CLIENT_SECRET}
|
||||
GOTIFY_USER: ${GOTIFY_USER}
|
||||
GOTIFY_PASSWORD: ${GOTIFY_PASSWORD}
|
||||
medwings-postgres:
|
||||
image: postgres:alpine
|
||||
container_name: ${PG_HOST}
|
||||
|
Loading…
x
Reference in New Issue
Block a user