Re-enable the verification route
This commit is contained in:
parent
c908123a74
commit
e2cb989560
14 changed files with 96 additions and 50 deletions
|
@ -137,7 +137,7 @@
|
||||||
<table border="0" cellpadding="0" cellspacing="0">
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" bgcolor="#1a82e2" style="border-radius: 6px;">
|
<td align="center" bgcolor="#1a82e2" style="border-radius: 6px;">
|
||||||
<a href="#$AUTH_TOKEN" target="_blank" style="display: inline-block; padding: 16px 36px; font-family: sans-serif; font-size: 16px; color: #ffffff; text-decoration: none; border-radius: 6px;">Confirm Email Address</a>
|
<a href="#$VERIFICATION_TOKEN" target="_blank" style="display: inline-block; padding: 16px 36px; font-family: sans-serif; font-size: 16px; color: #ffffff; text-decoration: none; border-radius: 6px;">Confirm Email Address</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -152,7 +152,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" bgcolor="#ffffff" style="padding: 24px; font-family: sans-serif; font-size: 16px; line-height: 24px;">
|
<td align="left" bgcolor="#ffffff" style="padding: 24px; font-family: sans-serif; font-size: 16px; line-height: 24px;">
|
||||||
<p style="margin: 0;">If that doesn't work, copy and paste the following link in your browser:</p>
|
<p style="margin: 0;">If that doesn't work, copy and paste the following link in your browser:</p>
|
||||||
<p style="margin: 0;"><a href="#$AUTH_TOKEN" target="_blank">$AUTH_TOKEN</a></p>
|
<p style="margin: 0;"><a href="#$VERIFICATION_TOKEN" target="_blank">$VERIFICATION_TOKEN</a></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- end copy -->
|
<!-- end copy -->
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use sqlx::prelude::FromRow;
|
use sqlx::prelude::FromRow;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::models::AppError;
|
use crate::models::AppError;
|
||||||
|
|
||||||
use super::DbPool;
|
use super::DbPool;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone)]
|
||||||
pub struct NewUserEntity {
|
pub struct NewUserEntity {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
@ -12,6 +15,17 @@ pub struct NewUserEntity {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for NewUserEntity {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("NewUserEntity")
|
||||||
|
.field("username", &self.username)
|
||||||
|
.field("password", &"********")
|
||||||
|
.field("email", &self.email)
|
||||||
|
.field("name", &self.name)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, FromRow)]
|
#[derive(Debug, FromRow)]
|
||||||
pub struct UserEntity {
|
pub struct UserEntity {
|
||||||
|
@ -30,7 +44,7 @@ pub async fn insert_new_user(
|
||||||
password,
|
password,
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
} = new_user;
|
} = new_user.clone();
|
||||||
|
|
||||||
sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (username, email, password, name) VALUES ($1, $2, $3, $4) RETURNING id, username, email, name, status_id;")
|
sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (username, email, password, name) VALUES ($1, $2, $3, $4) RETURNING id, username, email, name, status_id;")
|
||||||
.bind(username)
|
.bind(username)
|
||||||
|
@ -39,7 +53,8 @@ pub async fn insert_new_user(
|
||||||
.bind(name)
|
.bind(name)
|
||||||
.fetch_one(pool).await
|
.fetch_one(pool).await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
eprintln!("Error inserting NewUserEntity {err:?}");
|
error!(%err, record = ?new_user, "Cannot insert new user record");
|
||||||
|
|
||||||
AppError::from(err)
|
AppError::from(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ impl AppError {
|
||||||
Self::new(ErrorKind::InvalidToken)
|
Self::new(ErrorKind::InvalidToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn invalid_token_audience(audience: &str) -> Self {
|
pub fn invalid_token_audience(audience: &str) -> Self {
|
||||||
Self::new(ErrorKind::InvalidTokenAudience(audience.to_owned()))
|
Self::new(ErrorKind::InvalidTokenAudience(audience.to_owned()))
|
||||||
}
|
}
|
||||||
|
@ -116,7 +117,7 @@ enum ErrorKind {
|
||||||
|
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
if let ErrorKind::DuplicateRecord = &self.kind { }
|
if let ErrorKind::DuplicateRecord = &self.kind {}
|
||||||
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> {
|
||||||
let path = request
|
let path = request
|
||||||
.extensions()
|
.extensions()
|
||||||
.get::<MatchedPath>()
|
.get::<MatchedPath>()
|
||||||
.map(MatchedPath::as_str).unwrap();
|
.map(MatchedPath::as_str).unwrap_or(request.uri().path());
|
||||||
|
|
||||||
info_span!("api_request", request_id = %Ulid::new(), method = %request.method(), %path, status = tracing::field::Empty)
|
info_span!("api_request", request_id = %Ulid::new(), method = %request.method(), %path, status = tracing::field::Empty)
|
||||||
})
|
})
|
||||||
|
|
|
@ -69,13 +69,13 @@ async fn register_new_user_request(
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let (auth_token, expiration) = generate_new_user_token(signing_key, user_id);
|
let (verification_token, expiration) = generate_new_user_token(signing_key, user_id);
|
||||||
|
|
||||||
let response_body = if send_verification_email {
|
let response_body = if send_verification_email {
|
||||||
let new_user_confirmation_message = UserConfirmationMessage {
|
let new_user_confirmation_message = UserConfirmationMessage {
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
auth_token: auth_token.clone(),
|
verification_token: verification_token.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = email_sender
|
let _ = email_sender
|
||||||
|
@ -87,13 +87,13 @@ async fn register_new_user_request(
|
||||||
UserRegistrationResponse {
|
UserRegistrationResponse {
|
||||||
id: user_id,
|
id: user_id,
|
||||||
expiration,
|
expiration,
|
||||||
auth_token: None,
|
verification_token: None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
UserRegistrationResponse {
|
UserRegistrationResponse {
|
||||||
id: user_id,
|
id: user_id,
|
||||||
expiration,
|
expiration,
|
||||||
auth_token: Some(auth_token),
|
verification_token: Some(verification_token),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,5 +13,5 @@ pub struct UserRegistrationResponse {
|
||||||
#[serde(serialize_with = "humantime_serde::serialize")]
|
#[serde(serialize_with = "humantime_serde::serialize")]
|
||||||
pub expiration: SystemTime,
|
pub expiration: SystemTime,
|
||||||
|
|
||||||
pub auth_token: Option<String>,
|
pub verification_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
use axum::{routing::post, Router};
|
mod create;
|
||||||
|
mod verify;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
|
||||||
use create::user_registration_post_handler;
|
use create::user_registration_post_handler;
|
||||||
|
use verify::user_verification_get_handler;
|
||||||
|
|
||||||
use super::AppState;
|
use super::AppState;
|
||||||
|
|
||||||
pub mod create;
|
|
||||||
// pub mod verify;
|
|
||||||
|
|
||||||
pub fn requests(app_state: AppState) -> Router {
|
pub fn requests(app_state: AppState) -> Router {
|
||||||
Router::new().route(
|
Router::new()
|
||||||
"/user",
|
.route("/user", post(user_registration_post_handler))
|
||||||
post(user_registration_post_handler).with_state(app_state.clone()),
|
.route("/user/:user_id/verify", get(user_verification_get_handler))
|
||||||
)
|
.with_state(app_state.clone())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,43 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
|
debug_handler,
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
Json,
|
||||||
Json, Router,
|
|
||||||
};
|
};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use pasetors::claims::ClaimsValidationRules;
|
use pasetors::{claims::ClaimsValidationRules, keys::SymmetricKey, version4::V4};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::verify_user, models::ApiResponse, requests::AppState, services::auth_token::verify_token,
|
db::{verify_user, DbPool},
|
||||||
|
models::ApiResponse,
|
||||||
|
requests::AppState,
|
||||||
|
services::auth_token::verify_token,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{UserVerifyGetRequest, UserVerifyGetResponse};
|
use super::{UserVerifyGetParams, UserVerifyGetResponse};
|
||||||
|
|
||||||
pub fn request(app_state: AppState) -> Router {
|
#[debug_handler]
|
||||||
Router::new().route("/:user_id/verify", get(get_handler).with_state(app_state))
|
pub async fn user_verification_get_handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(user_id): Path<i32>,
|
||||||
|
Query(query): Query<UserVerifyGetParams>,
|
||||||
|
) -> Result<Response, Response> {
|
||||||
|
let pool = state.pool();
|
||||||
|
let env = state.env();
|
||||||
|
|
||||||
|
let UserVerifyGetParams { verification_token } = query;
|
||||||
|
let token_key = env.token_key();
|
||||||
|
verify_new_user_request(pool, user_id, verification_token, token_key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_handler(
|
async fn verify_new_user_request(
|
||||||
State(app_state): State<AppState>,
|
pool: &DbPool,
|
||||||
Path(user_id): Path<i32>,
|
user_id: i32,
|
||||||
Query(request): Query<UserVerifyGetRequest>,
|
verification_token: String,
|
||||||
|
token_key: &SymmetricKey<V4>,
|
||||||
) -> Result<Response, Response> {
|
) -> Result<Response, Response> {
|
||||||
let UserVerifyGetRequest { auth_token } = request;
|
debug!(user_id);
|
||||||
|
|
||||||
let validation_rules = {
|
let validation_rules = {
|
||||||
let mut rules = ClaimsValidationRules::new();
|
let mut rules = ClaimsValidationRules::new();
|
||||||
|
@ -31,13 +46,18 @@ async fn get_handler(
|
||||||
rules
|
rules
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = app_state.env().token_key();
|
let response = verify_token(
|
||||||
let response = verify_token(key, auth_token.as_str(), Some(validation_rules))
|
token_key,
|
||||||
.map(|_| UserVerifyGetResponse::new(key, user_id))
|
verification_token.as_str(),
|
||||||
.map_err(|err| err.into_response())?;
|
Some(validation_rules),
|
||||||
|
)
|
||||||
|
.map(|_| UserVerifyGetResponse::new(token_key, user_id))
|
||||||
|
.inspect_err(|err| error!(?err))
|
||||||
|
.map_err(|err| err.into_response())?;
|
||||||
|
|
||||||
verify_user(app_state.pool(), user_id)
|
verify_user(pool, user_id)
|
||||||
.await
|
.await
|
||||||
|
.inspect_err(|err| error!(?err))
|
||||||
.map_err(|err| err.into_response())?;
|
.map_err(|err| err.into_response())?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
mod handler;
|
||||||
mod models;
|
mod models;
|
||||||
mod request;
|
|
||||||
|
|
||||||
|
pub use handler::*;
|
||||||
pub use models::*;
|
pub use models::*;
|
||||||
pub use request::*;
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mod request;
|
mod params;
|
||||||
mod response;
|
mod response;
|
||||||
|
|
||||||
pub use request::*;
|
pub use params::*;
|
||||||
pub use response::*;
|
pub use response::*;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct UserVerifyGetRequest {
|
pub struct UserVerifyGetParams {
|
||||||
#[serde(alias = "t")]
|
#[serde(alias = "t")]
|
||||||
pub auth_token: String,
|
pub verification_token: String,
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@ use pasetors::{
|
||||||
token::UntrustedToken,
|
token::UntrustedToken,
|
||||||
version4::V4,
|
version4::V4,
|
||||||
};
|
};
|
||||||
|
use tracing::error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::models::AppError;
|
use crate::models::AppError;
|
||||||
|
@ -21,7 +22,9 @@ pub fn verify_token(
|
||||||
token: &str,
|
token: &str,
|
||||||
validation_rules: Option<ClaimsValidationRules>,
|
validation_rules: Option<ClaimsValidationRules>,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let token = UntrustedToken::try_from(token).map_err(|_| AppError::invalid_token())?;
|
let token = UntrustedToken::try_from(token)
|
||||||
|
.inspect_err(|err| error!(?err))
|
||||||
|
.map_err(|_| AppError::invalid_token())?;
|
||||||
|
|
||||||
let validation_rules = if let Some(validation_rules) = validation_rules {
|
let validation_rules = if let Some(validation_rules) = validation_rules {
|
||||||
validation_rules
|
validation_rules
|
||||||
|
@ -42,6 +45,7 @@ pub fn verify_token(
|
||||||
Some(&footer),
|
Some(&footer),
|
||||||
Some("TODO_ENV_NAME_HERE".as_bytes()),
|
Some("TODO_ENV_NAME_HERE".as_bytes()),
|
||||||
)
|
)
|
||||||
|
.inspect_err(|err| error!(?err))
|
||||||
.map_err(|_| AppError::invalid_token())?;
|
.map_err(|_| AppError::invalid_token())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -60,7 +64,7 @@ pub fn generate_new_user_token(key: &SymmetricKey<V4>, user_id: i32) -> (String,
|
||||||
key,
|
key,
|
||||||
user_id,
|
user_id,
|
||||||
Some(FIFTEEN_MINUTES),
|
Some(FIFTEEN_MINUTES),
|
||||||
Some(format!("api.debtpirate.bikeshedengineering.internal/user/{user_id}/verify").as_str()),
|
Some(format!("/user/{user_id}/verify").as_str()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub fn start_emailer_service(
|
||||||
let UserConfirmationMessage {
|
let UserConfirmationMessage {
|
||||||
email: recipient_email,
|
email: recipient_email,
|
||||||
name,
|
name,
|
||||||
auth_token,
|
verification_token,
|
||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
runtime.spawn(async move {
|
runtime.spawn(async move {
|
||||||
|
@ -60,7 +60,7 @@ pub fn start_emailer_service(
|
||||||
recipient_email.as_str(),
|
recipient_email.as_str(),
|
||||||
new_user_confirmation_template_text.as_str(),
|
new_user_confirmation_template_text.as_str(),
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
auth_token.as_str(),
|
verification_token.as_str(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
|
@ -72,11 +72,11 @@ async fn send_new_user_confirmation_email(
|
||||||
recipient_email: &str,
|
recipient_email: &str,
|
||||||
new_user_confirmation_template_text: &str,
|
new_user_confirmation_template_text: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
auth_token: &str,
|
verification_token: &str,
|
||||||
) {
|
) {
|
||||||
let body = new_user_confirmation_template_text
|
let body = new_user_confirmation_template_text
|
||||||
.replace("$NAME", name)
|
.replace("$NAME", name)
|
||||||
.replace("$AUTH_TOKEN", auth_token);
|
.replace("$VERIFICATION_TOKEN", verification_token);
|
||||||
|
|
||||||
let message = Message::builder()
|
let message = Message::builder()
|
||||||
.from(FROM_MAILBOX.clone())
|
.from(FROM_MAILBOX.clone())
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct UserConfirmationMessage {
|
pub struct UserConfirmationMessage {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub auth_token: String,
|
pub verification_token: String,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue