generated from nathanwoodburn/python-webserver-template
feat: Add initial code
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
This commit is contained in:
parent
8508e5fbd7
commit
039a24bc64
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ __pycache__/
|
|||||||
.env
|
.env
|
||||||
.vs/
|
.vs/
|
||||||
.venv/
|
.venv/
|
||||||
|
notifications.json
|
@ -9,7 +9,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
|||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
# Optionally mount /data to store the data
|
# Optionally mount /data to store the data
|
||||||
# VOLUME /data
|
VOLUME /data
|
||||||
|
|
||||||
ENTRYPOINT ["python3"]
|
ENTRYPOINT ["python3"]
|
||||||
CMD ["main.py"]
|
CMD ["main.py"]
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
flask
|
flask
|
||||||
gunicorn
|
gunicorn
|
||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
cachetools
|
||||||
|
markdownify
|
352
server.py
352
server.py
@ -15,11 +15,17 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import dotenv
|
import dotenv
|
||||||
|
from functools import cache
|
||||||
|
from markdownify import markdownify as md
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
notification_file = "/data/notifications.json"
|
||||||
|
if not os.path.exists("/data"):
|
||||||
|
notification_file = "./notifications.json"
|
||||||
|
|
||||||
|
|
||||||
def find(name, path):
|
def find(name, path):
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
@ -27,6 +33,8 @@ def find(name, path):
|
|||||||
return os.path.join(root, name)
|
return os.path.join(root, name)
|
||||||
|
|
||||||
# Assets routes
|
# Assets routes
|
||||||
|
|
||||||
|
|
||||||
@app.route("/assets/<path:path>")
|
@app.route("/assets/<path:path>")
|
||||||
def send_assets(path):
|
def send_assets(path):
|
||||||
if path.endswith(".json"):
|
if path.endswith(".json"):
|
||||||
@ -74,7 +82,335 @@ def wellknown(path):
|
|||||||
# region Main routes
|
# region Main routes
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
# Check if user is logged in
|
||||||
|
token = request.cookies.get("token")
|
||||||
|
if token:
|
||||||
|
return redirect("/inbox")
|
||||||
|
|
||||||
|
return render_template("login.html", login=f"https://login.hns.au/auth?return={request.url}login")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/login")
|
||||||
|
def login():
|
||||||
|
# Get username and token from args
|
||||||
|
token = request.args.get("token")
|
||||||
|
# Set token cookie
|
||||||
|
response = make_response(redirect("/inbox"))
|
||||||
|
response.set_cookie("token", token)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/logout")
|
||||||
|
def logout():
|
||||||
|
# Delete token cookie
|
||||||
|
response = make_response(redirect("/"))
|
||||||
|
response.set_cookie("token", "")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/inbox")
|
||||||
|
def inbox():
|
||||||
|
# Check if user is logged in
|
||||||
|
token = request.cookies.get("token")
|
||||||
|
if not token:
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
|
||||||
|
user = get_user(token)
|
||||||
|
if not user:
|
||||||
|
return redirect("/logout")
|
||||||
|
|
||||||
|
email = user["hemail"]
|
||||||
|
return render_template("inbox.html", emails=get_email(email))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/delete/<path:path>")
|
||||||
|
def delete(path):
|
||||||
|
# Check if user is logged in
|
||||||
|
token = request.cookies.get("token")
|
||||||
|
if not token:
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
user = get_user(token)
|
||||||
|
if not user:
|
||||||
|
return redirect("/logout")
|
||||||
|
|
||||||
|
email = user["hemail"]
|
||||||
|
delete_email(path, email)
|
||||||
|
return redirect("/inbox")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/notifications")
|
||||||
|
def notifications():
|
||||||
|
# Check if user is logged in
|
||||||
|
token = request.cookies.get("token")
|
||||||
|
if not token:
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
user = get_user(token)
|
||||||
|
if not user:
|
||||||
|
return redirect("/logout")
|
||||||
|
|
||||||
|
email = user["hemail"]
|
||||||
|
notification = get_notification_settings(email)
|
||||||
|
if not notification:
|
||||||
|
return render_template("notifications.html", email=email)
|
||||||
|
return render_template("notifications.html", email=email, webhook=notification["webhook"], email_alert=notification["email"])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/notifications", methods=["POST"])
|
||||||
|
def notification_set():
|
||||||
|
# Check if user is logged in
|
||||||
|
token = request.cookies.get("token")
|
||||||
|
if not token:
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
user = get_user(token)
|
||||||
|
if not user:
|
||||||
|
return redirect("/logout")
|
||||||
|
|
||||||
|
email = user["hemail"]
|
||||||
|
webhook = request.form.get("webhook")
|
||||||
|
email_alert = request.form.get("email")
|
||||||
|
|
||||||
|
save_notification_settings(email, webhook, email_alert)
|
||||||
|
|
||||||
|
return redirect("/notifications")
|
||||||
|
|
||||||
|
|
||||||
|
def save_notification_settings(email, webhook, email_alert):
|
||||||
|
if not os.path.exists(notification_file):
|
||||||
|
with open(notification_file, "w") as f:
|
||||||
|
json.dump({
|
||||||
|
email: {
|
||||||
|
"webhook": webhook,
|
||||||
|
"email": email_alert
|
||||||
|
}
|
||||||
|
}, f, indent=4)
|
||||||
|
|
||||||
|
with open(notification_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data[email] = {
|
||||||
|
"webhook": webhook,
|
||||||
|
"email": email_alert
|
||||||
|
}
|
||||||
|
with open(notification_file, "w") as f:
|
||||||
|
json.dump(data, f, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def get_notification_settings(email):
|
||||||
|
if not os.path.exists(notification_file):
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(notification_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if email not in data:
|
||||||
|
return {"webhook": "", "email": ""}
|
||||||
|
return data[email]
|
||||||
|
|
||||||
|
|
||||||
|
def delete_email(conversation_id, email):
|
||||||
|
# Get email by id
|
||||||
|
conversation = get_conversation(conversation_id)
|
||||||
|
if not conversation:
|
||||||
|
return
|
||||||
|
print(conversation)
|
||||||
|
|
||||||
|
if len(conversation) < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"),
|
||||||
|
}
|
||||||
|
for thread in conversation:
|
||||||
|
for message in thread["messages"]:
|
||||||
|
if email in message["to"]:
|
||||||
|
requests.delete(
|
||||||
|
f"https://ticket.woodburn.au/api/conversations/{thread['id']}", headers=headers)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/email", methods=["POST"])
|
||||||
|
def newemail():
|
||||||
|
# Get the json
|
||||||
|
json = request.get_json()
|
||||||
|
|
||||||
|
# Get header x-freescout-event
|
||||||
|
event = request.headers.get("x-freescout-event")
|
||||||
|
|
||||||
|
if event == "convo.created":
|
||||||
|
send_notification(json)
|
||||||
|
elif event == "convo.customer.reply.created":
|
||||||
|
send_notification(json)
|
||||||
|
|
||||||
|
return jsonify(json)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/email/<path:path>")
|
||||||
|
def getemail(path):
|
||||||
|
email = f'{path}@login.hns.au'
|
||||||
|
return jsonify(get_email(email))
|
||||||
|
|
||||||
|
|
||||||
|
def send_notification(data):
|
||||||
|
emails = data["cc"]
|
||||||
|
for email in emails:
|
||||||
|
notifications = get_notification_settings(email)
|
||||||
|
if not notifications:
|
||||||
|
continue
|
||||||
|
if notifications["email"] != "":
|
||||||
|
send_email(notifications["email"], data)
|
||||||
|
if notifications["webhook"] != "":
|
||||||
|
send_webhook(notifications["webhook"], data)
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(email, data):
|
||||||
|
print(f"Sending email to {email}")
|
||||||
|
headers = {
|
||||||
|
"X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"),
|
||||||
|
}
|
||||||
|
sender = f"{data['createdBy']['firstName']} {
|
||||||
|
data['createdBy']['lastName']} ({data['createdBy']['email']})"
|
||||||
|
message = data["_embedded"]["threads"][0]["body"]
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"type": "email",
|
||||||
|
"mailboxId": os.getenv("FREESCOUT_MAILBOX"),
|
||||||
|
"subject": f"New email from {sender}",
|
||||||
|
"customer": {
|
||||||
|
"email": email
|
||||||
|
},
|
||||||
|
"threads": [
|
||||||
|
{
|
||||||
|
"text": f"You have a new email from {sender}\n\n{message}",
|
||||||
|
"type": "message",
|
||||||
|
"user": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"imported": False,
|
||||||
|
"status": "closed",
|
||||||
|
}
|
||||||
|
req = requests.post(
|
||||||
|
"https://ticket.woodburn.au/api/conversations", json=body, headers=headers)
|
||||||
|
if req.status_code != 201:
|
||||||
|
print(f"Failed to send email")
|
||||||
|
print(req.text)
|
||||||
|
|
||||||
|
|
||||||
|
def send_webhook(webhook, data):
|
||||||
|
print(f"Sending webhook to {webhook}")
|
||||||
|
sender = f"{data['createdBy']['firstName']} {
|
||||||
|
data['createdBy']['lastName']} ({data['createdBy']['email']})"
|
||||||
|
message = data["_embedded"]["threads"][0]["body"]
|
||||||
|
|
||||||
|
message = md(message)
|
||||||
|
|
||||||
|
trimmed = len(message) > 3999
|
||||||
|
# Only keep 4000 chars
|
||||||
|
message = message[:4000]
|
||||||
|
|
||||||
|
subject = data["subject"]
|
||||||
|
body = {
|
||||||
|
"content": "You have a new email",
|
||||||
|
"embeds": [
|
||||||
|
{
|
||||||
|
"title": subject,
|
||||||
|
"description": message,
|
||||||
|
"url": "https://email.hns.au",
|
||||||
|
"color": 5814783,
|
||||||
|
"author": {
|
||||||
|
"name": sender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"username": "HNS Login Email",
|
||||||
|
"avatar_url": "https://hns.au/favicon.png",
|
||||||
|
"attachments": []
|
||||||
|
}
|
||||||
|
if trimmed:
|
||||||
|
body["embeds"][0]["footer"] = {
|
||||||
|
"text": "Email has been trimmed"
|
||||||
|
}
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
webhook, json=body)
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get_user(token):
|
||||||
|
req = requests.get(f"https://login.hns.au/auth/user?token={token}")
|
||||||
|
if req.status_code != 200:
|
||||||
|
return False
|
||||||
|
return req.json()
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get_email(email):
|
||||||
|
params = {
|
||||||
|
"embed": "threads",
|
||||||
|
"mailboxId": os.getenv("FREESCOUT_MAILBOX"),
|
||||||
|
"status": "active",
|
||||||
|
"type": "email",
|
||||||
|
"sortField": "updatedAt",
|
||||||
|
"sortOrder": "desc",
|
||||||
|
"page": "1",
|
||||||
|
"pageSize": "100"
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"),
|
||||||
|
}
|
||||||
|
|
||||||
|
conversations = requests.get(
|
||||||
|
f"https://ticket.woodburn.au/api/conversations", json=params, headers=headers)
|
||||||
|
|
||||||
|
if conversations.status_code != 200:
|
||||||
|
return jsonify({"email": email, "error": "Could not get conversations"}, status=400)
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
conversations = conversations.json()
|
||||||
|
for conversation in conversations["_embedded"]["conversations"]:
|
||||||
|
addresses = conversation["cc"]
|
||||||
|
for address in addresses:
|
||||||
|
if address == email:
|
||||||
|
threads.append(parse_email(conversation))
|
||||||
|
|
||||||
|
return {"email": email, "conversations": threads}
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get_conversation(id):
|
||||||
|
params = {
|
||||||
|
"number": id,
|
||||||
|
"embed": "threads",
|
||||||
|
"mailboxId": os.getenv("FREESCOUT_MAILBOX"),
|
||||||
|
"status": "active",
|
||||||
|
"type": "email",
|
||||||
|
"sortField": "updatedAt",
|
||||||
|
"sortOrder": "desc",
|
||||||
|
"page": "1",
|
||||||
|
"pageSize": "100",
|
||||||
|
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"),
|
||||||
|
}
|
||||||
|
|
||||||
|
conversations = requests.get(
|
||||||
|
f"https://ticket.woodburn.au/api/conversations", json=params, headers=headers)
|
||||||
|
|
||||||
|
if conversations.status_code != 200:
|
||||||
|
return {"conversations": []}
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
conversations = conversations.json()
|
||||||
|
for conversation in conversations["_embedded"]["conversations"]:
|
||||||
|
threads.append(parse_email(conversation))
|
||||||
|
|
||||||
|
return threads
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
@ -100,10 +436,24 @@ def catch_all(path: str):
|
|||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
def parse_email(conversation):
|
||||||
|
messages = []
|
||||||
|
threads = conversation["_embedded"]["threads"]
|
||||||
|
for thread in threads:
|
||||||
|
messages.append({
|
||||||
|
"from": thread["createdBy"]["email"],
|
||||||
|
"name": f'{thread["createdBy"]["firstName"]} {thread["createdBy"]["lastName"]}',
|
||||||
|
"body": thread["body"],
|
||||||
|
"date": thread["createdAt"],
|
||||||
|
"to": thread["to"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"messages": messages, "id": conversation["id"]}
|
||||||
|
|
||||||
# region Error Catching
|
# region Error Catching
|
||||||
# 404 catch all
|
# 404 catch all
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
@ -2,19 +2,103 @@ body {
|
|||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centre {
|
.centre {
|
||||||
margin-top: 10%;
|
margin-top: 10%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #000000;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #000000;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation {
|
||||||
|
border: 5px solid #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from {
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: inline;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #000000;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body a {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #000000;
|
||||||
|
padding: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: min(500px, 90%);
|
||||||
}
|
}
|
37
templates/inbox.html
Normal file
37
templates/inbox.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Inbox | HNS Login Email</title>
|
||||||
|
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
|
<link rel="stylesheet" href="/assets/css/index.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header style="display: inline;">
|
||||||
|
<h1>HNS Login Email</h1> <span style="float:right">{{emails.email}} | <a href="/notifications">Notifications</a> | <a href="/logout">Logout</a></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="centre">
|
||||||
|
{% for conversation in emails.conversations %}
|
||||||
|
<div class="conversation">
|
||||||
|
<span>Conversation: {{conversation.id}}</span>
|
||||||
|
{% for message in conversation.messages %}
|
||||||
|
<div class="email">
|
||||||
|
<div class="from">{{message.name}} ({{message.from}})</div> <div class="date">{{message.date}}</div>
|
||||||
|
<div class="body">{{message.body|safe}}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<a class="button" href="/delete/{{conversation.id}}">Delete Conversation</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
19
templates/login.html
Normal file
19
templates/login.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HNS Login Email</title>
|
||||||
|
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
|
<link rel="stylesheet" href="/assets/css/index.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="centre">
|
||||||
|
<a href="{{login}}"><h1>Login to HNS Login Email</h1></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
33
templates/notifications.html
Normal file
33
templates/notifications.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Notifications | HNS Login Email</title>
|
||||||
|
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
|
<link rel="stylesheet" href="/assets/css/index.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header style="display: inline;">
|
||||||
|
<h1>HNS Login Email</h1> <span style="float:right">{{email}} | <a href="/inbox">Inbox</a> | <a
|
||||||
|
href="/logout">Logout</a></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="centre">
|
||||||
|
<form action="/notifications" method="post">
|
||||||
|
<!-- Discord webhook url -->
|
||||||
|
<label for="webhook">Discord Webhook URL</label>
|
||||||
|
<input type="text" name="webhook" placeholder="https://discord.com/api/webhooks/..." value="{{webhook}}">
|
||||||
|
<!-- Email -->
|
||||||
|
<br>
|
||||||
|
<label for="email">Email Notification</label>
|
||||||
|
<input type="email" name="email" placeholder="example@example.com" value="{{email_alert}}">
|
||||||
|
<br>
|
||||||
|
<input class="button" type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user