feat: Add api routes for email and other basic info
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
This commit is contained in:
parent
855f6b3c99
commit
42aff1f455
88
mail.py
Normal file
88
mail.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import smtplib
|
||||||
|
import re
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.utils import formataddr
|
||||||
|
from flask import jsonify
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def validateSender(email):
|
||||||
|
domains = os.getenv("EMAIL_DOMAINS").split(",")
|
||||||
|
for domain in domains:
|
||||||
|
if re.match(r".+@" + domain, email):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def sendEmail(data):
|
||||||
|
fromEmail = "noreply@woodburn.au"
|
||||||
|
if "from" in data:
|
||||||
|
fromEmail = data["from"]
|
||||||
|
|
||||||
|
if not validateSender(fromEmail):
|
||||||
|
return jsonify({
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad request 'from' email invalid"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if "to" not in data:
|
||||||
|
return jsonify({
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad request 'to' json data missing"
|
||||||
|
})
|
||||||
|
to = data["to"]
|
||||||
|
|
||||||
|
if "subject" not in data:
|
||||||
|
return jsonify({
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad request 'subject' json data missing"
|
||||||
|
})
|
||||||
|
subject = data["subject"]
|
||||||
|
|
||||||
|
if "body" not in data:
|
||||||
|
return jsonify({
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad request 'body' json data missing"
|
||||||
|
})
|
||||||
|
body = data["body"]
|
||||||
|
|
||||||
|
if not re.match(r"[^@]+@[^@]+\.[^@]+", to):
|
||||||
|
raise ValueError("Invalid recipient email address.")
|
||||||
|
|
||||||
|
if not subject:
|
||||||
|
raise ValueError("Subject cannot be empty.")
|
||||||
|
|
||||||
|
if not body:
|
||||||
|
raise ValueError("Body cannot be empty.")
|
||||||
|
|
||||||
|
fromName = "Nathan Woodburn"
|
||||||
|
if 'sender' in data:
|
||||||
|
fromName = data['sender']
|
||||||
|
|
||||||
|
# Create the email message
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
msg['From'] = formataddr((fromName, fromEmail))
|
||||||
|
msg['To'] = to
|
||||||
|
msg['Subject'] = subject
|
||||||
|
msg.attach(MIMEText(body, 'plain'))
|
||||||
|
|
||||||
|
# Sending the email
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP_SSL(os.getenv("EMAIL_SMTP"), 465) as server:
|
||||||
|
server.login(os.getenv("EMAIL_USER"), os.getenv("EMAIL_PASS"))
|
||||||
|
server.sendmail(fromEmail, to, msg.as_string())
|
||||||
|
print("Email sent successfully.")
|
||||||
|
return jsonify({
|
||||||
|
"status": 200,
|
||||||
|
"message": "Send email successfully"
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
"status": 500,
|
||||||
|
"error": "Sending email failed",
|
||||||
|
"exception":e
|
||||||
|
})
|
||||||
|
|
||||||
|
|
46
now.py
Normal file
46
now.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import os
|
||||||
|
from flask import render_template
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def list_now_page_files():
|
||||||
|
now_pages = os.listdir("templates/now")
|
||||||
|
now_pages = [
|
||||||
|
page for page in now_pages if page != "template.html" and page != "old.html"
|
||||||
|
]
|
||||||
|
now_pages.sort(reverse=True)
|
||||||
|
return now_pages
|
||||||
|
|
||||||
|
def list_now_dates():
|
||||||
|
now_pages = list_now_page_files()
|
||||||
|
now_dates = [page.split(".")[0] for page in now_pages]
|
||||||
|
return now_dates
|
||||||
|
|
||||||
|
def get_latest_now_date(formatted=False):
|
||||||
|
if formatted:
|
||||||
|
date=list_now_dates()[0]
|
||||||
|
date = datetime.strptime(date, "%y_%m_%d")
|
||||||
|
date = date.strftime("%A, %B %d, %Y")
|
||||||
|
return date
|
||||||
|
return list_now_dates()[0]
|
||||||
|
|
||||||
|
#region Rendering
|
||||||
|
def render_now_page(date,handshake_scripts=None):
|
||||||
|
# If the date is not available, render the latest page
|
||||||
|
if date is None:
|
||||||
|
return render_latest_now(handshake_scripts=handshake_scripts)
|
||||||
|
if not date in list_now_dates():
|
||||||
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
|
date_formatted = datetime.strptime(date, "%y_%m_%d")
|
||||||
|
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
|
||||||
|
return render_template(f"now/{date}.html",DATE=date_formatted,handshake_scripts=handshake_scripts)
|
||||||
|
|
||||||
|
|
||||||
|
def render_latest_now(handshake_scripts=None):
|
||||||
|
now_page = list_now_dates()[0]
|
||||||
|
print(now_page)
|
||||||
|
return render_now_page(now_page,handshake_scripts=handshake_scripts)
|
||||||
|
|
||||||
|
#endregion
|
207
server.py
207
server.py
@ -31,6 +31,8 @@ from solders.message import MessageV0
|
|||||||
from solders.transaction import VersionedTransaction
|
from solders.transaction import VersionedTransaction
|
||||||
from solders.null_signer import NullSigner
|
from solders.null_signer import NullSigner
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from mail import sendEmail
|
||||||
|
import now
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
@ -295,7 +297,7 @@ def donateAmount(amount):
|
|||||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
||||||
"label": f"Donate {amount} SOL to Nathan.Woodburn/",
|
"label": f"Donate {amount} SOL to Nathan.Woodburn/",
|
||||||
"title": "Donate to Nathan.Woodburn/",
|
"title": "Donate to Nathan.Woodburn/",
|
||||||
"description": "Donate {amount} SOL to Nathan.Woodburn/",
|
"description": f"Donate {amount} SOL to Nathan.Woodburn/",
|
||||||
}
|
}
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
@ -343,12 +345,90 @@ def donateAmountPost(amount):
|
|||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
#region Other API routes
|
||||||
|
@app.route("/api/time")
|
||||||
|
def time():
|
||||||
|
timezone_offset = datetime.timedelta(hours=ncConfig["time-zone"])
|
||||||
|
timezone = datetime.timezone(offset=timezone_offset)
|
||||||
|
time = datetime.datetime.now(tz=timezone)
|
||||||
|
return jsonify({
|
||||||
|
"timestring": time.strftime("%A, %B %d, %Y %I:%M %p"),
|
||||||
|
"timestamp": time.timestamp(),
|
||||||
|
"timezone": ncConfig["time-zone"],
|
||||||
|
"timeISO": time.isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/timezone")
|
||||||
|
def timezone():
|
||||||
|
return jsonify({"timezone": ncConfig["time-zone"]})
|
||||||
|
|
||||||
|
@app.route("/api/timezone", methods=["POST"])
|
||||||
|
def timezonePost():
|
||||||
|
# Refresh config
|
||||||
|
global ncConfig
|
||||||
|
conf = requests.get("https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json")
|
||||||
|
if conf.status_code != 200:
|
||||||
|
return jsonify({"message": "Error: Could not get timezone"})
|
||||||
|
if not conf.json():
|
||||||
|
return jsonify({"message": "Error: Could not get timezone"})
|
||||||
|
conf = conf.json()
|
||||||
|
if "time-zone" not in conf:
|
||||||
|
return jsonify({"message": "Error: Could not get timezone"})
|
||||||
|
|
||||||
|
ncConfig = conf
|
||||||
|
return jsonify({"message": "Successfully pulled latest timezone", "timezone": ncConfig["time-zone"]})
|
||||||
|
|
||||||
|
@app.route("/api/message")
|
||||||
|
def nc():
|
||||||
|
return jsonify({"message": ncConfig["message"]})
|
||||||
|
|
||||||
|
@app.route("/api/ip")
|
||||||
|
def ip():
|
||||||
|
return jsonify({"ip": request.remote_addr})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/email", methods=["POST"])
|
||||||
|
def email():
|
||||||
|
# Verify json
|
||||||
|
if not request.is_json:
|
||||||
|
return jsonify({
|
||||||
|
"status": 400,
|
||||||
|
"error": "Bad request JSON Data missing"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check if api key sent
|
||||||
|
data = request.json
|
||||||
|
if "key" not in data:
|
||||||
|
return jsonify({
|
||||||
|
"status": 401,
|
||||||
|
"error": "Unauthorized 'key' missing"
|
||||||
|
})
|
||||||
|
|
||||||
|
if data["key"] != os.getenv("EMAIL_KEY"):
|
||||||
|
return jsonify({
|
||||||
|
"status": 401,
|
||||||
|
"error": "Unauthorized 'key' invalid"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return sendEmail(data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
# region Main routes
|
# region Main routes
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
|
global handshake_scripts
|
||||||
|
global projects
|
||||||
|
global projectsUpdated
|
||||||
|
|
||||||
# Check if host if podcast.woodburn.au
|
# Check if host if podcast.woodburn.au
|
||||||
if "podcast.woodburn.au" in request.host:
|
if "podcast.woodburn.au" in request.host:
|
||||||
return render_template("podcast.html")
|
return render_template("podcast.html")
|
||||||
@ -361,6 +441,16 @@ def index():
|
|||||||
|
|
||||||
# Check if crawler
|
# Check if crawler
|
||||||
if request.headers:
|
if request.headers:
|
||||||
|
# Check if curl
|
||||||
|
if "curl" in request.headers.get("User-Agent"):
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"message": "Welcome to Nathan.Woodburn/! This is a personal website. For more information, visit https://nathan.woodburn.au",
|
||||||
|
"ip": request.remote_addr,
|
||||||
|
"dev": handshake_scripts == "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if "Googlebot" not in request.headers.get(
|
if "Googlebot" not in request.headers.get(
|
||||||
"User-Agent"
|
"User-Agent"
|
||||||
) and "Bingbot" not in request.headers.get("User-Agent"):
|
) and "Bingbot" not in request.headers.get("User-Agent"):
|
||||||
@ -376,11 +466,7 @@ def index():
|
|||||||
)
|
)
|
||||||
resp.set_cookie("loaded", "true", max_age=604800)
|
resp.set_cookie("loaded", "true", max_age=604800)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
global handshake_scripts
|
|
||||||
global projects
|
|
||||||
global projectsUpdated
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
git = requests.get(
|
git = requests.get(
|
||||||
"https://git.woodburn.au/api/v1/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",
|
||||||
@ -477,26 +563,15 @@ def index():
|
|||||||
<script>
|
<script>
|
||||||
function startClock(timezoneOffset) {
|
function startClock(timezoneOffset) {
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
// Get current UTC time
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Calculate the local time based on the timezone offset
|
|
||||||
const localTime = new Date(now.getTime() + timezoneOffset * 3600 * 1000);
|
const localTime = new Date(now.getTime() + timezoneOffset * 3600 * 1000);
|
||||||
|
|
||||||
// Generate timezone name dynamically
|
|
||||||
const tzName = timezoneOffset >= 0 ? `UTC+${timezoneOffset}` : `UTC`;
|
const tzName = timezoneOffset >= 0 ? `UTC+${timezoneOffset}` : `UTC`;
|
||||||
|
|
||||||
// Format the local time as HH:MM:SS
|
|
||||||
const hours = String(localTime.getUTCHours()).padStart(2, '0');
|
const hours = String(localTime.getUTCHours()).padStart(2, '0');
|
||||||
const minutes = String(localTime.getUTCMinutes()).padStart(2, '0');
|
const minutes = String(localTime.getUTCMinutes()).padStart(2, '0');
|
||||||
const seconds = String(localTime.getUTCSeconds()).padStart(2, '0');
|
const seconds = String(localTime.getUTCSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
// Display the formatted time with the timezone name
|
|
||||||
const timeString = `${hours}:${minutes}:${seconds} ${tzName}`;
|
const timeString = `${hours}:${minutes}:${seconds} ${tzName}`;
|
||||||
document.getElementById('time').textContent = timeString;
|
document.getElementById('time').textContent = timeString;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the clock immediately and then every second
|
|
||||||
updateClock();
|
updateClock();
|
||||||
setInterval(updateClock, 1000);
|
setInterval(updateClock, 1000);
|
||||||
}
|
}
|
||||||
@ -536,7 +611,7 @@ def index():
|
|||||||
# region Now Pages
|
# region Now Pages
|
||||||
@app.route("/now")
|
@app.route("/now")
|
||||||
@app.route("/now/")
|
@app.route("/now/")
|
||||||
def now():
|
def now_page():
|
||||||
global handshake_scripts
|
global handshake_scripts
|
||||||
|
|
||||||
# If localhost, don't load handshake
|
# If localhost, don't load handshake
|
||||||
@ -548,18 +623,7 @@ def now():
|
|||||||
):
|
):
|
||||||
handshake_scripts = ""
|
handshake_scripts = ""
|
||||||
|
|
||||||
# Get latest now page
|
return now.render_latest_now(handshake_scripts)
|
||||||
files = os.listdir("templates/now")
|
|
||||||
# Remove template
|
|
||||||
files = [file for file in files if file != "template.html" and file != "old.html"]
|
|
||||||
files.sort(reverse=True)
|
|
||||||
date = files[0].strip(".html")
|
|
||||||
# Convert to date
|
|
||||||
date = datetime.datetime.strptime(date, "%y_%m_%d")
|
|
||||||
date = date.strftime("%A, %B %d, %Y")
|
|
||||||
return render_template(
|
|
||||||
"now/" + files[0], handshake_scripts=handshake_scripts, DATE=date
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/now/<path:path>")
|
@app.route("/now/<path:path>")
|
||||||
@ -573,31 +637,9 @@ def now_path(path):
|
|||||||
or request.host == "test.nathan.woodburn.au"
|
or request.host == "test.nathan.woodburn.au"
|
||||||
):
|
):
|
||||||
handshake_scripts = ""
|
handshake_scripts = ""
|
||||||
|
|
||||||
|
|
||||||
date = path
|
return now.render_now_page(path,handshake_scripts)
|
||||||
date = date.strip(".html")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Convert to date
|
|
||||||
date = datetime.datetime.strptime(date, "%y_%m_%d")
|
|
||||||
date = date.strftime("%A, %B %d, %Y")
|
|
||||||
except:
|
|
||||||
date = ""
|
|
||||||
|
|
||||||
if path.lower().replace(".html", "") == "template":
|
|
||||||
return render_template("404.html"), 404
|
|
||||||
|
|
||||||
# If file exists, load it
|
|
||||||
if os.path.isfile("templates/now/" + path):
|
|
||||||
return render_template(
|
|
||||||
"now/" + path, handshake_scripts=handshake_scripts, DATE=date
|
|
||||||
)
|
|
||||||
if os.path.isfile("templates/now/" + path + ".html"):
|
|
||||||
return render_template(
|
|
||||||
"now/" + path + ".html", handshake_scripts=handshake_scripts, DATE=date
|
|
||||||
)
|
|
||||||
|
|
||||||
return render_template("404.html"), 404
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/old")
|
@app.route("/old")
|
||||||
@ -614,20 +656,16 @@ def now_old():
|
|||||||
or request.host == "test.nathan.woodburn.au"
|
or request.host == "test.nathan.woodburn.au"
|
||||||
):
|
):
|
||||||
handshake_scripts = ""
|
handshake_scripts = ""
|
||||||
|
|
||||||
now_pages = os.listdir("templates/now")
|
now_dates = now.list_now_dates()[1:]
|
||||||
now_pages = [
|
|
||||||
page for page in now_pages if page != "template.html" and page != "old.html"
|
|
||||||
]
|
|
||||||
now_pages.sort(reverse=True)
|
|
||||||
html = '<ul class="list-group">'
|
html = '<ul class="list-group">'
|
||||||
latest = " (Latest)"
|
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{now.get_latest_now_date(True)}</li></a>'
|
||||||
for page in now_pages:
|
|
||||||
link = page.strip(".html")
|
for date in now_dates:
|
||||||
date = datetime.datetime.strptime(link, "%y_%m_%d")
|
link = date
|
||||||
|
date = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||||
date = date.strftime("%A, %B %d, %Y")
|
date = date.strftime("%A, %B %d, %Y")
|
||||||
html += f'<a style="text-decoration:none;" href="/now/{link}"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{date}{latest}</li></a>'
|
html += f'<a style="text-decoration:none;" href="/now/{link}"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{date}</li></a>'
|
||||||
latest = ""
|
|
||||||
|
|
||||||
html += "</ul>"
|
html += "</ul>"
|
||||||
return render_template(
|
return render_template(
|
||||||
@ -640,11 +678,7 @@ def now_rss():
|
|||||||
if ":" in request.host:
|
if ":" in request.host:
|
||||||
host = "http://" + request.host
|
host = "http://" + request.host
|
||||||
# Generate RSS feed
|
# Generate RSS feed
|
||||||
now_pages = os.listdir("templates/now")
|
now_pages = now.list_now_page_files()
|
||||||
now_pages = [
|
|
||||||
page for page in now_pages if page != "template.html" and page != "old.html"
|
|
||||||
]
|
|
||||||
now_pages.sort(reverse=True)
|
|
||||||
rss = f'<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nathan.Woodburn/</title><link>{host}</link><description>See what I\'ve been up to</description><language>en-us</language><lastBuildDate>{datetime.datetime.now(tz=datetime.timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")}</lastBuildDate><atom:link href="{host}/now.rss" rel="self" type="application/rss+xml" />'
|
rss = f'<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nathan.Woodburn/</title><link>{host}</link><description>See what I\'ve been up to</description><language>en-us</language><lastBuildDate>{datetime.datetime.now(tz=datetime.timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")}</lastBuildDate><atom:link href="{host}/now.rss" rel="self" type="application/rss+xml" />'
|
||||||
for page in now_pages:
|
for page in now_pages:
|
||||||
link = page.strip(".html")
|
link = page.strip(".html")
|
||||||
@ -656,11 +690,7 @@ def now_rss():
|
|||||||
|
|
||||||
@app.route("/now.json")
|
@app.route("/now.json")
|
||||||
def now_json():
|
def now_json():
|
||||||
now_pages = os.listdir("templates/now")
|
now_pages = now.list_now_page_files()
|
||||||
now_pages = [
|
|
||||||
page for page in now_pages if page != "template.html" and page != "old.html"
|
|
||||||
]
|
|
||||||
now_pages.sort(reverse=True)
|
|
||||||
host = "https://" + request.host
|
host = "https://" + request.host
|
||||||
if ":" in request.host:
|
if ":" in request.host:
|
||||||
host = "http://" + request.host
|
host = "http://" + request.host
|
||||||
@ -878,7 +908,7 @@ def catch_all(path: str):
|
|||||||
|
|
||||||
if path.lower().replace(".html", "") in restricted:
|
if path.lower().replace(".html", "") in restricted:
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
print(path)
|
|
||||||
if path in redirects:
|
if path in redirects:
|
||||||
return redirect(redirects[path], code=302)
|
return redirect(redirects[path], code=302)
|
||||||
|
|
||||||
@ -904,6 +934,16 @@ def catch_all(path: str):
|
|||||||
if filename:
|
if filename:
|
||||||
return send_file(filename)
|
return send_file(filename)
|
||||||
|
|
||||||
|
if request.headers:
|
||||||
|
# Check if curl
|
||||||
|
if "curl" in request.headers.get("User-Agent"):
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": 404,
|
||||||
|
"message": "Page not found",
|
||||||
|
"ip": request.remote_addr,
|
||||||
|
}
|
||||||
|
), 404
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
@ -996,6 +1036,17 @@ def podsync():
|
|||||||
# 404 catch all
|
# 404 catch all
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
|
if request.headers:
|
||||||
|
# Check if curl
|
||||||
|
if "curl" in request.headers.get("User-Agent"):
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": 404,
|
||||||
|
"message": "Page not found",
|
||||||
|
"ip": request.remote_addr,
|
||||||
|
}
|
||||||
|
), 404
|
||||||
|
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user