commit 1bc1305b66336e5a19ba0fd20fff9ce8dc845cf1
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date: Sun, 4 Aug 2024 21:38:28 -0700
Initial commit
This interface will be developed into the end product eventually,
but for now it's really a testing ground to see what might be useful
or useless in the API, etc.
Diffstat:
15 files changed, 334 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,6 @@
+prefix = /var/www/htdocs/www.spaceplanner.app
+
+install:
+ rsync -va --del files/ ${prefix}
+
+.PHONY: install
diff --git a/files/css/main.css b/files/css/main.css
@@ -0,0 +1,40 @@
+.error {
+ background-color: red;
+ border: thin solid darkred;
+ padding: .5rem;
+}
+
+body {
+ margin: 4rem;
+}
+
+nav {
+ top: 0;
+ left: 0;
+ position: absolute;
+ margin: 0;
+ width: 100%;
+ height: 2rem;
+ background-color: lightgrey;
+}
+
+nav > ul {
+ margin: 0;
+ padding: 0;
+}
+
+nav > ul > li {
+ /* not quite needed */
+ display: inline-block;
+ float: left;
+}
+
+nav > ul:nth-child(2) {
+ float: right;
+}
+
+nav > ul > li {
+ display: block;
+ padding: .5rem;
+ height: 1rem;
+}
diff --git a/files/floorplans/index.html b/files/floorplans/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<head>
+ <title>Spaceplanner - Floorplans</title>
+ <link rel="stylesheet" type="text/css" href="/css/main.css">
+ <script src="/lib/api.js" async></script>
+ <script src="/lib/bar.js" async></script>
+ <script src="/lib/etc.js" async></script>
+ <script src="./main.js" async></script>
+</head>
+<html>
+ <h1>Floorplans</h1>
+</html>
diff --git a/files/floorplans/main.js b/files/floorplans/main.js
@@ -0,0 +1,6 @@
+function init() {
+ authorize()
+ show_bar()
+}
+
+window.onload = init
diff --git a/files/index.html b/files/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<head>
+ <title>Spaceplanner</title>
+</head>
+<html>
+ <h1>Spaceplanner</h1>
+ <p>Easy. Fast. Simple.</p>
+
+ <p><a href="/login">Login</a> or <a href="/signup.html">signup</a> now!</p>
+</html>
diff --git a/files/lib/api.js b/files/lib/api.js
@@ -0,0 +1,99 @@
+let api_proto = "http"
+let api_host = "api.spaceplanner.app"
+let api_version = "v0"
+
+function api_verify_response(response) {
+ let type = response.headers.get("Content-Type")
+ if (type != "application/json; charset=utf-8") {
+ return Promise.reject(new Error("API returned unacceptable format: " + type))
+ } else {
+ return Promise.resolve(response)
+ }
+}
+
+function api_parse_response(response) {
+ return response.json()
+}
+
+function api_status(response) {
+ // response.code is from appleboy's golang JWT LoginHandler
+ // May figure out how to change in the future
+ if (response.code >= 200 || response.code < 300 ||
+ response.status == "ok") {
+ return Promise.resolve(response)
+ }
+
+ if (response.error) {
+ return Promise.reject(new Error(response.error))
+ }
+ return Promise.reject(new Error("Error undefined"))
+}
+
+function api_fetch(method, endpoint, body) {
+ params = { "method": method, "headers": { "Content-Type": "application/json" } };
+
+ let token = api_token()
+ if (api_authorized_duration(token) > 0) {
+ params["headers"]["Authorization"] = "Bearer " + token
+ }
+
+ if (body) {
+ params["body"] = JSON.stringify(body)
+ }
+
+ return fetch(api_proto + "://" + api_host + "/" + api_version + "/" + endpoint, params)
+ .then(api_verify_response)
+ .then(api_parse_response)
+ .then(api_status)
+}
+
+function api_refresh_token() {
+ api_fetch("GET", "tokens/refresh")
+ .then(function(resp) {
+ api_update_token(resp.token)
+ })
+}
+
+function api_update_token(token) {
+ console.log("api_update_token(" + token + ")")
+ if (!token) {
+ localStorage.removeItem("token")
+ } else {
+ localStorage.setItem("token", token)
+ }
+}
+
+function api_token() {
+ let t = localStorage.getItem("token")
+ console.log("api_token() > " + t)
+ return t
+}
+
+function api_token_payload(token) {
+ if (!token) {
+ token = api_token()
+ if (!token) {
+ return token
+ }
+ }
+ let a = token.split('.')
+ if (a.length != 3) {
+ throw new Error("Invalid token")
+ }
+ return JSON.parse(atob(a[1]))
+}
+
+// Returns seconds until authorization expires, or negative the
+// number of seconds it has been expired.
+function api_authorized_duration(token) {
+ let payload = api_token_payload(token)
+ if (!payload) {
+ return -1
+ }
+
+ return payload["exp"] - (Date.now() / 1000)
+}
+
+function api_logged_in() {
+ return api_authorized_duration() > 0
+}
diff --git a/files/lib/bar.js b/files/lib/bar.js
@@ -0,0 +1,38 @@
+function link(name, href) {
+ let a = document.createElement("a")
+ a.href = href
+ a.appendChild(document.createTextNode(name))
+ return a
+}
+
+function additem(list, element) {
+ let i = document.createElement("li")
+ i.appendChild(element)
+ return list.append(i)
+}
+
+function show_bar(on) {
+ if (!on) {
+ on = document.querySelector("body")
+ }
+
+ let nav = document.createElement("nav")
+ let left = document.createElement("ul")
+ nav.appendChild(left)
+ let right = document.createElement("ul")
+ nav.appendChild(right)
+
+ if (!api_logged_in()) {
+ additem(right, link("Login", "/login"))
+ } else {
+ let jwt_payload = api_token_payload()
+ let li = document.createElement("li")
+ li.appendChild(document.createTextNode("Welcome "))
+ li.appendChild(link(jwt_payload["id"], "/settings"))
+ left.appendChild(li)
+ additem(right, link("Floorplans", "/floorplans"))
+ additem(right, link("Logout", "/logout"))
+ }
+
+ on.prepend(nav)
+}
diff --git a/files/lib/etc.js b/files/lib/etc.js
@@ -0,0 +1,23 @@
+function authorize() {
+ if (api_authorized_duration() <= 0) {
+ // Maybe add a parameter which has /login redirect
+ // back to the page that was trying to be accessed
+ window.location.href = "/login"
+ }
+}
+
+function set_error(message, on) {
+ if (!on) {
+ on = document.body
+ }
+
+ let err_elem = on.parentElement.querySelector(":scope > .error")
+ if (err_elem) {
+ err_elem.textContent = message
+ } else {
+ let err_elem = document.createElement("p")
+ err_elem.textContent = message
+ err_elem.classList = "error"
+ on.before(err_elem)
+ }
+}
diff --git a/files/login/index.html b/files/login/index.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<head>
+ <title>Spaceplanner Login</title>
+ <link rel="stylesheet" type="text/css" href="/css/main.css">
+ <script src="/lib/api.js" async></script>
+ <script src="/lib/etc.js" async></script>
+ <script src="./main.js" async></script>
+</head>
+<html>
+ <h1>Login</h1>
+
+ <form id="login">
+ <label for="username">Username:</label>
+ <input id="username" autocomplete="username" name="username"/>
+
+ <label for="password">Password:</label>
+ <input id="password" autocomplete="current-password" type="password" name="password"/>
+
+ <input type="submit" value="Login"/>
+ </form>
+</html>
diff --git a/files/login/main.js b/files/login/main.js
@@ -0,0 +1,38 @@
+let default_page = "/floorplans"
+
+function handle_token(resp) {
+ api_update_token(resp.token)
+ window.location.href = default_page
+}
+
+function login(username, password, err_callback) {
+ api_fetch("POST", "tokens", { "username": username, "password": password })
+ .then(handle_token)
+ .catch(err_callback)
+ return false;
+}
+
+function init() {
+ if (api_authorized_duration() > 0) {
+ window.location.href = default_page
+ }
+
+ let username_input = document.getElementById("username")
+ let password_input = document.getElementById("password")
+ if (!username_input || !password_input) {
+ throw new Error("unable to select username or password")
+ }
+
+ let login_form = document.getElementById("login")
+ if (!login_form) {
+ throw new Error("unable to get login form")
+ }
+ login_form.onsubmit = function () {
+ return login(
+ username_input.value, password_input.value,
+ function (error) { return set_error(error, login_form) }
+ );
+ };
+}
+
+window.onload = init
diff --git a/files/logout/index.html b/files/logout/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<head>
+ <title>Spaceplanner Logout</title>
+ <link rel="stylesheet" type="text/css" href="/css/main.css">
+ <script src="/lib/api.js" async></script>
+ <script src="/lib/etc.js" async></script>
+ <script src="./main.js" async></script>
+</head>
+<html>
+ <h1>Logout</h1>
+
+ <p>You will be logged out momentarily</p>
+</html>
diff --git a/files/logout/main.js b/files/logout/main.js
@@ -0,0 +1,8 @@
+let default_page = "/"
+
+function init() {
+ api_update_token(null)
+ window.location.href = default_page
+}
+
+window.onload = init
diff --git a/files/settings/index.html b/files/settings/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<head>
+ <title>Spaceplanner - Settings</title>
+ <link rel="stylesheet" type="text/css" href="/css/main.css">
+ <script src="/lib/api.js" async></script>
+ <script src="/lib/bar.js" async></script>
+ <script src="/lib/etc.js" async></script>
+ <script src="./main.js" async></script>
+</head>
+<html>
+ <h1>Settings</h1>
+</html>
diff --git a/files/settings/main.js b/files/settings/main.js
@@ -0,0 +1,6 @@
+function init() {
+ authorize()
+ show_bar()
+}
+
+window.onload = init
diff --git a/files/success.html b/files/success.html
@@ -0,0 +1,2 @@
+<!DOCTYPE HTML>
+<html><body><h1>Success, Logged in</h1></body></html>