Initial Commit

This commit is contained in:
2026-04-13 11:01:20 -05:00
commit 1aadf40164
12 changed files with 840 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.venv/
__pycache__/
helpers/
config.json

192
app.py Normal file
View File

@@ -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/<collection>/<oid>", 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,
)

47
static/database.css Normal file
View File

@@ -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;
}

10
static/icon.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg version="1.1"
width="42" height="42"
xmlns="http://www.w3.org/2000/svg">
<polyline points="21,42 31,32 21,22 11,32"/>
<polyline points="32,31 42,21 32,11 22,21"/>
<polyline points="21,20 31,10 21,0 11,10"/>
<polyline points="10,31 20,21 10,11 0,21"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

22
static/icon_long.svg Normal file
View File

@@ -0,0 +1,22 @@
<svg version="1.1"
width="196" height="46"
xmlns="http://www.w3.org/2000/svg">
<polyline points="23,46 173,46 196,23 173,0 23,0 0,23" fill="#FFFFFF" class="icon-bg"/>
<polyline points="23,44 33,34 23,24 13,34" fill="#000000"/>
<polyline points="34,33 44,23 34,13 24,23" fill="#000000"/>
<polyline points="23,22 33,12 23,2 13,12" fill="#000000"/>
<polyline points="12,33 22,23 12,13 2,23" fill="#000000"/>
<text x="54" y="22" fill="#000000">INDEX</text>
<style>
<![CDATA[
text {
dominant-baseline: central;
letter-spacing: 0.5ch;
font: 28px Arial, sans-serif;
}
]]>
</style>
</svg>

After

Width:  |  Height:  |  Size: 669 B

153
static/index.css Normal file
View File

@@ -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;
}
}

21
static/static.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg
xmlns='http://www.w3.org/2000/svg'
xmlns:xlink='http://www.w3.org/1999/xlink'
width='300' height='300'>
<filter id='n' x='0' y='0'>
<feTurbulence
type="fractalNoise"
baseFrequency="0.35"
stitchTiles="stitch"/>
<feColorMatrix
type="matrix"
values="0.3 0.3 0.3 0 0
0.3 0.3 0.3 0 0
0.3 0.3 0.3 0 0
0 0 0 1 0" />
</filter>
<rect width='300' height='300' fill='#fff'/>
<rect width='300' height='300' filter="url(#n)" opacity='1.0'/>
</svg>

After

Width:  |  Height:  |  Size: 634 B

202
static/style.css Normal file
View File

@@ -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);
}
}

48
templates/base.html Normal file
View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html style="--hue: var(--{{ hue }});">
<head>
<title>Icolotl Index</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block head %}{% endblock %}
</head>
<body>
<header>
<a href="/">
<svg version=" 1.1" width="196" height="46" xmlns="http://www.w3.org/2000/svg">
<polyline points="23,46 173,46 196,23 173,0 23,0 0,23" fill="#FFFFFF" class="icon-bg" />
<polyline points="23,44 33,34 23,24 13,34" fill="var(--color-max)" style="--hue: var(--des);" />
<polyline points="34,33 44,23 34,13 24,23" fill="var(--color-max)" style="--hue: var(--bea);" />
<polyline points="23,22 33,12 23,2 13,12" fill="var(--color-max)" style="--hue: var(--ade);" />
<polyline points="12,33 22,23 12,13 2,23" fill="var(--color-max)" style="--hue: var(--cam);" />
<text x="54" y="22" fill="#FFFFFF" class="icon-txt">INDEX</text>
</svg>
</a>
<div>
<div id="header-top">
<div>
<span class="spacer"></span>
<a href="/account">Account</a>
<a>Console</a>
</div>
</div>
<div id="header-bottom">
<span class="spacer"></span>
<form action="{{ url_for('search') }}" method="get" id="searchform">
<label for="search-type">Search Type:</label>
<select name="search-type" id="search-type">
<option value="classic">Classic</option>
<option value="oid">Object ID</option>
</select>
<input type="search" name="search" id="search">
<input type="submit" value="Search">
</form>
</div>
</div>
</header>
{% block content %}{% endblock %}
</body>
</html>

35
templates/database.html Normal file
View File

@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='database.css') }}">
{% endblock %}
{% block content %}
<main>
<form method="post">
<div>
<label for="collection">Collection:</label>
<input id="collection" name="collection" list="cnames">
<datalist id="cnames">
{% for cname in cnames %}
<option value="{{ cname }}">{{ cname }}</option>
{% endfor %}
</datalist>
</div>
<div id="query-box">
<label for="query">Query:</label>
<input id="query" name="query">
</div>
<input type="submit" value="Submit Query">
</form>
<hr>
{% if results %}
<ul>
{% for result in results %}
<li><a href="{{ url_for('database_edit', collection=collection, oid=result['_id']) }}">{{ result["id"] }} ({{
result["_id"] }})</a></li>
{% endfor %}
</ul>
{% endif %}
</main>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='database.css') }}">
{% endblock %}
{% block content %}
<main>
<form id="edit-form" method="post">
<div>
<label for="collection">Collection:</label>
<output id="collection">{{ collection }}</output>
</div>
<div id="object-box">
<label for="object">Document:</label>
<output id="object">{{ name }} ({{ oid }})</output>
</div>
<input type="submit" value="Save Changes">
</form>
<hr>
<div id="doc-box">
<textarea form="edit-form" id="document" name="document">{{ document }}</textarea>
</div>
</main>
{% endblock %}

81
templates/index.html Normal file
View File

@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}">
{% endblock %}
{% block content %}
<div id="outer">
<nav id="sidebar">
{% set quads = {"ade":"♢", "bea":"♡", "cam":"♧", "des":"♤"} %}
{% for quad in quads %}
{% if domains[quad] %}
<div style="--hue: var(--{{ quad }});">
<h2>{{ quads[quad] }}</h2>
<ul>
{% for category in domains[quad] %}
{% for domain in domains[quad][category] %}
<li><a href="{{ url_for(domains[quad][category][domain]) }}">{{ domain }}</a></li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
</nav>
<main>
<nav id="sector">
<a href="/ade" style="--hue: 130;"></a>
<a href="/bea" style="--hue: 242;"></a>
<a href="/cam" style="--hue: 350;"></a>
<a href="/des" style="--hue: 73;"></a>
</nav>
<hr id="sectorbar">
<div id="main-split">
<div id="apps">
{% for category in domains[quad] %}
<div>
<h2>{{ category }}</h2>
<ul>
{% for domain in domains[quad][category] %}
<li><a href="{{ url_for(domains[quad][category][domain]) }}">{{ domain }}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
<div id="other">
<div id="welcome">
<h2>{{ wdata["header"] }}</h2>
<p>{{ wdata["text"] }}</p>
<i>For assistance, please contact the Sector Administrator at <a
href="mailto:{{ wdata['email'] }}@icolotl.com">{{ wdata['email'] }}@icolotl.com</a>.</i>
</div>
<div id="login">
<h2>Index Unified Login</h2>
<form method="post" action="{{ url_for('login') }}">
{% if session["username"] %}
<span>Welcome, <b>{{ session["username"] }}</b>.</span>
<input type="submit" value="Log Out">
{% else %}
<label for="username">Username:</label>
<input id="username" name="username" autocomplete="username">
<label for="password">Password:</label>
<input type="password" id="password" name="password" autocomplete="current-password">
<input type="submit" value="Log In">
{% endif %}
</form>
</div>
<div id="news">
<h2>News</h2>
<h3>Test1</h3>
<p>Foo, Bar, Baz</p>
<hr>
<h3>Test2</h3>
<p>Quux, Quuux, Quuuux</p>
</div>
</div>
</div>
</main>
</div>
{% endblock %}