diff --git a/.gitignore b/.gitignore index 5ac73e1..f7f20ae 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,6 @@ cython_debug/ db/db.sqlite /HabitTracker.iml + +static/profile_images/* +!/static/profile_images/no_avatar/ diff --git a/ER.dia b/ER.dia index 2e41641..5d00e0b 100644 Binary files a/ER.dia and b/ER.dia differ diff --git a/ER.png b/ER.png index 77f8f50..49ebe6e 100644 Binary files a/ER.png and b/ER.png differ diff --git a/UML.dia b/UML.dia index fd38487..54da21e 100644 Binary files a/UML.dia and b/UML.dia differ diff --git a/UML.png b/UML.png index 1770aa7..2515778 100644 Binary files a/UML.png and b/UML.png differ diff --git a/app.py b/app.py index 5419869..7387f4b 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,14 @@ import datetime import hashlib +import os +from PIL import Image -from flask import Flask, render_template, redirect, url_for, request +from flask import Flask, render_template, redirect, url_for, request, jsonify from flask_login import login_required, LoginManager, login_user, logout_user, current_user from models.Habit import Habit from models.HabitList import HabitList -from models.HabitTrackings import HabitTrackings +from models.HabitTracking import HabitTracking from models.User import User from utils import anonymous_required @@ -136,6 +138,7 @@ def index(): title=name, utc_dt=datetime.datetime.now().strftime("%d.%m.%Y %H:%M %A"), habit_lists=habit_lists, + heatmap_values=current_user.get_heatmap(), errors={}, ) @@ -270,7 +273,7 @@ def profile(): "profile.html", name=current_user.name, email=current_user.email, - errors={} + profile_image_url=current_user.profile_image, ) @@ -279,34 +282,37 @@ def profile(): def profile_change(): newName = request.form.get('newName') newEmail = request.form.get('newEmail') - newPassword = request.form.get('newPassword') - oldPassword = request.form.get('oldPassword') - - # Check for errors - errors = {} - if not newName: - errors['newName'] = 'Der Name ist erforderlich.' - - if not newEmail: - errors['newEmail'] = 'Die E-Mail Adresse ist erforderlich.' - - if not oldPassword: - errors['oldPassword'] = 'Du musst dein aktuelles Passwort angeben.' - else: - if hashlib.sha256(oldPassword.encode()).hexdigest() != current_user.password: - errors['oldPassword'] = 'Das Passwort ist falsch.' - - if errors: - return render_template( - "profile.html", - name=current_user.name, - email=current_user.email, - errors=errors - ) # Update user current_user.name = newName current_user.email = newEmail + current_user.update() + + # Back to profile + return render_template( + "profile.html", + name=current_user.name, + email=current_user.email, + profile_image_url=current_user.profile_image, + ) + +@app.route('/check_password', methods=['POST']) +@login_required +def check_password(): + # Get the password sent from the client + password = request.json.get('password') + + if hashlib.sha256(password.encode()).hexdigest() == current_user.password: + return jsonify({"valid": True}) + else: + return jsonify({"valid": False}) + +@app.route('/password', methods=['POST']) +@login_required +def password_change(): + newPassword = request.form.get('newPassword') + + # Update user if newPassword: current_user.password = hashlib.sha256(newPassword.encode()).hexdigest() current_user.update() @@ -316,6 +322,66 @@ def profile_change(): "profile.html", name=current_user.name, email=current_user.email, + profile_image_url=current_user.profile_image, + ) + +UPLOAD_FOLDER = 'static/profile_images/' # Folder to store profile images +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + +def save_profile_image(image_file): + filename = image_file.filename + if '.' not in filename: + # Ensure the filename has an extension + raise ValueError("Invalid filename") + + # Check if the file extension is allowed + allowed_extensions = {'jpg', 'jpeg', 'png', 'gif'} + file_extension = filename.rsplit('.', 1)[1].lower() + if file_extension not in allowed_extensions: + raise ValueError("Invalid file extension") + + # Get the filename from the image path saved in the user + filename = os.path.basename(current_user.profile_image) + + # Open the uploaded image + image = Image.open(image_file) + + # Convert the image to RGB mode (required for JPEG) + image = image.convert('RGB') + + # Determine the size of the square image + min_dimension = min(image.size) + square_size = (min_dimension, min_dimension) + + # Calculate the coordinates for cropping + left = (image.width - min_dimension) / 2 + top = (image.height - min_dimension) / 2 + right = (image.width + min_dimension) / 2 + bottom = (image.height + min_dimension) / 2 + + # Crop the image to a square and resize it + image = image.crop((left, top, right, bottom)) + image.thumbnail(square_size) + image = image.resize((256, 256)) + + # Save the processed image + image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + image.save(image_path, 'JPEG', quality=100) + +@app.route('/upload', methods=['POST']) +def upload_profile_image(): + if 'file' not in request.files: + return 'No file part' + + file = request.files['file'] + save_profile_image(file) + + # Back to profile + return render_template( + "profile.html", + name=current_user.name, + email=current_user.email, + profile_image_url=current_user.profile_image, errors={} ) @@ -344,7 +410,7 @@ def check_habit(): delete_tracking = tracking if not delete_tracking: - HabitTrackings.create(habit_id, 1) + HabitTracking.create(habit_id) else: delete_tracking.delete() @@ -374,7 +440,6 @@ def delete_habit(): habit.delete() return {} - @app.route('/reorder', methods=['POST']) @login_required def reorder_habits(): @@ -396,4 +461,4 @@ def reorder_habits(): # Run the application if __name__ == '__main__': - app.run(port=5000, debug=True) + app.run(port=5000, debug=True) \ No newline at end of file diff --git a/db/SQLiteClient.py b/db/SQLiteClient.py index 9c7d21b..fb183f3 100644 --- a/db/SQLiteClient.py +++ b/db/SQLiteClient.py @@ -1,6 +1,8 @@ from datetime import datetime, timedelta import hashlib import sqlite3 +import os +import shutil def con3(): @@ -8,18 +10,40 @@ def con3(): return conn -### User.py ### -def create_user(name: str, email: str, password: str): +### User ### +def create_user_profile_image(user_id): + script_directory = os.path.dirname(os.path.abspath(__file__)) + source_path = os.path.join(script_directory, '../static/profile_images/no_avatar/user.jpg') + destination_directory = os.path.join(script_directory, '../static/profile_images/') + new_filename = f'user{user_id}-profile.jpg' + + destination_path = os.path.join(destination_directory, new_filename) + shutil.copyfile(source_path, destination_path) + + # Just save the part after static + static_index = destination_path.find('static') + relative_destination_path = destination_path[static_index:] + + return relative_destination_path + + +def create_user(name: str, email: str, password: str, profile_image:str = None): password = hashlib.sha256(password.encode()).hexdigest() now = datetime.now().isoformat() - query = (f"INSERT INTO users (name, email, password, created_at, updated_at) VALUES ('{name}', '{email}', " - f"'{password}', '{now}', '{now}');") + query = (f"INSERT INTO users (name, email, password, profile_image, created_at, updated_at) VALUES " + f"('{name}', '{email}', '{password}', '{profile_image}', '{now}', '{now}');") conn = con3() cursor = conn.cursor() cursor.execute(query) + + id = cursor.lastrowid + profile_image = create_user_profile_image(id) + query2 = f"UPDATE users SET profile_image = '{profile_image}' WHERE id = '{id}';" + cursor.execute(query2) + conn.commit() conn.close() - return cursor.lastrowid + return id, profile_image def get_user(id: int): @@ -42,12 +66,13 @@ def get_user_by_email(email: str): return user -def update_user(id: int, name: str, email: str, password: str = None): +def update_user(id: int, name: str, email: str, password: str): now = datetime.now().isoformat() if password: - query = f"UPDATE users SET name = '{name}', email = '{email}', password = '{password}', updated_at = '{now}' WHERE id = {id};" + query = (f"UPDATE users SET name = '{name}', email = '{email}', password = '{password}', updated_at = '{now}' " + f"WHERE id = {id};") else: - query = f"UPDATE users SET name = '{name}', email = '{email}', updated_at = '{now}' WHERE id = {id};" + query = f"UPDATE users SET name = '{name}', email = '{email}', updated_at = '{now}' WHERE id = {id};" conn = con3() cursor = conn.cursor() cursor.execute(query) @@ -57,22 +82,20 @@ def update_user(id: int, name: str, email: str, password: str = None): def delete_user(id: int): - query = f"DELETE FROM habit_lists WHERE (SELECT list_id FROM habit_users WHERE user_id = {id}) = id;" - query2 = f"DELETE FROM users WHERE id = {id};" + query = f"DELETE FROM users WHERE id = {id};" conn = con3() cursor = conn.cursor() cursor.execute(query) - cursor.execute(query2) conn.commit() conn.close() return cursor.lastrowid -### Habit.py ### -def create_habit(list_id: int, name: str, times: int, unit: int, slot: int, note: str | None=None): +### Habit ### +def create_habit(list_id: int, name: str, note: str, times: int, unit: int, slot: int): now = datetime.now().isoformat() - query = (f"INSERT INTO habits (list_id, name, note, times, unit, slot, created_at, updated_at) VALUES ('{list_id}', " - f"'{name}', '{note}', '{times}', '{unit}', '{slot}', '{now}', '{now}');") + query = (f"INSERT INTO habits (list_id, name, note, times, unit, slot, created_at, updated_at) " + f"VALUES ('{list_id}', '{name}', '{note}', '{times}', '{unit}', '{slot}', '{now}', '{now}');") conn = con3() cursor = conn.cursor() cursor.execute(query) @@ -168,7 +191,8 @@ def update_slot(id: int, slot: int): def update_habit(id: int, name: str, note: str, times: int, unit: int): now = datetime.now().isoformat() - query = f"UPDATE habits SET name = {name}, note = {note}, times = {times}, unit = {unit}, updated_at = '{now}' WHERE id = {id};" + query = (f"UPDATE habits SET name = {name}, note = {note}, times = {times}, unit = {unit}, updated_at = '{now}' " + f"WHERE id = {id};") conn = con3() cursor = conn.cursor() cursor.execute(query) @@ -186,8 +210,8 @@ def delete_habit(id: int): conn.close() -### HabitTrackings.py ### -def create_habitTrackings(habit_id: int): +### HabitTracking ### +def create_habitTracking(habit_id: int): now = datetime.now().isoformat() query = f"INSERT INTO habit_trackings (habit_id, created_at) VALUES ('{habit_id}','{now}');" conn = con3() @@ -198,7 +222,7 @@ def create_habitTrackings(habit_id: int): return cursor.lastrowid -def get_habitTrackings(id: int): +def get_habitTracking(id: int): query = f"SELECT * FROM habit_trackings WHERE id = {id};" conn = con3() cursor = conn.cursor() @@ -208,7 +232,7 @@ def get_habitTrackings(id: int): return habit_tracking -def get_habitTrackings_by_habit_id(habit_id: int): +def get_habitTrackings(habit_id: int): query = f"SELECT * FROM habit_trackings WHERE habit_id = {habit_id};" conn = con3() cursor = conn.cursor() @@ -218,7 +242,7 @@ def get_habitTrackings_by_habit_id(habit_id: int): return habit_trackings -def delete_habitTrackings(id: int): +def delete_habitTracking(id: int): query = f"DELETE FROM habit_trackings WHERE id = {id};" conn = con3() cursor = conn.cursor() @@ -227,7 +251,7 @@ def delete_habitTrackings(id: int): conn.close() -### HabitList.py ### +### HabitList ### def create_habitList(user_id: int, name: str, description: str): now = datetime.now().isoformat() query = (f"INSERT INTO habit_lists (name, description, created_at, updated_at) " @@ -264,6 +288,37 @@ def get_habitLists(user_id: int): return habit_lists +def get_users(list_id: int): + query = (f"SELECT users.* FROM users JOIN habit_users ON users.id = habit_users.user_id WHERE " + f"habit_users.list_id = {list_id};") + conn = con3() + cursor = conn.cursor() + cursor.execute(query) + users = cursor.fetchall() + conn.close() + return users + + +def add_user(list_id: int, user_id: int): + now = datetime.now().isoformat() + query = (f"INSERT INTO habit_users (user_id, list_id, created_at, updated_at)" + f" VALUES ('{user_id}', '{list_id}', '{now}', '{now}');") + conn = con3() + cursor = conn.cursor() + cursor.execute(query) + conn.commit() + conn.close() + + +def remove_user(list_id: int, user_id: int): + query = f"DELETE FROM habit_lists WHERE user_id = {user_id} AND list_id = {list_id};" + conn = con3() + cursor = conn.cursor() + cursor.execute(query) + conn.commit() + conn.close() + + def update_habitList(id: int, name: str, description: str): now = datetime.now().isoformat() query = f"UPDATE habit_lists SET name = {name}, description = {description}, updated_at = '{now}' WHERE id = {id};" @@ -284,16 +339,6 @@ def delete_habitList(id: int): conn.close() -def get_users(list_id: int): - query = f"SELECT users.* FROM users JOIN habit_users ON users.id = habit_users.user_id WHERE habit_users.list_id = {list_id};" - conn = con3() - cursor = conn.cursor() - cursor.execute(query) - users = cursor.fetchall() - conn.close() - return users - - if __name__ == "__main__": habits = get_habits(1) for habit in habits: diff --git a/db/migrations/1708105700_delete_users_table.sql b/db/migrations/1708105700_delete_users_table.sql new file mode 100644 index 0000000..441087a --- /dev/null +++ b/db/migrations/1708105700_delete_users_table.sql @@ -0,0 +1 @@ +DROP TABLE users; \ No newline at end of file diff --git a/db/migrations/1708105768_create_users_table.sql b/db/migrations/1708105768_create_users_table.sql new file mode 100644 index 0000000..5210527 --- /dev/null +++ b/db/migrations/1708105768_create_users_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS users +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT NOT NULL, + password TEXT NOT NULL, + profile_image TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); diff --git a/models/HabitList.py b/models/HabitList.py index d59e265..bdff3f5 100644 --- a/models/HabitList.py +++ b/models/HabitList.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -from datetime import date, datetime -from db.SQLiteClient import delete_habitList, create_habitList, get_habitList, get_habits, get_users from models.Habit import Habit from models.User import User +from db.SQLiteClient import (create_habitList, get_habitList, get_habits, get_users, add_user, remove_user, + update_habitList, delete_habitList) @dataclass @@ -11,24 +11,34 @@ class HabitList: id: int name: str description: str - created_at: date - updated_at: date - habits: list = None + habits: list = None #? unclear usage @staticmethod def create(user_id: int, name: str, description: str): id = create_habitList(user_id, name, description) - return HabitList(id, name, description, datetime.now(), datetime.now()) + return HabitList(id, name, description) @staticmethod def get(id: int): habitList = get_habitList(id) - return HabitList(habitList[0], habitList[1], habitList[2], datetime.strptime(habitList[3], "%Y-%m-%dT%H:%M:%S.%f"), datetime.strptime(habitList[4], "%Y-%m-%dT%H:%M:%S.%f")) if habitList else None + return HabitList(habitList[0], habitList[1], habitList[2]) if habitList else None - def delete(self): - delete_habitList(self.id) - def get_habits(self): + # Updates: name, description + def update(self): + update_habitList(self.id, self.name, self.description) + + + # Deletes the HabitList | The id of the current user is necessary + def delete(self, user_id): + if len(get_users) > 1: + self.remove_user(user_id) + else: + delete_habitList(self.id) + + + # Returns the Habits connected with the HabitList + def get_habits(self) -> list: raw_habits = get_habits(self.id) habits = [] for habit in raw_habits: @@ -37,7 +47,9 @@ class HabitList: return habits - def get_users(self): + + # Returns the Users connected with the HabitList + def get_users(self) -> list: raw_users = get_users(self.id) users = [] for user in raw_users: @@ -45,3 +57,14 @@ class HabitList: users.append(user) return users + + + # Adds a User by email to the HabitList + def add_user(self, email: str): + user = User.get_by_email(email) + add_user(self.id, user.id) + + + # Removes a User from the HabitList + def remove_user(self, user_id): + remove_user(self.id, user_id) diff --git a/models/HabitTracking.py b/models/HabitTracking.py new file mode 100644 index 0000000..76ee855 --- /dev/null +++ b/models/HabitTracking.py @@ -0,0 +1,26 @@ +from datetime import date, datetime +from dataclasses import dataclass +from db.SQLiteClient import create_habitTracking, get_habitTracking, delete_habitTracking + + +@dataclass +class HabitTracking: + id: int + habit_id: int + created_at: date + + @staticmethod + def create(habit_id: int): + id = create_habitTracking(habit_id) + return HabitTracking(id, habit_id, datetime.now()) + + @staticmethod + def get(id: int): + habitTrackings = get_habitTracking(id) + return HabitTracking(habitTrackings[0], habitTrackings[1], + datetime.strptime(habitTrackings[2], "%Y-%m-%dT%H:%M:%S.%f")) \ + if habitTrackings else None + + # Deletes the HabitTracking + def delete(self): + delete_habitTracking(self.id) diff --git a/models/HabitTrackings.py b/models/HabitTrackings.py deleted file mode 100644 index 9ad30f1..0000000 --- a/models/HabitTrackings.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses import dataclass -from datetime import date, datetime - -from db.SQLiteClient import create_habitTrackings, get_habitTrackings, delete_habitTrackings - - -@dataclass -class HabitTrackings: - id: int - habit_id: int - created_at: date - - @staticmethod - def create(habit_id: int, times: int): - id = create_habitTrackings(habit_id) - return HabitTrackings(id, habit_id, datetime.now()) - - @staticmethod - def get(id: int): - habitTrackings = get_habitTrackings(id) - return HabitTrackings(habitTrackings[0], habitTrackings[1], datetime.strptime(habitTrackings[2], "%Y-%m-%dT%H:%M:%S.%f")) if habitTrackings else None - - def delete(self): - delete_habitTrackings(self.id) diff --git a/models/User.py b/models/User.py index 13f9f72..d455980 100644 --- a/models/User.py +++ b/models/User.py @@ -1,52 +1,75 @@ from datetime import datetime from flask_login import UserMixin -from db.SQLiteClient import create_user, get_user, get_user_by_email, delete_user, update_user, \ - get_habitLists, get_heatmap_value +from db.SQLiteClient import (create_user, get_user, get_user_by_email, update_user, delete_user, + get_habitLists, get_heatmap_value) class User(UserMixin): - def __init__(self, id: int, name: str, email: str, password: str | None = None): + def __init__(self, id: int, name: str, email: str, password: str = None, profile_image:str = None): self.id = id self.name = name self.email = email self.password = password + self.profile_image = profile_image + @staticmethod def create(name: str, email: str, password: str): - id = create_user(name, email, password) - return User(id, name, email) + id, profile_image = create_user(name, email, password) + return User(id=id, name=name, email=email, profile_image=profile_image) @staticmethod def get(id: int): user = get_user(id) - return User(user[0], user[1], user[2], user[3]) if user else None + return User(user[0], user[1], user[2], user[3], user[4]) if user else None @staticmethod def get_by_email(email: str): user = get_user_by_email(email) - return User(user[0], user[1], user[2], user[3]) if user else None + return User(user[0], user[1], user[2], user[3], user[4]) if user else None + + # Updates: name, email, password def update(self): update_user(self.id, self.name, self.email, self.password if self.password else None) + + # Deletes the User def delete(self): + # calls the deletion of the users habitLists + habitLists = self.get_habitLists() + for habitList in habitLists: + habitList.delete(self.id) + + # deletes the user delete_user(self.id) - def get_habitLists(self): + + # Returns all HabitLists connected with the user + def get_habitLists(self) -> list: from models.HabitList import HabitList raw_habitLists = get_habitLists(self.id) habitLists = [] for habitList in raw_habitLists: - habitList = HabitList(habitList[0], habitList[1], habitList[2], datetime.strptime(habitList[3], "%Y-%m-%dT%H:%M:%S.%f"), datetime.strptime(habitList[4], "%Y-%m-%dT%H:%M:%S.%f")) + habitList = HabitList(habitList[0], habitList[1], habitList[2]) habitLists.append(habitList) return habitLists - def get_heatmap(self): + + # Returns all heatmap-values from the last 28 days + def get_heatmap(self) -> list: + # get current day of week as integer. monday is 0 and sunday is 6 + weekday = datetime.today().weekday() heatmap = [] - for day in range (0, 27): + + # append the heatmap values of the current week + for day in range(0, weekday): + heatmap.append(0) + + for day in range (0, 28): value = get_heatmap_value(self.id, day) heatmap.append(value) return heatmap \ No newline at end of file diff --git a/static/main.css b/static/css/background.css similarity index 100% rename from static/main.css rename to static/css/background.css diff --git a/static/css/profile.css b/static/css/profile.css new file mode 100644 index 0000000..af7832c --- /dev/null +++ b/static/css/profile.css @@ -0,0 +1,73 @@ + /* Profile image */ +.profile-image-container { + position: relative; + width: 150px; /* Adjust the size as needed */ + height: 150px; /* Adjust the size as needed */ +} + +.profile-image { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.profile-image-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); /* Grey overlay */ + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + color: white; + font-size: 16px; + opacity: 0; /* Initially hidden */ + transition: opacity 0.3s ease; + cursor: pointer; +} + +.profile-image-overlay:hover { + opacity: 1; /* Show overlay on hover */ +} + +.profile-image-overlay span { + /* Center the text and make it bold */ + text-align: center; + font-weight: bold; +} + +.profile-image-overlay:hover span { + /* Style the text when hovering */ + color: white; +} + + + /* Edit-Modal Close Button */ +.close-icon { + fill: #aaa; + cursor: pointer; +} + +.close-icon:hover { + fill: #777; /* Change the color to whatever you'd like on hover */ +} + + +.fade-out { + -webkit-animation: fadeOut 3s forwards; + animation: fadeOut 3s forwards; +} + +@keyframes fadeOut { + 0% { opacity: 1; } + 100% { opacity: 0; display: none; } +} + +@-webkit-keyframes fadeOut { + 0% { opacity: 1; } + 100% { opacity: 0; display: none; } +} \ No newline at end of file diff --git a/static/css/symbols/eye-closed.svg b/static/css/symbols/eye-closed.svg new file mode 100644 index 0000000..1176af5 --- /dev/null +++ b/static/css/symbols/eye-closed.svg @@ -0,0 +1 @@ +324324Created with Sketch.Created by Sonya Nikolaevafrom the Noun Project \ No newline at end of file diff --git a/static/css/symbols/eye-opened.svg b/static/css/symbols/eye-opened.svg new file mode 100644 index 0000000..9caef02 --- /dev/null +++ b/static/css/symbols/eye-opened.svg @@ -0,0 +1 @@ +123123Created with Sketch.Created by Sonya Nikolaevafrom the Noun Project \ No newline at end of file diff --git a/static/profile_images/no_avatar/user.jpg b/static/profile_images/no_avatar/user.jpg new file mode 100644 index 0000000..dc70cad Binary files /dev/null and b/static/profile_images/no_avatar/user.jpg differ diff --git a/templates/layouts/main.html b/templates/layouts/main.html index c375f6a..4077001 100644 --- a/templates/layouts/main.html +++ b/templates/layouts/main.html @@ -6,12 +6,24 @@ {{ title }} - HabitTracker - + + + + + + + + + + + + + @@ -51,9 +63,6 @@
{% block content %} {% endblock %} - - -
\ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html index e5f81e4..06f64bd 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -2,45 +2,289 @@ {% block content %} -

Account Einstellungen👤

+
+

Account Einstellungen👤

- -
-
- - -
- {{ errors.get('newName', '') }} + +
+
+
+
Profilbild
+
+ Profile Image +
+ Profilbild aktualisieren +
+
+
+ + + +
+
+
+
Name
+

{{ name }}

+
Email
+

{{ email }}

+ +
-
- - -
- {{ errors.get('newEmail', '') }} + +
+
+
Passwort ändern
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+
-
- - -
- {{ errors.get('newPassword', '') }} + + + -
- - -
- {{ errors.get('oldPassword', '') }} -
-
+ {% endblock %} \ No newline at end of file