allow website content to be updated

This commit is contained in:
Charlotte 🦝 Delenk 2024-12-05 09:23:54 +01:00
parent a5ffb69ccd
commit f4b45cb2f8
Signed by: darkkirb
GPG key ID: AB2BD8DAF2E37122
14 changed files with 405 additions and 38 deletions

47
Cargo.lock generated
View file

@ -702,9 +702,9 @@ dependencies = [
[[package]]
name = "b64-ct"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32261e5a95e234446dd31774ef4305dc164039370fe2622fd3e935a0f2730689"
checksum = "41dd1e07aafe656df62d40e74d4627a0cce26da43b0bec233ac1305207a5a768"
[[package]]
name = "backtrace"
@ -962,9 +962,13 @@ dependencies = [
"chir-rs-http-api",
"clap",
"color-eyre",
"dotenvy",
"eyre",
"mime",
"mime_guess",
"reqwest",
"tokio",
"tracing",
"tracing-subscriber",
]
@ -1028,10 +1032,13 @@ dependencies = [
"chir-rs-misc",
"chrono",
"eyre",
"futures",
"mime",
"rusty_paseto",
"sentry-tower",
"serde",
"tokio",
"tokio-util",
"tower-http 0.6.2",
"tracing",
"unicode-normalization",
@ -2482,6 +2489,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -3064,10 +3081,12 @@ dependencies = [
"sync_wrapper 1.0.2",
"tokio",
"tokio-rustls 0.26.0",
"tokio-util",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
"windows-registry",
@ -4170,12 +4189,13 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
@ -4408,6 +4428,12 @@ dependencies = [
"libc",
]
[[package]]
name = "unicase"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
[[package]]
name = "unicode-bidi"
version = "0.3.17"
@ -4626,6 +4652,19 @@ version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49"
[[package]]
name = "wasm-streams"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.74"

167
Cargo.nix
View file

@ -32,7 +32,7 @@ args@{
ignoreLockHash,
}:
let
nixifiedLockHash = "7dc55944ae2b0d1b65a004e3be3f5314339c3d2e023e794d54b030b197c5bf87";
nixifiedLockHash = "51a600afd0b9503f1748762462902306933599d46304da01c1ecdecdb6049697";
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
lockHashIgnored =
@ -1927,7 +1927,7 @@ else
inherit profileName;
}).out;
tokio_util =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.12" {
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" {
inherit profileName;
}).out;
};
@ -2262,15 +2262,15 @@ else
};
});
"registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.2" =
"registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.3" =
overridableMkRustCrate
(profileName: rec {
name = "b64-ct";
version = "0.1.2";
version = "0.1.3";
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo {
inherit name version;
sha256 = "32261e5a95e234446dd31774ef4305dc164039370fe2622fd3e935a0f2730689";
sha256 = "41dd1e07aafe656df62d40e74d4627a0cce26da43b0bec233ac1305207a5a768";
};
features = builtins.concatLists [
[ "default" ]
@ -3010,10 +3010,22 @@ else
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".color-eyre."0.6.3" {
inherit profileName;
}).out;
dotenvy =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".dotenvy."0.15.7" {
inherit profileName;
}).out;
eyre =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".eyre."0.6.12" {
inherit profileName;
}).out;
mime =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" {
inherit profileName;
}).out;
mime_guess =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime_guess."2.0.5" {
inherit profileName;
}).out;
reqwest =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".reqwest."0.12.9" {
inherit profileName;
@ -3022,6 +3034,10 @@ else
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.42.0" {
inherit profileName;
}).out;
tracing =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.41" {
inherit profileName;
}).out;
tracing_subscriber =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-subscriber."0.3.19" {
inherit profileName;
@ -3164,7 +3180,7 @@ else
inherit profileName;
}).out;
b64_ct =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.2" {
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.3" {
inherit profileName;
}).out;
bincode =
@ -3184,6 +3200,10 @@ else
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".eyre."0.6.12" {
inherit profileName;
}).out;
futures =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.31" {
inherit profileName;
}).out;
mime =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" {
inherit profileName;
@ -3196,10 +3216,18 @@ else
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".sentry-tower."0.34.0" {
inherit profileName;
}).out;
serde =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.215" {
inherit profileName;
}).out;
tokio =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.42.0" {
inherit profileName;
}).out;
tokio_util =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" {
inherit profileName;
}).out;
tower_http =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-http."0.6.2" {
inherit profileName;
@ -3226,7 +3254,6 @@ else
[ "axum-core" ]
[ "bytes" ]
[ "mime" ]
[ "tracing" ]
];
dependencies = {
async_trait =
@ -3238,7 +3265,7 @@ else
inherit profileName;
}).out;
b64_ct =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.2" {
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".b64-ct."0.1.3" {
inherit profileName;
}).out;
bincode =
@ -5111,6 +5138,7 @@ else
[ "async-await" ]
[ "async-await-macro" ]
[ "channel" ]
[ "default" ]
[ "futures-channel" ]
[ "futures-io" ]
[ "futures-macro" ]
@ -5351,7 +5379,7 @@ else
inherit profileName;
}).out;
tokio_util =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.12" {
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" {
inherit profileName;
}).out;
tracing =
@ -5409,7 +5437,7 @@ else
inherit profileName;
}).out;
tokio_util =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.12" {
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" {
inherit profileName;
}).out;
tracing =
@ -7269,6 +7297,38 @@ else
};
});
"registry+https://github.com/rust-lang/crates.io-index".mime_guess."2.0.5" =
overridableMkRustCrate
(profileName: rec {
name = "mime_guess";
version = "2.0.5";
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo {
inherit name version;
sha256 = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e";
};
features = builtins.concatLists [
[ "default" ]
[ "rev-mappings" ]
];
dependencies = {
mime =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" {
inherit profileName;
}).out;
unicase =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".unicase."2.8.0" {
inherit profileName;
}).out;
};
buildDependencies = {
unicase =
(buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".unicase."2.8.0" {
profileName = "__noProfile";
}).out;
};
});
"registry+https://github.com/rust-lang/crates.io-index".minimal-lexical."0.2.1" =
overridableMkRustCrate
(profileName: rec {
@ -8815,6 +8875,7 @@ else
[ "rustls-tls" ]
[ "rustls-tls-webpki-roots" ]
[ "rustls-tls-webpki-roots-no-provider" ]
[ "stream" ]
];
dependencies = {
base64 =
@ -8929,6 +8990,10 @@ else
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-rustls."0.26.0" {
inherit profileName;
}).out;
${if !(hostPlatform.parsed.cpu.name == "wasm32") then "tokio_util" else null} =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" {
inherit profileName;
}).out;
tower_service =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.3" {
inherit profileName;
@ -8945,6 +9010,10 @@ else
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen-futures."0.4.47"
{ inherit profileName; }
).out;
${if hostPlatform.parsed.cpu.name == "wasm32" then "wasm_streams" else null} =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-streams."0.4.2" {
inherit profileName;
}).out;
${if hostPlatform.parsed.cpu.name == "wasm32" then "web_sys" else null} =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.74" {
inherit profileName;
@ -12344,19 +12413,21 @@ else
};
});
"registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.12" =
"registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.13" =
overridableMkRustCrate
(profileName: rec {
name = "tokio-util";
version = "0.7.12";
version = "0.7.13";
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo {
inherit name version;
sha256 = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a";
sha256 = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078";
};
features = builtins.concatLists [
[ "codec" ]
[ "compat" ]
[ "default" ]
[ "futures-io" ]
[ "io" ]
];
dependencies = {
@ -12368,6 +12439,10 @@ else
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.31" {
inherit profileName;
}).out;
futures_io =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.31" {
inherit profileName;
}).out;
futures_sink =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.31" {
inherit profileName;
@ -13030,6 +13105,18 @@ else
};
});
"registry+https://github.com/rust-lang/crates.io-index".unicase."2.8.0" =
overridableMkRustCrate
(profileName: rec {
name = "unicase";
version = "2.8.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo {
inherit name version;
sha256 = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df";
};
});
"registry+https://github.com/rust-lang/crates.io-index".unicode-bidi."0.3.17" =
overridableMkRustCrate
(profileName: rec {
@ -13590,6 +13677,40 @@ else
};
});
"registry+https://github.com/rust-lang/crates.io-index".wasm-streams."0.4.2" =
overridableMkRustCrate
(profileName: rec {
name = "wasm-streams";
version = "0.4.2";
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo {
inherit name version;
sha256 = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65";
};
dependencies = {
futures_util =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.31" {
inherit profileName;
}).out;
js_sys =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".js-sys."0.3.74" {
inherit profileName;
}).out;
wasm_bindgen =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen."0.2.97" {
inherit profileName;
}).out;
wasm_bindgen_futures =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen-futures."0.4.47"
{ inherit profileName; }
).out;
web_sys =
(rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.74" {
inherit profileName;
}).out;
};
});
"registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.74" =
overridableMkRustCrate
(profileName: rec {
@ -13612,16 +13733,36 @@ else
[ "Headers" ]
[ "MessageEvent" ]
[ "Performance" ]
[ "QueuingStrategy" ]
[ "ReadableByteStreamController" ]
[ "ReadableStream" ]
[ "ReadableStreamByobReader" ]
[ "ReadableStreamByobRequest" ]
[ "ReadableStreamDefaultController" ]
[ "ReadableStreamDefaultReader" ]
[ "ReadableStreamGetReaderOptions" ]
[ "ReadableStreamReadResult" ]
[ "ReadableStreamReaderMode" ]
[ "ReadableStreamType" ]
[ "ReadableWritablePair" ]
[ "Request" ]
[ "RequestCredentials" ]
[ "RequestInit" ]
[ "RequestMode" ]
[ "Response" ]
[ "ServiceWorkerGlobalScope" ]
[ "StreamPipeOptions" ]
[ "TransformStream" ]
[ "TransformStreamDefaultController" ]
[ "Transformer" ]
[ "UnderlyingSink" ]
[ "UnderlyingSource" ]
[ "Window" ]
[ "Worker" ]
[ "WorkerGlobalScope" ]
[ "WritableStream" ]
[ "WritableStreamDefaultController" ]
[ "WritableStreamDefaultWriter" ]
[ "default" ]
[ "std" ]
];

View file

@ -95,9 +95,9 @@ impl CaStore {
{
let mut reader = Box::pin(reader);
let string_id = lexicographic_base64::encode(id.to_be_bytes());
info!("Starting multipart upload {id}");
let source_fname = format!("temp/{string_id}");
/*info!("Starting multipart upload {id}");
let multipart_result = self
.client
.create_multipart_upload()
@ -163,7 +163,28 @@ impl CaStore {
.set_upload_id(multipart_result.upload_id)
.send()
.await
.context("Completing multipart upload")?;
.context("Completing multipart upload")?;*/
let hasher = Arc::new(Mutex::new(Hasher::new()));
let mut buf = Vec::new();
reader.read_to_end(&mut buf).await?;
let buf = Bytes::from(buf);
let buf2 = buf.clone();
let hasher2 = Arc::clone(&hasher);
spawn_blocking(move || {
hasher2.blocking_lock().update_rayon(&buf2);
})
.await?;
self.client
.put_object()
.bucket(&*self.bucket)
.key(&source_fname)
.body(ByteStream::from(buf.to_vec()))
.send()
.await
.context("Uploading file")?;
let hash = hasher.lock().await.finalize();
let target_fname = lexicographic_base64::encode(hash.as_bytes());

View file

@ -8,9 +8,11 @@ bincode = "2.0.0-rc.3"
chir-rs-http-api = { version = "0.1.0", path = "../chir-rs-http-api" }
clap = { version = "4.5.21", features = ["derive"] }
color-eyre = "0.6.3"
dotenvy = "0.15.7"
eyre = "0.6.12"
reqwest = { version = "0.12.9", default-features = false, features = [
"rustls-tls",
] }
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] }
mime = "0.3.17"
mime_guess = "2.0.5"
reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "stream"] }
tokio = { version = "1.41.1", features = ["fs", "macros", "rt-multi-thread"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"

View file

@ -1,11 +1,15 @@
use std::collections::HashSet;
use std::{collections::HashSet, future::Future, path::Path, pin::Pin};
use chir_rs_http_api::{
auth::{LoginRequest, PasetoToken, Scope},
errors::APIError,
};
use clap::{arg, Parser, Subcommand};
use eyre::Result;
use eyre::{eyre, Context as _, OptionExt as _, Result};
use mime_guess::{Mime, MimeGuess};
use reqwest::Body;
use tokio::join;
use tracing::instrument;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
@ -24,6 +28,18 @@ enum Command {
#[arg(short, long)]
password: String,
},
Upload {
#[arg(short, long)]
source: String,
#[arg(short, long)]
dest: String,
},
UploadDir {
#[arg(short, long)]
source: String,
#[arg(short, long)]
dest: String,
},
}
async fn login(url: String, username: String, password: String) -> Result<()> {
@ -58,14 +74,76 @@ async fn login(url: String, username: String, password: String) -> Result<()> {
Ok(())
}
#[instrument(skip(source))]
async fn upload(url: String, source: impl AsRef<Path>, dest: String) -> Result<()> {
let client = reqwest::Client::new();
let token = std::env::var("CHIR_RS_TOKEN")?;
let file = tokio::fs::File::open(&source).await?;
let res = client
.post(format!("{url}{dest}"))
.header(
"Content-Type",
MimeGuess::from_path(&source)
.first()
.unwrap_or(mime::APPLICATION_OCTET_STREAM)
.to_string(),
)
.header("Authorization", format!("Bearer {token}"))
.body(Body::from(file))
.send()
.await?;
if !res.status().is_success() {
let response = res.bytes().await?;
let response: APIError =
bincode::decode_from_slice(&response, bincode::config::standard())?.0;
Err(response).with_context(|| format!("Uploading to {dest}"))?;
}
Ok(())
}
#[instrument(skip(source))]
async fn upload_dir(url: String, source: impl AsRef<Path>, dest: String) -> Result<()> {
let mut dir = tokio::fs::read_dir(source).await?;
while let Some(ent) = dir.next_entry().await? {
let file_type = ent.file_type().await?;
let file_name_str = ent
.file_name()
.into_string()
.map_err(|_| eyre!("Invalid file name encountered"))?;
let tgt = if dest.is_empty() {
file_name_str.clone()
} else {
format!("{dest}/{file_name_str}")
};
if file_type.is_dir() {
let sub_fut: Pin<Box<dyn Future<Output = Result<()>>>> =
Box::pin(upload_dir(url.clone(), ent.path(), tgt));
sub_fut.await?;
continue;
}
if file_name_str == "index.html" {
if dest.is_empty() {
upload(url.clone(), ent.path(), "".to_string()).await?;
} else {
upload(url.clone(), ent.path(), format!("{dest}/")).await?;
}
}
upload(url.clone(), ent.path(), tgt).await?;
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install().ok();
dotenvy::dotenv().ok();
tracing_subscriber::fmt::init();
let matches = Args::parse();
match matches.command {
Command::Login { username, password } => login(matches.url, username, password).await?,
Command::Upload { source, dest } => upload(matches.url, source, dest).await?,
Command::UploadDir { source, dest } => upload_dir(matches.url, source, dest).await?,
}
Ok(())

View file

@ -165,11 +165,11 @@ impl File {
/// Creates a new file
///
/// # Errors
/// This function returns an error if a database error occurs when writing, or there is a conflict
/// This function returns an error if a database error occurs when writing
#[instrument(skip(db))]
pub async fn new(db: &Database, path: &str, mime: &str, hash: &Hash) -> Result<()> {
query_as(
r#"INSERT INTO file_map ("file_path", "mime", "b3hash") VALUES ($1, $2, $3) RETURNING *"#,
r#"INSERT INTO file_map ("file_path", "mime", "b3hash") VALUES ($1, $2, $3) ON CONFLICT ("file_path", "mime") DO UPDATE SET "b3hash" = $3 RETURNING *"#,
)
.bind(path)
.bind(mime)

View file

@ -23,7 +23,7 @@ pub async fn expire(db: &Database) -> Result<()> {
let oldest_acceptable_id = oldest_acceptable_id.to_be_bytes();
#[expect(clippy::panic, reason = "sqlx moment")]
query!(
r#"DELETE FROM "session_scopes" WHERE session_id < $1"#,
r#"DELETE FROM "sessions" WHERE id < $1"#,
&oldest_acceptable_id
)
.execute(&*db.0)

View file

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[features]
axum = ["axum-core", "async-trait", "bytes", "mime", "tracing"]
axum = ["axum-core", "async-trait", "bytes", "mime"]
[dependencies]
async-trait = { version = "0.1.83", optional = true }
@ -18,7 +18,7 @@ http = "1.1.0"
mime = { version = "0.3.17", optional = true }
serde = { version = "1.0.215", features = ["derive"] }
thiserror = "2.0.3"
tracing = { version = "0.1.40", optional = true }
tracing = { version = "0.1.40" }
[lints.rust]
deprecated-safe = "forbid"

View file

@ -4,13 +4,27 @@ use bincode::{Decode, Encode};
use educe::Educe;
use eyre::{bail, eyre, Result};
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, fmt::Debug};
use std::{
collections::HashSet,
fmt::{Debug, Display},
};
/// List of supported scopes for authentication
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, Hash)]
pub enum Scope {
/// Full scope granted by logging in.
Full,
/// The ability to create or update files.
CreateUpdateFile,
}
impl Display for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::Full => write!(f, "Full permissions"),
Self::CreateUpdateFile => write!(f, "Create and update files"),
}
}
}
impl Scope {
@ -19,6 +33,7 @@ impl Scope {
pub const fn to_i64(self) -> i64 {
match self {
Self::Full => 0,
Self::CreateUpdateFile => 1,
}
}
@ -29,6 +44,7 @@ impl Scope {
pub fn from_i64(id: i64) -> Result<Self> {
match id {
0 => Ok(Self::Full),
1 => Ok(Self::CreateUpdateFile),
_ => bail!("Invalid scope ID {id}"),
}
}

View file

@ -3,6 +3,9 @@
use bincode::{Decode, Encode};
use http::StatusCode;
use thiserror::Error;
use tracing::error;
use crate::auth::Scope;
/// The main error type
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Error)]
@ -31,7 +34,7 @@ pub enum APIError {
#[error("Invalid payload")]
PayloadInvalid,
/// Returned when the error is unknown
#[error("Unknown Error")]
#[error("Unknown Error {0}")]
Unknown(String),
/// Returned when there is a database error
#[error("Database error: {0}")]
@ -57,6 +60,16 @@ pub enum APIError {
/// Invalid session
#[error("Invalid session")]
InvalidSession,
/// Missing scope
#[error("Missing required scope for request: {0}")]
MissingScope(Scope),
}
impl From<eyre::Report> for APIError {
fn from(value: eyre::Report) -> Self {
error!("Error while handling request: {value:?}");
Self::Unknown(format!("Error while handling request: {value:?}"))
}
}
impl APIError {
@ -75,7 +88,8 @@ impl APIError {
| Self::InvalidAuthorizationHeader(_)
| Self::InvalidAuthorizationMethod(_, _)
| Self::Unauthorized
| Self::InvalidSession => StatusCode::UNAUTHORIZED,
| Self::InvalidSession
| Self::MissingScope(_) => StatusCode::UNAUTHORIZED,
Self::Unknown(_) | Self::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}

View file

@ -20,10 +20,13 @@ chir-rs-misc = { version = "0.1.0", path = "../chir-rs-misc", features = [
] }
chrono = "0.4.38"
eyre = "0.6.12"
futures = "0.3.31"
mime = "0.3.17"
rusty_paseto = { version = "0.7.1", default-features = false, features = ["batteries_included", "v4_local"] }
sentry-tower = { version = "0.34.0", features = ["axum", "axum-matched-path"] }
serde = { version = "1.0.215", features = ["derive"] }
tokio = { version = "1.41.1", features = ["fs", "net"] }
tokio-util = { version = "0.7.13", features = ["compat"] }
tower-http = { version = "0.6.2", features = ["trace"] }
tracing = "0.1.40"
unicode-normalization = "0.1.24"

View file

@ -20,6 +20,23 @@ use crate::AppState;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthHeader(pub String, pub HashSet<Scope>);
impl AuthHeader {
/// Checks whether or not a scope is granted for the session
///
/// # Errors
/// This function returns an error if the current session does not have a scope granted.
pub fn assert_scope(&self, scope: Scope) -> Result<(), APIError> {
if self.1.contains(&Scope::Full) {
return Ok(());
}
if self.1.contains(&scope) {
Ok(())
} else {
Err(APIError::MissingScope(scope))
}
}
}
#[async_trait]
impl FromRequestParts<AppState> for AuthHeader {
type Rejection = APIError;

View file

@ -2,20 +2,24 @@
use axum::{
body::Body,
extract::State,
extract::{Query, State},
http::{
header::{ACCEPT, CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE, ETAG, IF_NONE_MATCH},
HeaderMap, StatusCode, Uri,
HeaderMap, Request, StatusCode, Uri,
},
response::Response,
};
use chir_rs_db::file::File;
use chir_rs_http_api::{auth::Scope, errors::APIError};
use chir_rs_misc::lexicographic_base64;
use eyre::Context as _;
use futures::TryStreamExt;
use mime::MimeIter;
use serde::Deserialize;
use tokio_util::compat::FuturesAsyncReadCompatExt as _;
use tracing::{debug, error, info};
use crate::AppState;
use crate::{auth::req_auth::auth_header::AuthHeader, AppState};
/// Formats an eyre error message
#[expect(clippy::expect_used, reason = "fatal error in error handling function")]
@ -153,3 +157,35 @@ pub async fn serve_files(
.with_context(|| format!("Serving file for {path}"))
.map_err(format_error)
}
/// Creates a static file
///
/// # Errors
///
/// This function returns an error if the request fails.
pub async fn create_files(
State(state): State<AppState>,
session: AuthHeader,
uri: Uri,
headers: HeaderMap,
req: Request<Body>,
) -> Result<[u8; 32], APIError> {
session.assert_scope(Scope::CreateUpdateFile)?;
let mime = headers
.get(CONTENT_TYPE)
.ok_or_else(|| APIError::ClientMissingContentType {
expected: "*/*".to_string(),
})?
.to_str()
.map_err(|e| APIError::ClientInvalidContentType {
expected: "*/*".to_string(),
received: format!("{e:?}"),
})?;
let mut stream =
TryStreamExt::map_err(req.into_body().into_data_stream(), std::io::Error::other)
.into_async_read()
.compat();
let result = state.ca.upload(&mut stream).await?;
File::new(&state.db, uri.path(), mime, &result).await?;
Ok(*result.as_bytes())
}

View file

@ -80,7 +80,7 @@ pub async fn main(cfg: Arc<ChirRs>, db: Database, castore: CaStore) -> Result<()
get(|| async move { metric_handle.render() }),
)
.route("/.api/auth/login", post(auth::password_login::login))
.fallback(get(ca_server::serve_files))
.fallback(get(ca_server::serve_files).post(ca_server::create_files))
.with_state(AppState {
db,
ca: castore,