wqInitial Commit

This commit is contained in:
2026-04-23 01:24:24 +00:00
parent 0925125422
commit 2ccc1d0292
3474 changed files with 787147 additions and 647 deletions

250
app.py
View File

@@ -1,4 +1,4 @@
from flask import Flask, request, redirect, url_for, render_template, session, flash
from flask import Flask, request, redirect, url_for, render_template, session, flash, jsonify, abort
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func
from dotenv import load_dotenv
@@ -8,24 +8,39 @@ from flask_login import LoginManager, login_required, logout_user, UserMixin, lo
from flask_mail import Mail, Message
import json
from slugify import slugify
from datetime import datetime
import hmac, hashlib, json, time
from pathlib import Path
import json, hmac, hashlib, requests, time
load_dotenv()
app = Flask(__name__)
load_dotenv(Path(__file__).with_name(".env"))
app = Flask(__name__, static_folder="static", static_url_path="/static")
HUB_PUSH_SECRET = os.getenv("HUB_PUSH_SECRET") # match the api_secret you set in the Hub
CONTACT = {
"name": os.environ.get("BH_CONTACT_NAME", "Benjamin Mosley"),
"title": os.environ.get("BH_CONTACT_TITLE", "E-Commerce Manager, United Supermarkets"),
"email": os.environ.get("BH_CONTACT_EMAIL", "ben@bennyshouse.net"),
"phone": os.environ.get("BH_CONTACT_PHONE", "(806) 655 2300"),
"city": os.environ.get("BH_CONTACT_CITY", "Canyon / Amarillo / Borger / Remote"),
"cal": os.environ.get("BH_CONTACT_CAL", "https://calendly.com/bennyshouse24/30min"),
"link": os.environ.get("BH_CONTACT_LINK", "https://www.linkedin.com/in/benjamin-mosley-849643329/"),
"site": os.environ.get("BH_CONTACT_SITE", "https://bennyshouse.net"),
"hours": os.environ.get("BH_CONTACT_HOURS", "MonFri, 9a5p CT"),
}
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///posts.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'static/uploads'
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY')
app.config['MAIL_SERVER'] = 'mail.bennyshouse.net'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.getenv("MAIL_USERNAME")
app.config['MAIL_PASSWORD'] = os.getenv("MAIL_PASSWORD")
app.config['MAIL_DEFAULT_SENDER'] = os.getenv("MAIL_DEFAULT_SENDER")
mail = Mail(app)
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
@@ -45,6 +60,12 @@ def load_user(user_id):
return User()
return None
@app.context_processor
def inject_now():
# lets you use {{ now().year }} in any template
return {"now": datetime.now}
class BlogPost(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=False)
@@ -75,6 +96,154 @@ def from_json(value):
except (TypeError, json.JSONDecodeError):
return []
def push_to_domain(push_url: str, api_secret: str, payload: dict):
# Serialize deterministically
body = json.dumps(payload, separators=(',', ':'), ensure_ascii=False).encode('utf-8')
# Compute signature over the exact bytes
sig = hmac.new(api_secret.encode('utf-8'), body, hashlib.sha256).hexdigest()
headers = {
"Content-Type": "application/json",
"X-Hub-Signature-256": f"sha256={sig}",
# Optional if you later check timestamps:
# "X-Timestamp": str(int(time.time())),
}
# Send the *exact same* bytes you signed
r = requests.post(push_url, headers=headers, data=body, timeout=15)
return r
def _verify_hub_signature(raw: bytes) -> None:
if not HUB_PUSH_SECRET:
abort(403, description="Missing HUB_PUSH_SECRET")
# Accept both header names
sig_hdr = (request.headers.get("X-Signature")
or request.headers.get("X-Hub-Signature-256")
or "").strip()
# Accept either "sha256=<hex>" or just "<hex>"
provided = sig_hdr
if provided.lower().startswith("sha256="):
provided = provided.split("=", 1)[1].strip()
if not provided or any(c.isspace() for c in provided):
abort(403, description="Bad signature format")
expected = hmac.new(HUB_PUSH_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(provided, expected):
abort(403, description="Signature mismatch")
ts = request.headers.get("X-Timestamp")
if ts and ts.isdigit() and abs(time.time() - int(ts)) > 300:
abort(403, description="Stale request")
def _to_slug(text: str) -> str:
s = slugify(text or "")
return s or f"post-{int(time.time())}"
def _to_slug(text: str) -> str:
s = slugify(text or "")
return s or f"post-{int(time.time())}"
def _verify_hub_signature(raw: bytes) -> None:
if not HUB_PUSH_SECRET:
abort(403, description="Missing HUB_PUSH_SECRET")
# Accept both header names
sig_hdr = (request.headers.get("X-Signature")
or request.headers.get("X-Hub-Signature-256")
or "").strip()
# Accept either "sha256=<hex>" or just "<hex>"
provided = sig_hdr
if provided.lower().startswith("sha256="):
provided = provided.split("=", 1)[1].strip()
# must be 64 hex chars, no spaces
if not provided or any(c.isspace() for c in provided) or len(provided) != 64:
abort(403, description="Bad signature format")
expected = hmac.new(HUB_PUSH_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(provided, expected):
abort(403, description="Signature mismatch")
ts = request.headers.get("X-Timestamp")
if ts and ts.isdigit() and abs(time.time() - int(ts)) > 300:
abort(403, description="Stale request")
def _to_slug(text: str) -> str:
s = slugify(text or "")
return s or f"post-{int(time.time())}"
@app.post("/api/v1/publish")
def api_publish():
raw = request.get_data(cache=False)
_verify_hub_signature(raw)
try:
data = json.loads(raw.decode("utf-8"))
except Exception:
abort(400, description="Invalid JSON")
kind = (data.get("kind") or "blog").lower()
title = data.get("title") or "Untitled"
summary = data.get("summary")
content_html = data.get("content_html") or ""
content_text = data.get("content_text") or ""
tags = ",".join(data.get("tags") or [])
images = data.get("images") or []
pinned = bool(data.get("pinned"))
external_id = data.get("external_id") # optional stable id (slug-like)
slug = _to_slug(external_id or title)
# Normalize content to your existing schema
body = content_html or content_text.replace("\n", "<br>")
images_json = json.dumps(images) if images else None
if kind == "project":
obj = ProjectPost(
title=title,
slug=slug,
content=body,
category=None,
tags=tags,
pinned=pinned,
images=images_json
)
else:
obj = BlogPost(
title=title,
slug=slug,
content=body,
category=None,
tags=tags,
pinned=pinned,
images=images_json
)
db.session.add(obj)
db.session.commit()
# Return the canonical URL so the hub can log it if you want
url = url_for("view_project" if kind == "project" else "view_blog", slug=slug, _external=True)
return jsonify({"ok": True, "url": url, "id": obj.id, "slug": slug})
computed = hmac.new(
(HUB_PUSH_SECRET or "").encode(), raw, hashlib.sha256
).hexdigest()
return jsonify({
"saw_header": sig_hdr,
"provided": provided,
"len_provided": len(provided),
"computed": computed,
"matches": hmac.compare_digest(provided, computed),
"body_len": len(raw),
})
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
@@ -96,6 +265,7 @@ def logout():
logout_user()
return redirect(url_for("index"))
@app.route('/admin')
@login_required
def admin_panel():
@@ -279,30 +449,9 @@ def view_project(slug):
projectpost = ProjectPost.query.filter_by(slug=slug).first_or_404()
return render_template('view_project.html', projectpost=projectpost)
@app.route("/contact", methods=["GET", "POST"])
@app.route('/contact')
def contact():
if request.method == "POST":
name = request.form.get("name")
email = request.form.get("email")
message = request.form.get("message")
# Compose the email
msg = Message(subject=f"New Contact Message from {name}",
sender=("Portfolio Updates", "ben@bennyshouse.net"),
recipients=["ben@bennyshouse.net"],
body=f"Name: {name}\nEmail: {email}\n\n{message}")
try:
mail.send(msg)
flash("Your message has been sent!", "success")
except Exception as e:
flash("Something went wrong. Please try again later.", "error")
print(f"Email error: {e}")
return redirect(url_for("contact"))
return render_template("contact.html")
return render_template('contact.html', CONTACT=CONTACT)
@app.route('/resume')
def resume():
@@ -316,5 +465,32 @@ def about():
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
# ---- patched helpers (appended) ----
def _verify_hub_signature(raw: bytes) -> None:
if not HUB_PUSH_SECRET:
abort(403, description="Missing HUB_PUSH_SECRET")
# Accept both header names and both formats
sig_hdr = (request.headers.get("X-Signature")
or request.headers.get("X-Hub-Signature-256")
or "").strip()
provided = sig_hdr
if provided.lower().startswith("sha256="):
provided = provided.split("=", 1)[1].strip()
# must be a 64-char hex, no whitespace
if not provided or any(c.isspace() for c in provided) or len(provided) != 64:
abort(403, description="Bad signature format")
expected = hmac.new(HUB_PUSH_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(provided, expected):
abort(403, description="Signature mismatch")
ts = request.headers.get("X-Timestamp")
if ts and ts.isdigit() and abs(time.time() - int(ts)) > 300:
abort(403, description="Stale request")
def _to_slug(text: str) -> str:
s = slugify(text or "")
return s or f"post-{int(time.time())}"