From e460f88a7f8606a2ac1b798f149db749e51c2ee3 Mon Sep 17 00:00:00 2001 From: Kenneth Jao Date: Mon, 26 May 2025 01:22:21 -0400 Subject: [PATCH] refactor: website files are now templated and compiled --- .gitignore | 8 +- Cargo.lock | 67 ++++++++ Cargo.toml | 1 + {static => assets}/favicon.ico | Bin {static => assets}/fragile.svg | 0 {static => assets}/keep_dry.svg | 0 {static => assets}/qr_code.svg | 0 {static => assets}/recycle.svg | 0 pack.yml.template | 10 ++ src/api.rs | 59 +++---- src/compile.rs | 104 +++++++++++ src/main.rs | 9 +- static/index.html | 191 --------------------- templates/html/base.html | 42 +++++ templates/html/contentDefault.html | 47 +++++ templates/html/labelBot.svg | 75 ++++++++ templates/html/labelTop.html | 27 +++ templates/html/svgTape.svg | 78 +++++++++ {static => templates}/index.css | 27 +-- templates/js/base.js | 63 +++++++ static/index.js => templates/js/default.js | 139 +-------------- templates/js/token.js | 56 ++++++ 22 files changed, 627 insertions(+), 376 deletions(-) rename {static => assets}/favicon.ico (100%) rename {static => assets}/fragile.svg (100%) rename {static => assets}/keep_dry.svg (100%) rename {static => assets}/qr_code.svg (100%) rename {static => assets}/recycle.svg (100%) create mode 100644 pack.yml.template create mode 100644 src/compile.rs delete mode 100644 static/index.html create mode 100644 templates/html/base.html create mode 100644 templates/html/contentDefault.html create mode 100644 templates/html/labelBot.svg create mode 100644 templates/html/labelTop.html create mode 100644 templates/html/svgTape.svg rename {static => templates}/index.css (95%) create mode 100644 templates/js/base.js rename static/index.js => templates/js/default.js (63%) create mode 100644 templates/js/token.js diff --git a/.gitignore b/.gitignore index e631a67..99b9d49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -upload data.db -target/* -logs/* +pack.yml +upload/ +target/ +logs/ +static/ diff --git a/Cargo.lock b/Cargo.lock index 3fce1ba..c748e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,48 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.100", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -120,6 +162,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -871,6 +922,7 @@ name = "pack" version = "0.0.1" dependencies = [ "anyhow", + "askama", "axum", "bytes", "env_filter", @@ -1093,6 +1145,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.0.5" @@ -2099,6 +2157,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 66bec94..cc7b8a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ authors = ["Kenneth Jao"] [dependencies] anyhow = "1.0.98" +askama = { version = "0.14.0", features = ["blocks"] } axum = { version = "0.8.1", features = ["multipart"] } bytes = "1.10.1" env_filter = "0.1.3" diff --git a/static/favicon.ico b/assets/favicon.ico similarity index 100% rename from static/favicon.ico rename to assets/favicon.ico diff --git a/static/fragile.svg b/assets/fragile.svg similarity index 100% rename from static/fragile.svg rename to assets/fragile.svg diff --git a/static/keep_dry.svg b/assets/keep_dry.svg similarity index 100% rename from static/keep_dry.svg rename to assets/keep_dry.svg diff --git a/static/qr_code.svg b/assets/qr_code.svg similarity index 100% rename from static/qr_code.svg rename to assets/qr_code.svg diff --git a/static/recycle.svg b/assets/recycle.svg similarity index 100% rename from static/recycle.svg rename to assets/recycle.svg diff --git a/pack.yml.template b/pack.yml.template new file mode 100644 index 0000000..5aade98 --- /dev/null +++ b/pack.yml.template @@ -0,0 +1,10 @@ +--- +host: "127.0.0.1" +port: "3000" +domain: +db_path: "data.db" +log_path: "logs" +upload_path: "upload" +gitea_host: +client_id: +client_secret: diff --git a/src/api.rs b/src/api.rs index ef854fb..0c68e89 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,4 +1,4 @@ -use std::{fs, path, slice::Iter}; +use std::{fs, fmt, path, slice::Iter}; use bytes::Bytes; use std::process::Command; use rand::distr::{Alphanumeric, SampleString}; @@ -17,9 +17,9 @@ use serde_json::Value; use crate::{query, ServerState}; -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy)] #[repr(u8)] -enum RepoFeature { +pub enum RepoFeature { Docs = 0, Builds = 1, Nightly = 2, @@ -35,6 +35,32 @@ impl RepoFeature { } } +impl fmt::Display for RepoFeature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +impl From for String { + fn from(value: RepoFeature) -> Self { + value.to_string().to_lowercase() + } +} + +impl TryFrom<&str> for RepoFeature { + type Error = APIError; + + fn try_from(value: &str) -> Result { + for feature in RepoFeature::iter() { + if feature.to_string().to_lowercase() == value.to_lowercase() { + return Ok(*feature); + } + } + Err(APIError::InvalidFeature { feature: value.to_owned() }) + } +} + + #[derive(Error, Debug)] pub enum APIError { #[error("Request error: {0}")] @@ -105,37 +131,12 @@ impl IntoResponse for APIError { } } -impl From for &str { - fn from(value: RepoFeature) -> Self { - match value { - RepoFeature::Docs => "docs", - RepoFeature::Builds => "builds", - RepoFeature::Nightly => "nightly", - RepoFeature::PreProd => "preprod", - } - } -} - -impl TryFrom<&str> for RepoFeature { - type Error = APIError; - - fn try_from(value: &str) -> Result { - match value.to_lowercase().as_str() { - "docs" => Ok(RepoFeature::Docs), - "builds" => Ok(RepoFeature::Builds), - "nightly" => Ok(RepoFeature::Nightly), - "preprod" => Ok(RepoFeature::PreProd), - other => Err(APIError::InvalidFeature { feature: other.to_owned() }), - } - } -} - fn as_feature_list(value: u64) -> FeatureList { let mut v: Vec = Vec::new(); for feature in RepoFeature::iter() { let feat_num = 1 << (*feature as u64); if (value & feat_num) == feat_num { - v.push(Into::<&str>::into(*feature).to_owned()); + v.push(Into::::into(*feature).to_owned()); } } v diff --git a/src/compile.rs b/src/compile.rs new file mode 100644 index 0000000..94eb1c3 --- /dev/null +++ b/src/compile.rs @@ -0,0 +1,104 @@ +use anyhow::{Context, Result}; +use askama::Template; +use std::{fs, io, path::Path}; + +#[derive(Clone, Copy)] +struct WebStates<'a> { + auth: &'a str, + error: &'a str, + loading: &'a str, + repo_selected: &'a str, + repo_created: &'a str, + button_on: &'a str, + feature_on: &'a str, + confirm_on: &'a str, +} + +#[derive(Template)] +#[template( + ext = "html", + path = "html/contentDefault.html", + whitespace = "suppress" +)] +struct Default<'a> { + config: crate::ServerConfig, + features: Vec, + content_text: String, + state: WebStates<'a>, +} + +#[derive(Template)] +#[template(ext = "html", path = "html/base.html", whitespace = "suppress")] +struct NotFound { + config: crate::ServerConfig, + content_text: String, +} + +#[derive(Template)] +#[template(path = "index.css", escape = "txt", whitespace = "suppress")] +struct Css<'a> { + state: WebStates<'a>, +} + +pub fn make_templates(config: &crate::ServerConfig) -> Result<()> { + let states = WebStates { + auth: "auth", + error: "error", + loading: "loading", + repo_selected: "repoSelected", + repo_created: "repoCreated", + button_on: "enabled", + feature_on: "fEnabled", + confirm_on: "enabled", + }; + + let default = Default { + config: config.clone(), + features: crate::api::RepoFeature::iter() + .map(|f| f.to_string()) + .collect(), + content_text: "awaiting authorization...".to_owned(), + state: states, + } + .render() + .context("Unable to process 'Default' template")?; + + fs::write("static/index.html", default).context("Could not write to static/index.html")?; + + let not_found = NotFound { + config: config.clone(), + content_text: "404: not found...".to_owned(), + } + .render() + .context("Unable to process 'NotFound' template.")?; + + fs::write("static/404.html", not_found).context("Could not write to static/404.html")?; + + let css = Css { state: states } + .render() + .context("Unable to process 'CSS' template.")?; + + fs::write("static/index.css", css).context("Could not write to static/index.css")?; + + Ok(()) +} + +fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + fs::create_dir_all(&dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} + +pub fn copy_assets() -> Result<()> { + fs::create_dir("static").context("Could not create directory 'static'.")?; + copy_dir_all("assets", "static").context("Could not copy 'assets' into 'static'.")?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index a33df9f..c5ad183 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ use tracing_appender::rolling; use serde::Deserialize; mod api; +mod compile; mod util; type SQLConn = sqlite::ConnectionThreadSafe; @@ -34,6 +35,7 @@ type SQLConn = sqlite::ConnectionThreadSafe; struct ServerConfig { host: String, port: String, + domain: String, db_path: String, log_path: String, upload_path: String, @@ -81,9 +83,12 @@ async fn main() -> Result<()> { .context(format!("Failed to setup SQLite database at '{}'.", &config.db_path))?; // Make file upload folder. - fs::create_dir_all(&config.upload_path)?; + fs::create_dir_all(&config.upload_path) + .context(format!("Unable to make directory at '{}'.", &config.upload_path))?; let log_file = rolling::daily(&config.log_path, ""); let upload_path = config.upload_path.clone(); + compile::copy_assets()?; + compile::make_templates(&config)?; let sql: &'static sqlite::ConnectionThreadSafe = Box::leak(Box::new(sql)); let server_state = ServerState { config, sql }; @@ -118,10 +123,8 @@ async fn main() -> Result<()> { method = format!("{}", request.method()), version = format!("{:?}", request.version()), remote_ip = addr, - ) }); - // Build routes let api_routes = Router::new() diff --git a/static/index.html b/static/index.html deleted file mode 100644 index bab58e6..0000000 --- a/static/index.html +++ /dev/null @@ -1,191 +0,0 @@ - - - - - Pack - - - - - - - - - - -
- - -
-
- - - - -
- - - - - - PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK.PACK. - - -
-
-
- - - - - - - - - - - -
-
-
-

BYTE TRANSIT FEES PAID

-

64 OF 64

-

6.28 TB FIBRE OPTIC EXPRESS RATE

-

ACCEPTS TAR.GZ, ZIP

-
-
- - - -

-
-
-
-
-

-
-
-

AWAITING AUTHORIZATION...

-
-
-

REPOSITORY:

- - -

-
-
-

ACTIONS:

-
- - - -
-
-

LINK:

- - -

FEATURES:

-
- - - - -
-
-
-
-
-
-
-

CONFIRMATION:

- - - -

This will (irreversibly) delete all associated files! Please type in the full repository name to confirm.

-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - https://pack.kjao.me/ -
-
- - - - diff --git a/templates/html/base.html b/templates/html/base.html new file mode 100644 index 0000000..99e74d7 --- /dev/null +++ b/templates/html/base.html @@ -0,0 +1,42 @@ + + + + + Pack + + + + + + +
+ + +
+
+ + + + +
+ {% include "svgTape.svg" %} +
+
{% include "labelTop.html" %}
+
+

+
+
+

{{ content_text | upper }}

+ {% block content %}{% endblock %} +
+
+ {% include "labelBot.svg" %} + {{ config.domain }} +
+
+ + + diff --git a/templates/html/contentDefault.html b/templates/html/contentDefault.html new file mode 100644 index 0000000..01dc52e --- /dev/null +++ b/templates/html/contentDefault.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

REPOSITORY:

+ + +

+
+
+

ACTIONS:

+
+ + + +
+
+

LINK:

+ + +

FEATURES:

+
+ {% for feature in features %} + + {% endfor %} +
+
+
+
+
+
+
+

CONFIRMATION:

+ + + +

This will (irreversibly) delete all associated files! Please type in the full repository name to confirm.

+
+
+
+{% endblock %} + +{% block script %} +{% include "js/token.js" %} +{% include "js/default.js" %} +{% endblock %} diff --git a/templates/html/labelBot.svg b/templates/html/labelBot.svg new file mode 100644 index 0000000..ff0c211 --- /dev/null +++ b/templates/html/labelBot.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/html/labelTop.html b/templates/html/labelTop.html new file mode 100644 index 0000000..8773b2c --- /dev/null +++ b/templates/html/labelTop.html @@ -0,0 +1,27 @@ +
+ + + + + + + + + + + +
+
+
+

BYTE TRANSIT FEES PAID

+

64 OF 64

+

6.28 TB FIBRE OPTIC EXPRESS RATE

+

ACCEPTS TAR.GZ, ZIP

+
+
+ + + +

+
+
diff --git a/templates/html/svgTape.svg b/templates/html/svgTape.svg new file mode 100644 index 0000000..29fb8d7 --- /dev/null +++ b/templates/html/svgTape.svg @@ -0,0 +1,78 @@ + + + + + + + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + PACK.PACK.PACK.PACK. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/index.css b/templates/index.css similarity index 95% rename from static/index.css rename to templates/index.css index 5e72a0f..035df91 100644 --- a/static/index.css +++ b/templates/index.css @@ -438,7 +438,7 @@ body { font-family: 'Courier'; } - input.error { + input.{{ state.error }} { outline: 2px solid red !important; } @@ -454,7 +454,7 @@ body { pointer-events: none; } - input[type=submit].enabled { + input[type=submit].{{ state.button_on }} { color: var(--c-text); border-style: solid; pointer-events: auto; @@ -468,7 +468,7 @@ body { outline: none; } - input[type=submit].loading { + input[type=submit].{{ state.loading }} { border: 1px solid transparent; background: linear-gradient(var(--c-label), var(--c-label)) padding-box, conic-gradient( from var(--angle), @@ -537,19 +537,6 @@ body { font-weight: 800; pointer-events: auto; } - /* .feature { */ - /* border: 1px solid var(--c-lines); */ - /* padding: 2%; */ - /* margin-right: 2%; */ - /* font-size: 150%; */ - /* cursor: pointer; */ - /* user-select: none; */ - /* transition: background-color 0.15s ease-in-out; */ - /* } */ - - /* .feature:hover { */ - /* background-color: var(--c-login-hover); */ - /* } */ /* Feature states. */ .feature { @@ -557,7 +544,7 @@ body { border-style: dashed; } - .feature.fEnabled { + .feature.{{ state.feature_on }} { color: var(--c-text); border-style: solid; @@ -611,7 +598,7 @@ body { opacity: 0; } - #confirmOverlay.enabled { + #confirmOverlay.{{ state.confirm_on }} { pointer-events: auto; top: 0; opacity: 1; @@ -622,7 +609,7 @@ body { opacity: 0; } - #repoInfo.repoSelected, #repoFeatures.repoCreated { + #repoInfo.{{ state.repo_selected }}, #repoFeatures.{{ state.repo_created }} { opacity: 1 } } @@ -674,7 +661,7 @@ body { /* Default no authentication */ } } -body.auth { +body.{{ state.auth }} { #packageLabelTitle h1::before { content: "PACKAGE INFO" } diff --git a/templates/js/base.js b/templates/js/base.js new file mode 100644 index 0000000..15df7dc --- /dev/null +++ b/templates/js/base.js @@ -0,0 +1,63 @@ +const CONFIG = { + client_id: "{{ config.client_id }}", + redirect_uri: "{{ config.domain }}", + authorization_endpoint: "{{ config.gitea_host }}/login/oauth/authorize", + token_endpoint: "{{ config.gitea_host }}/login/oauth/access_token", + requested_scopes: "read:repository,write:repository" +}; + +var G_AUTH = window.localStorage.getItem("token") !== null; + +document.on = document.addEventListener; +function sugar(obj) { + obj.on = obj.addEventListener; + obj.addClass = (x) => obj.classList.add(x); + obj.delClass = (x) => obj.classList.remove(x); + obj.hasClass = (x) => obj.classList.contains(x); + obj.setClass = (x, y) => obj.classList.toggle(x, y); + return obj; +} + +function get(s) { + let x = [...document.querySelectorAll(s)].map(sugar); + return (x.length === 1) ? x[0] : x; +} + +// Check if object is empty. +function isEmpty(obj) { + for (const prop in obj) { + if (Object.hasOwn(obj, prop)) { + return false; + } + } + return true; +} + +// Returns error and resolves promise if necessary. +function getErr(f) { + return (err) => { + if (err instanceof Promise) { + return err.then(f); + } else { + return err; + } + } +} + +get("#login").on("click", async (e) => { + e.preventDefault(); + if (G_AUTH) { + window.localStorage.clear(); + window.location = "/"; + } else { + // Build the authorization URL + let url = CONFIG.authorization_endpoint + + "?response_type=code" + + "&client_id="+encodeURIComponent(CONFIG.client_id) + + "&scope="+encodeURIComponent(CONFIG.requested_scopes) + + "&redirect_uri="+encodeURIComponent(CONFIG.redirect_uri); + + // Redirect to the authorization server + window.location = url; + } +}); diff --git a/static/index.js b/templates/js/default.js similarity index 63% rename from static/index.js rename to templates/js/default.js index 1c69faf..184a835 100644 --- a/static/index.js +++ b/templates/js/default.js @@ -1,20 +1,12 @@ -const CONFIG = { - client_id: "3d463405-d098-4038-8b25-a44ca5b9ab9c", - redirect_uri: "http://127.0.0.1:3000/", - authorization_endpoint: "https://git.kjao.me/login/oauth/authorize", - token_endpoint: "https://git.kjao.me/login/oauth/access_token", - requested_scopes: "read:repository,write:repository" -}; - const STATES = { // Either the non-d - auth: "auth", - error: "error", - loading: "loading", - repoSelected: "repoSelected", - repoCreated: "repoCreated", - buttonOn: "enabled", - featureOn: "fEnabled", - confirmOn: "enabled", + auth: "{{ state.auth }}", + error: "{{ state.error }}", + loading: "{{ state.loading }}", + repoSelected: "{{ state.repo_selected }}", + repoCreated: "{{ state.repo_created }}", + buttonOn: "{{ state.button_on }}", + featureOn: "{{ state.feature_on }}", + confirmOn: "{{ state.confirm_on }}", } var G_REPO_VALUE = "", @@ -23,108 +15,15 @@ var G_REPO_VALUE = "", // UTILITY // -document.on = document.addEventListener; -function sugar(obj) { - obj.on = obj.addEventListener; - obj.addClass = (x) => obj.classList.add(x); - obj.delClass = (x) => obj.classList.remove(x); - obj.hasClass = (x) => obj.classList.contains(x); - obj.setClass = (x, y) => obj.classList.toggle(x, y); - return obj; -} - -function get(s) { - let x = [...document.querySelectorAll(s)].map(sugar); - return (x.length === 1) ? x[0] : x; -} - -// Check if object is empty. -function isEmpty(obj) { - for (const prop in obj) { - if (Object.hasOwn(obj, prop)) { - return false; - } - } - return true; -} - -// Returns error and resolves promise if necessary. -function getErr(f) { - return (err) => { - if (err instanceof Promise) { - return err.then(f); - } else { - return err; - } - } -} - // Parse a query string into an object function parseQueryString(string) { if(string == "") { return {}; } var segments = string.split("&").map(s => s.split("=") ); var queryString = {}; - segments.forEach(s => G_QUERYString[s[0]] = s[1]); + segments.forEach(s => queryString[s[0]] = s[1]); return queryString; } -// Fetch function for applet use case. -async function jfetch(url, method, obj={}) { - let request = { - headers: { - "Content-Type": "application/json", - "Authorization": window.localStorage.getItem("token"), - }, - method: method, - }; - - if (!isEmpty(obj)) request["body"] = JSON.stringify(obj); - - return fetch(url, request) - .then((response) => { - let contentType = response.headers.get("Content-Type"); - let json; - if (contentType && contentType.indexOf("application/json" !== -1)) { - json = response.json(); - } else { // Reprocess non-json into json. - json = response.text().then((text) => { - return { status: response.status, "message": text }; - }); - } - if (!response.ok) { throw json; } - return json; - }); -} - -// OAUTH // - -async function getToken() { - let response; - if (G_QUERY.code) { - response = await jfetch("/api/token", "POST", { code: G_QUERY.code }); - } else { - response = await jfetch("/api/token", "PATCH", { code: window.localStorage.getItem("refresh_token") }); - } - - window.localStorage.setItem("token", response.access_token); - window.localStorage.setItem("token_expiry", (Date.now() + response.expires_in*1000).toString()); - window.localStorage.setItem("refresh_token", response.refresh_token); -} - -async function tryRefresh() { - if (Date.now() < parseInt(window.localStorage.getItem("token_expiry"))) return; // Not expired. - await getToken().catch((err) => { - let json = err.json(); - if (json.status == 401 && json.code === 0) { // Could not refresh, so refresh token expired. - window.localStorage.clear(); - alert(`Could not get new session, did you remove access from Gitea?`); - window.location = "/"; - } else { - throw json; - } - }); -} - // BUTTON ONCLICK MIDDLEWARE // // Refresh token if necessary. @@ -137,7 +36,6 @@ function tryAuth(f) { } } - // Adds loading classes for UI while awaiting. function doLoading(f) { return async (e) => { @@ -188,24 +86,6 @@ document.on("keydown", (e) => { // BUTTON ONCLICK FUNCTIONS // -get("#login").on("click", async (e) => { - e.preventDefault(); - if (G_AUTH) { - window.localStorage.clear(); - window.location = "/"; - } else { - // Build the authorization URL - let url = CONFIG.authorization_endpoint - + "?response_type=code" - + "&client_id="+encodeURIComponent(CONFIG.client_id) - + "&scope="+encodeURIComponent(CONFIG.requested_scopes) - + "&redirect_uri="+encodeURIComponent(CONFIG.redirect_uri); - - // Redirect to the authorization server - window.location = url; - } -}); - get("#repoSelectInput").on("keydown", (e) => { if (e.keyCode === 13) { get("#repoSelectButton").click(); @@ -364,5 +244,4 @@ async function init() { } var G_QUERY = parseQueryString(window.location.search.substring(1)); -var G_AUTH = window.localStorage.getItem("token") !== null; init(); diff --git a/templates/js/token.js b/templates/js/token.js new file mode 100644 index 0000000..b78ddab --- /dev/null +++ b/templates/js/token.js @@ -0,0 +1,56 @@ +// Fetch function for applet use case. +async function jfetch(url, method, obj={}) { + let request = { + headers: { + "Content-Type": "application/json", + "Authorization": window.localStorage.getItem("token"), + }, + method: method, + }; + + if (!isEmpty(obj)) request["body"] = JSON.stringify(obj); + + return fetch(url, request) + .then((response) => { + let contentType = response.headers.get("Content-Type"); + let json; + if (contentType && contentType.indexOf("application/json" !== -1)) { + json = response.json(); + } else { // Reprocess non-json into json. + json = response.text().then((text) => { + return { status: response.status, "message": text }; + }); + } + if (!response.ok) { throw json; } + return json; + }); +} + +// OAUTH // + +async function getToken() { + let response; + if (G_QUERY.code) { + response = await jfetch("/api/token", "POST", { code: G_QUERY.code }); + } else { + response = await jfetch("/api/token", "PATCH", { code: window.localStorage.getItem("refresh_token") }); + } + + window.localStorage.setItem("token", response.access_token); + window.localStorage.setItem("token_expiry", (Date.now() + response.expires_in*1000).toString()); + window.localStorage.setItem("refresh_token", response.refresh_token); +} + +async function tryRefresh() { + if (Date.now() < parseInt(window.localStorage.getItem("token_expiry"))) return; // Not expired. + await getToken().catch((err) => { + let json = err.json(); + if (json.status == 401 && json.code === 0) { // Could not refresh, so refresh token expired. + window.localStorage.clear(); + alert(`Could not get new session, did you remove access from Gitea?`); + window.location = "/"; + } else { + throw json; + } + }); +}