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 --- src/api/error.rs | 5 ++- src/api/users.rs | 123 ++++++++++++++++++++++--------------------------------- 2 files changed, 53 insertions(+), 75 deletions(-) (limited to 'src/api') 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") -- cgit v1.2.3-70-g09d2