Initial commit - Customer Portal for Coolify
This commit is contained in:
332
customer_portal/web/routes/auth.py
Executable file
332
customer_portal/web/routes/auth.py
Executable file
@@ -0,0 +1,332 @@
|
||||
"""Authentication routes."""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from functools import wraps
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
flash,
|
||||
g,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
|
||||
from customer_portal.models import get_db
|
||||
from customer_portal.models.customer import Customer
|
||||
from customer_portal.services import AuthService, EmailService, OTPService
|
||||
|
||||
bp = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""Decorator to require authentication."""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = session.get("auth_token")
|
||||
if not token:
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
db = get_db()
|
||||
customer = AuthService.get_customer_by_token(db, token)
|
||||
if not customer:
|
||||
session.clear()
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
g.customer = customer
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
"""Login page - enter email."""
|
||||
# Already logged in?
|
||||
if session.get("auth_token"):
|
||||
db = get_db()
|
||||
if AuthService.get_customer_by_token(db, session["auth_token"]):
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email", "").strip().lower()
|
||||
|
||||
if not email or "@" not in email:
|
||||
flash("Bitte geben Sie eine gueltige E-Mail-Adresse ein.", "error")
|
||||
return render_template("auth/login.html")
|
||||
|
||||
db = get_db()
|
||||
|
||||
# Get or create customer
|
||||
customer = AuthService.get_or_create_customer(db, email)
|
||||
|
||||
# Rate limiting: max 5 OTPs per hour
|
||||
max_attempts = current_app.config.get("OTP_MAX_ATTEMPTS", 5)
|
||||
recent = OTPService.count_recent_attempts(db, customer.id)
|
||||
if recent >= max_attempts:
|
||||
flash(
|
||||
"Zu viele Anfragen. Bitte warten Sie eine Stunde.",
|
||||
"error",
|
||||
)
|
||||
return render_template("auth/login.html")
|
||||
|
||||
# Generate and send OTP
|
||||
code = OTPService.create_for_customer(db, customer.id, "login")
|
||||
EmailService.send_otp(email, code, "login")
|
||||
|
||||
# Store customer ID for verification step
|
||||
session["pending_customer_id"] = customer.id
|
||||
session["pending_email"] = email
|
||||
|
||||
return redirect(url_for("auth.verify"))
|
||||
|
||||
return render_template("auth/login.html")
|
||||
|
||||
|
||||
@bp.route("/verify", methods=["GET", "POST"])
|
||||
def verify():
|
||||
"""Verify OTP code."""
|
||||
customer_id = session.get("pending_customer_id")
|
||||
email = session.get("pending_email")
|
||||
|
||||
if not customer_id:
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
if request.method == "POST":
|
||||
code = request.form.get("code", "").strip()
|
||||
|
||||
if not code or len(code) != 6:
|
||||
flash("Bitte geben Sie den 6-stelligen Code ein.", "error")
|
||||
return render_template("auth/verify.html", email=email)
|
||||
|
||||
db = get_db()
|
||||
|
||||
if OTPService.verify(db, customer_id, code, "login"):
|
||||
# Create session
|
||||
token = AuthService.create_session(
|
||||
db,
|
||||
customer_id,
|
||||
request.remote_addr,
|
||||
request.user_agent.string if request.user_agent else "",
|
||||
)
|
||||
|
||||
# Store session token
|
||||
session["auth_token"] = token
|
||||
session.pop("pending_customer_id", None)
|
||||
session.pop("pending_email", None)
|
||||
|
||||
flash("Erfolgreich eingeloggt!", "success")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
flash("Ungueltiger oder abgelaufener Code.", "error")
|
||||
|
||||
return render_template("auth/verify.html", email=email)
|
||||
|
||||
|
||||
@bp.route("/resend", methods=["POST"])
|
||||
def resend():
|
||||
"""Resend OTP code."""
|
||||
customer_id = session.get("pending_customer_id")
|
||||
email = session.get("pending_email")
|
||||
|
||||
if not customer_id or not email:
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
db = get_db()
|
||||
|
||||
# Rate limiting
|
||||
max_attempts = current_app.config.get("OTP_MAX_ATTEMPTS", 5)
|
||||
recent = OTPService.count_recent_attempts(db, customer_id)
|
||||
if recent >= max_attempts:
|
||||
flash("Zu viele Anfragen. Bitte warten Sie eine Stunde.", "error")
|
||||
return redirect(url_for("auth.verify"))
|
||||
|
||||
# Generate new code
|
||||
code = OTPService.create_for_customer(db, customer_id, "login")
|
||||
EmailService.send_otp(email, code, "login")
|
||||
|
||||
flash("Ein neuer Code wurde gesendet.", "success")
|
||||
return redirect(url_for("auth.verify"))
|
||||
|
||||
|
||||
@bp.route("/logout")
|
||||
def logout():
|
||||
"""Logout and clear session."""
|
||||
token = session.get("auth_token")
|
||||
if token:
|
||||
db = get_db()
|
||||
AuthService.logout(db, token)
|
||||
|
||||
session.clear()
|
||||
flash("Sie wurden erfolgreich ausgeloggt.", "success")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Registration Routes
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@bp.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
"""Registration - Step 1: Enter email."""
|
||||
# Already logged in?
|
||||
if session.get("auth_token"):
|
||||
db = get_db()
|
||||
if AuthService.get_customer_by_token(db, session["auth_token"]):
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email", "").strip().lower()
|
||||
|
||||
if not email or "@" not in email:
|
||||
flash("Bitte geben Sie eine gueltige E-Mail-Adresse ein.", "error")
|
||||
return render_template("auth/register.html")
|
||||
|
||||
db = get_db()
|
||||
|
||||
# Check if customer already exists
|
||||
customer = db.query(Customer).filter(Customer.email == email).first()
|
||||
|
||||
if customer:
|
||||
flash(
|
||||
"Diese E-Mail ist bereits registriert. Bitte melden Sie sich an.",
|
||||
"info",
|
||||
)
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
# Store email in session for registration flow
|
||||
session["register_email"] = email
|
||||
|
||||
# Generate OTP
|
||||
otp_minutes = current_app.config.get("OTP_LIFETIME_MINUTES", 10)
|
||||
code = OTPService.generate()
|
||||
session["register_otp"] = code
|
||||
session["register_otp_expires"] = (
|
||||
datetime.now(UTC) + timedelta(minutes=otp_minutes)
|
||||
).isoformat()
|
||||
|
||||
# Send OTP email
|
||||
EmailService.send_otp(email, code, "register")
|
||||
|
||||
flash("Ein Bestaetigungscode wurde an Ihre E-Mail gesendet.", "success")
|
||||
return redirect(url_for("auth.register_verify"))
|
||||
|
||||
return render_template("auth/register.html")
|
||||
|
||||
|
||||
@bp.route("/register/verify", methods=["GET", "POST"])
|
||||
def register_verify():
|
||||
"""Registration - Step 2: Verify OTP."""
|
||||
email = session.get("register_email")
|
||||
if not email:
|
||||
return redirect(url_for("auth.register"))
|
||||
|
||||
if request.method == "POST":
|
||||
code = request.form.get("code", "").strip()
|
||||
stored_code = session.get("register_otp")
|
||||
expires_str = session.get("register_otp_expires")
|
||||
|
||||
if not code or len(code) != 6:
|
||||
flash("Bitte geben Sie den 6-stelligen Code ein.", "error")
|
||||
return render_template("auth/register_verify.html", email=email)
|
||||
|
||||
if stored_code and code == stored_code and expires_str:
|
||||
# Check if not expired
|
||||
expires = datetime.fromisoformat(expires_str)
|
||||
if expires > datetime.now(UTC):
|
||||
session["register_verified"] = True
|
||||
return redirect(url_for("auth.register_profile"))
|
||||
|
||||
flash("Ungueltiger oder abgelaufener Code.", "error")
|
||||
|
||||
return render_template("auth/register_verify.html", email=email)
|
||||
|
||||
|
||||
@bp.route("/register/resend", methods=["POST"])
|
||||
def register_resend():
|
||||
"""Resend registration OTP."""
|
||||
email = session.get("register_email")
|
||||
if not email:
|
||||
return redirect(url_for("auth.register"))
|
||||
|
||||
# Generate new OTP
|
||||
otp_minutes = current_app.config.get("OTP_LIFETIME_MINUTES", 10)
|
||||
code = OTPService.generate()
|
||||
session["register_otp"] = code
|
||||
session["register_otp_expires"] = (
|
||||
datetime.now(UTC) + timedelta(minutes=otp_minutes)
|
||||
).isoformat()
|
||||
|
||||
# Send OTP email
|
||||
EmailService.send_otp(email, code, "register")
|
||||
|
||||
flash("Ein neuer Code wurde gesendet.", "success")
|
||||
return redirect(url_for("auth.register_verify"))
|
||||
|
||||
|
||||
@bp.route("/register/profile", methods=["GET", "POST"])
|
||||
def register_profile():
|
||||
"""Registration - Step 3: Enter profile data."""
|
||||
email = session.get("register_email")
|
||||
verified = session.get("register_verified")
|
||||
|
||||
if not email or not verified:
|
||||
return redirect(url_for("auth.register"))
|
||||
|
||||
if request.method == "POST":
|
||||
name = request.form.get("name", "").strip()
|
||||
phone = request.form.get("phone", "").strip()
|
||||
|
||||
if not name:
|
||||
flash("Bitte geben Sie Ihren Namen ein.", "error")
|
||||
return render_template("auth/register_profile.html", email=email)
|
||||
|
||||
db = get_db()
|
||||
|
||||
# Double-check email doesn't exist (race condition protection)
|
||||
existing = db.query(Customer).filter(Customer.email == email).first()
|
||||
if existing:
|
||||
session.pop("register_email", None)
|
||||
session.pop("register_otp", None)
|
||||
session.pop("register_otp_expires", None)
|
||||
session.pop("register_verified", None)
|
||||
flash("Diese E-Mail ist bereits registriert.", "info")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
# Create customer with defaults from settings
|
||||
customer = Customer.create_with_defaults(
|
||||
db,
|
||||
email=email,
|
||||
name=name,
|
||||
phone=phone if phone else None,
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
|
||||
# Clear registration session data
|
||||
session.pop("register_email", None)
|
||||
session.pop("register_otp", None)
|
||||
session.pop("register_otp_expires", None)
|
||||
session.pop("register_verified", None)
|
||||
|
||||
# Auto-login after registration
|
||||
token = AuthService.create_session(
|
||||
db,
|
||||
customer.id,
|
||||
request.remote_addr,
|
||||
request.user_agent.string if request.user_agent else "",
|
||||
)
|
||||
session["auth_token"] = token
|
||||
|
||||
# Send welcome email
|
||||
EmailService.send_welcome(email, name)
|
||||
|
||||
flash("Registrierung erfolgreich! Willkommen im Kundenportal.", "success")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
return render_template("auth/register_profile.html", email=email)
|
||||
Reference in New Issue
Block a user