2026-04-26 18:20:54 +02:00
|
|
|
import os
|
|
|
|
|
import logging
|
2026-09-19 06:52:45 +02:00
|
|
|
import smtplib
|
2026-04-26 18:20:54 +02:00
|
|
|
import sqlite3
|
2026-09-19 06:52:45 +02:00
|
|
|
import threading
|
|
|
|
|
from email.message import EmailMessage
|
2026-04-26 18:20:54 +02:00
|
|
|
from functools import wraps
|
|
|
|
|
|
2026-05-22 10:30:28 +02:00
|
|
|
from flask import Flask, render_template, request, redirect, url_for, g, Response, send_from_directory
|
2026-04-26 18:20:54 +02:00
|
|
|
|
|
|
|
|
DATABASE = os.environ.get("DATABASE", "punktfri.db")
|
|
|
|
|
LOG_FILE = os.environ.get("LOG_FILE", "signups.log")
|
|
|
|
|
ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
|
|
|
|
|
ADMIN_PASS = os.environ.get("ADMIN_PASS", "punktfri2024")
|
|
|
|
|
PORT = int(os.environ.get("PORT", 5000))
|
2026-09-19 06:52:45 +02:00
|
|
|
NOTIFY_EMAIL = os.environ.get("NOTIFY_EMAIL", "henrik@i80.dk")
|
|
|
|
|
SMTP_HOST = os.environ.get("SMTP_HOST", "")
|
|
|
|
|
SMTP_PORT = int(os.environ.get("SMTP_PORT", 587))
|
|
|
|
|
SMTP_USER = os.environ.get("SMTP_USER", "")
|
|
|
|
|
SMTP_PASS = os.environ.get("SMTP_PASS", "")
|
|
|
|
|
SMTP_FROM = os.environ.get("SMTP_FROM", "") or SMTP_USER
|
2026-04-26 18:20:54 +02:00
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
|
signup_logger = logging.getLogger("signups")
|
|
|
|
|
signup_logger.setLevel(logging.INFO)
|
|
|
|
|
_fh = logging.FileHandler(LOG_FILE)
|
|
|
|
|
_fh.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
|
|
|
|
signup_logger.addHandler(_fh)
|
|
|
|
|
|
|
|
|
|
|
2026-09-19 06:52:45 +02:00
|
|
|
def build_notification(navn, email, domaener, egne_ns, kommentar):
|
|
|
|
|
msg = EmailMessage()
|
|
|
|
|
msg["Subject"] = f"PunktFri: ny tilmelding fra {navn}"
|
|
|
|
|
msg["From"] = SMTP_FROM
|
|
|
|
|
msg["To"] = NOTIFY_EMAIL
|
|
|
|
|
msg["Reply-To"] = email
|
|
|
|
|
msg.set_content(
|
|
|
|
|
f"Ny interessetilkendegivelse på punktfri.dk\n\n"
|
|
|
|
|
f"Navn: {navn}\n"
|
|
|
|
|
f"E-mail: {email}\n"
|
|
|
|
|
f"Antal domæner: {domaener}\n"
|
|
|
|
|
f"Egne navneservere: {egne_ns}\n"
|
|
|
|
|
f"Kommentar:\n{kommentar or '(ingen)'}\n"
|
|
|
|
|
)
|
|
|
|
|
return msg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _send(msg):
|
|
|
|
|
try:
|
|
|
|
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=15) as s:
|
|
|
|
|
s.starttls()
|
|
|
|
|
if SMTP_USER:
|
|
|
|
|
s.login(SMTP_USER, SMTP_PASS)
|
|
|
|
|
s.send_message(msg)
|
|
|
|
|
except Exception:
|
|
|
|
|
app.logger.exception("Kunne ikke sende tilmeldingsnotifikation til %s", NOTIFY_EMAIL)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def notify_signup(**fields):
|
|
|
|
|
if not SMTP_HOST:
|
|
|
|
|
app.logger.warning("SMTP_HOST ikke sat — springer notifikation over")
|
|
|
|
|
return
|
|
|
|
|
threading.Thread(target=_send, args=(build_notification(**fields),), daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
2026-04-26 18:20:54 +02:00
|
|
|
def get_db():
|
|
|
|
|
db = getattr(g, "_database", None)
|
|
|
|
|
if db is None:
|
|
|
|
|
db = g._database = sqlite3.connect(DATABASE)
|
|
|
|
|
db.row_factory = sqlite3.Row
|
|
|
|
|
return db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.teardown_appcontext
|
|
|
|
|
def close_db(exception):
|
|
|
|
|
db = getattr(g, "_database", None)
|
|
|
|
|
if db is not None:
|
|
|
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_db():
|
|
|
|
|
with app.app_context():
|
|
|
|
|
db = get_db()
|
|
|
|
|
db.execute(
|
|
|
|
|
"""
|
|
|
|
|
CREATE TABLE IF NOT EXISTS signups (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
navn TEXT NOT NULL,
|
|
|
|
|
email TEXT NOT NULL,
|
|
|
|
|
domaener INTEGER NOT NULL,
|
|
|
|
|
egne_ns TEXT NOT NULL,
|
|
|
|
|
kommentar TEXT,
|
|
|
|
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
)
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def require_admin(f):
|
|
|
|
|
@wraps(f)
|
|
|
|
|
def decorated(*args, **kwargs):
|
|
|
|
|
auth = request.authorization
|
|
|
|
|
if not auth or auth.username != ADMIN_USER or auth.password != ADMIN_PASS:
|
|
|
|
|
return Response(
|
|
|
|
|
"Login krævet.",
|
|
|
|
|
401,
|
|
|
|
|
{"WWW-Authenticate": 'Basic realm="PunktFri Admin"'},
|
|
|
|
|
)
|
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
return decorated
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/", methods=["GET", "POST"])
|
|
|
|
|
def index():
|
|
|
|
|
error = None
|
|
|
|
|
if request.method == "POST":
|
|
|
|
|
navn = request.form.get("navn", "").strip()
|
|
|
|
|
email = request.form.get("email", "").strip().lower()
|
|
|
|
|
domaener_raw = request.form.get("domaener", "").strip()
|
|
|
|
|
egne_ns = request.form.get("egne_ns", "").strip()
|
|
|
|
|
kommentar = request.form.get("kommentar", "").strip()
|
|
|
|
|
|
|
|
|
|
if not navn or not email or not domaener_raw or egne_ns not in ("ja", "nej"):
|
|
|
|
|
error = "Udfyld venligst alle påkrævede felter."
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
domaener = int(domaener_raw)
|
|
|
|
|
if domaener < 1:
|
|
|
|
|
raise ValueError
|
|
|
|
|
except ValueError:
|
|
|
|
|
error = "Angiv et gyldigt antal domæner (mindst 1)."
|
|
|
|
|
else:
|
|
|
|
|
db = get_db()
|
|
|
|
|
existing = db.execute(
|
|
|
|
|
"SELECT id FROM signups WHERE email = ?", (email,)
|
|
|
|
|
).fetchone()
|
|
|
|
|
if existing:
|
|
|
|
|
error = "Denne e-mail er allerede tilmeldt — tak for din interesse!"
|
|
|
|
|
else:
|
|
|
|
|
db.execute(
|
|
|
|
|
"INSERT INTO signups (navn, email, domaener, egne_ns, kommentar) VALUES (?, ?, ?, ?, ?)",
|
|
|
|
|
(navn, email, domaener, egne_ns, kommentar or None),
|
|
|
|
|
)
|
|
|
|
|
db.commit()
|
|
|
|
|
signup_logger.info(
|
|
|
|
|
f"SIGNUP navn={navn!r} email={email!r} domaener={domaener} "
|
|
|
|
|
f"egne_ns={egne_ns} kommentar={kommentar!r}"
|
|
|
|
|
)
|
2026-09-19 06:52:45 +02:00
|
|
|
notify_signup(
|
|
|
|
|
navn=navn, email=email, domaener=domaener,
|
|
|
|
|
egne_ns=egne_ns, kommentar=kommentar,
|
|
|
|
|
)
|
2026-04-26 18:20:54 +02:00
|
|
|
return redirect(url_for("tak"))
|
|
|
|
|
|
|
|
|
|
return render_template("index.html", error=error)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/tak")
|
|
|
|
|
def tak():
|
|
|
|
|
return render_template("tak.html")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/admin")
|
|
|
|
|
@require_admin
|
|
|
|
|
def admin():
|
|
|
|
|
db = get_db()
|
|
|
|
|
signups = db.execute(
|
|
|
|
|
"SELECT * FROM signups ORDER BY timestamp DESC"
|
|
|
|
|
).fetchall()
|
|
|
|
|
count = db.execute("SELECT COUNT(*) FROM signups").fetchone()[0]
|
|
|
|
|
return render_template("admin.html", signups=signups, count=count)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/health")
|
|
|
|
|
def health():
|
2026-04-26 18:32:32 +02:00
|
|
|
return {
|
|
|
|
|
"status": "ok",
|
|
|
|
|
"version": os.environ.get("BUILD_VERSION", "dev"),
|
|
|
|
|
"commit": os.environ.get("GIT_COMMIT", "unknown"),
|
|
|
|
|
}, 200
|
2026-04-26 18:20:54 +02:00
|
|
|
|
|
|
|
|
|
2026-05-22 10:30:28 +02:00
|
|
|
@app.route("/robots.txt")
|
|
|
|
|
def robots():
|
|
|
|
|
return send_from_directory(app.static_folder, "robots.txt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/sitemap.xml")
|
|
|
|
|
def sitemap():
|
|
|
|
|
return send_from_directory(app.static_folder, "sitemap.xml")
|
|
|
|
|
|
|
|
|
|
|
2026-04-26 18:20:54 +02:00
|
|
|
init_db()
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
app.run(host="0.0.0.0", port=PORT, debug=False)
|