diff --git a/.gitignore b/.gitignore index e631a67..46a48a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -upload data.db -target/* -logs/* +pack.yml +upload/ +target/ +logs/ 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/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..d122a5b Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/fragile.svg b/assets/fragile.svg new file mode 100644 index 0000000..ed630f3 --- /dev/null +++ b/assets/fragile.svg @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/assets/keep_dry.svg b/assets/keep_dry.svg new file mode 100644 index 0000000..fa22250 --- /dev/null +++ b/assets/keep_dry.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/qr_code.svg b/assets/qr_code.svg new file mode 100644 index 0000000..6bf316b --- /dev/null +++ b/assets/qr_code.svg @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/recycle.svg b/assets/recycle.svg new file mode 100644 index 0000000..70be3c8 --- /dev/null +++ b/assets/recycle.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/pack.yml b/pack.yml index 565d0ea..a676345 100644 --- a/pack.yml +++ b/pack.yml @@ -1,6 +1,7 @@ --- -host: "192.168.0.220" +host: "127.0.0.1" port: "3000" +domain: "http://127.0.0.1:3000/" db_path: "data.db" log_path: "logs" upload_path: "upload" 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/404.html b/static/404.html new file mode 100644 index 0000000..e4410aa --- /dev/null +++ b/static/404.html @@ -0,0 +1,272 @@ + + + + + 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

+
+
+ + + +

+
+
+
+

+
+
+

404: NOT FOUND...

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +http://127.0.0.1:3000/ +
+
+ + + \ No newline at end of file diff --git a/static/index.css b/static/index.css index 5e72a0f..868c9de 100644 --- a/static/index.css +++ b/static/index.css @@ -438,7 +438,7 @@ body { font-family: 'Courier'; } - input.error { + input.error{ outline: 2px solid red !important; } @@ -454,7 +454,7 @@ body { pointer-events: none; } - input[type=submit].enabled { + input[type=submit].enabled{ color: var(--c-text); border-style: solid; pointer-events: auto; @@ -468,7 +468,7 @@ body { outline: none; } - input[type=submit].loading { + input[type=submit].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.fEnabled{ color: var(--c-text); border-style: solid; @@ -611,7 +598,7 @@ body { opacity: 0; } - #confirmOverlay.enabled { + #confirmOverlay.enabled{ pointer-events: auto; top: 0; opacity: 1; @@ -622,7 +609,7 @@ body { opacity: 0; } - #repoInfo.repoSelected, #repoFeatures.repoCreated { + #repoInfo.repoSelected, #repoFeatures.repoCreated{ opacity: 1 } } @@ -674,7 +661,7 @@ body { /* Default no authentication */ } } -body.auth { +body.auth{ #packageLabelTitle h1::before { content: "PACKAGE INFO" } @@ -700,4 +687,4 @@ body.auth { background: var(--c-label); color: var(--c-text); } -} +} \ No newline at end of file diff --git a/static/index.html b/static/index.html index bab58e6..bcec602 100644 --- a/static/index.html +++ b/static/index.html @@ -6,12 +6,8 @@ - - - -
@@ -22,170 +18,589 @@ -
- - - + + + - - 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.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

-
-
- - - -

-
-
-
+
+
+
+

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.

-
-
-
+

AWAITING AUTHORIZATION...

+
+

REPOSITORY:

+ + +

+
+
+

ACTIONS:

+
+ + +
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - https://pack.kjao.me/ +
+

LINK:

+ + +

FEATURES:

+
+
+
+
+
+
+
+

CONFIRMATION:

+ + + +

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

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +http://127.0.0.1:3000/
- - - + + \ No newline at end of file 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/templates/index.css b/templates/index.css new file mode 100644 index 0000000..035df91 --- /dev/null +++ b/templates/index.css @@ -0,0 +1,690 @@ +:root { + --font-family: 'Quicksand', sans-serif; + + /* Light Colors */ + --cl-bg: #e0ffdd; + --cl-text: black; + --cl-dec-text: black; + --cl-box: #e7bb69; + --cl-box-edge: #c19d59; + --cl-box-ridge: #e1b768; + --cl-label: white; + --cl-lines: black; + --cl-login-hover: rgba(255, 255, 255, 0.8); + --cl-null-text: #9b9b9b; + + /* Dark Colors */ + --cd-bg: #111; + --cd-text: #fff; + --cd-dec-text: #9b9b9b; + --cd-box: #262933; + --cd-box-edge: #1a1a1a; + --cd-box-ridge: #282b35; + --cd-label: #222; + --cd-lines: #4d4d4d; + --cd-login-hover: rgba(0, 0, 0, 0.5); + --cd-null-text: #9b9b9b; + + --c-loading: #d6d6d6; + + @media (prefers-color-scheme: light) { + --c-bg: var(--cl-bg); + --c-text: var(--cl-text); + --c-dec-text: var(--cl-dec-text); + --c-box: var(--cl-box); + --c-box-edge: var(--cl-box-edge); + --c-box-ridge: var(--cl-box-ridge); + --c-label: var(--cl-label); + --c-lines: var(--cl-lines); + --c-login-hover: var(--cl-login-hover); + --c-null-text: var(--cl-null-text); + + --p-theme-switch-0: 0; + --p-theme-switch-1: 1; + } + + @media (prefers-color-scheme: dark) { + --c-bg: var(--cd-bg); + --c-text: var(--cd-text); + --c-dec-text: var(--cd-dec-text); + --c-box: var(--cd-box); + --c-box-edge: var(--cd-box-edge); + --c-box-ridge: var(--cd-box-ridge); + --c-label: var(--cd-label); + --c-lines: var(--cd-lines); + --c-login-hover: var(--cd-login-hover); + --c-null-text: var(--cd-null-text); + + --p-theme-switch-0: 1; + --p-theme-switch-1: 0; + } +} + +:root:has(#themeSwitch:checked) { + /* Swapped light and dark themes. */ + + @media (prefers-color-scheme: dark) { + --c-bg: var(--cl-bg); + --c-text: var(--cl-text); + --c-dec-text: var(--cl-dec-text); + --c-box: var(--cl-box); + --c-box-edge: var(--cl-box-edge); + --c-box-ridge: var(--cl-box-ridge); + --c-label: var(--cl-label); + --c-lines: var(--cl-lines); + --c-login-hover: var(--cl-login-hover); + --c-null-text: var(--cl-null-text); + } + + @media (prefers-color-scheme: light) { + --c-bg: var(--cd-bg); + --c-text: var(--cd-text); + --c-dec-text: var(--cd-dec-text); + --c-box: var(--cd-box); + --c-box-edge: var(--cd-box-edge); + --c-box-ridge: var(--cd-box-ridge); + --c-label: var(--cd-label); + --c-lines: var(--cd-lines); + --c-login-hover: var(--cd-login-hover); + --c-null-text: var(--cd-null-text); + } +} + +html { + width: 100%; height: 100%; + font-family: var(--font-family); + color: var(--c-text); + overflow: hidden; + + @media (min-aspect-ratio: 100/135) { /* Wider */ + font-size: min(calc(100/135 * 1vh), 62.5%); + /* Centered around the main box: packageLabel with net height/width 100/135 */ + } + + @media (max-aspect-ratio: 100/135) { /* longer */ + font-size: min(calc(100/100 * 1vw), 62.5%); + } +} + +body { + width: 100vw; height: 100%; + margin: 0 auto 0 auto; + display: flex; + justify-content: center; + align-items: center; + + background-color: var(--c-bg); +} + +#themeSwitchContainer { + width: 10rem; height: 10rem; + position: fixed; + top: 0; left: 0; + z-index: 5; + user-select: none; + + --sun-color: #fffb15; + --moon-color: #ffe598; + + @media (prefers-color-scheme: light) { + --day-color: #a3e0ee; + --night-color: #222; + } + + @media (prefers-color-scheme: dark) { + --night-color: #a3e0ee; + --day-color: #222; + } + + --w: 6deg; + --spokes: 10; + + input { + width: 100%; height: 100%; + position: absolute; + opacity: 0; + cursor: pointer; + } + + span { + transition: opacity 0.15s ease-in-out; + position: absolute; + pointer-events: none; + } + + span:nth-child(2) { + width: 100%; height: 100%; + top: 0; left: 0; + background-color: var(--day-color); + filter: brightness(100%); + transition: background-color 0.15s ease-in-out, + filter 0.15s ease-in-out; + } + + span:nth-child(3) { + width: 4rem; height: 4rem; + position: absolute; + top: 3rem; left: 3rem; + background-color: var(--sun-color); + border-radius: 50%; + opacity: var(--p-theme-switch-1); + } + + span:nth-child(3)::before { + content: ""; + width: 7rem; height: 7rem; + position: absolute; + top: -1.5rem; left: -1.5rem; + border-radius: inherit; + background-image: repeating-conic-gradient( + from calc(-1*var(--w)/2), var(--sun-color) 0 calc(var(--w) - 2deg), + transparent calc(var(--w)) calc(360deg/var(--spokes) - 2deg), + var(--sun-color) calc(360deg/var(--spokes)) + ); + } + + span:nth-child(4) { + width: 6rem; height: 6rem; + top: 1.75rem; left: 1.75rem; + background-color: var(--moon-color); + border-radius: 50%; + opacity: var(--p-theme-switch-0); + mask-image: radial-gradient(circle, rgba(0,0,0,0) 33%, black 34%); + mask-position: -3rem -3rem; + mask-repeat: no-repeat; + mask-size: 166.66% 166.66%; + } + + input:hover ~ span:nth-child(2) { + filter: brightness(110%); + } + + input:checked ~ span:nth-child(2) { + background-color: var(--night-color); + } + + input:checked ~ span:nth-child(3) { + opacity: var(--p-theme-switch-0); + } + + input:checked ~ span:nth-child(4) { + opacity: var(--p-theme-switch-1); + } +} + + +#package { + margin: auto; + width: 298rem; + height: 200rem; + --spacing: 0.8rem; + background: repeating-linear-gradient(90deg, + var(--c-box) 0rem, + var(--c-box) var(--spacing), + var(--c-box-ridge) var(--spacing), + var(--c-box-ridge) calc(2 *var(--spacing)) + ); + + border: 1rem solid var(--c-box-edge); + position: absolute; + z-index: 1; + user-select: none; + + img { + display: block; + position: absolute; + } + + :nth-child(1) { + width: 30rem; + top: 6rem; left: 263rem; + } + + :nth-child(2) { + width: 30rem; + top: 6rem; left: 228rem; + } + + :nth-child(3) { + width: 35rem; + top: 162rem; left: 257rem; + } + + :nth-child(4) { + width: 25rem; + top: 165rem; left: 10rem; + } +} + +#packageTape { + margin: auto; + height: 30rem; + position: absolute; + z-index: 1; + + text { + font: 1.5px var(--font-family); + fill: rgba(255, 255, 255, 0.4); + user-select: none; + } +} + +#packageLabel { + width: 80rem; + height: 115rem; + + outline-width: 5rem; + outline-style: solid; + outline-color: var(--c-label); + + border-radius: 12px; + background-color: var(--c-label); + border: 0.5rem solid var(--c-lines); + + transition: background-color 0.15s ease-in-out, + border 0.15s ease-in-out, + outline-color 0.15s ease-in-out; + + display: grid; + grid-template-rows: 2fr 1fr 7fr 2fr; + + z-index: 2; + + svg { + fill: var(--c-dec-text); + } +} + +#packageLabelTop { + display: grid; + grid-template-columns: 1fr 3fr; + border-bottom: 0.5rem solid var(--c-lines); + + > div:nth-child(1) { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + border-right: 0.5rem solid var(--c-lines); + + svg { + width: 80%; height: 80%; + } + } + + > div:nth-child(2) { + padding: 3%; + display: grid; + grid-template-rows: 1fr 1fr; + + > div:nth-child(1) { + display: block; + color: var(--c-dec-text); + user-select: none; + + p { + margin: 0; + font-size: 150%; + } + + p:nth-child(1) { + display: inline-block; + } + + p:nth-child(2) { + display: inline-block; + float: right; + } + } + + > div:nth-child(2) { + width: 80%; + position: relative; + + svg { + position: absolute; + left: 0; top: 0; + width: 35rem; height: 7rem; + pointer-events: none; + } + } + } +} + +#login { + display: block; + margin: 0; + width: 24.8rem; height: 7rem; + line-height: 7rem; + position: relative; + left: 5rem; + + font-size: 6rem; + font-family: 'Courier'; + font-weight: 800; + text-align: center; + + transition: background 0.15s ease-in-out, + color 0.15s ease-in-out; + + z-index: 4; + cursor: pointer; + user-select: none; +} + +#packageLabelTitle { + display: flex; + align-items: center; + justify-content: center; + border-bottom: 0.5rem solid var(--c-lines); + user-select: none; + + h1 { + margin: 1%; + font-size: 430%; + } +} + +@keyframes cursorWait { + 0% { content: "█"; } + 50% { content: ""; } + 100% { content: "█"; } +} + +@keyframes rotate { + to { --angle: 360deg; } +} + +@property --angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + +#packageLabelContent { + border-bottom: 0.5rem solid var(--c-lines); + font-size: 150%; + font-family: 'Courier'; + + > h1 { + padding: 7%; + margin: 0; + line-height: 100%; + color: var(--c-lines); + } + + > h1::after { + animation: 1.5s infinite normal cursorWait; + content: "hello"; + } + + > div:nth-child(2) { + padding: 7% 7% 0 7%; + color: var(--c-text); + } + + h1 { + margin: 0; + line-height: 100%; + } + + input { + background: none; + color: var(--c-text); + border: 1px solid var(--c-lines); + padding: 2%; + margin: 2% 0 2% 0; + font-size: 150%; + font-family: 'Courier'; + } + + input.{{ state.error }} { + outline: 2px solid red !important; + } + + input[type=submit] { + cursor: pointer; + transition: background-color 0.15s ease-in-out; + } + + /* Button enabled/disabled. */ + input[type=submit] { + color: var(--c-null-text); + border-style: dashed; + pointer-events: none; + } + + input[type=submit].{{ state.button_on }} { + color: var(--c-text); + border-style: solid; + pointer-events: auto; + } + + input[type=submit]:hover { + background-color: var(--c-login-hover); + } + + input:focus { + outline: none; + } + + 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), + var(--c-lines) 0% 30%, + var(--c-loading) 30% 50%, + var(--c-lines) 50% 80%, + var(--c-loading) 80% 100% + ) border-box; + animation: 1.5s rotate linear infinite; + cursor: not-allowed; + } + + #repoSelect { + border-bottom: 2px solid var(--c-lines); + + #repoSelectInput { + margin-bottom: 0; + } + + #repoSelectButton { + float: right; + } + + #repoSelectError { + line-height: 100%; + height: 3rem; + margin: 2% 0 2% 0; + } + } + + #repoInfo { + margin-top: 2%; + transition: opacity 0.15s ease-in-out; + + > h1:first-child { + margin-top: 4%; + } + + #repoActions { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + } + + #repoLinkInput::selection { + background-color: transparent; + } + + #repoLinkButton { + float: right; + } + + #repoFeatures { + margin-top: 2%; + transition: opacity 0.15s ease-in-out; + + #featureBox { + display: flex; + flex-flow: row wrap; + justify-content: flex-start; + align-items: center; + + .feature { + margin-right: 2%; + font-weight: 800; + pointer-events: auto; + } + + /* Feature states. */ + .feature { + color: var(--c-null-text); + border-style: dashed; + } + + .feature.{{ state.feature_on }} { + color: var(--c-text); + border-style: solid; + + } + } + } + } + + #confirmOverlay { + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + + position: absolute; + left: 0; + z-index: 10; + + transition: opacity 0.15s ease-in-out; + + > div { + margin: 30% auto 0 auto; + + width: 80rem; + height: 30rem; + + outline-width: 5rem; + outline-style: solid; + outline-color: var(--c-label); + + border-radius: 12px; + background-color: var(--c-label); + border: 0.5rem solid var(--c-lines); + + transition: background-color 0.15s ease-in-out, + border 0.15s ease-in-out, + outline-color 0.15s ease-in-out; + + > div { + padding: 7%; + + input[type=submit] { + float: right; + } + } + } + } + + #confirmOverlay { + pointer-events: none; + top: 100%; + opacity: 0; + } + + #confirmOverlay.{{ state.confirm_on }} { + pointer-events: auto; + top: 0; + opacity: 1; + } + + /* Repo selected and created states. */ + #repoInfo, #repoFeatures { + opacity: 0; + } + + #repoInfo.{{ state.repo_selected }}, #repoFeatures.{{ state.repo_created }} { + opacity: 1 + } +} + +#packageLabelBot { + display: grid; + grid-template-rows: 6fr 1fr; + + svg { + margin: 3% auto 0 auto; + width: 80%; height: 85%; + } + + a { + margin: auto; + margin-bottom: 1%; + font-size: 2rem; + color: var(--c-dec-text); + text-decoration: none; + transition: filter 0.15s ease-in-out; + filter: brightness(100%); + } + + a:hover { + filter: brightness(120%); + } +} + +body { /* Default no authentication */ + #packageLabelTitle h1::before { + content: "PRIORITY PACKAGING" + } + + #packageLabelContent > div { + display: none; + } + + #login { + background: var(--c-label); + color: var(--c-text); + } + + #login::before { + content: "LOGIN"; + } + + #login:hover { + background-color: var(--c-login-hover); + } +} + +body.{{ state.auth }} { + #packageLabelTitle h1::before { + content: "PACKAGE INFO" + } + + #packageLabelContent > div { + display: block; + } + + #packageLabelContent > h1 { + display: none; + } + + #login { + color: transparent; + background:transparent; + } + + #login::before { + content: "LOGOUT"; + } + + #login:hover { + background: var(--c-label); + color: var(--c-text); + } +} 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; + } + }); +}