www.spaceplanner.app

Web client to the spaceplanner API
git clone git://jacobedwards.org/www.spaceplanner.app
Log | Files | Refs

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:
AMakefile | 6++++++
Afiles/css/main.css | 40++++++++++++++++++++++++++++++++++++++++
Afiles/floorplans/index.html | 12++++++++++++
Afiles/floorplans/main.js | 6++++++
Afiles/index.html | 10++++++++++
Afiles/lib/api.js | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afiles/lib/bar.js | 38++++++++++++++++++++++++++++++++++++++
Afiles/lib/etc.js | 23+++++++++++++++++++++++
Afiles/login/index.html | 21+++++++++++++++++++++
Afiles/login/main.js | 38++++++++++++++++++++++++++++++++++++++
Afiles/logout/index.html | 13+++++++++++++
Afiles/logout/main.js | 8++++++++
Afiles/settings/index.html | 12++++++++++++
Afiles/settings/main.js | 6++++++
Afiles/success.html | 2++
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>