diff options
-rw-r--r-- | Cargo.lock | 157 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/api.rs | 8 | ||||
-rw-r--r-- | src/api/account.rs | 16 | ||||
-rw-r--r-- | src/api/users.rs | 83 | ||||
-rw-r--r-- | src/auth.rs | 8 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/state.rs | 40 |
8 files changed, 268 insertions, 47 deletions
@@ -213,6 +213,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.5.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.52", +] + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -264,10 +284,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -276,6 +320,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -590,6 +645,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] name = "h2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -817,6 +878,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -850,18 +920,51 @@ dependencies = [ ] [[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.4", +] + +[[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] +name = "librocksdb-sys" +version = "0.16.0+8.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "lz4-sys", + "pkg-config", + "zstd-sys", +] + +[[package]] name = "libsqlite3-sys" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -873,6 +976,17 @@ dependencies = [ ] [[package]] +name = "libz-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -895,6 +1009,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + +[[package]] name = "main_error" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1343,6 +1467,16 @@ dependencies = [ ] [[package]] +name = "rocksdb" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] name = "rsa" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1369,6 +1503,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] name = "rustix" version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1506,6 +1646,12 @@ dependencies = [ ] [[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2205,6 +2351,7 @@ dependencies = [ "mime", "once_cell", "pgtemp", + "rocksdb", "serde", "serde_json", "sqlx", @@ -2535,3 +2682,13 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] @@ -15,6 +15,7 @@ email_address = "0.2.4" jsonwebtoken = "9.3.0" main_error = "0.1.2" once_cell = "1.19.0" +rocksdb = { version = "0.22.0", features = ["multi-threaded-cf", "io-uring"] } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" sqlx = { version = "0.7.3", features = ["postgres", "runtime-tokio", "uuid", "time"] } @@ -40,7 +40,9 @@ mod tests { async fn test_healthcheck_ok(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let request = Request::builder().uri("/healthcheck").body(Body::empty())?; @@ -55,7 +57,9 @@ mod tests { async fn test_fallback_not_found(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let request = Request::builder() .uri("/does-not-exist") diff --git a/src/api/account.rs b/src/api/account.rs index bae7c54..4fb2675 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -97,7 +97,9 @@ mod tests { async fn test_login_ok(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let auth = Authorization::basic(USER_EMAIL, USER_PASSWORD); @@ -141,7 +143,9 @@ mod tests { async fn test_login_unauthorized(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let auth = Authorization::basic(USER_EMAIL, "hunter2"); @@ -162,7 +166,9 @@ mod tests { async fn test_login_not_found(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let auth = Authorization::basic(USER_EMAIL, USER_PASSWORD); @@ -183,7 +189,9 @@ mod tests { async fn test_logout_ok(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let request = Request::builder() .uri("/logout") diff --git a/src/api/users.rs b/src/api/users.rs index e07bf7e..24bcf97 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -13,7 +13,7 @@ use uuid::Uuid; use crate::{ auth::{credentials::Credential, AccessClaims}, - state::AppState, + state::{AppState, KVStore}, }; use super::error::Error; @@ -40,54 +40,53 @@ pub struct Registration { } pub async fn create( - State(pool): State<PgPool>, + State(state): State<AppState>, Json(Registration { name, email, password, }): Json<Registration>, -) -> impl IntoResponse { +) -> Result<impl IntoResponse, Error> { email_address::EmailAddress::from_str(&email)?; - let exists: Option<bool> = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM user_ WHERE email = $1 LIMIT 1)", - email.to_ascii_lowercase() - ) - .fetch_one(&pool) - .await?; - - if exists.is_some_and(|b| b) { - return Err(Error::EmailExists); - } - // TODO: Move this into a micro service, possibly behind a feature flag. let (status, (access, refresh)) = - crate::auth::credentials::create(State(pool.clone()), Json(Credential { password })) + crate::auth::credentials::create(State(state.pool.clone()), Json(Credential { password })) .await?; - let user = sqlx::query_as!( - User, - "INSERT INTO user_ (id,name,email) VALUES ($1, $2, $3) RETURNING *", - refresh.sub, + let user = User { + id: refresh.sub, name, - email.to_ascii_lowercase(), - ) - .fetch_one(&pool) - .await?; + email, + created_at: OffsetDateTime::now_utc(), + updated_at: OffsetDateTime::now_utc(), + }; + + let tx = state.kv_store.transaction(); + tx.put(refresh.sub, serde_json::to_vec(&user).unwrap()) + .unwrap(); Ok((status, access, refresh, Json(user))) } pub async fn show( Path(uuid): Path<Uuid>, - State(pool): State<PgPool>, + State(state): State<AppState>, _: AccessClaims, -) -> Result<impl IntoResponse, Error> { - sqlx::query_as!(User, "SELECT * FROM user_ WHERE id = $1 LIMIT 1", uuid) - .fetch_optional(&pool) - .await? +) -> Result<Json<User>, Error> { + let tx = state.kv_store.transaction(); + Ok(tx + .get(uuid) + .unwrap() .ok_or_else(|| Error::UserNotFound) + .map(|s| serde_json::from_slice(&s))? .map(Json) + .unwrap()) + //sqlx::query_as!(User, "SELECT * FROM user_ WHERE id = $1 LIMIT 1", uuid) + // .fetch_optional(&pool) + // .await? + // .ok_or_else(|| Error::UserNotFound) + // .map(Json) } #[cfg(test)] @@ -120,7 +119,9 @@ mod tests { async fn test_get_ok_self(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let request = Request::builder() .uri(format!("/users/{}", USER_ID)) @@ -150,7 +151,9 @@ mod tests { async fn test_get_ok_other(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let request = Request::builder() .uri(format!("/users/{}", USER_ID)) @@ -182,7 +185,9 @@ mod tests { async fn test_get_not_found(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let request = Request::builder() .uri(format!("/users/{}", USER_ID)) @@ -203,7 +208,9 @@ mod tests { async fn test_get_unauthorized_invalid_token_format(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let request = Request::builder() .uri(format!("/users/{}", USER_ID)) @@ -221,7 +228,9 @@ mod tests { async fn test_get_unauthorized_missing_token(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let request = Request::builder() .uri(format!("/users/{}", USER_ID)) @@ -238,7 +247,9 @@ mod tests { async fn test_post_created(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let user = serde_json::json!( { "name": USER_NAME, @@ -269,7 +280,9 @@ mod tests { async fn test_post_conflict(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let user = serde_json::json!( { "name": USER_NAME, diff --git a/src/auth.rs b/src/auth.rs index 909534e..3c40661 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -94,7 +94,9 @@ mod tests { async fn test_issue_ok(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let auth = Authorization::basic( "4c14f795-86f0-4361-a02f-0edb966fb145", @@ -118,7 +120,9 @@ mod tests { async fn test_issue_unauthorized(pool: PgPool) -> TestResult { setup_test_env(); - let router = Router::new().merge(router()).with_state(AppState { pool }); + let router = Router::new() + .merge(router()) + .with_state(AppState::with_pool(pool, "./rocks.db")); let auth = Authorization::basic("4c14f795-86f0-4361-a02f-0edb966fb145", "hunter2"); diff --git a/src/main.rs b/src/main.rs index 71e68f9..31fa7da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ async fn main() -> Result<(), main_error::MainError> { let config = Config::builder().file()?.env().build()?; let listener = TcpListener::bind(config.listen_addr).await?; - let state = AppState::new(config.database_url).await?; + let state = AppState::new(config.database_url, "./rocks.db").await?; let router = unnamed_server::router().with_state(state); tracing::info!("Listening on http://{}", listener.local_addr()?); diff --git a/src/state.rs b/src/state.rs index 771647d..f7fe10e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,17 +1,40 @@ -use std::fmt::Debug; +use std::{fmt::Debug, path::Path, sync::Arc}; use axum::extract::FromRef; use sqlx::PgPool; use crate::Error; +pub type KVStore = rocksdb::OptimisticTransactionDB<rocksdb::MultiThreaded>; + #[derive(Debug, Clone, FromRef)] pub struct AppState { pub pool: PgPool, + pub kv_store: Arc<rocksdb::OptimisticTransactionDB<rocksdb::MultiThreaded>>, } impl AppState { - pub async fn new(uri: String) -> Result<Self, Error> { + pub async fn new<P>(uri: String, path: P) -> Result<Self, Error> + where + P: AsRef<Path>, + { + Ok(Self { + pool: Self::init_pool(uri).await?, + kv_store: Arc::new(Self::init_kv_store(path).unwrap()), + }) + } + + pub fn with_pool<P>(pool: PgPool, path: P) -> Self + where + P: AsRef<Path>, + { + Self { + pool, + kv_store: Arc::new(Self::init_kv_store(path).unwrap()), + } + } + + pub async fn init_pool(uri: String) -> Result<PgPool, sqlx::Error> { tracing::debug!("Attempting to connect to database..."); let pool = sqlx::postgres::PgPoolOptions::new() @@ -23,6 +46,17 @@ impl AppState { sqlx::migrate!().run(&pool).await?; - Ok(Self { pool }) + Ok(pool) + } + + pub fn init_kv_store<P>( + path: P, + ) -> Result<rocksdb::OptimisticTransactionDB<rocksdb::MultiThreaded>, rocksdb::Error> + where + P: AsRef<Path>, + { + let mut opts = rocksdb::Options::default(); + opts.create_if_missing(true); + rocksdb::OptimisticTransactionDB::open(&opts, path) } } |