7 Commits

Author SHA1 Message Date
c7395b07d9 Move logout route to auth blueprint
The logout route was relocated from the main app to the auth blueprint for better organization. The logout link in the base template was updated to reference the new route location.
2025-08-11 18:05:30 +02:00
97ff4acf02 Add change password functionality for users
Introduces a change password route, form, and template, allowing authenticated users to update their password. Updates the User model with a method to set the must_change_password flag. Adjusts login and navigation logic to support the new flow and ensures users are redirected to change their password if required.
2025-08-11 18:03:18 +02:00
0da580faf1 Refactor login flow to use auth blueprint
Moved login route and logic from app.py to application/auth/routes.py under the 'auth' blueprint. Updated all references to the login route to use 'auth.login'. Added a dedicated login.html template under application/auth/templates. Adjusted login_required utility and default_return logic for consistency.
2025-08-11 17:43:46 +02:00
ea2ea27d9e Refactor login_required and add auth blueprint
Moved the login_required logic to a new utils.py for reuse. Added a new auth blueprint and registered it in app.py. Updated user blueprint to use the shared login_required function.
2025-08-11 17:33:47 +02:00
cd9ae72864 Reapply "Add must_change_password field to User model"
This reverts commit 47241e341e.
2025-08-11 17:20:45 +02:00
47241e341e Revert "Add must_change_password field to User model"
This reverts commit 42747df92f.
2025-08-11 17:17:30 +02:00
42747df92f Add must_change_password field to User model
Introduces a new boolean column 'must_change_password' to the User model and database schema. This field enforces password change requirements for users and is included in the model's constructor and migration.
2025-08-11 17:17:10 +02:00
11 changed files with 233 additions and 41 deletions

36
app.py
View File

@@ -1,28 +1,25 @@
from flask import (
render_template,
redirect,
url_for,
request,
send_from_directory,
)
from flask_login import (
login_required,
logout_user,
login_user,
current_user,
)
from forms import LoginForm
from models import User
from application import db, app, login_manager
from application.admin.routes import admin_bp
from application.user.routes import user_bp
from application.add_meal.routes import bp as add_meal_bp
from application.auth.routes import bp as auth_bp
from typing import Optional
# Config
app.config["SECRET_KEY"] = "Stef123"
login_manager.login_view = "login" # type: ignore
login_manager.login_view = "auth.login" # type: ignore
@login_manager.user_loader # type: ignore
@@ -34,6 +31,7 @@ def load_user(user_id: int):
app.register_blueprint(admin_bp)
app.register_blueprint(user_bp)
app.register_blueprint(add_meal_bp)
app.register_blueprint(auth_bp)
# Routes
@@ -49,7 +47,7 @@ def default_return(next_page: Optional[str] = None):
@app.route("/")
@login_required
def index():
return redirect(url_for("login"))
return redirect(url_for("auth.login"))
@app.route("/favicon.ico")
@@ -57,32 +55,6 @@ def favicon():
return send_from_directory("static", "favicon.ico")
@app.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return default_return()
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(password=form.password.data):
# User found and password correct
next_page = request.args.get("next") # Get next page if given
login_user(user) # Log in the user
return default_return(next_page=next_page)
else:
pass
# invalid user
return render_template("login.html", form=form)
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("index"))
# Run
if __name__ == "__main__":
# If there are no users, create admin account

View File

@@ -28,7 +28,7 @@ bp = Blueprint(
@bp.before_request
def login_required():
if not current_user.is_authenticated:
return redirect(url_for("login"))
return redirect(url_for("auth.login"))
@bp.route("/select_meal/<int:meal_type>", methods=["GET"])

View File

@@ -0,0 +1,59 @@
from flask import Blueprint, request, render_template, redirect, url_for
from flask_login import current_user, login_user, logout_user
from forms import LoginForm, ChangePasswordForm
from models import User
from application.utils import default_return
from application import db
bp = Blueprint(
"auth",
__name__,
template_folder="templates",
)
@bp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return default_return()
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(password=form.password.data):
# User found and password correct
next_page = request.args.get("next") # Get next page if given
login_user(user) # Log in the user
return default_return(next_page=next_page)
else:
pass
# invalid user
return render_template("login.html", form=form)
@bp.route("/change_password", methods=["GET", "POST"])
def change_password():
if not current_user.is_authenticated:
return redirect(url_for("auth.login"))
form = ChangePasswordForm()
if form.validate_on_submit():
cur_check = current_user.check_password(
password=form.current_password.data
)
eq_check = form.new_password.data == form.confirm_password.data
if cur_check and eq_check:
current_user.change_password(form.new_password.data)
current_user.set_pw_change(False)
db.session.commit()
return default_return()
return render_template("change_password.html", form=form)
@bp.route("/logout")
def logout():
if not current_user.is_authenticated:
return redirect(url_for("auth.login"))
logout_user()
return redirect(url_for("index"))

View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block content %}
<div class="container d-flex justify-content-center align-items-center">
<div class="card shadow-sm p-4" style="width: 100%; max-width: 400px;">
<h3 class="mb-4 text-center">Login</h3>
<form method="post">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.current_password.label(class="form-label") }}
{{ form.current_password(class="form-control", placeholder="") }}
{% if form.current_password.errors %}
<div class="text-danger small">
{{ form.current_password.errors[0] }}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.new_password.label(class="form-label") }}
{{ form.new_password(class="form-control", placeholder="Enter password") }}
{% if form.new_password.errors %}
<div class="text-danger small">
{{ form.new_password.errors[0] }}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control", placeholder="Enter password") }}
{% if form.confirm_password.errors %}
<div class="text-danger small">
{{ form.confirm_password.errors[0] }}
</div>
{% endif %}
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-primary btn-lg") }}
</div>
</form>
</div>
</div>
{% endblock%}

View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<div class="container d-flex justify-content-center align-items-center">
<div class="card shadow-sm p-4" style="width: 100%; max-width: 400px;">
<h3 class="mb-4 text-center">Login</h3>
<form method="post">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control", placeholder="Enter username") }}
{% if form.username.errors %}
<div class="text-danger small">
{{ form.username.errors[0] }}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control", placeholder="Enter password") }}
{% if form.password.errors %}
<div class="text-danger small">
{{ form.password.errors[0] }}
</div>
{% endif %}
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-primary btn-lg") }}
</div>
</form>
</div>
</div>
{% endblock%}

View File

@@ -32,11 +32,11 @@
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
</li>
{% endif %}
<li class="nav-item">

View File

@@ -12,6 +12,7 @@ from application import db
from forms import FoodItemForm
from models import FoodItem, FoodLog
from datetime import datetime, timezone, timedelta
from application.utils import login_required
user_bp = Blueprint(
"user",
@@ -20,10 +21,7 @@ user_bp = Blueprint(
)
@user_bp.before_request
def login_required():
if not current_user.is_authenticated:
return redirect(url_for("login"))
user_bp.before_request(login_required)
@user_bp.route("/dashboard", methods=["GET"])

21
application/utils.py Normal file
View File

@@ -0,0 +1,21 @@
from flask_login import current_user
from flask import redirect, url_for, flash
from typing import Optional
def login_required():
if not current_user.is_authenticated:
return redirect(url_for("auth.login"))
if current_user.must_change_password:
flash("You have to change your password")
return redirect(url_for("auth.change_password"))
return
def default_return(next_page: Optional[str] = None):
return redirect(url_for("user.daily_log"))
if next_page:
return redirect(next_page)
if current_user.is_admin:
return redirect(url_for("admin.food_items"))
return redirect(url_for("dashboard"))

View File

@@ -11,7 +11,18 @@ from wtforms.validators import DataRequired, InputRequired, Optional
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Login")
submit = SubmitField("Log in")
class ChangePasswordForm(FlaskForm):
current_password = PasswordField(
"Current password", validators=[DataRequired()]
)
new_password = PasswordField("New password", validators=[DataRequired()])
confirm_password = PasswordField(
"Confirm new password", validators=[DataRequired()]
)
submit = SubmitField("Change password")
class FoodItemForm(FlaskForm):

View File

@@ -0,0 +1,40 @@
"""empty message
Revision ID: 101002a6ef17
Revises: dea130d45cec
Create Date: 2025-08-11 17:16:34.617851
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "101002a6ef17"
down_revision = "dea130d45cec"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"must_change_password",
sa.Boolean(),
nullable=False,
server_default="1",
)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_column("must_change_password")
# ### end Alembic commands ###

View File

@@ -12,17 +12,23 @@ class User(UserMixin, db.Model):
username = db.Column(db.String(150), unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False)
must_change_password = db.Column(db.Boolean, nullable=False, default=False)
food_items = db.relationship("FoodItem", lazy="dynamic", backref="user")
food_logs = db.relationship("FoodLog", lazy="dynamic", backref="user")
def __init__(
self, username: str, password: str, is_admin: bool = False
self,
username: str,
password: str,
is_admin: bool = False,
must_change_password: bool = False,
) -> None:
super().__init__()
self.username = username
self.password = generate_password_hash(password=password)
self.is_admin = is_admin
self.must_change_password = must_change_password
def check_password(self, password: str) -> bool:
return check_password_hash(pwhash=self.password, password=password)
@@ -30,6 +36,9 @@ class User(UserMixin, db.Model):
def change_password(self, password: str) -> None:
self.password = generate_password_hash(password=password)
def set_pw_change(self, change: bool) -> None:
self.must_change_password = change
class Unit(db.Model):
__tablename__ = "unit"