feat: welcome activation email for users who never logged in
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
New email template with friendly tone and green CTA button for first-time account activation. Script with --dry-run, --test-email, --user-id flags and 72h token validity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
37ff5a8c6f
commit
fac832c80c
@ -441,6 +441,83 @@ https://nordabiznes.pl
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_welcome_activation_email(email: str, name: str, reset_url: str) -> bool:
|
||||||
|
"""Send welcome activation email to users who never received credentials."""
|
||||||
|
subject = "Witamy w Norda Biznes Partner — Ustaw hasło do konta"
|
||||||
|
|
||||||
|
body_text = f"""Witaj {name}!
|
||||||
|
|
||||||
|
Twoje konto w portalu Norda Biznes Partner jest gotowe.
|
||||||
|
|
||||||
|
Aby ustawić hasło i zalogować się, kliknij w poniższy link:
|
||||||
|
{reset_url}
|
||||||
|
|
||||||
|
Link jest ważny przez 72 godziny.
|
||||||
|
|
||||||
|
Co znajdziesz w portalu:
|
||||||
|
- Katalog firm członkowskich Izby
|
||||||
|
- Forum dyskusyjne dla członków
|
||||||
|
- Wiadomości prywatne i networking
|
||||||
|
- Asystent AI do wyszukiwania usług
|
||||||
|
|
||||||
|
Pozdrawiamy,
|
||||||
|
Zespół Norda Biznes Partner
|
||||||
|
https://nordabiznes.pl
|
||||||
|
"""
|
||||||
|
|
||||||
|
check = (
|
||||||
|
'<div style="width:28px; height:28px; background:#16a34a; border-radius:50%; '
|
||||||
|
'text-align:center; line-height:28px; display:inline-block; margin-right:10px;">'
|
||||||
|
'<span style="color:#fff; font-size:16px; font-weight:bold;">✓</span></div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = f'''
|
||||||
|
<p style="margin:0 0 16px; color:#1e293b; font-size:16px;">Witaj <strong>{name}</strong>!</p>
|
||||||
|
<p style="margin:0 0 28px; color:#475569; font-size:15px; line-height:1.5;">Twoje konto w portalu <strong>Norda Biznes Partner</strong> jest gotowe. Kliknij poniższy przycisk, aby ustawić hasło i zalogować się po raz pierwszy.</p>
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:28px;">
|
||||||
|
<tr><td align="center" style="padding: 8px 0;">
|
||||||
|
<a href="{reset_url}" style="display:inline-block; padding:16px 40px; background: linear-gradient(135deg, #16a34a, #15803d); color:#ffffff; text-decoration:none; border-radius:8px; font-size:15px; font-weight:600;">Ustaw hasło i zaloguj się</a>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#fef3c7; border-radius:8px; border: 1px solid #fcd34d; margin-bottom:28px;">
|
||||||
|
<tr><td style="padding: 14px 16px;">
|
||||||
|
<p style="margin:0; color:#92400e; font-size:13px;">Link jest ważny przez <strong>72 godziny</strong>. Po tym czasie możesz poprosić o nowy link na stronie logowania.</p>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin:0 0 14px; color:#1e293b; font-size:16px; font-weight:600;">Co znajdziesz w portalu:</p>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fdf8; border-radius:10px; border: 1px solid #d1fae5; margin-bottom:28px;">
|
||||||
|
<tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
|
||||||
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Katalog firm członkowskich Izby</span>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
|
||||||
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Forum dyskusyjne dla członków</span>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
|
||||||
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Wiadomości prywatne i networking</span>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding: 14px 16px;">
|
||||||
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Asystent AI do wyszukiwania usług</span>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin:0 0 8px; color:#64748b; font-size:13px;">Jeśli przycisk nie działa, skopiuj i wklej ten link:</p>
|
||||||
|
<p style="margin:0; color:#2563eb; font-size:13px; word-break:break-all;">{reset_url}</p>'''
|
||||||
|
|
||||||
|
body_html = _email_v3_wrap('Witamy w portalu!', 'Norda Biznes Partner', content)
|
||||||
|
|
||||||
|
return send_email(
|
||||||
|
to=[email],
|
||||||
|
subject=subject,
|
||||||
|
body_text=body_text,
|
||||||
|
body_html=body_html,
|
||||||
|
email_type='welcome_activation',
|
||||||
|
recipient_name=name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_welcome_email(email: str, name: str, verification_url: str) -> bool:
|
def send_welcome_email(email: str, name: str, verification_url: str) -> bool:
|
||||||
"""Send welcome/verification email after registration."""
|
"""Send welcome/verification email after registration."""
|
||||||
subject = "Witamy w Norda Biznes Partner — Potwierdź email"
|
subject = "Witamy w Norda Biznes Partner — Potwierdź email"
|
||||||
|
|||||||
220
scripts/send_welcome_activation.py
Normal file
220
scripts/send_welcome_activation.py
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
"""
|
||||||
|
Send welcome activation emails to users who never logged in.
|
||||||
|
|
||||||
|
These users were imported (batch import, admin panel, WhatsApp registration)
|
||||||
|
with passwords but never received welcome emails or password reset links.
|
||||||
|
This script generates reset tokens (72h validity) and sends a welcoming
|
||||||
|
activation email with a link to set their password.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Preview who would receive emails (no sending)
|
||||||
|
python3 send_welcome_activation.py --dry-run
|
||||||
|
|
||||||
|
# Send test email to admin
|
||||||
|
python3 send_welcome_activation.py --test-email maciej.pienczyn@inpi.pl
|
||||||
|
|
||||||
|
# Send to a specific user by ID
|
||||||
|
python3 send_welcome_activation.py --user-id 53
|
||||||
|
|
||||||
|
# Send to all never-logged-in users (requires confirmation)
|
||||||
|
python3 send_welcome_activation.py --send-all
|
||||||
|
|
||||||
|
Run on production server with DATABASE_URL set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from database import SessionLocal, User
|
||||||
|
import email_service
|
||||||
|
|
||||||
|
TOKEN_VALIDITY_HOURS = 72
|
||||||
|
BASE_URL = 'https://nordabiznes.pl'
|
||||||
|
|
||||||
|
|
||||||
|
def get_never_logged_in_users(db):
|
||||||
|
"""Find all active, verified users who never logged in."""
|
||||||
|
return (
|
||||||
|
db.query(User)
|
||||||
|
.filter(
|
||||||
|
User.last_login.is_(None),
|
||||||
|
User.is_active.is_(True),
|
||||||
|
)
|
||||||
|
.order_by(User.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_activation_email(db, user):
|
||||||
|
"""Generate token and send welcome activation email to a user."""
|
||||||
|
token = secrets.token_urlsafe(32)
|
||||||
|
expires = datetime.now() + timedelta(hours=TOKEN_VALIDITY_HOURS)
|
||||||
|
|
||||||
|
user.reset_token = token
|
||||||
|
user.reset_token_expires = expires
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
reset_url = f"{BASE_URL}/reset-password/{token}"
|
||||||
|
|
||||||
|
success = email_service.send_welcome_activation_email(
|
||||||
|
email=user.email,
|
||||||
|
name=user.name,
|
||||||
|
reset_url=reset_url
|
||||||
|
)
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_dry_run(db):
|
||||||
|
"""Show users who would receive activation emails."""
|
||||||
|
users = get_never_logged_in_users(db)
|
||||||
|
|
||||||
|
if not users:
|
||||||
|
print("Brak użytkowników do aktywacji (wszyscy się zalogowali).")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Użytkownicy, którzy nigdy się nie zalogowali ({len(users)}):\n")
|
||||||
|
print(f"{'ID':>4} {'Imię i nazwisko':<30} {'Email':<40} {'Aktywny':>7} {'Zweryfikowany':>13}")
|
||||||
|
print("-" * 100)
|
||||||
|
|
||||||
|
for u in users:
|
||||||
|
print(f"{u.id:>4} {u.name:<30} {u.email:<40} {'tak' if u.is_active else 'nie':>7} {'tak' if u.is_verified else 'nie':>13}")
|
||||||
|
|
||||||
|
print(f"\nRazem: {len(users)} użytkowników")
|
||||||
|
print("\nAby wysłać e-mail testowy: --test-email <adres>")
|
||||||
|
print("Aby wysłać do jednej osoby: --user-id <ID>")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_test_email(db, test_email):
|
||||||
|
"""Send a test activation email to specified address."""
|
||||||
|
# Find or create a fake context for the test
|
||||||
|
print(f"Wysyłanie testowego e-maila powitalnego do: {test_email}")
|
||||||
|
|
||||||
|
token = secrets.token_urlsafe(32)
|
||||||
|
reset_url = f"{BASE_URL}/reset-password/{token}"
|
||||||
|
|
||||||
|
# Use test data
|
||||||
|
success = email_service.send_welcome_activation_email(
|
||||||
|
email=test_email,
|
||||||
|
name="Testowy Użytkownik",
|
||||||
|
reset_url=reset_url
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f" OK: E-mail testowy wysłany do {test_email}")
|
||||||
|
print(f" Link (nieaktywny - testowy token): {reset_url}")
|
||||||
|
else:
|
||||||
|
print(f" FAIL: Nie udało się wysłać e-maila do {test_email}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_send_user(db, user_id):
|
||||||
|
"""Send activation email to a specific user."""
|
||||||
|
user = db.query(User).get(user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
print(f"ERROR: Nie znaleziono użytkownika o ID {user_id}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not user.is_active:
|
||||||
|
print(f"ERROR: Użytkownik {user.name} (ID {user_id}) jest nieaktywny")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if user.last_login is not None:
|
||||||
|
print(f"SKIP: Użytkownik {user.name} (ID {user_id}) już się logował ({user.last_login})")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Wysyłanie e-maila powitalnego do: {user.name} <{user.email}> (ID {user_id})")
|
||||||
|
|
||||||
|
success = send_activation_email(db, user)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f" OK: E-mail wysłany do {user.name} <{user.email}>")
|
||||||
|
else:
|
||||||
|
print(f" FAIL: Nie udało się wysłać e-maila do {user.name} <{user.email}>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_send_all(db):
|
||||||
|
"""Send activation emails to all never-logged-in users (with confirmation)."""
|
||||||
|
users = get_never_logged_in_users(db)
|
||||||
|
|
||||||
|
if not users:
|
||||||
|
print("Brak użytkowników do aktywacji.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Wysyłka do {len(users)} użytkowników:\n")
|
||||||
|
for u in users:
|
||||||
|
print(f" {u.id:>4} {u.name:<30} {u.email}")
|
||||||
|
|
||||||
|
confirm = input(f"\nCzy wysłać e-maile do {len(users)} użytkowników? (tak/nie): ")
|
||||||
|
if confirm.strip().lower() != 'tak':
|
||||||
|
print("Anulowano.")
|
||||||
|
return
|
||||||
|
|
||||||
|
results = {'sent': [], 'failed': []}
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
try:
|
||||||
|
success = send_activation_email(db, user)
|
||||||
|
if success:
|
||||||
|
results['sent'].append(f"{user.name} <{user.email}>")
|
||||||
|
print(f" OK: {user.name} <{user.email}>")
|
||||||
|
else:
|
||||||
|
results['failed'].append(f"{user.name} <{user.email}>")
|
||||||
|
print(f" FAIL: {user.name} <{user.email}>")
|
||||||
|
except Exception as e:
|
||||||
|
results['failed'].append(f"{user.name} <{user.email}>: {e}")
|
||||||
|
print(f" ERROR: {user.name} <{user.email}>: {e}")
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"Wysłano: {len(results['sent'])}")
|
||||||
|
print(f"Błędy: {len(results['failed'])}")
|
||||||
|
|
||||||
|
if results['failed']:
|
||||||
|
print("\nSzczegóły błędów:")
|
||||||
|
for f in results['failed']:
|
||||||
|
print(f" - {f}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Wyślij e-maile powitalne do użytkowników, którzy nigdy się nie zalogowali.'
|
||||||
|
)
|
||||||
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument('--dry-run', action='store_true',
|
||||||
|
help='Pokaż listę użytkowników bez wysyłania e-maili')
|
||||||
|
group.add_argument('--test-email', type=str,
|
||||||
|
help='Wyślij testowy e-mail na podany adres')
|
||||||
|
group.add_argument('--user-id', type=int,
|
||||||
|
help='Wyślij e-mail do użytkownika o podanym ID')
|
||||||
|
group.add_argument('--send-all', action='store_true',
|
||||||
|
help='Wyślij e-maile do wszystkich niezalogowanych (wymaga potwierdzenia)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.dry_run and not email_service.is_configured():
|
||||||
|
print("ERROR: Email service not configured. Set Azure credentials in .env")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
if args.dry_run:
|
||||||
|
cmd_dry_run(db)
|
||||||
|
elif args.test_email:
|
||||||
|
cmd_test_email(db, args.test_email)
|
||||||
|
elif args.user_id:
|
||||||
|
cmd_send_user(db, args.user_id)
|
||||||
|
elif args.send_all:
|
||||||
|
cmd_send_all(db)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user