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

This commit is contained in:
2026-04-27 14:44:02 -04:00
parent b18b23a796
commit bd05384a59
3 changed files with 134 additions and 44 deletions
+79 -24
View File
@@ -127,54 +127,41 @@ def index():
@login_required
@supervisor_required
def create():
"""Create a customer account via email invitation.
"""Create a customer account with admin-supplied credentials.
Only Full Name and Email are required. A username is auto-generated
from the email address. A one-time set-password link is emailed to
the customer; they cannot log in until that link is used.
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.
"""
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 username from email local part, made unique if needed
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 random placeholder password (password_set=False blocks login)
user = User(
username = username,
full_name = full_name,
email = email,
role = 'customer',
active = True,
password_set = False,
password_set = True, # credentials set by admin — account active immediately
)
user.set_password(secrets.token_hex(32))
user.set_password(form.password.data)
db.session.add(user)
db.session.flush()
token = user.generate_set_password_token(expires_hours=72)
db.session.commit()
logger.info('CUSTOMERS | invite | admin=%s new_customer=%s email=%s',
logger.info('CUSTOMERS | create | 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}; invite_sent=True')
f'role=customer; email={user.email}; credentials_set_by_admin=True')
_send_invite_email(user, token)
_send_welcome_email(user)
flash(
f'Customer account created for {full_name}. '
f'An invitation email has been sent to {email} with a link to set their password.',
f'They can now log in with username '{username}'.',
'success'
)
return redirect(url_for('customers.manage', customer_id=user.id))
@@ -255,6 +242,74 @@ 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'])