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}" "$(