From 0954e5b9fbfccb3e823b873a1b0459b034c584a1 Mon Sep 17 00:00:00 2001
From: Sandro Lutz <code@temparus.ch>
Date: Sun, 12 Jul 2020 19:42:45 +0200
Subject: [PATCH] Add login pages

---
 app/auth.py                                   | 45 ++++++++++++
 app/bouncer/views.py                          | 15 ++--
 app/controller.py                             | 26 -------
 app/controllers/__init__.py                   |  4 +-
 app/controllers/user.py                       | 68 +++++++++++++++++--
 app/exceptions.py                             | 11 +++
 app/login/auth.py                             | 22 ------
 app/login/views.py                            | 51 +++++++++++---
 app/models/user.py                            |  5 +-
 app/static/css/style.css                      | 45 ++++++++----
 app/templates/base.html                       |  3 +-
 app/templates/bouncer/home.html               | 15 ----
 app/templates/bouncer/home_anonymous.html     | 21 ++++++
 app/templates/bouncer/home_authenticated.html | 19 ++++++
 app/templates/email/confirm.txt               |  2 +-
 app/templates/error/401.html                  |  2 +-
 app/templates/error/403.html                  |  2 +-
 app/templates/error/404.html                  |  2 +-
 app/templates/error/500.html                  |  2 +-
 app/templates/login/confirm_email.html        | 26 +++++++
 .../login/confirm_email_confirmed.html        | 20 ++++++
 app/templates/login/login.html                | 12 +++-
 app/templates/login/logout.html               |  5 +-
 app/templates/login/register.html             | 16 ++++-
 app/templates/login/register_success.html     | 20 ++++++
 instance/config.dev.py                        |  4 +-
 requirements.in                               |  3 +-
 requirements.txt                              | 12 ++--
 28 files changed, 357 insertions(+), 121 deletions(-)
 create mode 100644 app/auth.py
 delete mode 100644 app/controller.py
 delete mode 100644 app/login/auth.py
 delete mode 100644 app/templates/bouncer/home.html
 create mode 100644 app/templates/bouncer/home_anonymous.html
 create mode 100644 app/templates/bouncer/home_authenticated.html
 create mode 100644 app/templates/login/confirm_email.html
 create mode 100644 app/templates/login/confirm_email_confirmed.html
 create mode 100644 app/templates/login/register_success.html

diff --git a/app/auth.py b/app/auth.py
new file mode 100644
index 0000000..813dfaa
--- /dev/null
+++ b/app/auth.py
@@ -0,0 +1,45 @@
+import json
+from functools import wraps
+from flask import request, redirect, abort, session
+from app import db
+from .models import User
+
+
+def login_required(f):
+    """
+    Requires that the user is logged in.
+
+    This is a wrapper for the @login_required decorator.
+
+    Error 403: shwon if trying to access with an api key (Authorization header).
+    """
+    @wraps(f)
+    def wrapped(*args, **kwargs):
+        if not is_authenticated():
+            abort(403)
+
+        return f(*args, **kwargs)
+    return wrapped
+
+
+def is_authenticated():
+    return 'userID' in session and User.query.get(session['userID']) is not None
+
+
+def get_authenticated_user():
+    return User.query.get(session['userID']) if 'userID' in session else None
+
+
+def login(email, password):
+    user = User.query.filter(User.email == email).first()
+    if user.check_password(password):
+        session['userID'] = user._id
+        return True
+    return False
+
+
+def logout():
+    if is_authenticated():
+        session['userID'] = None
+        return True
+    return False
diff --git a/app/bouncer/views.py b/app/bouncer/views.py
index c635504..8825957 100644
--- a/app/bouncer/views.py
+++ b/app/bouncer/views.py
@@ -1,12 +1,17 @@
 from flask import flash, redirect, render_template, url_for, request, abort, make_response, session
 
 from . import bouncer_bp
-from ..login.auth import login_required
+from ..auth import login_required, is_authenticated, get_authenticated_user
+from ..controllers import FreeSpotController, UserController, ReservationController, RecordController
 
 @bouncer_bp.route('/')
 def home():
-    """
-    Handle requests to the /logout route
-    """
-    return make_response(render_template('bouncer/home.html', title='Home'))
+    free_spots = FreeSpotController.get_free_spots()
 
+    if is_authenticated():
+        user = get_authenticated_user()
+        reservation = ReservationController.get_active_reservation(user)
+        record = RecordController.get_active_record(user)
+    
+        return make_response(render_template('bouncer/home_authenticated.html', title='Home', free_spots=free_spots, name=user.name, email=user.email))
+    return make_response(render_template('bouncer/home_anonymous.html', title='Home', free_spots=free_spots))
diff --git a/app/controller.py b/app/controller.py
deleted file mode 100644
index 0c48b65..0000000
--- a/app/controller.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import threading
-from app import models
-from app import db
-from .exceptions import ReservationExpiredError
-
-class Controller():
-    lock = threading.Lock()
-
-    @classmethod
-    def create_record_from_reservation(cls, reservation):
-        with cls.lock:
-            if reservation.is_valid:
-                record = models.Record()
-                report.user = reservation.user
-                report.organisation = organisation
-                report.product = product
-
-                db.session.add(report)
-                db.session.commit()
-            else:
-                raise ReservationExpiredError
-
-    @classmethod
-    def create_new_record(cls, user, ):
-        with cls.lock:
-            pass
diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py
index d51b900..fb7217f 100644
--- a/app/controllers/__init__.py
+++ b/app/controllers/__init__.py
@@ -1,2 +1,4 @@
 from .record import RecordController
-from .free_spot import FreeSpotController
\ No newline at end of file
+from .free_spot import FreeSpotController
+from .reservation import ReservationController
+from .user import UserController
diff --git a/app/controllers/user.py b/app/controllers/user.py
index 3b532e2..2abbc9d 100644
--- a/app/controllers/user.py
+++ b/app/controllers/user.py
@@ -1,8 +1,9 @@
 import threading
-from datetime import datetime, timedelta
+from datetime import datetime
 from sqlalchemy import DateTime, cast, func
+from validate_email import validate_email
 from flask_mail import Message
-from flask import current_app
+from flask import current_app, render_template
 from app import db, mail
 from app.models import User
 from .lock import Lock
@@ -10,13 +11,44 @@ from .reservation import ReservationController
 from .record import RecordController
 from ..exceptions import ActiveReservationExistsError, \
     ActiveRecordExistsError, NoActiveRecordError, ReservationExpiredError, \
-    NoFreeSpotError
+    NoFreeSpotError, UserRegistrationInvalidDataError
 
 
 class UserController():
 
+    @staticmethod
+    def check_user_data(name, email, telegram_id=None, password=None, password2=None):
+        errors = []
+
+        if name is None or len(name) < 3:
+            errors.append('name')
+        if not validate_email(email_address=email, \
+                check_regex=True, \
+                check_mx=False, \
+                use_blacklist=True, \
+                debug=False) or \
+                User.query.filter(User.email == email).count():
+            errors.append('email')
+        if telegram_id is not None and User.query.filter(User.telegram_id == telegram_id):
+            errors.append('telegram_id')
+        if telegram_id is None and (password is None or len(password) == 0):
+            errors.append('password')
+        if password != password2:
+            errors.append('password2')
+
+        if len(errors) > 0:
+            raise UserRegistrationInvalidDataError(errors)
+
+
     @classmethod
     def create(cls, name, email, telegram_id=None, telegram_chat_id=None, password=None):
+        cls.check_user_data( \
+            name=name, \
+            email=email, \
+            telegram_id=telegram_id, \
+            password=password, \
+            password2=password)
+
         user = User()
         user.name = name
         user.email = email
@@ -30,7 +62,7 @@ class UserController():
         db.session.add(user)
         db.session.commit()
 
-        cls.send_confirm_email(user)
+        cls._send_confirm_email(user)
 
 
     @staticmethod
@@ -38,6 +70,25 @@ class UserController():
         return User.query.get(user_id)
 
 
+    @staticmethod
+    def verify_confirm_email(email, token):
+        user = User.query.filter(User.email == email).first()
+        return user is not None and user.is_token_valid(token)
+
+
+    @staticmethod
+    def confirm_email(email, token):
+        user = User.query.filter(User.email == email).first()
+
+        if user is None or not user.is_token_valid(token):
+            return False
+        
+        user.is_confirmed = True
+        user.generate_new_token()
+        db.session.commit()
+        return True
+
+
     @staticmethod
     def reserve(user):
         if ReservationController.has_active_reservation(user):
@@ -74,13 +125,16 @@ class UserController():
     def send_password_reset_email(user):
         user.generate_new_token()
         sesssion.commit()
-        msg = Message(render_template('email.password_reset', user=user),
+        msg = Message(render_template('email/password_reset.txt', user=user),
                   recipients=[user.email])
         mail.send(msg)
 
 
     @staticmethod
     def _send_confirm_email(user):
-        msg = Message(render_template('email.confirm', user=user),
-                  recipients=[user.email])
+        msg = Message(
+            subject='[Bastli Bouncer] Confirm Email Address',
+            body=render_template('email/confirm.txt', user=user),
+            recipients=[user.email])
+        print(msg)
         mail.send(msg)
diff --git a/app/exceptions.py b/app/exceptions.py
index 98fbad5..40cf14d 100644
--- a/app/exceptions.py
+++ b/app/exceptions.py
@@ -34,3 +34,14 @@ class NoActiveRecordError(Error):
 class UserNotConfirmedError(Error):
     """Raised when user has no confirmed email address"""
     pass
+
+
+class UserRegistrationInvalidDataError(Error):
+    """Raised when registration data is invalid"""
+    
+    def __init__(self, errors):
+        super(UserRegistrationInvalidDataError, self).__init__()
+        self.errors = errors
+
+    def get_errors(self):
+        return self.errors
diff --git a/app/login/auth.py b/app/login/auth.py
deleted file mode 100644
index 2e07dd2..0000000
--- a/app/login/auth.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import json
-from functools import wraps
-from flask import request, redirect, abort, session
-from app import db
-from ..models import User
-
-
-def login_required(f):
-    """
-    Requires that the user is logged in.
-
-    This is a wrapper for the @login_required decorator.
-
-    Error 403: shwon if trying to access with an api key (Authorization header).
-    """
-    @wraps(f)
-    def wrapped(*args, **kwargs):
-        if session['userID'] is None or User.query.get(session['userID']) is None:
-            abort(403)
-        
-        return f(*args, **kwargs)
-    return wrapped
diff --git a/app/login/views.py b/app/login/views.py
index bbfc189..a6aeec8 100644
--- a/app/login/views.py
+++ b/app/login/views.py
@@ -1,21 +1,54 @@
 from flask import flash, redirect, render_template, url_for, request, abort, make_response
 
+from ..controllers.user import UserController
+from ..exceptions import UserRegistrationInvalidDataError
+from ..auth import login as auth_login, logout as auth_logout
 from . import login_bp
 
 @login_bp.route('/register', methods=['GET', 'POST'])
 def register():
-    # TODO
-    return make_response(render_template('login/register.html', title='Register'))
+    errors = None
+    if request.method == 'POST':
+        try:
+            UserController.check_user_data( \
+                name=request.form['name'], \
+                email=request.form['email'], \
+                password=request.form['password'], \
+                password2=request.form['password2'])
+            UserController.create( \
+                name=request.form['name'], \
+                email=request.form['email'], \
+                password=request.form['password'])
+            
+            return make_response(render_template('login/register_success.html', title='Registration'))
+
+        except UserRegistrationInvalidDataError as e:
+            errors = e.errors
+            flash('Some fields have errors: ' + ', '.join(errors), 'error')
+    return make_response(render_template('login/register.html', title='Registration', errors=errors))
+
+
+@login_bp.route('/confirm_email')
+def confirm_email():
+    email = request.args.get('email')
+    token = request.args.get('token')
+    action = request.args.get('action')
+    if action == 'confirm':
+        if not UserController.confirm_email(email, token):
+            abort(404)
+        return make_response(render_template('login/confirm_email_confirmed.html', title='Email Address confirmed'))
+    elif not UserController.verify_confirm_email(email, token):
+        abort(404)
+
+    return make_response(render_template('login/confirm_email.html', title='Confirm Email Address', email=email, token=token))
 
 
 @login_bp.route('/login', methods=['GET', 'POST'])
 def login():
-    error = None
     if request.method == 'POST':
-        if request.form['username'] != 'admin' or request.form['password'] != 'admin':
-            error = 'Invalid Credentials. Please try again.'
-        else:
-            return redirect(url_for('home'))
+        if auth_login(request.form['email'], request.form['password']):
+            return redirect(url_for('bouncer.home'))
+        flash('Invalid Credentials. Please try again.', 'error')
     return make_response(render_template('login/login.html', title='Login'))
 
 
@@ -26,8 +59,6 @@ def password_reset(token, email):
 
 @login_bp.route('/logout')
 def logout():
-    session['userID'] = None
-
-    if authenticated:
+    if auth_logout():
         return make_response(render_template('login/logout.html', title='Logout'))
     return redirect(url_for('bouncer.home'))
diff --git a/app/models/user.py b/app/models/user.py
index cbbcf11..df9db67 100644
--- a/app/models/user.py
+++ b/app/models/user.py
@@ -1,6 +1,6 @@
 import secrets
 from flask import current_app
-from datetime import datetime
+from datetime import datetime, timedelta
 from werkzeug.security import generate_password_hash, check_password_hash
 from app import db
 
@@ -21,7 +21,8 @@ class User(db.Model):
     token = db.Column(db.String(32), nullable=False)
     token_expiration = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
     created = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
-
+    reservations = db.relationship("Reservation", back_populates="user")
+    records = db.relationship("Record", back_populates="user")
 
     @property
     def is_telegram_user(self):
diff --git a/app/static/css/style.css b/app/static/css/style.css
index 547e582..d6c7acd 100644
--- a/app/static/css/style.css
+++ b/app/static/css/style.css
@@ -15,28 +15,25 @@ h2,
 h3 {
   color: #1f2d54;
 }
-.navbar-default {
-  background-color: #1f2d54;
+
+a {
+  color: #1f2d54;
 }
-a,
-.navbar-default .navbar-brand,
-.navbar-default .navbar-nav > li > a {
+a:hover {
   color: #e8462b;
+  text-decoration: none;
 }
-a:hover,
-.navbar-default .navbar-brand:hover,
-.navbar-default .navbar-nav > li > a:hover {
-  color: #ffffff;
-}
-a.table_link:hover {
-  color: #1f2d54;
-}
+
 footer {
   padding-top: 30px;
   padding-right: 0;
   padding-left: 0;
   padding-bottom: 20px;
-  background-color: #1f2d54;
+}
+footer > .container {
+  border-top: 4px solid #1f2d54;
+  text-align: center;
+  padding: 2em 0 0;
 }
 p.copyright {
   margin: 15px 0 0;
@@ -64,6 +61,18 @@ p.copyright {
   border-color: #e8462b;
   color: #e8462b;
 }
+.btn-primary {
+  background-color: #1f2d54;
+  color: #fff;
+}
+.btn-primary:hover {
+  background-color: #ffffff;
+  border-color: #e8462b;
+  color: #e8462b;
+  /* border-color: #1f2d54;
+  background-color: #fff;
+  color: #1f2d54; */
+}
 .center {
   margin: auto;
   width: 70%;
@@ -74,6 +83,14 @@ p.copyright {
   width: 40%;
   padding: 10px;
 }
+.error {
+  text-align: center;
+}
+.free_spots {
+  font-size: 1.2em;
+  font-weight: bold;
+  text-decoration: underline;
+}
 .content-section {
   padding: 50px 0;
   border-top: 1px solid #e7e7e7;
diff --git a/app/templates/base.html b/app/templates/base.html
index ac7074d..21ad9ee 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -24,7 +24,8 @@
             <div class="row">
                 <div class="col-lg-12">
                     <ul class="list-inline">
-                        <li><a target="_blank" href="https://bastli.ethz.ch"><img src="{{ url_for('static', filename='img/bastli_logo.png') }}" style="width: 150px;"></a></li>
+                        <!-- <li><a target="_blank" href="https://bastli.ethz.ch"><img src="{{ url_for('static', filename='img/bastli_logo.png') }}" style="width: 150px;"></a></li> -->
+                        <li>© <a target="_blank" href="https://bastli.ethz.ch">AMIV Bastli</a></li>
                         <li><a href="mailto:it@bastli.ethz.ch">Contact</a></li>
                         <li><a target="_blank" href="https://gitlab.ethz.ch/bastli/bastli-bouncer">Source Code & Issue Tracker</a></li>
                     </ul>
diff --git a/app/templates/bouncer/home.html b/app/templates/bouncer/home.html
deleted file mode 100644
index a25a87f..0000000
--- a/app/templates/bouncer/home.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% import "bootstrap/utils.html" as utils %}
-{% extends "base.html" %}
-{% block body %}
-<div class="content-section">
-  <br/>
-  {{ utils.flashed_messages() }}
-  <br/>
-  <div class="center-narrow">
-    <h1>Bastli Bouncer</h1>
-    <br/>
-    <p>You have landed on the home page!</p>
-    <a class="btn btn-default" role="button" href="{{ url_for('login.login') }}">Login</a>
-  </div>
-</div>
-{% endblock %}
diff --git a/app/templates/bouncer/home_anonymous.html b/app/templates/bouncer/home_anonymous.html
new file mode 100644
index 0000000..f6bc104
--- /dev/null
+++ b/app/templates/bouncer/home_anonymous.html
@@ -0,0 +1,21 @@
+{% import "bootstrap/utils.html" as utils %}
+{% extends "base.html" %}
+{% block body %}
+<div class="content-section">
+  <br/>
+  {{ utils.flashed_messages() }}
+  <br/>
+  <div class="center-narrow">
+    <h1>Bastli Bouncer</h1>
+    <br/>
+    <p>We currently have <span class="free_spots">{{ free_spots }} free spots</span> in our workshop.</p>
+    <br/>
+    <p>Register and reserve your spot here before you come by! Thank you.</p>
+    <br/>
+    <p>
+      <a class="btn btn-default" role="button" href="{{ url_for('login.login') }}">Login</a>
+      <a class="btn btn-default" role="button" href="{{ url_for('login.register') }}">Register</a>
+    </p>
+  </div>
+</div>
+{% endblock %}
diff --git a/app/templates/bouncer/home_authenticated.html b/app/templates/bouncer/home_authenticated.html
new file mode 100644
index 0000000..a4dd68c
--- /dev/null
+++ b/app/templates/bouncer/home_authenticated.html
@@ -0,0 +1,19 @@
+{% import "bootstrap/utils.html" as utils %}
+{% extends "base.html" %}
+{% block body %}
+<div class="content-section">
+  <br/>
+  {{ utils.flashed_messages() }}
+  <br/>
+  <div class="center-narrow">
+    <h1>Bastli Bouncer</h1>
+    <br/>
+    <p>We currently have <span class="free_spots">{{ free_spots }} free spots</span> in our workshop.</p>
+    <p>
+      Welcome, {{ name }} ({{ email }}).
+      <a class="btn btn-default" role="button" href="{{ url_for('login.logout') }}">Logout</a>
+    </p>
+    <br/>
+  </div>
+</div>
+{% endblock %}
diff --git a/app/templates/email/confirm.txt b/app/templates/email/confirm.txt
index 5d2666e..ae6ca5c 100644
--- a/app/templates/email/confirm.txt
+++ b/app/templates/email/confirm.txt
@@ -2,6 +2,6 @@ Hi {{ user.name }}
 
 Please visit the link below to confirm your email address.
 
-{{ url_for('login.email_confirm', token=user.token, email=user.email, _external=True) }}
+{{ url_for('login.confirm_email', token=user.token, email=user.email, _external=True) }}
 
 The link will expire after some time.
diff --git a/app/templates/error/401.html b/app/templates/error/401.html
index 998e51f..20e95dc 100644
--- a/app/templates/error/401.html
+++ b/app/templates/error/401.html
@@ -5,7 +5,7 @@
   <br/>
   {{ utils.flashed_messages() }}
   <br/>
-  <div class="center-narrow">
+  <div class="center-narrow error">
     <h1>Oops, that's an error!</h1>
     <br/>
     <p>Error 401: Authorization header missing! An API key is required to access this resource.</p>
diff --git a/app/templates/error/403.html b/app/templates/error/403.html
index 5b822c8..89f6901 100644
--- a/app/templates/error/403.html
+++ b/app/templates/error/403.html
@@ -5,7 +5,7 @@
   <br/>
   {{ utils.flashed_messages() }}
   <br/>
-  <div class="center-narrow">
+  <div class="center-narrow error">
     <h1>Oops, that's an error!</h1>
     <br/>
     <p>Error 403: You are not authorized to access this page.</p>
diff --git a/app/templates/error/404.html b/app/templates/error/404.html
index 56b2225..c758893 100644
--- a/app/templates/error/404.html
+++ b/app/templates/error/404.html
@@ -5,7 +5,7 @@
   <br/>
   {{ utils.flashed_messages() }}
   <br/>
-  <div class="center-narrow">
+  <div class="center-narrow error">
     <h1>Oops, that's a error!</h1>
     <br/>
     <p>Error 404: The page you are looking for could not be found.</p>
diff --git a/app/templates/error/500.html b/app/templates/error/500.html
index 2ad0caf..b308c49 100644
--- a/app/templates/error/500.html
+++ b/app/templates/error/500.html
@@ -5,7 +5,7 @@
   <br/>
   {{ utils.flashed_messages() }}
   <br/>
-  <div class="center-narrow">
+  <div class="center-narrow error">
     <h1>Oops, that's an error!</h1>
     <br/>
     <p>
diff --git a/app/templates/login/confirm_email.html b/app/templates/login/confirm_email.html
new file mode 100644
index 0000000..0e2604d
--- /dev/null
+++ b/app/templates/login/confirm_email.html
@@ -0,0 +1,26 @@
+{% import "bootstrap/utils.html" as utils %}
+{% extends "base.html" %}
+{% block body %}
+<div class="content-section">
+  <br/>
+  {{ utils.flashed_messages() }}
+  <br/>
+  <div class="center-narrow">
+    <h1>Bastli Bouncer</h1>
+    <br/>
+    <h2>Confirm Email Address</h2>
+    <br>
+    <p>Do you want to confirm your email address {{ email }}?</p>
+    <p>
+      <form action="" method="get">
+        <input type="hidden" name="email" value="{{ email }}">
+        <input type="hidden" name="token" value="{{ token }}">
+        <input type="hidden" name="action" value="confirm">
+        <input class="btn btn-primary" type="submit" value="Confirm">
+        <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Abort</a>
+      </form>
+      
+    </p>
+  </div>
+</div>
+{% endblock %}
diff --git a/app/templates/login/confirm_email_confirmed.html b/app/templates/login/confirm_email_confirmed.html
new file mode 100644
index 0000000..afd2099
--- /dev/null
+++ b/app/templates/login/confirm_email_confirmed.html
@@ -0,0 +1,20 @@
+{% import "bootstrap/utils.html" as utils %}
+{% extends "base.html" %}
+{% block body %}
+<div class="content-section">
+  <br/>
+  {{ utils.flashed_messages() }}
+  <br/>
+  <div class="center-narrow">
+    <h1>Bastli Bouncer</h1>
+    <br/>
+    <h2>Email address confirmed!</h2>
+    <br>
+    <p>Your email address is now confirmed. Sign in to use your new account.</p>
+    <p>
+      <a class="btn btn-default" role="button" href="{{ url_for('login.login') }}">Login</a>
+      <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Home</a>
+    </p>
+  </div>
+</div>
+{% endblock %}
diff --git a/app/templates/login/login.html b/app/templates/login/login.html
index 6428ad5..025203c 100644
--- a/app/templates/login/login.html
+++ b/app/templates/login/login.html
@@ -8,8 +8,16 @@
   <div class="center-narrow">
     <h1>Bastli Bouncer</h1>
     <br/>
-    <p>You have successfully been logged out.</p>
-    <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Login</a>
+    <h2>Log in to your personal Account</h2>
+    <br>
+    <form action="" method="post">
+      <label for="email">Email:</label><br>
+      <input type="email" id="email" name="email" value="{{ request.form.email }}"><br/>
+      <label for="name">Password:</label><br>
+      <input type="password" id="password" name="password" value="{{ request.form.password }}"><br/>
+      <input class="btn btn-primary" type="submit" value="Login"> or 
+      <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Go back</a>
+    </form>
   </div>
 </div>
 {% endblock %}
diff --git a/app/templates/login/logout.html b/app/templates/login/logout.html
index 6428ad5..df5cc37 100644
--- a/app/templates/login/logout.html
+++ b/app/templates/login/logout.html
@@ -9,7 +9,10 @@
     <h1>Bastli Bouncer</h1>
     <br/>
     <p>You have successfully been logged out.</p>
-    <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Login</a>
+    <br/>
+    <a class="btn btn-default" role="button" href="{{ url_for('login.login') }}">Login</a>
+     or 
+    <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Home</a>
   </div>
 </div>
 {% endblock %}
diff --git a/app/templates/login/register.html b/app/templates/login/register.html
index f0cecfc..ac13ea7 100644
--- a/app/templates/login/register.html
+++ b/app/templates/login/register.html
@@ -8,8 +8,20 @@
   <div class="center-narrow">
     <h1>Bastli Bouncer</h1>
     <br/>
-    <p>Here you should see a registration form in the near future.</p>
-    <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Login</a>
+    <h2>Register a new Account</h2>
+    <br>
+    <form action="" method="post">
+      <label for="name">Name:</label><br>
+      <input type="text" id="name" name="name" value="{{ request.form.name }}"><br/>
+      <label for="email">Email:</label><br>
+      <input type="text" id="email" name="email" value="{{ request.form.email }}"><br/>
+      <label for="name">Password:</label><br>
+      <input type="password" id="password" name="password" value="{{ request.form.password }}"><br/>
+      <label for="name">Repeat Password:</label><br>
+      <input type="password" id="password2" name="password2" value="{{ request.form.password2 }}"><br/>
+      <input class="btn btn-primary" type="submit" value="Register"> or 
+      <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Go back</a>
+    </form>
   </div>
 </div>
 {% endblock %}
diff --git a/app/templates/login/register_success.html b/app/templates/login/register_success.html
new file mode 100644
index 0000000..d4fe5bb
--- /dev/null
+++ b/app/templates/login/register_success.html
@@ -0,0 +1,20 @@
+{% import "bootstrap/utils.html" as utils %}
+{% extends "base.html" %}
+{% block body %}
+<div class="content-section">
+  <br/>
+  {{ utils.flashed_messages() }}
+  <br/>
+  <div class="center-narrow">
+    <h1>Bastli Bouncer</h1>
+    <br/>
+    <h2>Registration successful!</h2>
+    <br>
+    <p>Please check the inbox of your email address and follow the insttructions.</p>
+    <p>
+      <a class="btn btn-default" role="button" href="{{ url_for('login.login') }}">Login</a>
+      <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Home</a>
+    </p>
+  </div>
+</div>
+{% endblock %}
diff --git a/instance/config.dev.py b/instance/config.dev.py
index 37f78c5..ab0915c 100644
--- a/instance/config.dev.py
+++ b/instance/config.dev.py
@@ -9,11 +9,11 @@ USER_TOKEN_TIMEOUT = timedelta(hours=12)
 SERVER_NAME = 'localhost:5000'
 
 MAIL_SERVER = 'smtp'
-MAIL_PORT = 1025
+MAIL_PORT = 1030
 MAIL_USE_TLS = False
 MAIL_USERNAME = None
 MAIL_PASSWORD = None
-MAIL_DEFAULT_SENDER = 'Bastli Bouncer <noreply@bastli.ethz.ch>'
+MAIL_DEFAULT_SENDER = ('Bastli Bouncer', 'noreply@bastli.ethz.ch')
 
 
 DEBUG = True
diff --git a/requirements.in b/requirements.in
index 15d253c..31cc083 100644
--- a/requirements.in
+++ b/requirements.in
@@ -5,9 +5,10 @@ flask
 flask-bootstrap
 flask-migrate
 flask-sqlalchemy
-flask-wtf
 Flask-Mail
 
+py3-validate-email
+
 # database connector
 pymysql
 
diff --git a/requirements.txt b/requirements.txt
index 97af918..4393a99 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,17 +7,20 @@
 alembic==1.4.2            # via flask-migrate
 blinker==1.4              # via flask-mail
 click==7.1.2              # via flask
+dnspython==1.16.0         # via py3-validate-email
 dominate==2.5.1           # via flask-bootstrap
+filelock==3.0.12          # via py3-validate-email
 flask-bootstrap==3.3.7.1  # via -r requirements.in
 flask-mail==0.9.1         # via -r requirements.in
 flask-migrate==2.5.3      # via -r requirements.in
 flask-sqlalchemy==2.4.3   # via -r requirements.in, flask-migrate
-flask-wtf==0.14.3         # via -r requirements.in
-flask==1.1.2              # via -r requirements.in, flask-bootstrap, flask-mail, flask-migrate, flask-sqlalchemy, flask-wtf
-itsdangerous==1.1.0       # via flask, flask-wtf
+flask==1.1.2              # via -r requirements.in, flask-bootstrap, flask-mail, flask-migrate, flask-sqlalchemy
+idna==2.10                # via py3-validate-email
+itsdangerous==1.1.0       # via flask
 jinja2==2.11.2            # via flask
 mako==1.1.3               # via alembic
-markupsafe==1.1.1         # via jinja2, mako, wtforms
+markupsafe==1.1.1         # via jinja2, mako
+py3-validate-email==0.2.9  # via -r requirements.in
 pymysql==0.9.3            # via -r requirements.in
 python-dateutil==2.8.1    # via alembic
 python-editor==1.0.4      # via alembic
@@ -25,4 +28,3 @@ six==1.15.0               # via python-dateutil
 sqlalchemy==1.3.18        # via alembic, flask-sqlalchemy
 visitor==0.1.3            # via flask-bootstrap
 werkzeug==1.0.1           # via flask
-wtforms==2.3.1            # via flask-wtf
-- 
GitLab