Jul 29 - Hotfix auth email sender
This commit is contained in:
+30
-9
@@ -1,5 +1,4 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort, session
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort, session
|
||||||
from urllib.parse import urlparse
|
|
||||||
from flask_login import login_user, logout_user, login_required, current_user
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
from app import db, limiter
|
from app import db, limiter
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -199,7 +198,7 @@ def profile():
|
|||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
current_user.full_name = form.full_name.data.strip() or None
|
current_user.full_name = form.full_name.data.strip() or None
|
||||||
current_user.email = form.email.data
|
current_user.email = form.email.data.strip().lower()
|
||||||
|
|
||||||
if form.new_password.data:
|
if form.new_password.data:
|
||||||
current_user.set_password(form.new_password.data)
|
current_user.set_password(form.new_password.data)
|
||||||
@@ -289,7 +288,7 @@ def create_user():
|
|||||||
user = User(
|
user = User(
|
||||||
username=form.username.data,
|
username=form.username.data,
|
||||||
full_name=form.full_name.data.strip() or None,
|
full_name=form.full_name.data.strip() or None,
|
||||||
email=form.email.data,
|
email=form.email.data.strip().lower(),
|
||||||
role=role
|
role=role
|
||||||
)
|
)
|
||||||
user.set_password(form.password.data)
|
user.set_password(form.password.data)
|
||||||
@@ -323,7 +322,7 @@ def edit_user(user_id):
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user.username = form.username.data
|
user.username = form.username.data
|
||||||
user.full_name = form.full_name.data.strip() or None
|
user.full_name = form.full_name.data.strip() or None
|
||||||
user.email = form.email.data
|
user.email = form.email.data.strip().lower()
|
||||||
|
|
||||||
if not director_editing:
|
if not director_editing:
|
||||||
user.role = form.role.data
|
user.role = form.role.data
|
||||||
@@ -536,7 +535,6 @@ def _send_password_reset_email(user, token, base_url=None):
|
|||||||
from flask import current_app, render_template_string, url_for as _url_for
|
from flask import current_app, render_template_string, url_for as _url_for
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from app import mail
|
from app import mail
|
||||||
from urllib.parse import urlparse
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
if not current_app.config.get('MAIL_SERVER'):
|
if not current_app.config.get('MAIL_SERVER'):
|
||||||
@@ -545,8 +543,17 @@ def _send_password_reset_email(user, token, base_url=None):
|
|||||||
|
|
||||||
effective_base = (base_url or current_app.config.get('APP_BASE_URL', '')).rstrip('/')
|
effective_base = (base_url or current_app.config.get('APP_BASE_URL', '')).rstrip('/')
|
||||||
reset_link = f'{effective_base}{_url_for("auth.reset_password", token=token)}'
|
reset_link = f'{effective_base}{_url_for("auth.reset_password", token=token)}'
|
||||||
host = urlparse(effective_base).netloc or 'janitorialqc.local'
|
|
||||||
sender = f'noreply@{host}'
|
# Branded From as a (display_name, address) tuple, exactly as
|
||||||
|
# customers._send_invite_email does. The display NAME tracks the tenant; the
|
||||||
|
# ADDRESS is branded only for DNS-authorized domains and otherwise stays the
|
||||||
|
# authenticated SMTP identity so the mail still delivers. The previous
|
||||||
|
# `noreply@{host}` sent from whatever host the browser was on, which is not
|
||||||
|
# an authorized sender for that domain — the mail server accepted it and it
|
||||||
|
# was then dropped downstream by SPF/DMARC. See app/utils/mail_utils.py and
|
||||||
|
# CLAUDE.md rule 64.
|
||||||
|
from app.utils.mail_utils import branded_sender
|
||||||
|
sender = branded_sender(effective_base)
|
||||||
|
|
||||||
html_body = render_template_string("""<!DOCTYPE html>
|
html_body = render_template_string("""<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@@ -612,12 +619,26 @@ def forgot_password():
|
|||||||
|
|
||||||
form = ForgotPasswordForm()
|
form = ForgotPasswordForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query.filter_by(email=form.email.data.strip().lower()).first()
|
# Explicit case-insensitive lookup. This is NOT fixing a live bug: no
|
||||||
|
# table here declares a COLLATE, so `users.email` inherits the utf8mb4
|
||||||
|
# default (utf8mb4_general_ci / utf8mb4_0900_ai_ci), both of which are
|
||||||
|
# case-insensitive — a bare `== lower(input)` already matched a
|
||||||
|
# mixed-case stored address. The point is to stop depending on that
|
||||||
|
# server default: under a binary/_bin collation the bare comparison
|
||||||
|
# would silently find nothing and still show the success message below.
|
||||||
|
email_input = form.email.data.strip().lower()
|
||||||
|
user = User.query.filter(
|
||||||
|
db.func.lower(User.email) == email_input
|
||||||
|
).first()
|
||||||
if user and user.active:
|
if user and user.active:
|
||||||
token = user.generate_set_password_token(expires_hours=1)
|
token = user.generate_set_password_token(expires_hours=1)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
_send_password_reset_email(user, token, base_url=request.host_url)
|
_send_password_reset_email(user, token, base_url=request.host_url)
|
||||||
logger.info('AUTH | forgot_password | user=%s | email=%s', user.username, user.email)
|
logger.info('AUTH | forgot_password | reset link dispatched | user=%s | email=%s',
|
||||||
|
user.username, user.email)
|
||||||
|
else:
|
||||||
|
# No leak to the user (generic message below), but log for diagnosis.
|
||||||
|
logger.info('AUTH | forgot_password | no active account for email=%s', email_input)
|
||||||
# Always show the same message — never reveal whether the email exists
|
# Always show the same message — never reveal whether the email exists
|
||||||
flash(
|
flash(
|
||||||
'If an account with that email address exists, a password reset link '
|
'If an account with that email address exists, a password reset link '
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ def edit(customer_id):
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
customer.username = form.username.data
|
customer.username = form.username.data
|
||||||
customer.full_name = form.full_name.data.strip() or None
|
customer.full_name = form.full_name.data.strip() or None
|
||||||
customer.email = form.email.data
|
customer.email = form.email.data.strip().lower()
|
||||||
if form.password.data:
|
if form.password.data:
|
||||||
customer.set_password(form.password.data)
|
customer.set_password(form.password.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user