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

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:
Maciej Pienczyn 2026-02-23 10:03:29 +01:00
parent 37ff5a8c6f
commit fac832c80c
2 changed files with 297 additions and 0 deletions

View File

@ -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;">&#10003;</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:
"""Send welcome/verification email after registration."""
subject = "Witamy w Norda Biznes Partner — Potwierdź email"

View 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()