diff --git a/Cargo.lock b/Cargo.lock index fc97902..508b551 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -462,6 +462,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -897,6 +906,7 @@ dependencies = [ "eyre", "handlebars", "headers", + "jsonwebtoken", "rust-embed", "serde", "sqlx", @@ -955,6 +965,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09" +dependencies = [ + "base64", + "pem", + "ring 0.17.5", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1083,6 +1107,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1191,6 +1226,16 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pem" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1310,6 +1355,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1421,11 +1472,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + [[package]] name = "rsa" version = "0.9.2" @@ -1508,7 +1573,7 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "ring", + "ring 0.16.20", "rustls-webpki", "sct", ] @@ -1528,8 +1593,8 @@ version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -1565,8 +1630,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -1663,6 +1728,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -2030,6 +2107,35 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2246,6 +2352,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index 3da95c2..11efc61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ dotenvy = "0.15.7" eyre = "0.6.8" handlebars = { version = "4.4.0", features = ["rust-embed", "dir_source"] } headers = "0.3.9" +jsonwebtoken = "9.1.0" rust-embed = "8.0.0" serde = { version = "1.0.189", features = ["derive"] } sqlx = { version = "0.7.2", features = ["runtime-tokio", "tls-rustls", "postgres", "macros", "migrate", "chrono"] } diff --git a/src/config.rs b/src/config.rs index 7b14d60..b1226ad 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,4 +8,6 @@ pub struct Config { pub port: u16, #[arg(long, env, default_value_t = false)] pub dev_mode: bool, + #[arg(env)] + pub secret_key: String, } diff --git a/src/extractor.rs b/src/extractor.rs index 500c436..63daeab 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -7,7 +7,7 @@ use axum::{ use headers::Cookie; use tracing::error; -use crate::{model::user::User, state::AppState}; +use crate::{model::user::User, state::AppState, token::Claims}; pub struct ExtractUserToken(pub Option); @@ -33,17 +33,28 @@ where Err(err) => { error!("Getting cookie header: {}", err); return Ok(ExtractUserToken(None)); - }, + } }; - let user = match cookie.get("imgboard-token") { - Some(_token) => { - // TODO: get from token - None - }, - None => None, - }; + match cookie.get("imgboard-token") { + Some(token) => { + let claims = Claims::decode(token, &state.decoding_key).map_err(|e| { + error!("Decoding token claims: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error") + })?; - Ok(ExtractUserToken(user)) + match sqlx::query_as!(User, r#"SELECT * FROM users WHERE id = $1"#, claims.uid) + .fetch_one(&state.pool) + .await + { + Ok(u) => Ok(ExtractUserToken(Some(u))), + Err(err) => { + error!("Getting user from database: {}", err); + Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")) + } + } + } + None => Ok(ExtractUserToken(None)), + } } } diff --git a/src/main.rs b/src/main.rs index fb82c47..50724f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ pub mod model; pub mod pages; pub mod state; pub mod templates; +pub mod token; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; @@ -11,6 +12,7 @@ use std::sync::Arc; use axum::{routing::get, Router}; use clap::Parser; use eyre::{Result, WrapErr}; +use jsonwebtoken::{DecodingKey, EncodingKey}; use sqlx::migrate; use sqlx::postgres::PgPoolOptions; use tower_http::trace::TraceLayer; @@ -44,14 +46,24 @@ async fn main() -> Result<()> { migrate!().run(&pool).await.wrap_err("running migrations")?; - let state = Arc::new(AppState { pool, hbs }); + let state = Arc::new(AppState { + pool, + hbs, + encoding_key: EncodingKey::from_base64_secret(&config.secret_key) + .wrap_err("parsing $SECRET_KEY")?, + decoding_key: DecodingKey::from_base64_secret(&config.secret_key) + .wrap_err("parsing $SECRET_KEY")?, + }); debug!("Building router"); let app = Router::new() .route("/", get(pages::index)) .route("/users/:id", get(pages::get_user)) - .route("/users/new", get(pages::get_user_new).post(pages::post_user_new)) + .route( + "/users/new", + get(pages::get_user_new).post(pages::post_user_new), + ) .layer(TraceLayer::new_for_http()) .with_state(state); diff --git a/src/model/user.rs b/src/model/user.rs index 5086570..2d01ab9 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -13,7 +13,7 @@ pub struct User { pub last_active: DateTime, } -#[derive(sqlx::Type, Debug, Clone, PartialEq, Eq)] +#[derive(sqlx::Type, Debug, Copy, Clone, PartialEq, Eq)] #[repr(i32)] pub enum Role { Viewer = 1, @@ -62,3 +62,25 @@ impl TryFrom<&str> for Role { } } } + +impl Into<&str> for Role { + fn into(self) -> &'static str { + match self { + Self::Viewer => "viewer", + Self::Editor => "editor", + Self::Manager => "manager", + Self::Admin => "admin", + } + } +} + +impl Into for Role { + fn into(self) -> String { + match self { + Self::Viewer => "viewer".into(), + Self::Editor => "editor".into(), + Self::Manager => "manager".into(), + Self::Admin => "admin".into(), + } + } +} diff --git a/src/state.rs b/src/state.rs index ee7c0ca..5893496 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,12 +1,19 @@ use std::sync::Arc; -use axum::{extract::{FromRef, FromRequestParts}, async_trait, http::request::Parts}; +use axum::{ + async_trait, + extract::{FromRef, FromRequestParts}, + http::request::Parts, +}; +use jsonwebtoken::{DecodingKey, EncodingKey}; use sqlx::{postgres::Postgres, Pool}; #[derive(Clone, FromRef)] pub struct AppState { pub pool: Pool, pub hbs: handlebars::Handlebars<'static>, + pub encoding_key: EncodingKey, + pub decoding_key: DecodingKey, } #[async_trait] diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..c78d045 --- /dev/null +++ b/src/token.rs @@ -0,0 +1,31 @@ +use jsonwebtoken::{errors::Error, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; + +use crate::model::user::User; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub exp: usize, + pub uid: i32, + pub role: String, +} + +impl Claims { + pub fn new(user: &User) -> Self { + Self { + exp: 0, + uid: user.id, + role: user.role.into(), + } + } + + pub fn encode(self, key: &EncodingKey) -> Result { + jsonwebtoken::encode(&Header::default(), &self, key) + } + + pub fn decode(token: &str, key: &DecodingKey) -> Result { + let token = jsonwebtoken::decode::(token, key, &Validation::default())?; + + Ok(token.claims) + } +}