aboutsummaryrefslogtreecommitdiff
path: root/modules/services/nixos/jta
diff options
context:
space:
mode:
Diffstat (limited to 'modules/services/nixos/jta')
-rw-r--r--modules/services/nixos/jta/assignments.html148
-rw-r--r--modules/services/nixos/jta/default.nix1
-rw-r--r--modules/services/nixos/jta/index.html199
-rw-r--r--modules/services/nixos/jta/main.go31
-rw-r--r--modules/services/nixos/jta/style.css29
5 files changed, 311 insertions, 97 deletions
diff --git a/modules/services/nixos/jta/assignments.html b/modules/services/nixos/jta/assignments.html
new file mode 100644
index 0000000..3031a29
--- /dev/null
+++ b/modules/services/nixos/jta/assignments.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>jta@ganymede</title>
+ <link rel="stylesheet" href="style.css">
+ <style>
+ body {
+ margin: 0;
+ max-width: none;
+ padding: 0;
+ background: var(--bg);
+ }
+
+ header {
+ padding: 1.25rem 1.5rem;
+ text-align: center;
+ font-family: var(--font-heading);
+ font-size: var(--step-2);
+ line-height: 1.2;
+ letter-spacing: -0.01em;
+ font-weight: 700;
+ background: #020617;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .section {
+ padding: 1rem;
+ max-width: 1100px;
+ margin: 0 auto;
+ }
+
+ .section-title {
+ font-size: var(--step-1);
+ line-height: 1.25;
+ letter-spacing: -0.01em;
+ margin: 1rem 0 0.5rem 0.5rem;
+ color: var(--text-soft);
+ }
+
+ .container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+ gap: 1rem;
+ padding: 0 1rem 1rem 1rem;
+ }
+
+ .card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 1rem;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+ cursor: pointer;
+ text-decoration: none;
+ color: var(--text);
+ display: block;
+ }
+
+ .card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.6);
+ }
+
+ .title {
+ font-size: var(--step-1);
+ line-height: 1.3;
+ letter-spacing: -0.01em;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ }
+
+ .due {
+ font-size: var(--step--1);
+ color: var(--muted);
+ margin-bottom: 0.5rem;
+ }
+
+ .desc {
+ font-size: var(--step--1);
+ color: var(--text-soft);
+ line-height: 1.55;
+ }
+ </style>
+ </head>
+ <body>
+ <header>Juksere Trives Aldri</header>
+
+ <div id="app"></div>
+
+ <script>
+ const app = document.getElementById("app");
+
+ function renderHome(data) {
+ const groups = {};
+ data.forEach((item) => {
+ const cls = item.class || "Other";
+ if (!groups[cls]) groups[cls] = [];
+ groups[cls].push(item);
+ });
+
+ Object.entries(groups).forEach(([cls, items]) => {
+ const section = document.createElement("div");
+ section.className = "section";
+
+ const title = document.createElement("div");
+ title.className = "section-title";
+ title.textContent = cls;
+ section.appendChild(title);
+
+ const container = document.createElement("div");
+ container.className = "container";
+
+ items.forEach((item) => {
+ const card = document.createElement("a");
+ card.className = "card";
+ card.href = item.link || "#";
+
+ card.innerHTML = `
+ <div class="title">${item.title}</div>
+ <div class="due">Due: ${item.due}</div>
+ ${item.description ? `<div class="desc">${item.description}</div>` : ""}
+ `;
+
+ container.appendChild(card);
+ });
+
+ section.appendChild(container);
+ app.appendChild(section);
+ });
+ }
+
+ fetch("assignments.json")
+ .then((res) => {
+ if (!res.ok) {
+ throw new Error("Failed to load data");
+ }
+ return res.json();
+ })
+ .then((data) => renderHome(data))
+ .catch(() => {
+ app.innerHTML = "<p style='padding:1rem;'>Failed to load data.</p>";
+ });
+ </script>
+ </body>
+</html>
diff --git a/modules/services/nixos/jta/default.nix b/modules/services/nixos/jta/default.nix
index c1ea012..d2908bc 100644
--- a/modules/services/nixos/jta/default.nix
+++ b/modules/services/nixos/jta/default.nix
@@ -33,6 +33,7 @@ in {
environment = {
PORT = toString cfg.port;
ROOT_DIR = "/media/jta";
+ AUTH_PASSWORD_HASH = "550b6785c4bce2ab41a5e2eaa1eb43172d5abbd58237f1c78cd32cb190edc2c5"; # alfreddabuttlerwithtwots
};
serviceConfig = {
diff --git a/modules/services/nixos/jta/index.html b/modules/services/nixos/jta/index.html
index fd93de6..96b6f5f 100644
--- a/modules/services/nixos/jta/index.html
+++ b/modules/services/nixos/jta/index.html
@@ -3,127 +3,132 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>jta@ganymede</title>
+ <title>JTA Login</title>
<link rel="stylesheet" href="style.css">
<style>
body {
margin: 0;
- max-width: none;
- padding: 0;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ padding: 1rem;
background: var(--bg);
}
-
- header {
- padding: 1.25rem 1.5rem;
+
+ .card {
+ width: min(520px, 100%);
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 1.5rem;
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.38);
+ }
+
+ h1 {
+ margin: 0 0 0.25rem 0;
text-align: center;
font-family: var(--font-heading);
font-size: var(--step-2);
- line-height: 1.2;
- letter-spacing: -0.01em;
+ line-height: 1.15;
font-weight: 700;
- background: #020617;
- border-bottom: 1px solid var(--border);
- }
-
- .section {
- padding: 1rem;
- max-width: 1100px;
- margin: 0 auto;
}
-
- .section-title {
- font-size: var(--step-1);
- line-height: 1.25;
- letter-spacing: -0.01em;
- margin: 1rem 0 0.5rem 0.5rem;
+
+ p {
+ margin: 0.5rem 0 1.2rem 0;
+ text-align: center;
color: var(--text-soft);
+ line-height: 1.5;
}
-
- .container {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- gap: 1rem;
- padding: 0 1rem 1rem 1rem;
+
+ .actions {
+ display: flex;
+ justify-content: center;
+ gap: 0.75rem;
}
-
- .card {
- background: var(--card);
+
+ button {
+ appearance: none;
border: 1px solid var(--border);
- border-radius: 16px;
- padding: 1rem;
- box-shadow: 0 4px 12px rgba(0,0,0,0.4);
- transition: transform 0.15s ease, box-shadow 0.15s ease;
- cursor: pointer;
- text-decoration: none;
+ border-radius: 12px;
+ background: #0f172a;
color: var(--text);
- display: block;
+ padding: 0.65rem 1rem;
+ font-size: var(--step-0);
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
}
-
- .card:hover {
- transform: translateY(-4px);
- box-shadow: 0 10px 20px rgba(0,0,0,0.6);
+
+ button:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
+ }
+
+ #status {
+ margin-top: 1rem;
+ text-align: center;
+ min-height: 1.25rem;
+ font-size: var(--step--1);
+ color: var(--text-soft);
}
-
- .title { font-size: var(--step-1); line-height: 1.3; letter-spacing: -0.01em; font-weight: 600; margin-bottom: 0.5rem; }
- .due { font-size: var(--step--1); color: var(--muted); margin-bottom: 0.5rem; }
- .desc { font-size: var(--step--1); color: var(--text-soft); line-height: 1.55; }
</style>
</head>
<body>
- <header>Juksere Trives Aldri</header>
+ <div class="card">
+ <h1>JTA Portal</h1>
+ <p>Public landing page. Login is required to view assignments and all other pages.</p>
+
+ <div class="actions">
+ <button id="login-btn" type="button">Login</button>
+ </div>
- <div id="app"></div>
+ <div id="status" aria-live="polite"></div>
+ </div>
<script>
- const app = document.getElementById("app");
-
- function renderHome(data) {
- // Group by class
- const groups = {};
- data.forEach(item => {
- const cls = item.class || "Other";
- if (!groups[cls]) groups[cls] = [];
- groups[cls].push(item);
- });
-
- // Render sections
- Object.entries(groups).forEach(([cls, items]) => {
- const section = document.createElement("div");
- section.className = "section";
-
- const title = document.createElement("div");
- title.className = "section-title";
- title.textContent = cls;
- section.appendChild(title);
-
- const container = document.createElement("div");
- container.className = "container";
-
- items.forEach(item => {
- const card = document.createElement("a");
- card.className = "card";
- card.href = item.link || "#";
-
- card.innerHTML = `
- <div class="title">${item.title}</div>
- <div class="due">Due: ${item.due}</div>
- ${item.description ? `<div class="desc">${item.description}</div>` : ""}
- `;
-
- container.appendChild(card);
- });
-
- section.appendChild(container);
- app.appendChild(section);
- });
- }
-
- fetch("assignments.json")
- .then(res => res.json())
- .then(data => renderHome(data))
- .catch(() => {
- app.innerHTML = "<p style='padding:1rem;'>Failed to load data.</p>";
- });
+ const authCookieName = "jta_auth";
+ const status = document.getElementById("status");
+ const loginButton = document.getElementById("login-btn");
+
+ function toHex(buffer) {
+ const bytes = new Uint8Array(buffer);
+ return Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+ }
+
+ async function sha256Hex(input) {
+ const data = new TextEncoder().encode(input);
+ const digest = await crypto.subtle.digest("SHA-256", data);
+ return toHex(digest);
+ }
+
+ function setAuthCookie(hashValue) {
+ const maxAge = 10 * 365 * 24 * 60 * 60;
+ document.cookie = `${authCookieName}=${hashValue}; Path=/; Max-Age=${maxAge}; SameSite=Lax`;
+ }
+
+ async function loginWithPrompt() {
+ const password = prompt("Enter password");
+ if (password === null) {
+ status.textContent = "Login cancelled.";
+ return;
+ }
+
+ status.textContent = "Logging in...";
+
+ const hashValue = await sha256Hex(password);
+ setAuthCookie(hashValue);
+
+ status.textContent = "Success. Redirecting...";
+ window.location.href = "/assignments";
+ }
+
+ loginButton.addEventListener("click", () => {
+ loginWithPrompt().catch(() => {
+ status.textContent = "Network error while logging in.";
+ });
+ });
</script>
</body>
</html>
diff --git a/modules/services/nixos/jta/main.go b/modules/services/nixos/jta/main.go
index 3e5fc1f..1390d7f 100644
--- a/modules/services/nixos/jta/main.go
+++ b/modules/services/nixos/jta/main.go
@@ -29,8 +29,28 @@ var styleCSS string
//go:embed index.html
var indexHTML string
+//go:embed assignments.html
+var assignmentsHTML string
+
var pageTmpl = template.Must(template.New("md_template.html").Parse(mdTemplate))
+const authCookieName = "jta_auth"
+
+func isAuthenticated(r *http.Request) bool {
+ expectedHash := strings.TrimSpace(os.Getenv("AUTH_PASSWORD_HASH"))
+ if expectedHash == "" {
+ return false
+ }
+
+ cookie, err := r.Cookie(authCookieName)
+ if err != nil {
+ return false
+ }
+
+ providedHash := strings.TrimSpace(cookie.Value)
+ return strings.EqualFold(providedHash, expectedHash)
+}
+
func serveMarkdown(w http.ResponseWriter, r *http.Request) {
// Prevent path traversal
clean := filepath.Clean(r.URL.Path)
@@ -81,6 +101,17 @@ func main() {
return
}
+ if !isAuthenticated(r) {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if r.URL.Path == "/assignments" || r.URL.Path == "/assignments.html" {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Write([]byte(assignmentsHTML))
+ return
+ }
+
// Intercept markdown files
if strings.HasSuffix(r.URL.Path, ".md") {
serveMarkdown(w, r)
diff --git a/modules/services/nixos/jta/style.css b/modules/services/nixos/jta/style.css
index 1d52d19..ec09ca9 100644
--- a/modules/services/nixos/jta/style.css
+++ b/modules/services/nixos/jta/style.css
@@ -124,6 +124,35 @@ code {
border-radius: 6px;
}
+img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ margin: 1rem 0;
+ border-radius: 10px;
+}
+
+p > img,
+li > img {
+ margin: 0.75rem 0;
+}
+
+a > img {
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+
+a:hover > img {
+ transform: translateY(-1px);
+ box-shadow: 0 8px 20px rgba(2, 6, 23, 0.45);
+}
+
+img[width],
+img[height] {
+ width: auto;
+ height: auto;
+ max-width: 100%;
+}
+
/* Links */
a {
color: #93c5fd;