04/27 Updated customer invitation allow customer to create username & password 3

This commit is contained in:
2026-04-27 14:56:41 -04:00
parent 19fe1d8328
commit c31f08dbcc
4 changed files with 78 additions and 143 deletions
+32 -82
View File
@@ -127,41 +127,58 @@ def index():
@login_required
@supervisor_required
def create():
"""Create a customer account with admin-supplied credentials.
"""Create a customer account via email invitation.
Admin provides Full Name, Email, Username, and Password.
The account is immediately active — the customer can log in straight away.
A welcome email is sent so they know their account is ready.
Admin enters Full Name and Email only. A temporary username is
auto-generated from the email address. A one-time set-password link
is emailed; the customer chooses their own username and password when
they click it. The account is activated on completion.
"""
form = CustomerInviteForm()
if form.validate_on_submit():
import re, secrets
full_name = form.full_name.data.strip()
email = form.email.data.strip().lower()
username = form.username.data.strip()
# Auto-generate a temporary username from the email local part.
# The customer replaces this with their preferred username when
# they complete the set-password flow via the emailed link.
base_uname = re.sub(r'[^a-z0-9._-]', '', email.split('@')[0])[:40] or 'customer'
username = base_uname
suffix = 1
while User.query.filter_by(username=username).first():
username = f'{base_uname}{suffix}'
suffix += 1
# Create user with a random placeholder password (password_set=False
# blocks login until the customer completes the set-password flow).
user = User(
username = username,
full_name = full_name,
email = email,
role = 'customer',
active = True,
password_set = True, # credentials set by admin — account active immediately
password_set = False,
)
user.set_password(form.password.data)
user.set_password(secrets.token_hex(32))
db.session.add(user)
db.session.flush()
token = user.generate_set_password_token(expires_hours=72)
db.session.commit()
logger.info('CUSTOMERS | create | admin=%s new_customer=%s email=%s',
logger.info('CUSTOMERS | invite | admin=%s new_customer=%s email=%s',
current_user.username, user.username, user.email)
log_action(ACTION_CREATE, 'User', user.id, user.username,
f'role=customer; email={user.email}; credentials_set_by_admin=True')
f'role=customer; email={user.email}; invite_sent=True')
_send_welcome_email(user)
_send_invite_email(user, token)
flash(
f'Customer account created for {full_name}. '
f'They can now log in with username "{username}".',
f'An invitation email has been sent to {email} with a link to set their username and password.',
'success'
)
return redirect(url_for('customers.manage', customer_id=user.id))
@@ -242,74 +259,6 @@ def _send_invite_email(user, token):
threading.Thread(target=_send, daemon=True).start()
def _send_welcome_email(user):
"""Send a welcome email to a newly created customer whose credentials were set by admin."""
from flask import current_app, render_template_string
from flask_mail import Message
from app import mail
import threading
if not current_app.config.get('MAIL_SERVER'):
logger.warning('WELCOME EMAIL SKIPPED | no MAIL_SERVER | user=%s', user.username)
return
base_url = current_app.config.get('APP_BASE_URL', '').rstrip('/')
sender = current_app.config.get(
'MAIL_DEFAULT_SENDER',
current_app.config.get('MAIL_USERNAME', 'noreply@janitorialqc.local'),
)
html_body = render_template_string("""<!DOCTYPE html>
<html>
<body style="font-family:Arial,sans-serif;color:#333;max-width:600px;margin:auto;">
<h2 style="color:#0d6efd;">Your JQC Account is Ready</h2>
<p>Hi {{ name }},</p>
<p>
An account has been created for you on the Janitorial QC portal.
You can log in immediately using the credentials provided to you by your administrator.
</p>
<p>
<a href="{{ login_url }}"
style="background:#0d6efd;color:#fff;padding:10px 20px;
text-decoration:none;border-radius:4px;display:inline-block;">
Log In Now
</a>
</p>
<hr style="border:none;border-top:1px solid #eee;margin-top:32px;">
<p style="font-size:12px;color:#888;">
Janitorial QC System — automated notification. Do not reply to this email.
</p>
</body>
</html>""", name=user.display_name, login_url=f'{base_url}/auth/login')
text_body = (
f'Hi {user.display_name},\n\n'
f'An account has been created for you on the Janitorial QC portal.\n'
f'You can log in immediately using the credentials provided by your administrator.\n\n'
f'Login: {base_url}/auth/login\n\nJanitorial QC System'
)
msg = Message(
subject = '[JQC] Your account is ready',
sender = sender,
recipients = [user.email],
body = text_body,
html = html_body,
)
app = current_app._get_current_object()
def _send():
with app.app_context():
try:
mail.send(msg)
logger.info('WELCOME EMAIL SENT | to=%s | user=%s', user.email, user.username)
except Exception as exc:
logger.error('WELCOME EMAIL FAILED | to=%s | error=%s', user.email, exc)
threading.Thread(target=_send, daemon=True).start()
# ── Resend invitation email ───────────────────────────────────────────────────
@bp.route('/<int:customer_id>/resend-invite', methods=['POST'])
@@ -355,16 +304,17 @@ def set_password(token):
form = SetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
user.username = form.username.data.strip()
user.password_set = True
user.set_password(form.password.data)
user.clear_set_password_token()
db.session.commit()
logger.info('CUSTOMERS | password_set | user=%s', user.username)
log_action(ACTION_UPDATE, 'User', user.id, user.username,
'customer completed password setup via invite link')
'customer chose username and password via invite link')
flash('Your password has been set successfully. You can now log in.', 'success')
flash('Your account is ready. You can now log in.', 'success')
return redirect(url_for('auth.login'))
return render_template('customers/set_password.html', form=form, user=user)