Skip to content

Commit

Permalink
Merge pull request #1771 from fastn-stack/auth-changes
Browse files Browse the repository at this point in the history
Skip waiting for email verification to log the user in
  • Loading branch information
amitu authored Feb 15, 2024
2 parents d22d28c + fa8ecd6 commit fb88671
Show file tree
Hide file tree
Showing 15 changed files with 258 additions and 182 deletions.
55 changes: 31 additions & 24 deletions fastn-core/src/auth/email_password/confirm_email.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::auth::email_password::{email_confirmation_sent_ftd, key_expired};
use crate::auth::email_password::key_expired;

pub(crate) async fn confirm_email(
req_config: &mut fastn_core::RequestConfig,
Expand All @@ -11,17 +11,8 @@ pub(crate) async fn confirm_email(
let code = req_config.request.query().get("code");

if code.is_none() {
let main = fastn_core::Document {
package_name: req_config.config.package.name.clone(),
id: "/-/confirm-email/".to_string(),
content: email_confirmation_sent_ftd().to_string(),
parent_path: fastn_ds::Path::new("/"),
};

let resp = fastn_core::package::package_doc::read_ftd(req_config, &main, "/", false, false)
.await?;

return Ok(resp.into());
tracing::info!("finishing response due to bad ?code");
return Ok(fastn_core::http::api_error("Bad Request")?);
}

let code = match code.unwrap() {
Expand All @@ -39,7 +30,7 @@ pub(crate) async fn confirm_email(
message: format!("Failed to get connection to db. {:?}", e),
})?;

let conf_data: Option<(i32, i32, chrono::DateTime<chrono::Utc>)> =
let conf_data: Option<(i64, i64, chrono::DateTime<chrono::Utc>)> =
fastn_core::schema::fastn_email_confirmation::table
.select((
fastn_core::schema::fastn_email_confirmation::email_id,
Expand Down Expand Up @@ -72,20 +63,21 @@ pub(crate) async fn confirm_email(
));
}

diesel::update(fastn_core::schema::fastn_user_email::table)
.set(fastn_core::schema::fastn_user_email::verified.eq(true))
.filter(fastn_core::schema::fastn_user_email::id.eq(email_id))
.execute(&mut conn)
.await?;
let email: fastn_core::utils::CiString =
diesel::update(fastn_core::schema::fastn_user_email::table)
.set(fastn_core::schema::fastn_user_email::verified.eq(true))
.filter(fastn_core::schema::fastn_user_email::id.eq(email_id))
.returning(fastn_core::schema::fastn_user_email::email)
.get_result(&mut conn)
.await?;

let affected = diesel::update(fastn_core::schema::fastn_session::table)
.set(fastn_core::schema::fastn_session::active.eq(true))
.filter(fastn_core::schema::fastn_session::id.eq(session_id))
.execute(&mut conn)
let user_id: i64 = diesel::update(fastn_core::schema::fastn_user::table)
.set(fastn_core::schema::fastn_user::verified_email.eq(true))
.filter(fastn_core::schema::fastn_user::email.eq(&email))
.returning(fastn_core::schema::fastn_user::id)
.get_result(&mut conn)
.await?;

tracing::info!("session created, rows affected: {}", affected);

// Onboarding step is opt-in
let onboarding_enabled = req_config
.config
Expand All @@ -100,7 +92,22 @@ pub(crate) async fn confirm_email(
next.to_string()
};

let now = chrono::Utc::now();

// session always exists for new unverified user since it is created during `create-account`
let affected = diesel::update(fastn_core::schema::fastn_auth_session::table)
.set((
fastn_core::schema::fastn_auth_session::user_id.eq(&user_id),
fastn_core::schema::fastn_auth_session::updated_at.eq(&now),
))
.filter(fastn_core::schema::fastn_auth_session::id.eq(session_id))
.execute(&mut conn)
.await?;

tracing::info!("updated session. affected: {}", affected);

// redirect to onboarding route with a GET request
// if some user is already logged in, this will override their session with this one
let mut resp = fastn_core::auth::set_session_cookie_and_redirect_to_next(
&req_config.request,
&req_config.config.ds,
Expand Down
91 changes: 51 additions & 40 deletions fastn-core/src/auth/email_password/create_account.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use crate::auth::email_password::{
create_account_ftd, create_and_send_confirmation_email, redirect_url_from_next,
};
use crate::auth::email_password::{create_account_ftd, create_and_send_confirmation_email};

#[derive(serde::Deserialize, serde::Serialize, validator::Validate, Debug)]
struct UserPayload {
Expand All @@ -26,6 +24,8 @@ pub(crate) async fn create_account(
use diesel_async::RunQueryDsl;
use validator::ValidateArgs;

let now = chrono::Utc::now();

if req_config.request.method() != "POST" {
// TODO: if user is logged in redirect to next

Expand Down Expand Up @@ -78,12 +78,12 @@ pub(crate) async fn create_account(
);
}

let email_check: i64 = fastn_core::schema::fastn_user_email::table
let email_check: i64 = fastn_core::schema::fastn_user::table
.filter(
fastn_core::schema::fastn_user_email::email
fastn_core::schema::fastn_user::email
.eq(fastn_core::utils::citext(&user_payload.email)),
)
.select(diesel::dsl::count(fastn_core::schema::fastn_user_email::id))
.select(diesel::dsl::count(fastn_core::schema::fastn_user::id))
.first(&mut conn)
.await?;

Expand Down Expand Up @@ -113,30 +113,33 @@ pub(crate) async fn create_account(
fastn_core::schema::fastn_user::username.eq(user_payload.username),
fastn_core::schema::fastn_user::password.eq(hashed_password),
fastn_core::schema::fastn_user::name.eq(user_payload.name),
fastn_core::schema::fastn_user::email
.eq(fastn_core::utils::citext(&user_payload.email)),
fastn_core::schema::fastn_user::created_at.eq(now),
fastn_core::schema::fastn_user::updated_at.eq(now),
))
.returning(fastn_core::auth::FastnUser::as_returning())
.get_result(c)
.await?;

tracing::info!("fastn_user created. user_id: {}", &user.id);
// just for record keeping
// we do not use `fastn_user_email` for auth at all
diesel::insert_into(fastn_core::schema::fastn_user_email::table)
.values((
fastn_core::schema::fastn_user_email::user_id.eq(user.id),
fastn_core::schema::fastn_user_email::email
.eq(fastn_core::utils::citext(user_payload.email.as_str())),
fastn_core::schema::fastn_user_email::verified.eq(false),
fastn_core::schema::fastn_user_email::primary.eq(true),
fastn_core::schema::fastn_user_email::created_at.eq(now),
fastn_core::schema::fastn_user_email::updated_at.eq(now),
))
.returning(fastn_core::schema::fastn_user_email::email)
.execute(c)
.await?;

let email: fastn_core::utils::CiString =
diesel::insert_into(fastn_core::schema::fastn_user_email::table)
.values((
fastn_core::schema::fastn_user_email::user_id.eq(user.id),
fastn_core::schema::fastn_user_email::email
.eq(fastn_core::utils::citext(user_payload.email.as_str())),
fastn_core::schema::fastn_user_email::verified.eq(false),
fastn_core::schema::fastn_user_email::primary.eq(true),
))
.returning(fastn_core::schema::fastn_user_email::email)
.get_result(c)
.await?;

Ok::<
(fastn_core::auth::FastnUser, fastn_core::utils::CiString),
diesel::result::Error,
>((user, email))
tracing::info!("fastn_user created. user_id: {}", &user.id);
Ok::<fastn_core::auth::FastnUser, diesel::result::Error>(user)
})
})
.await;
Expand All @@ -151,25 +154,33 @@ pub(crate) async fn create_account(
);
}

let (user, email) = save_user_email_transaction.expect("expected transaction to yield Some");
let user = save_user_email_transaction.expect("expected transaction to yield Some");

tracing::info!("fastn_user email inserted");

let conf_link =
create_and_send_confirmation_email(email.0.to_string(), db_pool, req_config, next).await?;

let resp_body = serde_json::json!({
"user": user,
"success": true,
"redirect": redirect_url_from_next(&req_config.request, "/-/auth/confirm-email/".to_string()),
});

let mut resp = actix_web::HttpResponse::Ok();

if req_config.config.test_command_running {
resp.insert_header(("X-Fastn-Test", "true"))
.insert_header(("X-Fastn-Test-Email-Confirmation-Link", conf_link));
let (conf_link, session_id) = create_and_send_confirmation_email(
user.email.0.to_string(),
db_pool,
req_config,
next.clone(),
)
.await?;

// email is not enabled, we should log conf link assuming dev mode
if !req_config
.config
.ds
.env_bool("FASTN_ENABLE_EMAIL", true)
.await
{
println!("CONFIRMATION LINK: {}", conf_link);
}

Ok(resp.json(resp_body))
fastn_core::auth::set_session_cookie_and_redirect_to_next(
&req_config.request,
&req_config.config.ds,
session_id,
next,
)
.await
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ pub(crate) async fn create_and_send_confirmation_email(
db_pool: &fastn_core::db::PgPool,
req_config: &mut fastn_core::RequestConfig,
next: String,
) -> fastn_core::Result<String> {
) -> fastn_core::Result<(String, i64)> {
use diesel::prelude::*;
use diesel_async::RunQueryDsl;

let key = generate_key(64);
let now = chrono::Utc::now();

let mut conn = db_pool
.get()
Expand All @@ -18,7 +19,7 @@ pub(crate) async fn create_and_send_confirmation_email(
message: format!("Failed to get connection to db. {:?}", e),
})?;

let (email_id, user_id): (i32, i32) = fastn_core::schema::fastn_user_email::table
let (email_id, user_id): (i64, i64) = fastn_core::schema::fastn_user_email::table
.select((
fastn_core::schema::fastn_user_email::id,
fastn_core::schema::fastn_user_email::user_id,
Expand All @@ -30,13 +31,14 @@ pub(crate) async fn create_and_send_confirmation_email(
.first(&mut conn)
.await?;

// create a non active fastn_sesion entry for auto login
let session_id: i32 = diesel::insert_into(fastn_core::schema::fastn_session::table)
// create a non active fastn_auth_session entry for auto login
let session_id: i64 = diesel::insert_into(fastn_core::schema::fastn_auth_session::table)
.values((
fastn_core::schema::fastn_session::user_id.eq(&user_id),
fastn_core::schema::fastn_session::active.eq(false),
fastn_core::schema::fastn_auth_session::user_id.eq(&user_id),
fastn_core::schema::fastn_auth_session::created_at.eq(&now),
fastn_core::schema::fastn_auth_session::updated_at.eq(&now),
))
.returning(fastn_core::schema::fastn_session::id)
.returning(fastn_core::schema::fastn_auth_session::id)
.get_result(&mut conn)
.await?;

Expand All @@ -45,8 +47,8 @@ pub(crate) async fn create_and_send_confirmation_email(
.values((
fastn_core::schema::fastn_email_confirmation::email_id.eq(email_id),
fastn_core::schema::fastn_email_confirmation::session_id.eq(&session_id),
fastn_core::schema::fastn_email_confirmation::sent_at
.eq(chrono::offset::Utc::now()),
fastn_core::schema::fastn_email_confirmation::sent_at.eq(&now),
fastn_core::schema::fastn_email_confirmation::created_at.eq(&now),
fastn_core::schema::fastn_email_confirmation::key.eq(key),
))
.returning(fastn_core::schema::fastn_email_confirmation::key)
Expand Down Expand Up @@ -136,5 +138,5 @@ pub(crate) async fn create_and_send_confirmation_email(
.await
.map_err(|e| fastn_core::Error::generic(format!("failed to send email: {e}")))?;

Ok(confirmation_link)
Ok((confirmation_link, session_id))
}
20 changes: 20 additions & 0 deletions fastn-core/src/auth/email_password/email_confirmation_sent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::auth::email_password::email_confirmation_sent_ftd;

// TODO: this is unused right now
// the user is immediately logged in after creating an account and this page is never
// visited/redirected to
pub(crate) async fn email_confirmation_sent(
req_config: &mut fastn_core::RequestConfig,
) -> fastn_core::Result<fastn_core::http::Response> {
let main = fastn_core::Document {
package_name: req_config.config.package.name.clone(),
id: "/-/auth/confirmation-email-sent/".to_string(),
content: email_confirmation_sent_ftd().to_string(),
parent_path: fastn_ds::Path::new("/"),
};

let resp =
fastn_core::package::package_doc::read_ftd(req_config, &main, "/", false, false).await?;

Ok(resp.into())
}
16 changes: 10 additions & 6 deletions fastn-core/src/auth/email_password/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,9 @@ pub(crate) async fn login(
})?;

let query = fastn_core::schema::fastn_user::table
.inner_join(fastn_core::schema::fastn_user_email::table)
.filter(fastn_core::schema::fastn_user::username.eq(&payload.username))
.or_filter(
fastn_core::schema::fastn_user_email::email
.eq(fastn_core::utils::citext(&payload.username)),
fastn_core::schema::fastn_user::email.eq(fastn_core::utils::citext(&payload.username)),
)
.select(fastn_core::auth::FastnUser::as_select());

Expand Down Expand Up @@ -111,10 +109,16 @@ pub(crate) async fn login(
);
}

let now = chrono::Utc::now();

// TODO: session should store device that was used to login (chrome desktop on windows)
let session_id: i32 = diesel::insert_into(fastn_core::schema::fastn_session::table)
.values((fastn_core::schema::fastn_session::user_id.eq(&user.id),))
.returning(fastn_core::schema::fastn_session::id)
let session_id: i64 = diesel::insert_into(fastn_core::schema::fastn_auth_session::table)
.values((
fastn_core::schema::fastn_auth_session::user_id.eq(&user.id),
fastn_core::schema::fastn_auth_session::created_at.eq(now),
fastn_core::schema::fastn_auth_session::updated_at.eq(now),
))
.returning(fastn_core::schema::fastn_auth_session::id)
.get_result(&mut conn)
.await?;

Expand Down
2 changes: 2 additions & 0 deletions fastn-core/src/auth/email_password/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod confirm_email;
mod create_account;
mod create_and_send_confirmation_email;
mod email_confirmation_sent;
mod login;
mod onboarding;
mod resend_email;
Expand All @@ -10,6 +11,7 @@ pub(crate) use {
confirm_email::confirm_email,
create_account::create_account,
create_and_send_confirmation_email::create_and_send_confirmation_email,
email_confirmation_sent::email_confirmation_sent,
login::login,
onboarding::onboarding,
resend_email::resend_email,
Expand Down
Loading

0 comments on commit fb88671

Please sign in to comment.