Spaces:
Sleeping
Sleeping
Update gateway: store seeding, CORS, JWT auth, Postgres support
Browse files- .env.example +3 -0
- Cargo.lock +102 -0
- Cargo.toml +1 -0
- crates/gateway/Cargo.toml +1 -0
- crates/gateway/src/auth.rs +94 -0
- crates/gateway/src/main.rs +1 -0
- crates/gateway/src/routes.rs +7 -6
.env.example
CHANGED
|
@@ -2,5 +2,8 @@
|
|
| 2 |
UPSTREAM_API_KEY=replace-me
|
| 3 |
GATEWAY_BIND=0.0.0.0:8080
|
| 4 |
RUST_LOG=info,gateway=debug,ingestion=debug
|
|
|
|
|
|
|
|
|
|
| 5 |
# Comma-separated active regions; ingestion cadence scales with regionsΓproviders, not users (NFR-6.1).
|
| 6 |
ACTIVE_REGIONS=PL,GB
|
|
|
|
| 2 |
UPSTREAM_API_KEY=replace-me
|
| 3 |
GATEWAY_BIND=0.0.0.0:8080
|
| 4 |
RUST_LOG=info,gateway=debug,ingestion=debug
|
| 5 |
+
# Auth: set to enable HS256 JWT verification on write endpoints (sub = user id).
|
| 6 |
+
# Unset β dev fallback where the bearer token IS the user id. Set as a Space secret.
|
| 7 |
+
# AUTH_JWT_SECRET=replace-with-a-long-random-secret
|
| 8 |
# Comma-separated active regions; ingestion cadence scales with regionsΓproviders, not users (NFR-6.1).
|
| 9 |
ACTIVE_REGIONS=PL,GB
|
Cargo.lock
CHANGED
|
@@ -275,6 +275,15 @@ dependencies = [
|
|
| 275 |
"zeroize",
|
| 276 |
]
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
[[package]]
|
| 279 |
name = "digest"
|
| 280 |
version = "0.10.7"
|
|
@@ -471,6 +480,7 @@ dependencies = [
|
|
| 471 |
"axum",
|
| 472 |
"chrono",
|
| 473 |
"domain",
|
|
|
|
| 474 |
"serde",
|
| 475 |
"serde_json",
|
| 476 |
"store",
|
|
@@ -499,8 +509,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
| 499 |
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
| 500 |
dependencies = [
|
| 501 |
"cfg-if",
|
|
|
|
| 502 |
"libc",
|
| 503 |
"wasi",
|
|
|
|
| 504 |
]
|
| 505 |
|
| 506 |
[[package]]
|
|
@@ -818,6 +830,21 @@ dependencies = [
|
|
| 818 |
"wasm-bindgen",
|
| 819 |
]
|
| 820 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 821 |
[[package]]
|
| 822 |
name = "lazy_static"
|
| 823 |
version = "1.5.0"
|
|
@@ -939,6 +966,16 @@ dependencies = [
|
|
| 939 |
"windows-sys 0.61.2",
|
| 940 |
]
|
| 941 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 942 |
[[package]]
|
| 943 |
name = "num-bigint-dig"
|
| 944 |
version = "0.8.6"
|
|
@@ -955,6 +992,12 @@ dependencies = [
|
|
| 955 |
"zeroize",
|
| 956 |
]
|
| 957 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 958 |
[[package]]
|
| 959 |
name = "num-integer"
|
| 960 |
version = "0.1.46"
|
|
@@ -1020,6 +1063,16 @@ dependencies = [
|
|
| 1020 |
"windows-link",
|
| 1021 |
]
|
| 1022 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
[[package]]
|
| 1024 |
name = "pem-rfc7468"
|
| 1025 |
version = "0.7.0"
|
|
@@ -1083,6 +1136,12 @@ dependencies = [
|
|
| 1083 |
"zerovec",
|
| 1084 |
]
|
| 1085 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1086 |
[[package]]
|
| 1087 |
name = "ppv-lite86"
|
| 1088 |
version = "0.2.21"
|
|
@@ -1384,6 +1443,18 @@ dependencies = [
|
|
| 1384 |
"rand_core",
|
| 1385 |
]
|
| 1386 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1387 |
[[package]]
|
| 1388 |
name = "slab"
|
| 1389 |
version = "0.4.12"
|
|
@@ -1715,6 +1786,37 @@ dependencies = [
|
|
| 1715 |
"cfg-if",
|
| 1716 |
]
|
| 1717 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1718 |
[[package]]
|
| 1719 |
name = "tinystr"
|
| 1720 |
version = "0.8.3"
|
|
|
|
| 275 |
"zeroize",
|
| 276 |
]
|
| 277 |
|
| 278 |
+
[[package]]
|
| 279 |
+
name = "deranged"
|
| 280 |
+
version = "0.5.8"
|
| 281 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 282 |
+
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
| 283 |
+
dependencies = [
|
| 284 |
+
"powerfmt",
|
| 285 |
+
]
|
| 286 |
+
|
| 287 |
[[package]]
|
| 288 |
name = "digest"
|
| 289 |
version = "0.10.7"
|
|
|
|
| 480 |
"axum",
|
| 481 |
"chrono",
|
| 482 |
"domain",
|
| 483 |
+
"jsonwebtoken",
|
| 484 |
"serde",
|
| 485 |
"serde_json",
|
| 486 |
"store",
|
|
|
|
| 509 |
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
| 510 |
dependencies = [
|
| 511 |
"cfg-if",
|
| 512 |
+
"js-sys",
|
| 513 |
"libc",
|
| 514 |
"wasi",
|
| 515 |
+
"wasm-bindgen",
|
| 516 |
]
|
| 517 |
|
| 518 |
[[package]]
|
|
|
|
| 830 |
"wasm-bindgen",
|
| 831 |
]
|
| 832 |
|
| 833 |
+
[[package]]
|
| 834 |
+
name = "jsonwebtoken"
|
| 835 |
+
version = "9.3.1"
|
| 836 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 837 |
+
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
| 838 |
+
dependencies = [
|
| 839 |
+
"base64",
|
| 840 |
+
"js-sys",
|
| 841 |
+
"pem",
|
| 842 |
+
"ring",
|
| 843 |
+
"serde",
|
| 844 |
+
"serde_json",
|
| 845 |
+
"simple_asn1",
|
| 846 |
+
]
|
| 847 |
+
|
| 848 |
[[package]]
|
| 849 |
name = "lazy_static"
|
| 850 |
version = "1.5.0"
|
|
|
|
| 966 |
"windows-sys 0.61.2",
|
| 967 |
]
|
| 968 |
|
| 969 |
+
[[package]]
|
| 970 |
+
name = "num-bigint"
|
| 971 |
+
version = "0.4.6"
|
| 972 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 973 |
+
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
| 974 |
+
dependencies = [
|
| 975 |
+
"num-integer",
|
| 976 |
+
"num-traits",
|
| 977 |
+
]
|
| 978 |
+
|
| 979 |
[[package]]
|
| 980 |
name = "num-bigint-dig"
|
| 981 |
version = "0.8.6"
|
|
|
|
| 992 |
"zeroize",
|
| 993 |
]
|
| 994 |
|
| 995 |
+
[[package]]
|
| 996 |
+
name = "num-conv"
|
| 997 |
+
version = "0.2.2"
|
| 998 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 999 |
+
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
| 1000 |
+
|
| 1001 |
[[package]]
|
| 1002 |
name = "num-integer"
|
| 1003 |
version = "0.1.46"
|
|
|
|
| 1063 |
"windows-link",
|
| 1064 |
]
|
| 1065 |
|
| 1066 |
+
[[package]]
|
| 1067 |
+
name = "pem"
|
| 1068 |
+
version = "3.0.6"
|
| 1069 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1070 |
+
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
| 1071 |
+
dependencies = [
|
| 1072 |
+
"base64",
|
| 1073 |
+
"serde_core",
|
| 1074 |
+
]
|
| 1075 |
+
|
| 1076 |
[[package]]
|
| 1077 |
name = "pem-rfc7468"
|
| 1078 |
version = "0.7.0"
|
|
|
|
| 1136 |
"zerovec",
|
| 1137 |
]
|
| 1138 |
|
| 1139 |
+
[[package]]
|
| 1140 |
+
name = "powerfmt"
|
| 1141 |
+
version = "0.2.0"
|
| 1142 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1143 |
+
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
| 1144 |
+
|
| 1145 |
[[package]]
|
| 1146 |
name = "ppv-lite86"
|
| 1147 |
version = "0.2.21"
|
|
|
|
| 1443 |
"rand_core",
|
| 1444 |
]
|
| 1445 |
|
| 1446 |
+
[[package]]
|
| 1447 |
+
name = "simple_asn1"
|
| 1448 |
+
version = "0.6.4"
|
| 1449 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1450 |
+
checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
|
| 1451 |
+
dependencies = [
|
| 1452 |
+
"num-bigint",
|
| 1453 |
+
"num-traits",
|
| 1454 |
+
"thiserror",
|
| 1455 |
+
"time",
|
| 1456 |
+
]
|
| 1457 |
+
|
| 1458 |
[[package]]
|
| 1459 |
name = "slab"
|
| 1460 |
version = "0.4.12"
|
|
|
|
| 1786 |
"cfg-if",
|
| 1787 |
]
|
| 1788 |
|
| 1789 |
+
[[package]]
|
| 1790 |
+
name = "time"
|
| 1791 |
+
version = "0.3.47"
|
| 1792 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1793 |
+
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
| 1794 |
+
dependencies = [
|
| 1795 |
+
"deranged",
|
| 1796 |
+
"itoa",
|
| 1797 |
+
"num-conv",
|
| 1798 |
+
"powerfmt",
|
| 1799 |
+
"serde_core",
|
| 1800 |
+
"time-core",
|
| 1801 |
+
"time-macros",
|
| 1802 |
+
]
|
| 1803 |
+
|
| 1804 |
+
[[package]]
|
| 1805 |
+
name = "time-core"
|
| 1806 |
+
version = "0.1.8"
|
| 1807 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1808 |
+
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
| 1809 |
+
|
| 1810 |
+
[[package]]
|
| 1811 |
+
name = "time-macros"
|
| 1812 |
+
version = "0.2.27"
|
| 1813 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1814 |
+
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
| 1815 |
+
dependencies = [
|
| 1816 |
+
"num-conv",
|
| 1817 |
+
"time-core",
|
| 1818 |
+
]
|
| 1819 |
+
|
| 1820 |
[[package]]
|
| 1821 |
name = "tinystr"
|
| 1822 |
version = "0.8.3"
|
Cargo.toml
CHANGED
|
@@ -25,6 +25,7 @@ uuid = { version = "1", features = ["v4", "serde"] }
|
|
| 25 |
async-trait = "0.1"
|
| 26 |
thiserror = "2"
|
| 27 |
anyhow = "1"
|
|
|
|
| 28 |
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "macros", "migrate"] }
|
| 29 |
tracing = "0.1"
|
| 30 |
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
|
|
| 25 |
async-trait = "0.1"
|
| 26 |
thiserror = "2"
|
| 27 |
anyhow = "1"
|
| 28 |
+
jsonwebtoken = "9"
|
| 29 |
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "macros", "migrate"] }
|
| 30 |
tracing = "0.1"
|
| 31 |
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
crates/gateway/Cargo.toml
CHANGED
|
@@ -19,6 +19,7 @@ tower-http.workspace = true
|
|
| 19 |
serde.workspace = true
|
| 20 |
serde_json.workspace = true
|
| 21 |
chrono.workspace = true
|
|
|
|
| 22 |
tracing.workspace = true
|
| 23 |
tracing-subscriber.workspace = true
|
| 24 |
anyhow.workspace = true
|
|
|
|
| 19 |
serde.workspace = true
|
| 20 |
serde_json.workspace = true
|
| 21 |
chrono.workspace = true
|
| 22 |
+
jsonwebtoken.workspace = true
|
| 23 |
tracing.workspace = true
|
| 24 |
tracing-subscriber.workspace = true
|
| 25 |
anyhow.workspace = true
|
crates/gateway/src/auth.rs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Bearer-token β user-id resolution (B-6).
|
| 2 |
+
//!
|
| 3 |
+
//! Two modes, chosen by the `AUTH_JWT_SECRET` env var:
|
| 4 |
+
//! - **Set:** the bearer must be a valid HS256 JWT; the `sub` claim is the user
|
| 5 |
+
//! id (signature + expiry verified). This is the production mode β an OAuth
|
| 6 |
+
//! provider (Google/Apple/Firebase) issues the token; the gateway only verifies.
|
| 7 |
+
//! - **Unset (dev/MVP):** the bearer string *is* the user id (the current stub).
|
| 8 |
+
//!
|
| 9 |
+
//! Keeping the dev fallback means the live deployment works until you provision
|
| 10 |
+
//! a signing secret; setting the secret flips on real verification with no code change.
|
| 11 |
+
|
| 12 |
+
use std::sync::OnceLock;
|
| 13 |
+
|
| 14 |
+
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
| 15 |
+
use serde::Deserialize;
|
| 16 |
+
|
| 17 |
+
#[derive(Debug, Deserialize)]
|
| 18 |
+
struct Claims {
|
| 19 |
+
sub: String,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/// Cached signing secret (read once). `None` β dev stub mode.
|
| 23 |
+
pub fn jwt_secret() -> Option<&'static str> {
|
| 24 |
+
static SECRET: OnceLock<Option<String>> = OnceLock::new();
|
| 25 |
+
SECRET
|
| 26 |
+
.get_or_init(|| std::env::var("AUTH_JWT_SECRET").ok().filter(|s| !s.is_empty()))
|
| 27 |
+
.as_deref()
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/// Resolves the authenticated user id from a bearer token. Returns `None` when
|
| 31 |
+
/// the token is missing/empty (dev mode) or invalid/expired (JWT mode).
|
| 32 |
+
pub fn user_from_bearer(token: &str, secret: Option<&str>) -> Option<String> {
|
| 33 |
+
match secret {
|
| 34 |
+
None => {
|
| 35 |
+
let t = token.trim();
|
| 36 |
+
if t.is_empty() {
|
| 37 |
+
None
|
| 38 |
+
} else {
|
| 39 |
+
Some(t.to_string())
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
Some(sec) => {
|
| 43 |
+
// HS256; expiry validated by default when an `exp` claim is present.
|
| 44 |
+
let validation = Validation::new(Algorithm::HS256);
|
| 45 |
+
decode::<Claims>(token, &DecodingKey::from_secret(sec.as_bytes()), &validation)
|
| 46 |
+
.ok()
|
| 47 |
+
.map(|data| data.claims.sub)
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
#[cfg(test)]
|
| 53 |
+
#[allow(clippy::unwrap_used)]
|
| 54 |
+
mod tests {
|
| 55 |
+
use super::*;
|
| 56 |
+
use jsonwebtoken::{encode, EncodingKey, Header};
|
| 57 |
+
use serde::Serialize;
|
| 58 |
+
|
| 59 |
+
#[derive(Serialize)]
|
| 60 |
+
struct TestClaims {
|
| 61 |
+
sub: String,
|
| 62 |
+
exp: usize,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
fn sign(sub: &str, secret: &str, exp: usize) -> String {
|
| 66 |
+
encode(
|
| 67 |
+
&Header::new(Algorithm::HS256),
|
| 68 |
+
&TestClaims { sub: sub.into(), exp },
|
| 69 |
+
&EncodingKey::from_secret(secret.as_bytes()),
|
| 70 |
+
)
|
| 71 |
+
.unwrap()
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
#[test]
|
| 75 |
+
fn dev_mode_uses_token_as_user_id() {
|
| 76 |
+
assert_eq!(user_from_bearer("alice", None), Some("alice".into()));
|
| 77 |
+
assert_eq!(user_from_bearer(" ", None), None);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
#[test]
|
| 81 |
+
fn jwt_mode_accepts_valid_token_and_reads_sub() {
|
| 82 |
+
let t = sign("alice", "topsecret", 9_999_999_999);
|
| 83 |
+
assert_eq!(user_from_bearer(&t, Some("topsecret")), Some("alice".into()));
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
#[test]
|
| 87 |
+
fn jwt_mode_rejects_bad_signature_and_expiry() {
|
| 88 |
+
let t = sign("alice", "topsecret", 9_999_999_999);
|
| 89 |
+
assert_eq!(user_from_bearer(&t, Some("wrongsecret")), None); // bad signature
|
| 90 |
+
let expired = sign("alice", "topsecret", 1); // 1970 β expired
|
| 91 |
+
assert_eq!(user_from_bearer(&expired, Some("topsecret")), None);
|
| 92 |
+
assert_eq!(user_from_bearer("not-a-jwt", Some("topsecret")), None);
|
| 93 |
+
}
|
| 94 |
+
}
|
crates/gateway/src/main.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
//! WhereToWatch gateway β the ONLY backend the client talks to (NFR-1.3).
|
| 2 |
|
|
|
|
| 3 |
mod catalog;
|
| 4 |
mod dto;
|
| 5 |
mod routes;
|
|
|
|
| 1 |
//! WhereToWatch gateway β the ONLY backend the client talks to (NFR-1.3).
|
| 2 |
|
| 3 |
+
mod auth;
|
| 4 |
mod catalog;
|
| 5 |
mod dto;
|
| 6 |
mod routes;
|
crates/gateway/src/routes.rs
CHANGED
|
@@ -35,9 +35,9 @@ pub fn routes(state: AppState) -> Router {
|
|
| 35 |
|
| 36 |
// ββ Auth guard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
|
| 38 |
-
/// Authenticated user
|
| 39 |
-
///
|
| 40 |
-
///
|
| 41 |
pub struct AuthUser(pub String);
|
| 42 |
|
| 43 |
impl<S: Send + Sync> FromRequestParts<S> for AuthUser {
|
|
@@ -49,10 +49,11 @@ impl<S: Send + Sync> FromRequestParts<S> for AuthUser {
|
|
| 49 |
.get(axum::http::header::AUTHORIZATION)
|
| 50 |
.and_then(|v| v.to_str().ok())
|
| 51 |
.and_then(|v| v.strip_prefix("Bearer "))
|
| 52 |
-
.map(str::trim)
|
| 53 |
-
.filter(|t| !t.is_empty())
|
| 54 |
.ok_or(ApiError::Unauthorized)?;
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
}
|
| 58 |
|
|
|
|
| 35 |
|
| 36 |
// ββ Auth guard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
|
| 38 |
+
/// Authenticated user, resolved from the `Authorization: Bearer β¦` header by
|
| 39 |
+
/// [`crate::auth`]: a verified HS256 JWT's `sub` when `AUTH_JWT_SECRET` is set,
|
| 40 |
+
/// else the token-as-user-id dev fallback (FR-9.3). Every write handler requires it.
|
| 41 |
pub struct AuthUser(pub String);
|
| 42 |
|
| 43 |
impl<S: Send + Sync> FromRequestParts<S> for AuthUser {
|
|
|
|
| 49 |
.get(axum::http::header::AUTHORIZATION)
|
| 50 |
.and_then(|v| v.to_str().ok())
|
| 51 |
.and_then(|v| v.strip_prefix("Bearer "))
|
|
|
|
|
|
|
| 52 |
.ok_or(ApiError::Unauthorized)?;
|
| 53 |
+
// JWT-verified when AUTH_JWT_SECRET is set; else the token is the user id.
|
| 54 |
+
let user = crate::auth::user_from_bearer(token, crate::auth::jwt_secret())
|
| 55 |
+
.ok_or(ApiError::Unauthorized)?;
|
| 56 |
+
Ok(AuthUser(user))
|
| 57 |
}
|
| 58 |
}
|
| 59 |
|