From 00d63f5a5648f76d8e9cb8597446e05901543a0c Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Fri, 12 Apr 2024 16:42:43 -0500 Subject: refactor: improve auth flow and router layout --- ...a09f342aec103634b933e7efdc86859c83d31c3a2c.json | 22 ++++ ...8ecadd4e023b3772eb0cc03fc9ff760a42af16881d.json | 52 --------- ...1cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df.json | 48 ++++++++ ...c9499f0a808328933f821db1024229bfcb82948f06.json | 22 ++++ ...6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1.json | 53 --------- ...677c091b5c67a71c3a4b0d89b4e0253dced685b827.json | 46 ++++++++ ...bdf1992d22db324ee7bef62b75545252e5675b744d.json | 22 ++++ Cargo.toml | 2 +- fixtures/users.sql | 4 +- migrations/20240321225523_init.down.sql | 1 - migrations/20240321225523_init.up.sql | 15 --- .../20240412153145_create_credential.down.sql | 2 + migrations/20240412153145_create_credential.up.sql | 8 ++ migrations/20240412153150_create_user_.down.sql | 1 + migrations/20240412153150_create_user_.up.sql | 7 ++ src/api.rs | 8 +- src/api/error.rs | 5 +- src/api/users.rs | 123 ++++++++------------- src/auth.rs | 84 ++++++-------- src/lib.rs | 9 +- src/main.rs | 4 +- src/state.rs | 18 --- 22 files changed, 281 insertions(+), 275 deletions(-) create mode 100644 .sqlx/query-023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c.json delete mode 100644 .sqlx/query-5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d.json create mode 100644 .sqlx/query-6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df.json create mode 100644 .sqlx/query-9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06.json delete mode 100644 .sqlx/query-9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1.json create mode 100644 .sqlx/query-d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827.json create mode 100644 .sqlx/query-fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d.json delete mode 100644 migrations/20240321225523_init.down.sql delete mode 100644 migrations/20240321225523_init.up.sql create mode 100644 migrations/20240412153145_create_credential.down.sql create mode 100644 migrations/20240412153145_create_credential.up.sql create mode 100644 migrations/20240412153150_create_user_.down.sql create mode 100644 migrations/20240412153150_create_user_.up.sql diff --git a/.sqlx/query-023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c.json b/.sqlx/query-023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c.json new file mode 100644 index 0000000..35f05d9 --- /dev/null +++ b/.sqlx/query-023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT password_hash FROM credential WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "password_hash", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "023bcb5038e77f4662e8fea09f342aec103634b933e7efdc86859c83d31c3a2c" +} diff --git a/.sqlx/query-5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d.json b/.sqlx/query-5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d.json deleted file mode 100644 index 97b0e16..0000000 --- a/.sqlx/query-5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM users WHERE uuid = $1 LIMIT 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "uuid", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "session_epoch", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "updated_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false - ] - }, - "hash": "5be45c3dea798be562ed478ecadd4e023b3772eb0cc03fc9ff760a42af16881d" -} diff --git a/.sqlx/query-6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df.json b/.sqlx/query-6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df.json new file mode 100644 index 0000000..ec353da --- /dev/null +++ b/.sqlx/query-6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_ (id,name,email) VALUES ($1, $2, $3) RETURNING *", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Varchar" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "6b3de32778bebce842a5c91cdbf36e3a5172fb8afbc2ca83c3ab6e80c6fb57df" +} diff --git a/.sqlx/query-9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06.json b/.sqlx/query-9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06.json new file mode 100644 index 0000000..ea84914 --- /dev/null +++ b/.sqlx/query-9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM user_ WHERE email = $1 LIMIT 1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9442e8c2950fb2266df2aec9499f0a808328933f821db1024229bfcb82948f06" +} diff --git a/.sqlx/query-9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1.json b/.sqlx/query-9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1.json deleted file mode 100644 index 28227a3..0000000 --- a/.sqlx/query-9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO users (name,email) VALUES ($1, $2) RETURNING *", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "uuid", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "email", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "session_epoch", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "updated_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false - ] - }, - "hash": "9c856d093270e6097ea1da6c0ebdca92668c137ffb5bbac3c56bdb60d1c9fbc1" -} diff --git a/.sqlx/query-d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827.json b/.sqlx/query-d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827.json new file mode 100644 index 0000000..a857287 --- /dev/null +++ b/.sqlx/query-d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM user_ WHERE id = $1 LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "d747ef6749e05581395e21677c091b5c67a71c3a4b0d89b4e0253dced685b827" +} diff --git a/.sqlx/query-fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d.json b/.sqlx/query-fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d.json new file mode 100644 index 0000000..7287db6 --- /dev/null +++ b/.sqlx/query-fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO credential (password_hash) VALUES ($1) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Varchar" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fadb4e04e390af0d2e9946bdf1992d22db324ee7bef62b75545252e5675b744d" +} diff --git a/Cargo.toml b/Cargo.toml index 8696c1a..9ee0b70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ thiserror = "1.0.58" time = { version = "0.3.34", features = ["serde", "serde-human-readable"] } tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread", "signal"] } toml = "0.8.12" +tower = { version = "0.4.13", features = ["util"] } tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } @@ -29,7 +30,6 @@ uuid = { version = "1.8.0", features = ["serde", "v4"] } [dev-dependencies] pgtemp = "0.2.1" -tower = { version = "0.4.13", features = ["util"] } mime = "0.3.17" http-body-util = "0.1.1" test-log = { version = "0.2.15", features = ["trace"] } diff --git a/fixtures/users.sql b/fixtures/users.sql index 5904b1a..96c34a9 100644 --- a/fixtures/users.sql +++ b/fixtures/users.sql @@ -1,10 +1,10 @@ -INSERT INTO users (uuid, name, email) VALUES( +INSERT INTO user_ (id, name, email) VALUES( '4c14f795-86f0-4361-a02f-0edb966fb145', 'Arthur Dent', 'adent@earth.sol' ); -INSERT INTO credentials (uuid, password_hash) VALUES( +INSERT INTO credential (id, password_hash) VALUES( '4c14f795-86f0-4361-a02f-0edb966fb145', '$argon2id$v=19$m=19456,t=2,p=1$31LeWXQsq0wwHT0MgAliVA$V6pQ0nKpgcq+nOWT6p4AuyVM0zy/09Ct9XpSPHq3wSo' ); diff --git a/migrations/20240321225523_init.down.sql b/migrations/20240321225523_init.down.sql deleted file mode 100644 index e3470ec..0000000 --- a/migrations/20240321225523_init.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS users, credentials; diff --git a/migrations/20240321225523_init.up.sql b/migrations/20240321225523_init.up.sql deleted file mode 100644 index ca86893..0000000 --- a/migrations/20240321225523_init.up.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - -CREATE TABLE users ( - uuid UUID NOT NULL PRIMARY KEY DEFAULT (uuid_generate_v4()), - name VARCHAR(100) NOT NULL, - email VARCHAR(255) NOT NULL UNIQUE, - session_epoch TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() -); - -CREATE TABLE credentials ( - uuid UUID NOT NULL PRIMARY KEY, - password_hash VARCHAR(100) NOT NULL -); diff --git a/migrations/20240412153145_create_credential.down.sql b/migrations/20240412153145_create_credential.down.sql new file mode 100644 index 0000000..d2cb87f --- /dev/null +++ b/migrations/20240412153145_create_credential.down.sql @@ -0,0 +1,2 @@ +DROP TABLE credential; +DROP EXTENSION "uuid-ossp"; diff --git a/migrations/20240412153145_create_credential.up.sql b/migrations/20240412153145_create_credential.up.sql new file mode 100644 index 0000000..a2f0920 --- /dev/null +++ b/migrations/20240412153145_create_credential.up.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION "uuid-ossp"; + +CREATE TABLE credential ( + id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), + password_hash VARCHAR(100) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); diff --git a/migrations/20240412153150_create_user_.down.sql b/migrations/20240412153150_create_user_.down.sql new file mode 100644 index 0000000..2ed90ec --- /dev/null +++ b/migrations/20240412153150_create_user_.down.sql @@ -0,0 +1 @@ +DROP TABLE user_; diff --git a/migrations/20240412153150_create_user_.up.sql b/migrations/20240412153150_create_user_.up.sql new file mode 100644 index 0000000..e13fd90 --- /dev/null +++ b/migrations/20240412153150_create_user_.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE user_ ( + id UUID NOT NULL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); diff --git a/src/api.rs b/src/api.rs index f74e33a..17cbd03 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,14 +2,13 @@ use axum::{response::IntoResponse, routing::get}; use crate::state::AppState; -mod users; pub mod error; +mod users; -pub fn router(state: AppState) -> axum::Router { +pub fn router() -> axum::Router { axum::Router::new() .merge(users::router()) .route("/healthcheck", get(healthcheck)) - .with_state(state) } pub async fn healthcheck() -> impl IntoResponse { @@ -25,6 +24,7 @@ mod tests { use axum::{ body::Body, http::{Request, StatusCode}, + Router, }; use sqlx::PgPool; use tower::ServiceExt; @@ -33,7 +33,7 @@ mod tests { async fn test_healthcheck_ok(pool: PgPool) -> TestResult { setup_test_env(); - let router = router(AppState { pool }); + let router = Router::new().merge(router()).with_state(AppState { pool }); let request = Request::builder().uri("/healthcheck").body(Body::empty())?; diff --git a/src/api/error.rs b/src/api/error.rs index 4088b9b..f5d4291 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -15,6 +15,9 @@ pub enum Error { #[error("Invalid email: {0}")] EmailInvalid(#[from] email_address::Error), + #[error("Failed to reach authentication server: {0}")] + AuthRequest(#[from] axum::http::Error), + #[error("Authentication error: {0}")] Auth(#[from] crate::auth::error::Error), } @@ -28,7 +31,7 @@ impl axum::response::IntoResponse for Error { Self::EmailExists => StatusCode::CONFLICT, Self::EmailInvalid(_) => StatusCode::UNPROCESSABLE_ENTITY, Self::InvalidToken => StatusCode::UNAUTHORIZED, - Self::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::AuthRequest(_) | Self::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::Auth(err) => return err.into_response(), }; diff --git a/src/api/users.rs b/src/api/users.rs index 7a9bb6e..2440e6e 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -22,28 +22,13 @@ pub fn router() -> Resource { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow)] #[serde(rename_all = "camelCase")] pub struct UserSchema { - pub uuid: Uuid, + pub id: Uuid, pub name: String, pub email: String, - pub session_epoch: OffsetDateTime, pub created_at: OffsetDateTime, pub updated_at: OffsetDateTime, } -impl Default for UserSchema { - fn default() -> Self { - let now = time::OffsetDateTime::now_utc(); - Self { - uuid: Default::default(), - name: Default::default(), - email: Default::default(), - session_epoch: now, - created_at: now, - updated_at: now, - } - } -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RegisterSchema { pub name: String, @@ -61,37 +46,35 @@ pub async fn create( ) -> impl IntoResponse { email_address::EmailAddress::from_str(&email)?; - let exists: Option = - sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1 LIMIT 1)") - .bind(email.to_ascii_lowercase()) - .fetch_one(&state.pool) - .await?; + let exists: Option = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM user_ WHERE email = $1 LIMIT 1)", + email.to_ascii_lowercase() + ) + .fetch_one(&state.pool) + .await?; if exists.is_some_and(|b| b) { return Err(Error::EmailExists); } - let mut transaction = state.pool.begin().await?; + // TODO: Move this into a tower service + let (status, (access, refresh)) = crate::auth::create( + State(state.clone()), + TypedHeader(Authorization::basic(&email, &password)), + ) + .await?; let user = sqlx::query_as!( UserSchema, - "INSERT INTO users (name,email) VALUES ($1, $2) RETURNING *", + "INSERT INTO user_ (id,name,email) VALUES ($1, $2, $3) RETURNING *", + refresh.sub, name, email.to_ascii_lowercase(), ) - .fetch_one(&mut *transaction) + .fetch_one(&state.pool) .await?; - let (parts, _) = crate::auth::create( - State(state), - TypedHeader(Authorization::basic(&user.uuid.to_string(), &password)), - ) - .await? - .into_response() - .into_parts(); - - transaction.commit().await?; - Ok((parts, Json(user))) + Ok((status, access, refresh, Json(user))) } pub async fn show( @@ -103,15 +86,11 @@ pub async fn show( return Err(Error::InvalidToken); } - sqlx::query_as!( - UserSchema, - "SELECT * FROM users WHERE uuid = $1 LIMIT 1", - sub - ) - .fetch_optional(&state.pool) - .await? - .ok_or_else(|| Error::UserNotFound) - .map(Json) + sqlx::query_as!(UserSchema, "SELECT * FROM user_ WHERE id = $1 LIMIT 1", sub) + .fetch_optional(&state.pool) + .await? + .ok_or_else(|| Error::UserNotFound) + .map(Json) } #[cfg(test)] @@ -136,7 +115,10 @@ mod tests { tests::{setup_test_env, TestResult}, }; - const UUID: uuid::Uuid = uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145"); + const USER_ID: Uuid = uuid::uuid!("4c14f795-86f0-4361-a02f-0edb966fb145"); + const USER_NAME: &str = "Arthur Dent"; + const USER_EMAIL: &str = "adent@earth.sol"; + const USER_PASSWORD: &str = "solongandthanksforallthefish"; #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))] async fn test_uuid_ok(pool: PgPool) -> TestResult { @@ -144,16 +126,9 @@ mod tests { let router = Router::new().merge(router()).with_state(AppState { pool }); - let user = UserSchema { - uuid: UUID, - name: "Arthur Dent".to_string(), - email: "adent@earth.sol".to_string(), - ..Default::default() - }; - let request = Request::builder() - .uri(format!("/users/{UUID}")) - .header(COOKIE, HeaderValue::try_from(AccessClaims::new(UUID))?) + .uri(format!("/users/{}", USER_ID)) + .header(COOKIE, HeaderValue::try_from(AccessClaims::new(USER_ID))?) .body(Body::empty())?; let response = router.oneshot(request).await?; @@ -162,12 +137,12 @@ mod tests { let body_bytes = response.into_body().collect().await?.to_bytes(); let UserSchema { - uuid, name, email, .. + id, name, email, .. } = serde_json::from_slice(&body_bytes)?; - assert_eq!(user.uuid, uuid); - assert_eq!(user.name, name); - assert_eq!(user.email, email); + assert_eq!(USER_ID, id); + assert_eq!(USER_NAME, name); + assert_eq!(USER_EMAIL, email); Ok(()) } @@ -179,8 +154,8 @@ mod tests { let router = Router::new().merge(router()).with_state(AppState { pool }); let request = Request::builder() - .uri(format!("/users/{UUID}")) - .header(COOKIE, HeaderValue::try_from(AccessClaims::new(UUID))?) + .uri(format!("/users/{}", USER_ID)) + .header(COOKIE, HeaderValue::try_from(AccessClaims::new(USER_ID))?) .body(Body::empty())?; let response = router.oneshot(request).await?; @@ -197,7 +172,7 @@ mod tests { let router = Router::new().merge(router()).with_state(AppState { pool }); let request = Request::builder() - .uri(format!("/users/{UUID}")) + .uri(format!("/users/{}", USER_ID)) .header( COOKIE, HeaderValue::try_from(AccessClaims::new(uuid::Uuid::new_v4()))?, @@ -218,7 +193,7 @@ mod tests { let router = Router::new().merge(router()).with_state(AppState { pool }); let request = Request::builder() - .uri(format!("/users/{UUID}")) + .uri(format!("/users/{}", USER_ID)) .header(COOKIE, "token=sadfasdfsdfs") .body(Body::empty())?; @@ -236,7 +211,7 @@ mod tests { let router = Router::new().merge(router()).with_state(AppState { pool }); let request = Request::builder() - .uri(format!("/users/{UUID}")) + .uri(format!("/users/{}", USER_ID)) .body(Body::empty())?; let response = router.oneshot(request).await?; @@ -252,11 +227,11 @@ mod tests { let router = Router::new().merge(router()).with_state(AppState { pool }); - let user = RegisterSchema { - name: "Arthur Dent".to_string(), - email: "adent@earth.sol".to_string(), - password: "solongandthanksforallthefish".to_string(), - }; + let user = serde_json::json!( { + "name": USER_NAME, + "email": USER_EMAIL, + "password": USER_PASSWORD, + }); let request = Request::builder() .uri("/users") @@ -271,8 +246,8 @@ mod tests { let body_bytes = response.into_body().collect().await?.to_bytes(); let UserSchema { name, email, .. } = serde_json::from_slice(&body_bytes)?; - assert_eq!(user.name, name); - assert_eq!(user.email, email); + assert_eq!(USER_NAME, name); + assert_eq!(USER_EMAIL, email); Ok(()) } @@ -283,11 +258,11 @@ mod tests { let router = Router::new().merge(router()).with_state(AppState { pool }); - let user = RegisterSchema { - name: "Arthur Dent".to_string(), - email: "adent@earth.sol".to_string(), - password: "solongandthanksforallthefish".to_string(), - }; + let user = serde_json::json!( { + "name": USER_NAME, + "email": USER_EMAIL, + "password": USER_PASSWORD, + }); let request = Request::builder() .uri("/users") diff --git a/src/auth.rs b/src/auth.rs index 3e5443f..8756291 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -2,14 +2,10 @@ use argon2::{ password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier, }; -use axum::{ - extract::State, - http::StatusCode, - response::IntoResponse, - routing::{get, post}, -}; +use axum::{extract::State, http::StatusCode}; use axum_extra::{ headers::{authorization::Basic, Authorization}, + routing::Resource, TypedHeader, }; use uuid::Uuid; @@ -24,60 +20,51 @@ pub mod claims; pub mod error; pub mod jwt; -pub fn router(state: AppState) -> axum::Router { - axum::Router::new() - .route("/create", post(create)) - .route("/issue", get(issue)) - .route("/refresh", get(refresh)) - .with_state(state) -} - -pub async fn create( - State(state): State, - TypedHeader(Authorization(basic)): TypedHeader>, -) -> Result<(StatusCode, (AccessClaims, RefreshClaims)), Error> { - let uuid = Uuid::try_parse(basic.username())?; - - let salt = SaltString::generate(&mut OsRng); - let password_hash = Argon2::default().hash_password(basic.password().as_bytes(), &salt)?; - - let rows_affected = sqlx::query("INSERT INTO credentials (uuid,password_hash) VALUES ($1, $2)") - .bind(uuid) - .bind(password_hash.to_string()) - .execute(&state.pool) - .await? - .rows_affected(); - - if rows_affected == 0 { - Err(Error::Registration) - } else { - Ok(( - StatusCode::CREATED, - issue(State(state), TypedHeader(Authorization(basic))).await?, - )) - } +pub fn router() -> Resource { + Resource::named("auth").index(issue).create(create) } pub async fn issue( State(state): State, TypedHeader(Authorization(basic)): TypedHeader>, ) -> Result<(AccessClaims, RefreshClaims), Error> { - let uuid = basic.username().try_into()?; + let uuid = Uuid::try_parse(basic.username())?; - let p: String = sqlx::query_scalar("SELECT password_hash FROM credentials WHERE uuid = $1") - .bind(uuid) + let p: String = sqlx::query_scalar!("SELECT password_hash FROM credential WHERE id = $1", uuid) .fetch_optional(&state.pool) .await? .ok_or(Error::LoginInvalid)?; Argon2::default().verify_password(basic.password().as_bytes(), &PasswordHash::new(&p)?)?; - let claims = RefreshClaims::new(uuid); + let refresh = RefreshClaims::new(uuid); + let access = refresh.refresh(); + Ok((access, refresh)) +} + +pub async fn create( + State(state): State, + TypedHeader(Authorization(basic)): TypedHeader>, +) -> Result<(StatusCode, (AccessClaims, RefreshClaims)), Error> { + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default().hash_password(basic.password().as_bytes(), &salt)?; + + let uuid = sqlx::query!( + "INSERT INTO credential (password_hash) VALUES ($1) RETURNING id", + password_hash.to_string() + ) + .fetch_optional(&state.pool) + .await? + .ok_or(Error::Registration)? + .id; + + let refresh = RefreshClaims::new(uuid); + let access = refresh.refresh(); - Ok((claims.refresh(), claims)) + Ok((StatusCode::CREATED, (access, refresh))) } -pub async fn refresh(claims: RefreshClaims) -> impl IntoResponse { +pub async fn refresh(claims: RefreshClaims) -> AccessClaims { claims.refresh() } @@ -88,6 +75,7 @@ mod tests { use axum::{ body::Body, http::{header::AUTHORIZATION, Request, StatusCode}, + Router, }; use axum_extra::headers::authorization::Credentials; use sqlx::PgPool; @@ -110,7 +98,7 @@ mod tests { async fn test_issue_ok(pool: PgPool) -> TestResult { setup_test_env(); - let router = router(AppState { pool }); + let router = Router::new().merge(router()).with_state(AppState { pool }); let auth = Authorization::basic( "4c14f795-86f0-4361-a02f-0edb966fb145", @@ -118,7 +106,7 @@ mod tests { ); let request = Request::builder() - .uri("/issue") + .uri("/auth") .method("GET") .header(AUTHORIZATION, auth.0.encode()) .body(Body::empty())?; @@ -135,12 +123,12 @@ mod tests { async fn test_issue_unauthorized(pool: PgPool) -> TestResult { setup_test_env(); - let router = router(AppState { pool }); + let router = Router::new().merge(router()).with_state(AppState { pool }); let auth = Authorization::basic("4c14f795-86f0-4361-a02f-0edb966fb145", "hunter2"); let request = Request::builder() - .uri("/issue") + .uri("/auth") .method("GET") .header(AUTHORIZATION, auth.0.encode()) .body(Body::empty())?; diff --git a/src/lib.rs b/src/lib.rs index 8be6698..2afed5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,10 +9,10 @@ pub mod error; pub mod state; pub mod utils; -pub fn router(state: state::AppState) -> axum::Router { +pub fn router() -> axum::Router { axum::Router::new() - .nest("/api", api::router(state.clone())) - .nest("/auth", auth::router(state.clone())) + .nest("/api", api::router()) + .merge(auth::router()) .fallback(fallback) // TODO: do this correctly! .layer(CorsLayer::permissive()) @@ -34,6 +34,7 @@ pub(crate) mod tests { use axum::{ body::Body, http::{Request, StatusCode}, + Router, }; use sqlx::PgPool; use tower::ServiceExt; @@ -60,7 +61,7 @@ pub(crate) mod tests { async fn test_fallback_not_found(pool: PgPool) -> TestResult { setup_test_env(); - let router = router(AppState { pool }); + let router = Router::new().merge(router()).with_state(AppState { pool }); let request = Request::builder() .uri("/does-not-exist") diff --git a/src/main.rs b/src/main.rs index ae1df98..71e68f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,8 +21,8 @@ async fn main() -> Result<(), main_error::MainError> { let config = Config::builder().file()?.env().build()?; let listener = TcpListener::bind(config.listen_addr).await?; - let app_state = AppState::new(config.database_url).await?; - let router = unnamed_server::router(app_state); + let state = AppState::new(config.database_url).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 4f365b9..75c1e11 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,5 @@ use std::fmt::Debug; -use axum::{ - async_trait, - extract::{FromRef, FromRequestParts}, - http::request::Parts, -}; use sqlx::{Pool, Postgres}; use crate::Error; @@ -30,16 +25,3 @@ impl AppState { Ok(Self { pool }) } } - -#[async_trait] -impl FromRequestParts for AppState -where - Self: FromRef, - S: Send + Sync + Debug, -{ - type Rejection = Error; - - async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { - Ok(Self::from_ref(state)) - } -} -- cgit v1.2.3-70-g09d2