3 Commits

Author SHA1 Message Date
eaf363ee27 feat: Add curl support for blog pages
All checks were successful
Build Docker / BuildImage (push) Successful in 53s
2025-10-11 19:35:35 +11:00
0ea9db3473 feat: Update api routes to use similar json format to other routes 2025-10-11 19:16:50 +11:00
8d6acca5e9 feat: Add error message to header for HTML error responses 2025-10-11 18:55:24 +11:00
4 changed files with 159 additions and 83 deletions

View File

@@ -4,7 +4,7 @@ import datetime
import requests import requests
from mail import sendEmail from mail import sendEmail
from sol import create_transaction from sol import create_transaction
from tools import getClientIP, getGitCommit from tools import getClientIP, getGitCommit, json_response
api_bp = Blueprint('api', __name__) api_bp = Blueprint('api', __name__)
@@ -32,13 +32,18 @@ def help_get():
"/version": "Get the current version of the website", "/version": "Get the current version of the website",
"/help": "Get this help message" "/help": "Get this help message"
}, },
"version": getGitCommit() "base_url": "/api/v1",
"version": getGitCommit(),
"ip": getClientIP(request),
"status": 200
}) })
@api_bp.route("/version") @api_bp.route("/version")
def version_get(): def version_get():
return jsonify({"version": getGitCommit()}) return jsonify({"version": getGitCommit()})
@api_bp.route("/time") @api_bp.route("/time")
def time_get(): def time_get():
timezone_offset = datetime.timedelta(hours=ncConfig["time-zone"]) timezone_offset = datetime.timedelta(hours=ncConfig["time-zone"])
@@ -48,12 +53,20 @@ def time_get():
"timestring": time.strftime("%A, %B %d, %Y %I:%M %p"), "timestring": time.strftime("%A, %B %d, %Y %I:%M %p"),
"timestamp": time.timestamp(), "timestamp": time.timestamp(),
"timezone": ncConfig["time-zone"], "timezone": ncConfig["time-zone"],
"timeISO": time.isoformat() "timeISO": time.isoformat(),
"ip": getClientIP(request),
"status": 200
}) })
@api_bp.route("/timezone") @api_bp.route("/timezone")
def timezone_get(): def timezone_get():
return jsonify({"timezone": ncConfig["time-zone"]}) return jsonify({
"timezone": ncConfig["time-zone"],
"ip": getClientIP(request),
"status": 200
})
@api_bp.route("/timezone", methods=["POST"]) @api_bp.route("/timezone", methods=["POST"])
def timezone_post(): def timezone_post():
@@ -62,63 +75,68 @@ def timezone_post():
conf = requests.get( conf = requests.get(
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json") "https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json")
if conf.status_code != 200: if conf.status_code != 200:
return jsonify({"message": "Error: Could not get timezone"}) return json_response(request, "Error: Could not get timezone", 500)
if not conf.json(): if not conf.json():
return jsonify({"message": "Error: Could not get timezone"}) return json_response(request, "Error: Could not get timezone", 500)
conf = conf.json() conf = conf.json()
if "time-zone" not in conf: if "time-zone" not in conf:
return jsonify({"message": "Error: Could not get timezone"}) return json_response(request, "Error: Could not get timezone", 500)
ncConfig = conf ncConfig = conf
return jsonify({"message": "Successfully pulled latest timezone", "timezone": ncConfig["time-zone"]}) return jsonify({
"message": "Successfully pulled latest timezone",
"timezone": ncConfig["time-zone"],
"ip": getClientIP(request),
"status": 200
})
@api_bp.route("/message") @api_bp.route("/message")
def message_get(): def message_get():
return jsonify({"message": ncConfig["message"]}) return jsonify({
"message": ncConfig["message"],
"ip": getClientIP(request),
"status": 200
})
@api_bp.route("/ip") @api_bp.route("/ip")
def ip_get(): def ip_get():
return jsonify({"ip": getClientIP(request)}) return jsonify({
"ip": getClientIP(request),
"status": 200
})
@api_bp.route("/email", methods=["POST"]) @api_bp.route("/email", methods=["POST"])
def email_post(): def email_post():
# Verify json # Verify json
if not request.is_json: if not request.is_json:
return jsonify({ return json_response(request, "415 Unsupported Media Type", 415)
"status": 400,
"error": "Bad request JSON Data missing"
})
# Check if api key sent # Check if api key sent
data = request.json data = request.json
if not data: if not data:
return jsonify({ return json_response(request, "400 Bad Request", 400)
"status": 400,
"error": "Bad request JSON Data missing"
})
if "key" not in data: if "key" not in data:
return jsonify({ return json_response(request, "400 Bad Request 'key' missing", 400)
"status": 401,
"error": "Unauthorized 'key' missing"
})
if data["key"] != os.getenv("EMAIL_KEY"): if data["key"] != os.getenv("EMAIL_KEY"):
return jsonify({ return json_response(request, "401 Unauthorized", 401)
"status": 401,
"error": "Unauthorized 'key' invalid"
})
# TODO: Add client info to email
return sendEmail(data) return sendEmail(data)
@api_bp.route("/project") @api_bp.route("/project")
def project_get(): def project_get():
gitinfo = {
"website": None,
}
try: try:
git = requests.get( git = requests.get(
"https://git.woodburn.au/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1", "https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
headers={"Authorization": os.getenv("git_token")}, headers={"Authorization": os.getenv("git_token")},
) )
git = git.json() git = git.json()
@@ -126,24 +144,24 @@ def project_get():
repo_name = git["repo"]["name"] repo_name = git["repo"]["name"]
repo_name = repo_name.lower() repo_name = repo_name.lower()
repo_description = git["repo"]["description"] repo_description = git["repo"]["description"]
gitinfo["name"] = repo_name
gitinfo["description"] = repo_description
gitinfo["url"] = git["repo"]["html_url"]
if "website" in git["repo"]:
gitinfo["website"] = git["repo"]["website"]
except Exception as e: except Exception as e:
repo_name = "nathanwoodburn.github.io"
repo_description = "Personal website"
git = {
"repo": {
"html_url": "https://nathan.woodburn.au",
"name": "nathanwoodburn.github.io",
"description": "Personal website",
}
}
print(f"Error getting git data: {e}") print(f"Error getting git data: {e}")
return json_response(request, "500 Internal Server Error", 500)
return jsonify({ return jsonify({
"repo_name": repo_name, "repo_name": repo_name,
"repo_description": repo_description, "repo_description": repo_description,
"git": git, "repo": gitinfo,
"ip": getClientIP(request),
"status": 200
}) })
# region Solana Links # region Solana Links
SOLANA_HEADERS = { SOLANA_HEADERS = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -152,7 +170,6 @@ SOLANA_HEADERS = {
} }
@api_bp.route("/donate", methods=["GET", "OPTIONS"]) @api_bp.route("/donate", methods=["GET", "OPTIONS"])
def sol_donate_get(): def sol_donate_get():
data = { data = {
@@ -205,7 +222,6 @@ def sol_donate_post(amount):
if not request.json: if not request.json:
return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS
if "account" not in request.json: if "account" not in request.json:
return jsonify({"message": "Error: No account provided"}), 400, SOLANA_HEADERS return jsonify({"message": "Error: No account provided"}), 400, SOLANA_HEADERS
@@ -215,7 +231,7 @@ def sol_donate_post(amount):
try: try:
amount = float(amount) amount = float(amount)
except ValueError: except ValueError:
amount = 1 # Default to 1 SOL if invalid amount = 1 # Default to 1 SOL if invalid
if amount < 0.0001: if amount < 0.0001:
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
@@ -224,4 +240,3 @@ def sol_donate_post(amount):
return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS
# endregion # endregion

View File

@@ -1,8 +1,9 @@
import os import os
from flask import Blueprint, render_template, request from flask import Blueprint, render_template, request, jsonify
import markdown import markdown
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import re import re
from tools import isCurl, getClientIP
blog_bp = Blueprint('blog', __name__) blog_bp = Blueprint('blog', __name__)
@@ -105,30 +106,76 @@ def render_blog_home(handshake_scripts=None):
@blog_bp.route("/") @blog_bp.route("/")
def blog_index_get(): def blog_index_get():
global handshake_scripts if not isCurl(request):
global handshake_scripts
# If localhost, don't load handshake # If localhost, don't load handshake
if ( if (
request.host == "localhost:5000" request.host == "localhost:5000"
or request.host == "127.0.0.1:5000" or request.host == "127.0.0.1:5000"
or os.getenv("dev") == "true" or os.getenv("dev") == "true"
or request.host == "test.nathan.woodburn.au" or request.host == "test.nathan.woodburn.au"
): ):
handshake_scripts = "" handshake_scripts = ""
return render_blog_home(handshake_scripts)
# Get a list of pages
blog_pages = list_blog_page_files()
# Create a html list of pages
blog_pages = [
{"name":page.replace("_", " "),"url":f"/blog/{page}", "download": f"/blog/{page}.md"} for page in blog_pages
]
# Render the template
return jsonify({
"status": 200,
"message": "Check out my various blog postsa",
"ip": getClientIP(request),
"blogs": blog_pages
}), 200
return render_blog_home(handshake_scripts)
@blog_bp.route("/<path:path>") @blog_bp.route("/<path:path>")
def blog_path_get(path): def blog_path_get(path):
global handshake_scripts if not isCurl(request):
# If localhost, don't load handshake global handshake_scripts
if ( # If localhost, don't load handshake
request.host == "localhost:5000" if (
or request.host == "127.0.0.1:5000" request.host == "localhost:5000"
or os.getenv("dev") == "true" or request.host == "127.0.0.1:5000"
or request.host == "test.nathan.woodburn.au" or os.getenv("dev") == "true"
): or request.host == "test.nathan.woodburn.au"
handshake_scripts = "" ):
handshake_scripts = ""
return render_blog_page(path, handshake_scripts) return render_blog_page(path, handshake_scripts)
# Convert md to html
if not os.path.exists(f"data/blog/{path}.md"):
return render_template("404.html"), 404
with open(f"data/blog/{path}.md", "r") as f:
content = f.read()
# Get the title from the file name
title = path.replace("_", " ")
return jsonify({
"status": 200,
"message": f"Blog post: {title}",
"ip": getClientIP(request),
"title": title,
"content": content,
"download": f"/blog/{path}.md"
}), 200
@blog_bp.route("/<path:path>.md")
def blog_path_md_get(path):
if not os.path.exists(f"data/blog/{path}.md"):
return render_template("404.html"), 404
with open(f"data/blog/{path}.md", "r") as f:
content = f.read()
# Return the raw markdown file
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}

View File

@@ -149,6 +149,18 @@ def javascript_get(name):
return error_response(request) return error_response(request)
return send_from_directory("templates/assets/js", request.path.split("/")[-1]) return send_from_directory("templates/assets/js", request.path.split("/")[-1])
@app.route("/download/<path:path>")
def download_get(path):
if path not in DOWNLOAD_ROUTES:
return error_response(request, message="Invalid download")
# Check if file exists
path = DOWNLOAD_ROUTES[path]
if os.path.isfile(path):
return send_file(path)
return error_response(request, message="File not found")
# endregion # endregion
# region PWA routes # region PWA routes
@@ -195,6 +207,8 @@ def links_get():
@app.route("/api/<path:function>") @app.route("/api/<path:function>")
def api_legacy_get(function): def api_legacy_get(function):
# Check if function is in api blueprint
return redirect(f"/api/v1/{function}", code=301) return redirect(f"/api/v1/{function}", code=301)
@@ -553,7 +567,7 @@ def qrcode_get(data):
qr.make() qr.make()
qr_image: Image.Image = qr.make_image( qr_image: Image.Image = qr.make_image(
fill_color="black", back_color="white").convert('RGB') # type: ignore fill_color="black", back_color="white").convert('RGB') # type: ignore
# Add logo # Add logo
logo = Image.open("templates/assets/img/favicon/logo.png") logo = Image.open("templates/assets/img/favicon/logo.png")
@@ -583,18 +597,6 @@ def supersecretpath_get():
return render_template("ascii.html", ascii_art=ascii_art_html) return render_template("ascii.html", ascii_art=ascii_art_html)
@app.route("/download/<path:path>")
def download_get(path):
if path not in DOWNLOAD_ROUTES:
return error_response(request, message="Invalid download")
# Check if file exists
path = DOWNLOAD_ROUTES[path]
if os.path.isfile(path):
return send_file(path)
return error_response(request, message="File not found")
@app.route("/hosting/send-enquiry", methods=["POST"]) @app.route("/hosting/send-enquiry", methods=["POST"])
def hosting_post(): def hosting_post():
global EMAIL_REQUEST_COUNT global EMAIL_REQUEST_COUNT
@@ -756,11 +758,13 @@ def catch_all_get(path: str):
return error_response(request) return error_response(request)
@app.errorhandler(404) @app.errorhandler(404)
def not_found(e): def not_found(e):
return error_response(request) return error_response(request)
# endregion # endregion
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, port=5000, host="127.0.0.1") app.run(debug=True, port=5000, host="127.0.0.1")

View File

@@ -1,7 +1,8 @@
from flask import Request, render_template, jsonify from flask import Request, render_template, jsonify, make_response
import os import os
from functools import cache from functools import cache
def getClientIP(request): def getClientIP(request):
x_forwarded_for = request.headers.get("X-Forwarded-For") x_forwarded_for = request.headers.get("X-Forwarded-For")
if x_forwarded_for: if x_forwarded_for:
@@ -10,6 +11,7 @@ def getClientIP(request):
ip = request.remote_addr ip = request.remote_addr
return ip return ip
def getGitCommit(): def getGitCommit():
# if .git exists, get the latest commit hash # if .git exists, get the latest commit hash
if os.path.isdir(".git"): if os.path.isdir(".git"):
@@ -47,6 +49,7 @@ def isCurl(request: Request) -> bool:
return True return True
return False return False
def isCrawler(request: Request) -> bool: def isCrawler(request: Request) -> bool:
""" """
Check if the request is from a web crawler (e.g., Googlebot, Bingbot) Check if the request is from a web crawler (e.g., Googlebot, Bingbot)
@@ -79,6 +82,7 @@ def getFilePath(name, path):
if name in files: if name in files:
return os.path.join(root, name) return os.path.join(root, name)
def json_response(request: Request, message: str = "404 Not Found", code: int = 404): def json_response(request: Request, message: str = "404 Not Found", code: int = 404):
return jsonify( return jsonify(
{ {
@@ -94,6 +98,12 @@ def error_response(request: Request, message: str = "404 Not Found", code: int =
return json_response(request, message, code) return json_response(request, message, code)
# Check if <error code>.html exists in templates # Check if <error code>.html exists in templates
response = make_response(render_template(
"404.html", code=code, message=message), code)
if os.path.isfile(f"templates/{code}.html"): if os.path.isfile(f"templates/{code}.html"):
return render_template(f"{code}.html"), code response = make_response(render_template(
return render_template("404.html"), code f"{code}.html", code=code, message=message), code)
# Add message to response headers
response.headers["X-Error-Message"] = message
return response