diff --git a/blogin.css b/blogin.css
index ae15ca4..7a5b095 100644
--- a/blogin.css
+++ b/blogin.css
@@ -1,4 +1,6 @@
-.error {
+#error {
+ display: none;
+
border: 2px solid red;
border-radius: 4px;
@@ -7,7 +9,9 @@
color: white;
}
-.info {
+#success {
+ display: none;
+
border: 2px solid darkgreen;
border-radius: 4px;
@@ -15,3 +19,7 @@
background-color: limegreen;
color: black;
}
+
+#submit {
+ display: none;
+}
diff --git a/blogin.sh b/blogin.sh
index 37979fe..4f77100 100644
--- a/blogin.sh
+++ b/blogin.sh
@@ -26,36 +26,102 @@ TAGS=(meta interactive tadiweb)
action=$(jq -r '.action // ""' <<< "$BLAG_QUERY" || :)
if [[ "${action:+x}" == "x" ]]; then
- DONT_CACHE=1
-fi
-
-if [[ "$action" == "verify_token" ]]; then
RAW=application/json
- metadata
-
- #shellcheck disable=SC2266
- {
- jq -r '.token // ""' <<< "$BLAG_QUERY" |
- bwt_verify |
- [[ "$(jq -r ".till > now")" == "true" ]]
- } || echo false
-
- exit
-fi
-
-if [[ "$action" == "posts" ]]; then
- RAW=application/json
- metadata
-
- db_rows "SELECT * FROM posts" |
- jq -r 'map([.contents, .username, .ts, null, 0]) | select(. != [])'
-
- exit
fi
DONT_CACHE=1 # workaround
metadata
+case "$action" in
+ verify_token)
+ #shellcheck disable=SC2266
+ {
+ verified=$(jq -r '.token // ""' <<< "$BLAG_QUERY" | bwt_verify)
+ [[ "$(jq -r '.till > now' <<< "$verified")" == "true" ]]
+ printf '%s' "$verified"
+ } || echo false
+ exit
+ ;;
+
+ posts)
+ db_rows "SELECT * FROM posts" |
+ jq -r 'map([.contents, .username, .ts, null, 0]) | select(. != [])'
+ exit
+ ;;
+
+ login)
+ username=$(jq -r '.username' <<< "$BLAG_QUERY" || :)
+ password=$(jq -r '.password' <<< "$BLAG_QUERY" || :)
+
+ if ! printf '%s' "$username" | rg -Uq '^[a-zA-Z0-9_]{3,24}$'; then
+ json_err "username must be between 3 to 24 characters, and must consist of alphanumerical characters plus underscore"
+ exit
+ elif ! printf '%s' "$password" | rg -Uq '^.{8,108}$'; then
+ json_err "password must be between 8 and 108 characters"
+ exit
+ fi
+ {
+ query="SELECT password FROM users WHERE username=$(escape_sql_str "$username")"
+ row=$(db_row "$query")
+ if [[ "$row" == "null" ]]; then
+ hash=$(printf '%s' "$password" | argon2_hash)
+ echo "registering user $username" >&2
+ unset password
+ db_query "INSERT INTO users (username, password) VALUES ($(escape_sql_str "$username"), $(escape_sql_str "$hash"))" >&2
+
+ created=true
+ else
+ hash=$(jq -r '.password' <<< "$row")
+ if ! argon2_verify "$hash" <<< "$password"; then
+ json_err "invalid password"
+ exit
+ fi
+
+ created=false
+ fi
+
+ json_username=$(jq -Rr @json <<< "$username")
+ till=$(date -d "+ $SESSION_TIME" '+%s')
+
+ printf '{"till":%s,"user":%s}' \
+ "$till" \
+ "$json_username" |
+ bwt_sign |
+ jq -R "{ error: false, created: $created, user: $json_username, till: $till, token: . }"
+ } || json_err
+
+ exit
+ ;;
+
+ submit)
+ {
+ token=$(jq -r '.token // ""' <<< "$BLAG_QUERY" || :)
+ if ! verified=$(check_token "$token"); then
+ json_err "$verified token"
+ exit
+ fi
+
+ user=$(jq -r .user <<< "$verified")
+ status=$(jq -r .status <<< "$BLAG_QUERY")
+ chars=${#status}
+
+ if (( chars == 0 || chars > 200 )); then
+ json_err "status should be between 1 and 200 characters"
+ exit
+ fi
+
+ db_query "INSERT INTO posts (username, contents, ts) VALUES (
+ $(escape_sql_str "$user"),
+ $(escape_sql_str "$status"),
+ $(escape_sql_str "$(date --rfc-3339=seconds)")::timestamp
+ ) ON CONFLICT (username) DO UPDATE
+ SET contents=excluded.contents, ts=excluded.ts" >&2
+ echo '{"error":false}'
+ } || json_err
+ exit
+ ;;
+esac
+
echo ""
@@ -65,96 +131,27 @@ paragraph <<< "$DESCRIPTION"
link "https://git.slonk.ing/slonk/blogin" <<< "view source" | paragraph
-token=$(jq -r '.token // ""' <<< "$BLAG_QUERY" || :)
+cat partials/submit.html
+cat partials/login.html
-logged_in=0
-if [[ "$action" == "login" ]]; then
- username=$(jq -r '.username' <<< "$BLAG_QUERY" || :)
- password=$(jq -r '.password' <<< "$BLAG_QUERY" || :)
-
- if ! rg -Uq '^[a-zA-Z0-9_]{3,24}$' <<< "$username"; then
- error_box "username must be between 3 to 24 characters, and must consist of alphanumerical characters plus underscore"
- elif ! rg -Uq '^.{8,108}$' <<< "$password"; then
- error_box "password must be between 8 and 108 characters"
- else
- {
- query="SELECT password FROM users WHERE username=$(escape_sql_str "$username")"
- row=$(db_row "$query")
- if [[ "$row" == "null" ]]; then
- hash=$(printf '%s' "$password" | argon2_hash)
- echo "registering user $username" >&2
- unset password
- db_query "INSERT INTO users (username, password) VALUES ($(escape_sql_str "$username"), $(escape_sql_str "$hash"))" >&2
- token=$(printf '{"till":%s,"user":%s}' "$(date -d "+ $SESSION_TIME" '+%s')" "$(jq -Rr @json <<< "$username")" | bwt_sign)
- logged_in=1
- info_box "account created!"
- else
- hash=$(jq -r '.password' <<< "$row")
- if argon2_verify "$hash" <<< "$password" ; then
- token=$(printf '{"till":%s,"user":%s}' "$(date -d "+ $SESSION_TIME" '+%s')" "$(jq -Rr @json <<< "$username")" | bwt_sign)
- logged_in=1
- info_box "logged in!"
- else
- error_box "invalid password"
- fi
- fi
- } || {
- error_box "failed to log in"
- }
- fi
-elif [[ "${token:+x}" == "x" ]]; then
- if { verified=$(bwt_verify <<< "$token"); } then
- till=$(jq -r '.till' <<< "$verified")
- now=$(date '+%s')
- if (( till > now )); then
- user=$(jq -r '.user' <<< "$verified")
- logged_in=1
- else
- # token expired
- error_box "session expired, please log in again"
- fi
- else
- echo "warning: somebody tried something funny" >&2
- error_box "bwt failed to validate"
- fi
-fi
-
-if (( logged_in == 1 )); then
- escape <<< "hello, $user" | header 2
-
- if [[ "$action" == "submit" ]]; then
- {
- status=$(jq -r .status <<< "$BLAG_QUERY")
- chars=${#status}
-
- if (( chars == 0 || chars > 200 )); then
- error_box "status should be between 1 and 200 characters"
- fi
-
- db_query "INSERT INTO posts (username, contents, ts) VALUES (
- $(escape_sql_str "$user"),
- $(escape_sql_str "$status"),
- $(escape_sql_str "$(date --rfc-3339=seconds)")::timestamp
- ) ON CONFLICT (username) DO UPDATE
- SET contents=excluded.contents, ts=excluded.ts" >&2
- info_box "updated status :D"
- } || error_box "failed to update status :("
- fi
-
- render partials/submit_form.sh "$token"
-else
- header 2 <<< "(b)login"
- render partials/login_form.sh
-fi
+echo '
'
+echo ''
header 2 <<< "posts"
-#shellcheck disable=SC2162
-while read POST; do {
- if [[ "${POST:+x}" == "x" ]]; then
- declare -A post="$POST"
- render partials/post.sh "${post[username]}" "${post[instance]}" "${post[timestamp]}" "${post[contents]}"
- fi
-} & done < <(parse_posts "${INSTANCES[@]}" || :)
+echo 'loading...
'
-wait
+#shellcheck disable=SC2162
+#while read POST; do {
+# if [[ "${POST:+x}" == "x" ]]; then
+# declare -A post="$POST"
+# render partials/post.sh "${post[username]}" "${post[instance]}" "${post[timestamp]}" "${post[contents]}"
+# fi
+#} & done < <(parse_posts "${INSTANCES[@]}" || :)
+#
+#wait
+
+echo ''
+echo ''
diff --git a/load.js b/load.js
new file mode 100644
index 0000000..fcdd929
--- /dev/null
+++ b/load.js
@@ -0,0 +1,5 @@
+let post_name = location.pathname.match(/^\/posts\/([^\/]+)\/*$/)[1];
+let el = document.createElement("script");
+el.src = `/media/${post_name}/blogin.js`;
+el.type = "module";
+document.body.appendChild(el);
diff --git a/partials/login.html b/partials/login.html
new file mode 100644
index 0000000..061ad59
--- /dev/null
+++ b/partials/login.html
@@ -0,0 +1,24 @@
+
+
(b)login
+
+
diff --git a/partials/login_form.sh b/partials/login_form.sh
deleted file mode 100644
index 93442c2..0000000
--- a/partials/login_form.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-cat <
-
-
-
-
-
-
-
-EOF
diff --git a/partials/submit.html b/partials/submit.html
new file mode 100644
index 0000000..81ef89b
--- /dev/null
+++ b/partials/submit.html
@@ -0,0 +1,9 @@
+
+
welcome back!
+
+
diff --git a/partials/submit_form.sh b/partials/submit_form.sh
deleted file mode 100644
index 7c0229c..0000000
--- a/partials/submit_form.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-cat <
-
-
-
-
-
-
-EOF
-
diff --git a/static/blogin.js b/static/blogin.js
new file mode 100644
index 0000000..4bd0c95
--- /dev/null
+++ b/static/blogin.js
@@ -0,0 +1,238 @@
+import config from "./config.js";
+
+console.log(config);
+
+let token = localStorage.getItem("token");
+let verified = false;
+let loggedIn = false;
+
+const loginDiv = document.getElementById("login");
+const loginForm = document.getElementById("login-form");
+
+const submitDiv = document.getElementById("submit");
+const submitForm = document.getElementById("submit-form");
+const welcomeHeader = document.getElementById("welcome");
+
+const posts = document.getElementById("posts");
+
+const errorBox = document.getElementById("error");
+const successBox = document.getElementById("success");
+
+let errorHideTimeout = null;
+let successHideTimeout = null;
+
+function displayError(message) {
+ let p = document.createElement("p");
+ p.innerText = message;
+
+ errorBox.appendChild(p);
+ errorBox.style.display = "block";
+
+ setTimeout(() => p.remove(), config.box_timeout);
+
+ if (errorHideTimeout !== null) clearTimeout(errorHideTimeout);
+ errorHideTimeout = setTimeout(() => {
+ errorBox.style.display = "none";
+ errorHideTimeout = null;
+ }, config.box_timeout);
+}
+
+function displaySuccess(message) {
+ let p = document.createElement("p");
+ p.innerText = message;
+
+ successBox.appendChild(p);
+ successBox.style.display = "block";
+
+ setTimeout(() => p.remove(), config.box_timeout);
+
+ if (successHideTimeout !== null) clearTimeout(successHideTimeout);
+ successHideTimeout = setTimeout(() => {
+ successBox.style.display = "none";
+ successHideTimeout = null;
+ }, config.box_timeout);
+}
+
+async function verifyToken(token) {
+ let url = new URL(config.endpoint);
+ url.searchParams.set("action", "verify_token");
+ url.searchParams.set("token", token);
+ try {
+ return await (await fetch(url)).json();
+ } catch (err) {
+ console.error(err);
+ displayError(err.message);
+ return false;
+ }
+}
+
+window.login = login;
+
+let logoutTimeout = null;
+
+function login(verified) {
+ loggedIn = true;
+ welcomeHeader.innerText = `welcome, ${verified.user}`;
+ if (logoutTimeout !== null) clearTimeout(logoutTimeout);
+ logoutTimeout = setTimeout(() => {
+ logout();
+ logoutTimeout = null;
+ }, verified.till * 1000 - Date.now());
+
+ loginDiv.style.display = "none";
+ submitDiv.style.display = "block";
+}
+
+function logout() {
+ loggedIn = false;
+ localStorage.removeItem("token");
+ token = null;
+
+ loginDiv.style.display = "block";
+ submitDiv.style.display = "none";
+}
+
+if (token && (verified = await verifyToken(token))) login(verified);
+else logout();
+
+loginForm.onsubmit = async (e) => {
+ e.preventDefault();
+
+ let data = new FormData(loginForm);
+
+ let url = new URL(config.endpoint);
+ url.searchParams.set("action", "login");
+ for (let [key, value] of data.entries()) {
+ url.searchParams.set(key, value);
+ }
+
+ try {
+ let value = await (await fetch(url)).json();
+ console.log(value);
+
+ if (value.error) {
+ displayError(`couldn't log in: ${value.reason ?? "unknown error"}`);
+ } else {
+ token = value.token;
+ localStorage.setItem("token", value.token);
+ verified = {
+ user: value.user,
+ till: value.till,
+ };
+ login(verified);
+ if (value.created) displaySuccess("account created!");
+ else displaySuccess(`logged in!`);
+ }
+ } catch (err) {
+ console.error(err);
+ displayError(err.message);
+ }
+};
+
+submitForm.onsubmit = async (e) => {
+ e.preventDefault();
+
+ if (!loggedIn) return;
+
+ let data = new FormData(submitForm);
+
+ let url = new URL(config.endpoint);
+ url.searchParams.set("action", "submit");
+ url.searchParams.set("token", token);
+ for (let [key, value] of data.entries()) {
+ url.searchParams.set(key, value);
+ }
+
+ try {
+ let value = await (await fetch(url)).json();
+ console.log(value);
+
+ if (value.error) {
+ displayError(
+ `couldn't update status: ${value.reason ?? "unknown error"}`
+ );
+ } else {
+ updateStatuses();
+ displaySuccess(`updated status!`);
+ }
+ } catch (err) {
+ console.error(err);
+ displayError(err.message);
+ }
+};
+
+function renderStatus(status) {
+ let quote = document.createElement("blockquote");
+ quote.classList.add("post");
+
+ let meta = document.createElement("div");
+ meta.classList.add("meta");
+
+ let username = document.createElement("span");
+ username.classList.add("username");
+ username.innerText = status.username;
+ let at = document.createElement("span");
+ at.classList.add("at");
+ at.innerText = "@";
+ let instance = document.createElement("a");
+ instance.classList.add("instance");
+ instance.innerText = status.instance;
+ instance.href = "https://" + status.instance;
+ instance.target = "_blank";
+ let timestamp = document.createElement("span");
+ timestamp.classList.add("timestamp", "date", "date-rfc3339");
+ timestamp.innerText = new Date(status.timestamp).toISOString();
+ meta.append(username, at, instance, " at ", timestamp);
+ quote.appendChild(meta);
+
+ let contents = document.createElement("p");
+ contents.innerText = status.contents;
+ quote.appendChild(contents);
+
+ return quote;
+}
+
+async function updateStatuses() {
+ let statuses = [];
+ let promises = [];
+ for (let [instance, endpoint] of Object.entries(config.instances)) {
+ promises.push(
+ (async () => {
+ try {
+ console.log(instance, endpoint);
+ let res = await fetch(endpoint);
+ console.log(res);
+ let value = await res.json();
+
+ for (let [contents, username, timestamp] of value)
+ statuses.push({
+ username,
+ timestamp: Date.parse(timestamp),
+ contents,
+ instance,
+ });
+ } catch (err) {
+ console.error(err);
+ displayError(err.message);
+ }
+ })()
+ );
+ }
+ await Promise.all(promises);
+
+ statuses.sort(({ timestamp: a }, { timestamp: b }) => b - a);
+
+ let start = performance.now();
+
+ statuses = statuses.map(renderStatus);
+
+ let diff = performance.now() - start;
+ console.log(diff);
+
+ posts.replaceChildren(...statuses);
+ replaceDates();
+}
+
+// initialization code
+window.logout = logout;
+updateStatuses();
diff --git a/static/config.js b/static/config.js
new file mode 100644
index 0000000..25277cc
--- /dev/null
+++ b/static/config.js
@@ -0,0 +1,15 @@
+// who up logi they verse
+
+const config = {
+ endpoint: `${location.protocol}//${location.host}${location.pathname}`,
+ instances: {
+ "todepond.com": "https://todepond-lablogingetusers.web.val.run",
+ "svenlaa.com": "https://api.svenlaa.com/logiverse/logs",
+ "evolved.systems": "https://evol-lablogingetusers.web.val.run",
+ },
+ box_timeout: 10000,
+};
+
+config.instances[location.hostname] = `${config.endpoint}?action=posts`;
+
+export default config;
diff --git a/util.sh b/util.sh
index e763b85..8458f86 100644
--- a/util.sh
+++ b/util.sh
@@ -6,11 +6,11 @@ bwt_sign() {
"$(jq -Rsr @json <<< "$data")" \
"$(jq -Rsr @json <<< "$signature")" |
zstd -1 | base64 -w0 |
- sed 's,=,_E_,g;s,/,_S_,g;s,+,_P_,g' # urlsafe base64 but evil
+ sed 's,=,_E,g;s,/,_S,g;s,+,_P,g' # urlsafe base64 but evil
}
bwt_verify() {
- json=$(sed 's,_E_,=,g;s,_S_,/,g;s,_P_,+,g' | base64 -d | zstd -d)
+ json=$(sed 's,_E,=,g;s,_S,/,g;s,_P,+,g' | base64 -d | zstd -d)
data=$(jq -r '.d' <<< "$json")
signature=$(jq -r '.s' <<< "$json")
@@ -44,7 +44,7 @@ argon2_hash() {
}
argon2_verify() {
- $(cash -O2 -pipe -largon2 <
#include
#include
@@ -57,9 +57,7 @@ int main(int argc, char **argv) {
if (result == ARGON2_OK) return 0;
else return 1;
-}
-EOF
-) "${1?need hash}" "$(