diff --git a/rowers/forms.py b/rowers/forms.py index 0ce79b0c..f5716641 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -37,6 +37,19 @@ formaxlabels = axlabels.copy() formaxlabels.pop('None') parchoices = list(sorted(formaxlabels.items(), key=lambda x: x[1])) +class DeepWaterLoginForm(forms.Form): + username = forms.CharField( + max_length=150, label='Username', + widget=forms.TextInput(attrs={'placeholder': 'Username'})) + password = forms.CharField( + max_length=128, label='Password', + widget=forms.PasswordInput(attrs={'placeholder': 'Password'})) + + def clean(self): + cleaned_data = super(DeepWaterLoginForm, self).clean() + if not cleaned_data.get('username') or not cleaned_data.get('password'): + raise forms.ValidationError("Please enter both username and password.") + return cleaned_data class SurveyForm(forms.Form): surveydone = forms.ChoiceField( diff --git a/rowers/management/commands/loadnextweeksessions.py b/rowers/management/commands/loadnextweeksessions.py new file mode 100644 index 00000000..09440387 --- /dev/null +++ b/rowers/management/commands/loadnextweeksessions.py @@ -0,0 +1,44 @@ +#!/srv/venv/bin/python + +import sys +import os + +from django.core.management.base import BaseCommand +from rowers.models import Rower +from rowers.tasks import handle_loadnextweek +from rowers.utils import myqueue, dologging + +import django_rq + +PY3K = sys.version_info >= (3, 0) + +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('low') + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + '--testing', + action='store_true', + dest='testing', + default=False, + help="Run in testing mode, don't send emails", ) + + def handle(self, *args, **options): + if 'testing' in options: + testing = options['testing'] + else: + testing = False + + rowers = Rower.objects.filter(training_plan_code__isnull=False).exclude(training_plan_code__exact='') + rowers = rowers.filter(training_plan_secret__isnull=False).exclude(training_plan_secret__exact='') + + for rower in rowers: + _ = myqueue(queue, handle_loadnextweek, + rower) + + self.stdout.write(self.style.SUCCESS( + 'Successfully loaded next week plans' + )) diff --git a/rowers/tasks.py b/rowers/tasks.py index 7c222634..325d7210 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -18,6 +18,7 @@ from rowers.models import ( GraphImage, Team, PlannedSession ) from rowers.session_utils import is_session_complete + import math from rowers.courseutils import ( coursetime_paths, coursetime_first, time_in_path, @@ -333,6 +334,106 @@ def summaryfromsplitdata(splitdata, data, filename, sep='|', workouttype='rower' return sums, sa, results +from rowers.utils import intensitymap + +def strcapitalize(s): + if s is None: + return None + if isinstance(s, str): + return s[0].upper() + s[1:] + + return s + +def correct_intensity(workout): + # reads the steps and if the intensity is an integer, converts it to a string + steps = workout['steps'] + for step in steps: + if 'intensity' in step: + if isinstance(step['intensity'], int): + step['intensity'] = intensitymap[step['intensity']] + step['durationType'] = strcapitalize(step['durationType']) + step['targetType'] = strcapitalize(step['targetType']) + step['intensity'] = strcapitalize(step['intensity']) + + return workout + + +@app.task +def handle_loadnextweek(rower, debug=False, **kwargs): + + plan = rower.training_plan_code + secret = rower.training_plan_secret + post_data = { + 'fitness': rower.actualfit, + 'fatigue': rower.actualfatigue, + 'plan': plan, + 'secret': secret, + } + + url = "http://localhost:8898/next-week-plan/" + response = requests.post(url, data=post_data) + + if response.status_code in [200, 201]: + data = response.json() + + today = timezone.now() + startdate = today - timedelta(days=today.weekday())+timezone.timedelta(days=7) + enddate = startdate + timedelta(days=6) + + sps = PlannedSession.objects.filter( + rower__in=[rower], + startdate__gte=startdate, + enddate__lte=enddate, + is_template=False, + ) + + for ps in sps: + ps.delete() + + trainingdays = data['cycles'] + # start date is the first day of the following week + + ndays = 0 + for day in trainingdays: + try: + workouts = day[0][1:] + except IndexError: + workouts =[] + for workout in workouts: + sessionsport = 'water' + try: + sessionsport = mytypes.fitmappinginv[workout['sport'].lower()] + except KeyError: + pass + + preferreddate = startdate+timedelta(days=ndays) + sessionmode = 'time' + + ps = PlannedSession( + startdate=preferreddate - timedelta(days=preferreddate.weekday()), + enddate=preferreddate + timedelta(days=-preferreddate.weekday()-1, weeks=1), + preferreddate=preferreddate, + sessionsport=sessionsport, # change this + name=workout['workoutName'], + steps=correct_intensity(workout), + manager=rower.user, + sessionmode=sessionmode, + comment=workout['description'], + from_plan=None, + ) + ps.save() + ps.rower.add(rower) + ps.save() + if ps.fitfile: + ps.steps = {} + ps.save() + ndays += 1 + + + return 1 + + return 0 + @app.task def handle_assignworkouts(workouts, rowers, remove_workout, debug=False, **kwargs): for workout in workouts: diff --git a/rowers/templates/deepwaterlogin.html b/rowers/templates/deepwaterlogin.html new file mode 100644 index 00000000..5372e753 --- /dev/null +++ b/rowers/templates/deepwaterlogin.html @@ -0,0 +1,32 @@ +{% extends "newbase.html" %} +{% load static %} +{% load rowerfilters %} + +{% block title %}Login to Deep Water{% endblock %} + +{% block main %} + +

Login to Deep Water

+ + +
+ {% csrf_token %} + {% if error %} +
{{ error }}
+ {% endif %} + +
+ + +
+ +
+ + +
+ + +
+ +{% endblock %} + diff --git a/rowers/tokens.py b/rowers/tokens.py index 4db5af2e..8ae25a3d 100644 --- a/rowers/tokens.py +++ b/rowers/tokens.py @@ -1,6 +1,6 @@ from django.contrib.auth.tokens import PasswordResetTokenGenerator import six - +import hmac, hashlib, base64, json, time class AccountActivationTokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, user, timestamp): @@ -9,5 +9,15 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator): six.text_type(user.is_active) ) +# Function to create a custom token for user authentication +def create_token(user_id, username=None, secret_key="your-very-secret-key-here-change-this!"): + expires = int(time.time()) + (24 * 3600) # 24 hours + payload = json.dumps({"user_id": user_id, "username": username, "expires": expires}) + encoded_payload = base64.b64encode(payload.encode()).decode() + signature = base64.b64encode( + hmac.new(secret_key.encode(), encoded_payload.encode(), hashlib.sha256).digest() + ).decode() + return f"{encoded_payload}.{signature}" + account_activation_token = AccountActivationTokenGenerator() diff --git a/rowers/urls.py b/rowers/urls.py index 3fca9aa8..b2942bf8 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -1115,5 +1115,9 @@ urlpatterns = [ name='nextweekplan_view'), re_path(r'^currentweekplan/$', views.currentweekplan_view, name='currentweekplan_view'), + re_path(r'^deepwaterlogin/$', views.deepwatertoken_login, + name='deepwatertoken_login'), + re_path(r'^deepwatertoken/$', views.get_deepwater_token, + name='get_deepwater_token'), ] diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index 991a5c21..3a904289 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -1,5 +1,51 @@ from rowers.views.statements import * from rowers.rower_rules import user_is_not_basic, user_is_coachee +from rowers.tokens import create_token +from rowers.forms import DeepWaterLoginForm +import jwt + +def deepwatertoken_login(request): + if request.method == 'POST': + username = request.POST.get('username', '') + password = request.POST.get('password', '') + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + payload = { + 'user_id': user.id, + 'username': user.username, + 'email': user.email, + 'exp': timezone.now() + datetime.timedelta(days=1), # Token valid for 1 day + 'iat': timezone.now(), + } + + token = jwt.encode(payload, settings.DEEP_WATER_SECRET_KEY, algorithm='HS256') + + # Debug: Print the token + print(f"Generated token: {token}") + print(f"Token length: {len(token)}") + print(f"Token parts: {token.split('.')}") + + redirect_url = request.GET.get('redirect', settings.DEEP_WATER_URL) + return HttpResponseRedirect(f"{redirect_url}?token={token}") + else: + messages.error(request, 'Invalid credentials') + return render(request, 'deepwaterlogin.html') + redirect_url = request.GET.get('redirect', settings.DEEP_WATER_URL) + return render(request, "deepwaterlogin.html") + +@login_required() +def get_deepwater_token(request): + payload = { + 'user_id': request.user.id, + 'username': request.user.username, + 'email': request.user.email, + 'exp': timezone.now() + datetime.timedelta(days=1), # Token valid for 1 day + 'iat': timezone.now(), + } + + token = jwt.encode(payload, settings.DEEP_WATER_SECRET_KEY, algorithm='HS256') + return JsonResponse({'token': token}) @login_required() def deactivate_user(request): diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 00653b67..520449c3 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -31,7 +31,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = CFG['secret_key'] - +DEEP_WATER_SECRET_KEY = CFG['deep_water_secret_key'] +DEEP_WATER_URL = CFG['deep_water_url'] # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False