diff --git a/tracker/forms.py b/tracker/forms.py
index c862b47..3ab0890 100644
--- a/tracker/forms.py
+++ b/tracker/forms.py
@@ -35,3 +35,10 @@ class SettingsForm(forms.ModelForm):
class Meta:
model = Setting
fields = ('timezone', 'locale', 'duration_format', 'timestamp_rounding', 'allow_parallel_tracking')
+
+
+class ChangePasswordForm(forms.Form):
+ password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}),
+ help_text='Password must be at least 8 characters long and must contain'
+ ' at least one letter and one number')
+ confirm_password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'form-control'}))
diff --git a/tracker/templates/tracker/user/account.html b/tracker/templates/tracker/user/account.html
new file mode 100644
index 0000000..1fc284d
--- /dev/null
+++ b/tracker/templates/tracker/user/account.html
@@ -0,0 +1,54 @@
+{% extends "tracker/base.html" %}
+
+{% load django_bootstrap_breadcrumbs %}
+
+{% block breadcrumbs %}
+ {% breadcrumb_raw "Home" "tracker:index" %}
+ {% breadcrumb_raw user.get_full_name "/user/profile" %}
+ {% breadcrumb_raw "Account" "tracker:/user/account" %}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
Warning
+
+ Please use a password only as a last resort. Even if trrrakk hashes and salts your password,
+ stores it in an encrypted database and only uses secure communication channels (https), it is
+ still more save not storing a password at all.
+
+
+ The login mechanism with Google-OAuth2 or Github-OAuth2 doesn't require to store any
+ password on trrrakk. Instead it delegates the responsibility to the big players
+ and trusts their security standards.
+
+
+
+
+
+
+{% endblock %}
diff --git a/tracker/urls.py b/tracker/urls.py
index 549fd43..5acf5b6 100644
--- a/tracker/urls.py
+++ b/tracker/urls.py
@@ -33,5 +33,9 @@
path(f'{reports_base}monthly_distribution', views.user.monthly_distribution, name='user/reports/monthly_distribution'),
path(f'{reports_base}weekly_time', views.user.weekly_time, name='user/reports/weekly_time'),
path(f'{reports_base}weekly_worktime', views.user.weekly_worktime_report, name='user/reports/weekly_worktime'),
+
path('settings', views.user.settings, name='user/settings'),
+ path('account', views.user.account, name='user/account'),
+ path('account/change_password', views.user.change_password, name='user/account/change_password'),
+ path('account/clear_password', views.user.clear_password, name='user/account/clear_password'),
]
\ No newline at end of file
diff --git a/tracker/views/user.py b/tracker/views/user.py
index 72685f5..2158cdb 100644
--- a/tracker/views/user.py
+++ b/tracker/views/user.py
@@ -6,11 +6,11 @@
import pytz
from django.contrib import messages
from django.contrib.auth.decorators import login_required
-from django.shortcuts import render
+from django.shortcuts import render, redirect
from django.utils import formats
from django.utils.translation import activate as tl_activate
-from tracker.forms import SettingsForm
+from tracker.forms import SettingsForm, ChangePasswordForm
from tracker.models import TimeRecord, Project, Setting
@@ -222,6 +222,59 @@ def settings(request):
return render(request, 'tracker/user/settings.html', context={'form': form})
+@login_required
+def account(request):
+ return render(request, 'tracker/user/account.html', context={
+ 'change_password_form': ChangePasswordForm()
+ })
+
+
+@login_required
+def change_password(request):
+ if request.method != 'POST':
+ messages.error(request, 'Invalid request method')
+ return redirect('tracker:user/account')
+
+ password = request.POST['password']
+ confirmation = request.POST['confirm_password']
+
+ if password != confirmation:
+ messages.error(request, 'Passwords do not match')
+ return redirect('tracker:user/account')
+
+ min_len = 8
+ if len(password) < min_len:
+ messages.error(request, f'Passwords must be at least {min_len} characters long')
+ return redirect('tracker:user/account')
+
+ if not any(char.isdigit() for char in password):
+ messages.error(request, 'Password must contain at least 1 digit.')
+ return redirect('tracker:user/account')
+
+ if not any(char.isalpha() for char in password):
+ messages.error(request, 'Password must contain at least 1 letter.')
+ return redirect('tracker:user/account')
+
+ request.user.set_password(password)
+ request.user.save()
+
+ messages.success(request, 'Password successfully changed')
+ return redirect('tracker:user/account')
+
+
+@login_required
+def clear_password(request):
+ if request.method != 'POST':
+ messages.error(request, 'Invalid request method')
+ return redirect('tracker:user/account')
+
+ request.user.set_unusable_password()
+ request.user.save()
+
+ messages.success(request, 'Password successfully cleared')
+ return redirect('tracker:user/account')
+
+
def build_sum_for_dates(setting, dates, matrix):
init = 0 if setting.duration_format == Setting.DURATION_FORMAT_DECIMAL else timedelta()
return [sum([p['duration'][i] for p in matrix], init) for i in range(len(dates))]
diff --git a/trrrakk/templates/trrrakk/navigation.html b/trrrakk/templates/trrrakk/navigation.html
index d4ead05..a97792a 100644
--- a/trrrakk/templates/trrrakk/navigation.html
+++ b/trrrakk/templates/trrrakk/navigation.html
@@ -33,6 +33,9 @@
Settings
+
+ Account
+
Sign out