From 8be0b378787e24c608381ee7c2d8e354c138b37c Mon Sep 17 00:00:00 2001
From: nd <git@notandy.de>
Date: Wed, 15 Jul 2020 00:55:37 +0200
Subject: [PATCH] add basic password reset, missing mail sending

---
 uffd/selfservice/models.py                    | 22 +++++++
 .../selfservice/templates/reset_password.html | 31 +++++++++
 uffd/selfservice/views.py                     | 66 +++++++++++++++----
 uffd/user/models.py                           |  2 +-
 4 files changed, 106 insertions(+), 15 deletions(-)
 create mode 100644 uffd/selfservice/templates/reset_password.html

diff --git a/uffd/selfservice/models.py b/uffd/selfservice/models.py
index e69de29b..db19c520 100644
--- a/uffd/selfservice/models.py
+++ b/uffd/selfservice/models.py
@@ -0,0 +1,22 @@
+import datetime
+import secrets
+
+from sqlalchemy import Column, Integer, String, Text, LargeBinary, DateTime, Boolean, ForeignKey
+
+from uffd.database import db
+
+def random_token():
+	return secrets.token_hex(128)
+
+class Token():
+	token = Column(String(128), primary_key=True, default=random_token)
+	created = Column(DateTime, default=datetime.datetime.now)
+
+class PasswordToken(Token, db.Model):
+	__tablename__ = 'passwortToken'
+	loginname = Column(String(32))
+
+class MailToken(Token, db.Model):
+	__tablename__ = 'mailToken'
+	loginname = Column(String(32))
+	newmail = Column(String(255))
diff --git a/uffd/selfservice/templates/reset_password.html b/uffd/selfservice/templates/reset_password.html
new file mode 100644
index 00000000..990a542f
--- /dev/null
+++ b/uffd/selfservice/templates/reset_password.html
@@ -0,0 +1,31 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<form action="{{ url_for(".self_token_password", token=token) }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '') ">
+<div class="row mt-2 justify-content-center">
+	<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
+		<div class="text-center">
+			<img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
+		</div>
+		<div class="col-12">
+			<h2 class="text-center">Reset password</h2>
+		</div>
+		<div class="form-group col-12">
+			<label for="user-loginname">login name</label>
+			<input type="text" class="form-control" id="user-loginname" name="loginname" required="required" tabindex = "1">
+		</div>
+		<div class="form-group col-12">
+			<label for="user-password1">new password</label>
+			<input type="password" class="form-control" id="user-password1" name="password1" required="required" tabindex = "2">
+		</div>
+		<div class="form-group col-12">
+			<label for="user-password2">new password again</label>
+			<input type="password" class="form-control" id="user-password2" name="password2" required="required" tabindex = "2">
+		</div>
+		<div class="form-group col-12">
+			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Set password</button>
+		</div>
+	</div>
+</div>
+</form>
+{% endblock %}
diff --git a/uffd/selfservice/views.py b/uffd/selfservice/views.py
index d23351bb..9d745347 100644
--- a/uffd/selfservice/views.py
+++ b/uffd/selfservice/views.py
@@ -1,32 +1,70 @@
+import datetime
+
 from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
 
 from uffd.navbar import register_navbar
 from uffd.csrf import csrf_protect
-
 from uffd.user.models import User, Group
 from uffd.session import get_current_user, login_required, is_valid_session
-from uffd.ldap import get_conn, escape_filter_chars
+from uffd.ldap import get_conn, escape_filter_chars, loginname_to_dn
+from uffd.selfservice.models import PasswordToken
+from uffd.database import db
 
 bp = Blueprint("selfservice", __name__, template_folder='templates', url_prefix='/self/')
 
-@bp.before_request
-@login_required()
-def self_acl():
-	pass
-	#if not self_acl_check():
-	#	flash('Access denied')
-	#	return redirect(url_for('index'))
-
-def self_acl_check():
-	return is_valid_session() and get_current_user().is_in_group(current_app.config['ACL_SELFSERVICE_GROUP'])
-
 @bp.route("/")
 @register_navbar('Selfservice', icon='portrait', blueprint=bp, visible=is_valid_session)
+@login_required()
 def self_index():
 	return render_template('self.html', user=get_current_user())
 
 @bp.route("/update", methods=(['POST']))
 @csrf_protect(blueprint=bp)
+@login_required()
 def self_update():
-	pass
+	# TODO: actualy update the user...
+	send_passwordreset('uffdtest')
+	return 'OK', 200
+
+@bp.route("/token/password/<token>", methods=(['POST', 'GET']))
+def self_token_password(token):
+	session = db.session
+	dbtoken = PasswordToken.query.get(token)
+	if not dbtoken or dbtoken.created < (datetime.datetime.now() - datetime.timedelta(days=2)):
+		flash('Token expired, please try again.')
+		if dbtoken:
+			session.delete(dbtoken)
+			session.commit()
+		return redirect(url_for('session.login'))
+	if not 'loginname' in request.values:
+		flash('Please set a new password.')
+		return render_template('reset_password.html', token=token)
+	else:
+		if not request.values['loginname'] == dbtoken.loginname:
+			flash('That is not the correct login name. Please start the password reset process again')
+			session.delete(dbtoken)
+			session.commit()
+			return redirect(url_for('session.login'))
+		if not request.values['password1']:
+			flash('Please specify a new password.')
+			return render_template('reset_password.html', token=token)
+		user = User.from_ldap_dn(loginname_to_dn(dbtoken.loginname))
+		user.set_password(request.values['password1'])
+		user.to_ldap()
+		flash('New password set')
+		session.delete(dbtoken)
+		session.commit()
+		return redirect(url_for('session.login'))
+
+
 
+def send_passwordreset(loginname):
+	session = db.session
+	expired_tokens = PasswordToken.query.filter(PasswordToken.created < (datetime.datetime.now() - datetime.timedelta(days=2))).all()
+	for i in expired_tokens:
+		session.delete(i)
+	token = PasswordToken()
+	token.loginname = loginname
+	session.add(token)
+	# TODO: send mail
+	session.commit()
diff --git a/uffd/user/models.py b/uffd/user/models.py
index 2c8792ae..89918335 100644
--- a/uffd/user/models.py
+++ b/uffd/user/models.py
@@ -41,7 +41,7 @@ class User():
 			return None
 		return User.from_ldap(conn.entries[0])
 
-	def to_ldap(self, new):
+	def to_ldap(self, new=False):
 		conn = ldap.get_conn()
 		if new:
 			attributes= {
-- 
GitLab