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 urllib.parse import urlparse
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from app import db, limiter
|
||||
from app.models.user import User
|
||||
@@ -199,7 +198,7 @@ def profile():
|
||||
|
||||
if form.validate_on_submit():
|
||||
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:
|
||||
current_user.set_password(form.new_password.data)
|
||||
@@ -289,7 +288,7 @@ def create_user():
|
||||
user = User(
|
||||
username=form.username.data,
|
||||
full_name=form.full_name.data.strip() or None,
|
||||
email=form.email.data,
|
||||
email=form.email.data.strip().lower(),
|
||||
role=role
|
||||
)
|
||||
user.set_password(form.password.data)
|
||||
@@ -323,7 +322,7 @@ def edit_user(user_id):
|
||||
if form.validate_on_submit():
|
||||
user.username = form.username.data
|
||||
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:
|
||||
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_mail import Message
|
||||
from app import mail
|
||||
from urllib.parse import urlparse
|
||||
import threading
|
||||
|
||||
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('/')
|
||||
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>
|
||||
@@ -612,12 +619,26 @@ def forgot_password():
|
||||
|
||||
form = ForgotPasswordForm()
|
||||
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:
|
||||
token = user.generate_set_password_token(expires_hours=1)
|
||||
db.session.commit()
|
||||
_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
|
||||
flash(
|
||||
'If an account with that email address exists, a password reset link '
|
||||
|
||||
Reference in New Issue
Block a user