Initial commit - Customer Portal for Coolify

This commit is contained in:
2025-12-17 10:08:34 +01:00
commit 9fca32567c
153 changed files with 16432 additions and 0 deletions

237
customer_portal/services/email.py Executable file
View File

@@ -0,0 +1,237 @@
"""Email service.
Uses database settings from Admin panel instead of environment variables.
"""
import logging
import smtplib
from datetime import UTC, datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from flask import current_app, render_template, url_for
logger = logging.getLogger(__name__)
def get_mail_config():
"""Get mail configuration from database.
Returns:
dict: Mail configuration from database or defaults
"""
try:
from customer_portal.models import get_db
from customer_portal.models.settings import PortalSettings
db = get_db()
return PortalSettings.get_mail_config(db)
except Exception as e:
logger.error(f"Failed to get mail config from database: {e}")
# Fallback to app config (environment variables)
return {
"mail_server": current_app.config.get("MAIL_SERVER", ""),
"mail_port": current_app.config.get("MAIL_PORT", 587),
"mail_use_tls": current_app.config.get("MAIL_USE_TLS", True),
"mail_use_ssl": current_app.config.get("MAIL_USE_SSL", False),
"mail_username": current_app.config.get("MAIL_USERNAME", ""),
"mail_password": current_app.config.get("MAIL_PASSWORD", ""),
"mail_default_sender": current_app.config.get("MAIL_DEFAULT_SENDER", ""),
"mail_default_sender_name": "Kundenportal",
}
def send_email(to: str, subject: str, html_body: str, text_body: str = "") -> bool:
"""Send email using database SMTP settings.
Args:
to: Recipient email address
subject: Email subject
html_body: HTML content
text_body: Plain text content (optional)
Returns:
True if sent successfully, False otherwise
"""
config = get_mail_config()
if not config.get("mail_server"):
logger.error("Mail server not configured")
return False
try:
# Create message
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["To"] = to
# Build sender
sender_name = config.get("mail_default_sender_name", "Kundenportal")
sender_email = config.get("mail_default_sender", "")
if sender_name and sender_email:
msg["From"] = f"{sender_name} <{sender_email}>"
else:
msg["From"] = sender_email
# Add plain text
if text_body:
msg.attach(MIMEText(text_body, "plain", "utf-8"))
# Add HTML
msg.attach(MIMEText(html_body, "html", "utf-8"))
# Connect to SMTP server
server = config.get("mail_server")
port = config.get("mail_port", 587)
use_ssl = config.get("mail_use_ssl", False)
use_tls = config.get("mail_use_tls", True)
if use_ssl:
smtp = smtplib.SMTP_SSL(server, port)
else:
smtp = smtplib.SMTP(server, port)
if use_tls:
smtp.starttls()
# Login if credentials provided
username = config.get("mail_username")
password = config.get("mail_password")
if username and password:
smtp.login(username, password)
# Send
smtp.sendmail(sender_email, [to], msg.as_string())
smtp.quit()
logger.info(f"Email sent to {to}: {subject}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to}: {e}")
return False
class EmailService:
"""Send emails using database SMTP settings."""
SUBJECT_MAP = {
"login": "Ihr Login-Code fuer das Kundenportal",
"register": "Willkommen - Ihre Registrierung",
"reset": "Ihr Code zum Zuruecksetzen",
"prefill": "Ihr Code fuer die Buchung",
}
@staticmethod
def send_otp(email: str, code: str, purpose: str = "login") -> bool:
"""Send OTP code via email.
Args:
email: Recipient email address
code: OTP code
purpose: OTP purpose (login, register, reset, prefill)
Returns:
True if sent successfully, False otherwise
"""
subject = EmailService.SUBJECT_MAP.get(purpose, "Ihr Code")
html_body = render_template(
"emails/otp.html",
code=code,
purpose=purpose,
)
text_body = f"""
Ihr Code: {code}
Dieser Code ist 10 Minuten gueltig.
Falls Sie diesen Code nicht angefordert haben, ignorieren Sie diese E-Mail.
"""
success = send_email(email, subject, html_body, text_body)
if success:
logger.info(f"OTP email sent to {email} for {purpose}")
# In development, log the code for testing
elif current_app.debug:
logger.warning(f"DEBUG MODE - OTP code for {email}: {code}")
return success
@staticmethod
def send_login_notification(email: str, ip_address: str, user_agent: str) -> bool:
"""Send notification about new login.
Args:
email: Recipient email address
ip_address: Login IP address
user_agent: Browser user agent
Returns:
True if sent successfully
"""
html_body = render_template(
"emails/login_notification.html",
ip_address=ip_address,
user_agent=user_agent,
)
return send_email(email, "Neue Anmeldung in Ihrem Kundenportal", html_body)
@staticmethod
def send_welcome(email: str, name: str) -> bool:
"""Send welcome email after registration.
Args:
email: Recipient email address
name: Customer name
Returns:
True if sent successfully, False otherwise
"""
# Get portal URL from config or build it
portal_url = current_app.config.get("PORTAL_URL", "")
if not portal_url:
try:
portal_url = url_for("main.index", _external=True)
except RuntimeError:
portal_url = "http://localhost:8502"
html_body = render_template(
"emails/welcome.html",
name=name,
portal_url=portal_url,
year=datetime.now(UTC).year,
)
text_body = f"""
Willkommen, {name}!
Ihr Kundenkonto wurde erfolgreich erstellt.
Im Kundenportal koennen Sie:
- Ihre Buchungen einsehen
- Rechnungen herunterladen
- Videos ansehen
- Stornierungen beantragen
Zum Portal: {portal_url}
"""
success = send_email(
email, "Willkommen im Kundenportal - Webwerkstatt", html_body, text_body
)
if success:
logger.info(f"Welcome email sent to {email}")
elif current_app.debug:
logger.warning(f"DEBUG MODE - Welcome email would be sent to {email}")
return success
# Keep Flask-Mail for backwards compatibility (but not used anymore)
from flask_mail import Mail
mail = Mail()