Skip to content

Commit

Permalink
refa: architecture of data api (#161)
Browse files Browse the repository at this point in the history
* refa: moved mfa_info to features/queries architecture

* refa: applied changed to hypermedia as well

* refa(noop): rename Pool<Postgres> to PgPool

* fix: querie wasn't using conn from transaction on query

* refa: remove data/router abstraction

* refa(noop): rename pluggy to openfinance in code

* sqlx: cargo sqlx prepared for offline eval in CI
  • Loading branch information
nicolasauler authored Nov 29, 2024
1 parent ed0e34d commit 09c4c38
Show file tree
Hide file tree
Showing 16 changed files with 124 additions and 127 deletions.

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions src/data/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::sync::Arc;

use askama_axum::IntoResponse;
use axum::{extract::State, http::StatusCode, routing::get, Json, Router};
use serde::Serialize;

use crate::{auth::AuthSession, hypermedia::schema::auth::MfaTokenForm, AppState};

pub fn private_router() -> Router<Arc<AppState>> {
Router::new().route("/api/auth/mfa", get(mfa_info).post(mfa_verify))
}

#[derive(Serialize)]
struct MfaInfo {
otp_url: String,
}

async fn mfa_info(
auth_session: AuthSession,
State(shared_state): State<Arc<AppState>>,
) -> Result<Json<MfaInfo>, StatusCode> {
let Some(user) = auth_session.user else {
return Err(StatusCode::UNAUTHORIZED);
};

// TODO: create logic for changing MFA method
if user.otp_enabled {
todo!("Create logic for changing MFA method");
}

match crate::features::totp::set_otp_secret(shared_state.pool.clone(), user.id, user.email)
.await
{
Ok(otp_data) => Ok(Json(MfaInfo {
otp_url: otp_data.otp_url,
})),
Err(e) => {
tracing::error!(?user.id, "Error setting OTP secret: {e}");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}

async fn mfa_verify(
auth_session: AuthSession,
State(shared_state): State<Arc<AppState>>,
Json(mfa_token): Json<MfaTokenForm>,
) -> impl IntoResponse {
crate::hypermedia::service::auth::mfa_verify(
auth_session,
&shared_state.pool,
&shared_state.env,
mfa_token.token,
)
.await
}
File renamed without changes.
5 changes: 3 additions & 2 deletions src/data/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod router;
pub mod service;
pub mod auth;
pub mod expenses;
pub mod openfinance;
3 changes: 0 additions & 3 deletions src/data/router/pluggy.rs → src/data/openfinance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ struct ConnectRequest {
item_id: Uuid,
}

// TODO: create and move to service
async fn connect_success(
auth_session: AuthSession,
State(shared_state): State<Arc<AppState>>,
Expand Down Expand Up @@ -62,7 +61,6 @@ struct ListAccountsRequest {
item_id: Uuid,
}

// TODO: create and move to service
async fn list_accounts(
auth_session: AuthSession,
State(shared_state): State<Arc<AppState>>,
Expand Down Expand Up @@ -92,7 +90,6 @@ struct ListTransactionsRequest {
account_id: Uuid,
}

// TODO: create and move to service
async fn list_transactions(
auth_session: AuthSession,
State(shared_state): State<Arc<AppState>>,
Expand Down
31 changes: 0 additions & 31 deletions src/data/router/auth.rs

This file was deleted.

3 changes: 0 additions & 3 deletions src/data/router/mod.rs

This file was deleted.

34 changes: 0 additions & 34 deletions src/data/service/auth.rs

This file was deleted.

1 change: 0 additions & 1 deletion src/data/service/mod.rs

This file was deleted.

27 changes: 14 additions & 13 deletions src/features/totp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::util::generate_otp_token;
use anyhow::bail;
use sqlx::{Pool, Postgres};
use sqlx::PgPool;
use totp_rs::{Algorithm, Secret, TOTP};

pub struct OtpData {
Expand All @@ -10,20 +10,21 @@ pub struct OtpData {
pub otp_url: String,
}

pub async fn set_otp_secret(db_pool: &Pool<Postgres>, user_id: i32) -> anyhow::Result<OtpData> {
pub async fn set_otp_secret(
db_pool: PgPool,
user_id: i32,
user_email: String,
) -> anyhow::Result<OtpData> {
let secret = Secret::Raw(generate_otp_token().as_bytes().to_vec());
let mut transaction = db_pool.begin().await?;

let user_email = sqlx::query!(
r#"
UPDATE users SET otp_secret = $1 WHERE id = $2
RETURNING email
"#,
secret.to_encoded().to_string(),
user_id
)
.fetch_one(&mut *transaction)
.await?;
crate::queries::user::set_otp_secret(&mut *transaction, user_id, &secret)
.await
.map(|c| {
if c.rows_affected() > 1 {
tracing::error!("i really need a macro that cancels the transaction");
}
})?;

let totp = TOTP::new(
Algorithm::SHA1,
Expand All @@ -32,7 +33,7 @@ pub async fn set_otp_secret(db_pool: &Pool<Postgres>, user_id: i32) -> anyhow::R
30,
secret.to_bytes().unwrap(),
Some("Finnish".to_owned()),
user_email.email,
user_email,
)?;

transaction.commit().await?;
Expand Down
2 changes: 1 addition & 1 deletion src/features/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ pub async fn create(
let mut transaction = db_pool.begin().await?;

if let Some(user) =
crate::queries::user::user_state_for_signup(&db_pool, &create_user.email).await?
crate::queries::user::user_state_for_signup(&mut *transaction, &create_user.email).await?
{
let verification_token = generate_verification_token();
crate::queries::user::set_email_prereq(
Expand Down
2 changes: 1 addition & 1 deletion src/hypermedia/router/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async fn mfa_qr(
auth_session: AuthSession,
State(shared_state): State<Arc<AppState>>,
) -> impl IntoResponse {
crate::hypermedia::service::auth::mfa_qr(auth_session, &shared_state.pool).await
crate::hypermedia::service::auth::mfa_qr(auth_session, shared_state.pool.clone()).await
}

async fn mfa_verify(
Expand Down
26 changes: 14 additions & 12 deletions src/hypermedia/service/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use axum::{
response::{Html, Redirect},
};
use password_auth::generate_hash;
use sqlx::{Pool, Postgres};
use sqlx::PgPool;
use time::OffsetDateTime;
use totp_rs::{Algorithm, Secret, TOTP};
use validator::Validate;
Expand Down Expand Up @@ -90,7 +90,7 @@ pub async fn signin(

pub async fn mfa_qr(
auth_session: AuthSession,
db_pool: &Pool<Postgres>,
db_pool: PgPool,
) -> Result<Response<Body>, Response<Body>> {
let Some(user) = auth_session.user else {
return Err((StatusCode::UNAUTHORIZED, [("HX-Redirect", "/auth/signin")]).into_response());
Expand All @@ -102,10 +102,12 @@ pub async fn mfa_qr(
todo!("Create logic for changing MFA method");
}

let totp = set_otp_secret(db_pool, user.id).await.map_err(|e| {
tracing::error!(?user.id, "Error setting OTP secret: {e}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
})?;
let totp = set_otp_secret(db_pool, user.id, user.email)
.await
.map_err(|e| {
tracing::error!(?user.id, "Error setting OTP secret: {e}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
})?;

Ok(MfaTemplate {
mfa_url: "/auth/mfa".to_owned(),
Expand All @@ -118,7 +120,7 @@ pub async fn mfa_qr(

pub async fn mfa_verify(
auth_session: AuthSession,
db_pool: &Pool<Postgres>,
db_pool: &PgPool,
env: &Env,
mfa_token: String,
) -> impl IntoResponse {
Expand Down Expand Up @@ -228,7 +230,7 @@ pub fn email_confirmation(env: &Env) -> impl IntoResponse {
}

pub async fn resend_verification_email(
db_pool: &Pool<Postgres>,
db_pool: &PgPool,
env: &Env,
resend_email: ResendEmail,
) -> impl IntoResponse {
Expand Down Expand Up @@ -323,7 +325,7 @@ pub async fn resend_verification_email(
.into_response();
}

pub async fn verify_email(db_pool: &Pool<Postgres>, env: &Env, token: String) -> impl IntoResponse {
pub async fn verify_email(db_pool: &PgPool, env: &Env, token: String) -> impl IntoResponse {
let frc_sitekey = env.frc_sitekey.clone();

match sqlx::query!(
Expand Down Expand Up @@ -442,7 +444,7 @@ pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {

pub async fn change_password(
auth_session: AuthSession,
db_pool: &Pool<Postgres>,
db_pool: &PgPool,
change_password_input: ChangePasswordInput,
) -> impl IntoResponse {
let maybe_user = &auth_session.user;
Expand Down Expand Up @@ -497,7 +499,7 @@ pub fn forgot_password_screen(env: &Env) -> impl IntoResponse {
}

pub async fn forgot_password(
db_pool: &Pool<Postgres>,
db_pool: &PgPool,
env: &Env,
email_input: ResendEmail,
) -> impl IntoResponse {
Expand Down Expand Up @@ -607,7 +609,7 @@ pub fn change_forgotten_password_screen(secret_code: &str) -> impl IntoResponse
}

pub async fn change_forgotten_password(
db_pool: &Pool<Postgres>,
db_pool: &PgPool,
forgot_password_input: ForgotPasswordInput,
secret_code: String,
) -> impl IntoResponse {
Expand Down
6 changes: 3 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,15 @@ fn rest(
let helmet_layer = HelmetLayer::new(generate_general_helmet_headers());

let router = Router::new()
.merge(data::router::pluggy::router())
.merge(data::router::expenses::router())
.merge(data::openfinance::router())
.merge(data::expenses::router())
.merge(hypermedia::router::expenses::router())
.route_layer(permission_required!(
Backend,
login_url = "/auth/mfa",
"restricted:read",
))
.merge(data::router::auth::private_router())
.merge(data::auth::private_router())
.merge(hypermedia::router::auth::private_router())
.route_layer(permission_required!(
Backend,
Expand Down
17 changes: 17 additions & 0 deletions src/queries/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,20 @@ pub async fn set_email_prereq(
.execute(conn)
.await
}

pub async fn set_otp_secret(
conn: impl PgExecutor<'_>,
user_id: i32,
secret: &totp_rs::Secret,
) -> Result<sqlx::postgres::PgQueryResult, sqlx::Error> {
sqlx::query!(
r#"
UPDATE users SET otp_secret = $1
WHERE id = $2
"#,
secret.to_encoded().to_string(),
user_id
)
.execute(conn)
.await
}

0 comments on commit 09c4c38

Please sign in to comment.