From 1aadf401646019a7edfc9fe13a0a16a9e94dba60 Mon Sep 17 00:00:00 2001 From: Morgana Date: Mon, 13 Apr 2026 11:01:20 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 4 + app.py | 192 +++++++++++++++++++++++++++++++++ static/database.css | 47 ++++++++ static/icon.svg | 10 ++ static/icon_long.svg | 22 ++++ static/index.css | 153 ++++++++++++++++++++++++++ static/static.svg | 21 ++++ static/style.css | 202 +++++++++++++++++++++++++++++++++++ templates/base.html | 48 +++++++++ templates/database.html | 35 ++++++ templates/database_edit.html | 25 +++++ templates/index.html | 81 ++++++++++++++ 12 files changed, 840 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 static/database.css create mode 100644 static/icon.svg create mode 100644 static/icon_long.svg create mode 100644 static/index.css create mode 100644 static/static.svg create mode 100644 static/style.css create mode 100644 templates/base.html create mode 100644 templates/database.html create mode 100644 templates/database_edit.html create mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8368331 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +helpers/ +config.json \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..110b7a5 --- /dev/null +++ b/app.py @@ -0,0 +1,192 @@ +import json +from hashlib import scrypt +from os import urandom +from hmac import compare_digest + +from flask import Flask, render_template, request, redirect, url_for, session, abort +from flask_pymongo import PyMongo +from bson import ObjectId +import bson.json_util as bson + +SCRYPT_PARAMS = {"n": 16384, "r": 8, "p": 1} + +app = Flask(__name__) +with open("./config.json", "r", encoding="utf-8") as file: + config = json.load(file) + app.config["MONGO_URI"] = f"mongodb://{config['db']}/akasha" + app.config["SECRET_KEY"] = config["fsk"] + mongo = PyMongo( + app, + username="index", + password=config["key"], + authSource="akasha", + authMechanism="SCRAM-SHA-256", + ) + del config + if mongo.db is None: + raise Exception("Unable to connect to database.") + db = mongo.db + + +@app.context_processor +def inject_data(): + ddata = db.domains.find_one({"id": request.endpoint}) + return dict(hue=ddata["quad"]) + + +@app.before_request +def apply_checks(): + username = session.get("username") + if request.endpoint is "static": + return + ddata = db.domains.find_one_or_404({"id": request.endpoint}) + udata = db.users.find_one({"id": username}) if username else None + if not can_access(ddata, udata): + abort(401) + + +def can_access(ddata, udata) -> bool: + if udata: + if ddata["quad"] not in udata["quad"]: + return False + if (not ddata["public"]) and ddata["id"] not in udata["perm"]: + return False + else: + if ddata["quad"] != "ade": + return False + if not ddata["public"]: + return False + return True + + +@app.route("/") +def index(): + db.welcomes.find_one({"id": "index_ade"}) + return redirect(url_for("index_ade")) + + +@app.route("/ade") +def index_ade(): + return render_main() + + +@app.route("/bea") +def index_bea(): + return render_main() + + +@app.route("/cam") +def index_cam(): + return render_main() + + +@app.route("/des") +def index_des(): + return render_main() + + +def render_main(): + session["main"] = request.endpoint + wdata = db.welcomes.find_one({"id": request.endpoint}) + dsdata = db.domains.find({"cat": {"$ne": None}}) + udata = ( + db.users.find_one({"id": session["username"]}) if session["username"] else None + ) + results = {"ade": {}, "bea": {}, "cam": {}, "des": {}} + for ddata in dsdata: + if not can_access(ddata, udata): + continue + if ddata["cat"] not in results[ddata["quad"]]: + results[ddata["quad"]][ddata["cat"]] = {} + results[ddata["quad"]][ddata["cat"]][ddata["name"]] = ddata["id"] + return render_template( + "index.html", + wdata=wdata, + domains=results, + quad=(request.endpoint or "ade")[-3:], + ) + + +@app.post("/login") +def login(): + username = request.form.get("username") + if username: + password = request.form.get("password", "") + udata = db.users.find_one({"id": username}) + if not udata: + abort(401) + return + hash = udata["hash"] + salt = udata["salt"] + computed = scrypt(password.encode("utf-8"), salt=salt, **SCRYPT_PARAMS) + if not compare_digest(hash, computed): + abort(401) + return + session["username"] = username + else: + session["username"] = None + return redirect(url_for(session.get("main", "index_ade"))) + + +@app.route("/search") +def search(): + stype = request.args.get("search-type") + query = request.args.get("search") + print(stype, query) + return redirect(url_for("index")) + + +@app.route("/database", methods=["GET", "POST"]) +def database(): + cnames = db.list_collection_names() + results = [] + collection = None + if request.method == "POST": + collection = request.form.get("collection") + if not collection: + abort(400) + query = request.form.get("query") + if not query or not query.strip(): + query = "{}" + try: + query = json.loads(query) + except: + abort(400) + results = db[collection].find(query, {"_id": 1, "id": 1}) + return render_template( + "database.html", cnames=cnames, collection=collection, results=results + ) + + +@app.route("/database//", methods=["GET", "POST"]) +def database_edit(collection, oid): + if request.method == "POST": + document = request.form.get("document") + if not document: + abort(400) + document = bson.loads(document) + if document and "_id" in document: + del document["_id"] + try: + if not document: + db[collection].delete_one({"_id": ObjectId(oid)}) + print("DELETED") + elif oid: + db[collection].replace_one({"_id": ObjectId(oid)}, document) + print("REPLACED") + else: + db[collection].insert_one(document) + print("INSERTED") + except: + abort(500) + return redirect(url_for("database")) + result = db[collection].find_one_or_404({"_id": ObjectId(oid)}) + name = result["id"] + document = bson.dumps(result, indent=4) + return render_template( + "database_edit.html", + collection=collection, + oid=oid, + name=name, + document=document, + ) diff --git a/static/database.css b/static/database.css new file mode 100644 index 0000000..6c56577 --- /dev/null +++ b/static/database.css @@ -0,0 +1,47 @@ +main { + flex-grow: 1; + display: flex; + flex-direction: column; +} + +hr { + width: 100%; +} + +form { + display: flex; + flex-direction: row; + gap: 0.25em; + margin: 0 2.5em; + align-items: end; + + div { + display: flex; + flex-direction: column; + flex-shrink: 1; + + #query, + #object { + flex-grow: 1; + } + } + + #query-box, + #object-box { + flex-grow: 1; + } +} + +#doc-box { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +#document { + text-wrap: nowrap; + margin: 0 0.5em 0.5em 0.5em; + flex-grow: 1; + height: 80%; + font-family: monospace, monospace; +} \ No newline at end of file diff --git a/static/icon.svg b/static/icon.svg new file mode 100644 index 0000000..7b6f02a --- /dev/null +++ b/static/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/static/icon_long.svg b/static/icon_long.svg new file mode 100644 index 0000000..c711a81 --- /dev/null +++ b/static/icon_long.svg @@ -0,0 +1,22 @@ + + + + + + + + INDEX + + + + \ No newline at end of file diff --git a/static/index.css b/static/index.css new file mode 100644 index 0000000..8863cfd --- /dev/null +++ b/static/index.css @@ -0,0 +1,153 @@ +#outer { + flex-grow: 1; + display: flex; + flex-direction: row; +} + +nav#sidebar { + flex-basis: calc(196px + 1em); + background-color: var(--color-bg1); + flex-grow: 0; + flex-shrink: 0; + + div { + background-color: var(--color-bg2); + line-height: 1.5em; + } + + h2 { + margin-top: 1em; + padding: 0 1em; + } + + a { + color: var(--color); + text-decoration: none; + } + + ul { + margin: 0; + } +} + +main { + flex-grow: 1; + margin: 0 2.5em; +} + +nav#sector { + display: flex; + flex-direction: row; + gap: 1em; + + >a { + position: relative; + text-decoration: none; + flex-grow: 1; + flex-basis: 0; + background-color: var(--color-bg1); + color: var(--color); + text-align: center; + margin-left: 1lh; + font-weight: bold; + font-size: 1.5em; + height: 1lh; + } + + >a::before { + position: absolute; + left: -1lh; + content: ""; + display: block; + height: 1lh; + aspect-ratio: 1/1; + background-color: var(--color-bg1); + clip-path: polygon(100% 0, 100% 100%, 0 100%); + transform: scale(1.005); + } +} + +hr#sectorbar { + border: none; + height: 1em; + background-color: var(--color-bg1); + border-bottom: 1px solid var(--color); + margin: 0 0 0.5em 0; +} + +#contents { + background-color: var(--color-bg2); +} + +#main-split { + display: flex; + flex-direction: row; + gap: 0.5em; + + >div { + flex-grow: 1; + flex-basis: 0; + height: fit-content; + display: flex; + flex-direction: column; + gap: 0.5em; + + >div { + border: 1px solid var(--color); + background-color: var(--color-bg2); + } + + p { + text-align: justify; + padding: 0 0.25em; + margin: 0; + } + + i { + display: inline-block; + padding: 0 0.25em; + margin: 0; + } + + ul { + margin: 0.5em 0; + } + } + + #other>div { + padding-bottom: 0.25em; + } + + #news>h2 { + margin-bottom: 0.25em; + } + + #welcome>p { + margin: 0.25em 0; + } + + #login>form { + display: flex; + flex-direction: column; + margin: 0 0.25em; + gap: 0.25em; + + label { + margin-bottom: -0.5em; + } + } +} + +@media (max-width: 750px) { + #main-split { + flex-direction: column; + } + + #outer { + flex-direction: column-reverse; + } + + #sidebar { + margin-top: 2em; + } +} \ No newline at end of file diff --git a/static/static.svg b/static/static.svg new file mode 100644 index 0000000..5e185f7 --- /dev/null +++ b/static/static.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..f975c29 --- /dev/null +++ b/static/style.css @@ -0,0 +1,202 @@ +* { + --color: oklch(0.4 0.25 var(--hue)); + --color-max: oklch(0.8 0.25 var(--hue)); + --color-bg1: oklch(0.8 0.15 var(--hue)); + --color-bg2: oklch(1.0 0.05 var(--hue)); +} + +html { + background-color: white; + color: black; + font-family: Arial, Helvetica, sans-serif; + min-height: 100vh; + margin: 0; + --hue: 0; + --ade: 130; + --bea: 242; + --cam: 350; + --des: 73; +} + +body { + margin: 0; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +input, +button, +textarea, +select, +output { + border: 1px solid var(--color); + background-color: var(--color-bg2); + color: var(--color); + font-size: inherit; + font-family: inherit; + font-variant: inherit; + padding: 0.1em 0.25em; +} + +ul { + list-style-type: disclosure-closed; +} + +label { + font-size: 0.75em; + font-variant: small-caps; +} + +h2 { + font-variant: small-caps; + font-size: inherit; + margin: 0; + background-color: var(--color); + color: white; + width: fit-content; + padding: 0 0.25em; + position: relative; + height: 1lh; +} + +h2::before { + position: absolute; + top: 0; + right: -1lh; + content: ""; + display: block; + height: 100%; + aspect-ratio: 1/1; + background-color: var(--color); + clip-path: polygon(100% 0, 0 0, 0 100%); + transform: scale(1.005); +} + +h3 { + padding: 0 0.25em; + font-variant: small-caps; + font-size: inherit; + margin: 0; + color: var(--color); +} + +hr { + border: none; + border-bottom: 1px dashed var(--color); +} + +header { + display: flex; + flex-direction: row; + height: 5em; + color: var(--color); + font-variant: small-caps; + + >a { + background-color: var(--color-bg1); + padding: 0.5em; + display: flex; + flex-direction: row; + align-items: center; + + .icon-bg { + fill: var(--color); + } + + .icon-txt { + dominant-baseline: central; + letter-spacing: 0.5ch; + font: 28px Arial, sans-serif; + } + } + + >div { + flex-grow: 1; + display: flex; + flex-direction: column; + + >* { + flex-grow: 1; + align-content: center; + flex-basis: 0; + } + + } +} + +#header-top { + background-color: var(--color-bg1); + display: flex; + flex-direction: row; + justify-content: end; + min-height: 2.5em; + + >div { + margin: 0.25em 0; + padding-right: 1em; + background-color: var(--color-bg2); + display: flex; + flex-direction: row; + justify-content: end; + gap: 1em; + align-items: center; + + a { + color: inherit; + text-decoration: none; + white-space: nowrap; + } + } +} + +#header-bottom { + display: flex; + flex-direction: row; + min-height: 2.5em; + + >form { + flex-grow: 1; + margin-top: 0.25em; + display: flex; + flex-direction: row; + gap: 0.25em; + justify-content: center; + + >* { + height: fit-content; + } + } +} + +.spacer { + display: inline-block; + height: 100%; + aspect-ratio: 1/1; + background-color: var(--color-bg1); + clip-path: polygon(100% 0, 0 0, 0 100%); + transform: scale(1.005); +} + +@media (max-width: 750px) { + header { + height: fit-content; + flex-direction: column; + + #header-top>div>a { + display: none; + } + + #header-top>div>a:first-of-type { + display: block; + } + + #searchform { + display: none; + } + } + + .spacer { + transform: scale(1.1); + } +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..de61fde --- /dev/null +++ b/templates/base.html @@ -0,0 +1,48 @@ + + + + + Icolotl Index + + + {% block head %}{% endblock %} + + + +
+ + + + + + + + INDEX + + +
+
+
+ + Account + Console +
+
+
+ +
+ + + + +
+
+
+
+ {% block content %}{% endblock %} + + + \ No newline at end of file diff --git a/templates/database.html b/templates/database.html new file mode 100644 index 0000000..ff3680c --- /dev/null +++ b/templates/database.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+
+ + + + {% for cname in cnames %} + + {% endfor %} + +
+
+ + +
+ +
+
+ {% if results %} + + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/database_edit.html b/templates/database_edit.html new file mode 100644 index 0000000..710a75a --- /dev/null +++ b/templates/database_edit.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+
+ + {{ collection }} +
+
+ + {{ name }} ({{ oid }}) +
+ +
+
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..4cf0b1e --- /dev/null +++ b/templates/index.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+ +
+ +
+
+
+ {% for category in domains[quad] %} +
+

{{ category }}

+
    + {% for domain in domains[quad][category] %} +
  • {{ domain }}
  • + {% endfor %} +
+
+ {% endfor %} +
+
+
+

{{ wdata["header"] }}

+

{{ wdata["text"] }}

+ For assistance, please contact the Sector Administrator at {{ wdata['email'] }}@icolotl.com. +
+
+

Index Unified Login

+
+ {% if session["username"] %} + Welcome, {{ session["username"] }}. + + {% else %} + + + + + + {% endif %} +
+
+
+

News

+

Test1

+

Foo, Bar, Baz

+
+

Test2

+

Quux, Quuux, Quuuux

+
+
+
+
+
+{% endblock %} \ No newline at end of file