Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skip waiting for email verification to log the user in #1771

Merged
merged 8 commits into from
Feb 15, 2024
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
Loading