Re-enable the verification route

This commit is contained in:
Z. Charles Dziura 2024-10-03 15:27:30 -04:00
parent c908123a74
commit e2cb989560
14 changed files with 96 additions and 50 deletions

View file

@ -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 -->

View file

@ -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)
}) })
} }

View file

@ -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()
} }

View file

@ -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)
}) })

View file

@ -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),
} }
}; };

View file

@ -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>,
} }

View file

@ -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())
} }

View file

@ -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((

View file

@ -1,5 +1,5 @@
mod handler;
mod models; mod models;
mod request;
pub use handler::*;
pub use models::*; pub use models::*;
pub use request::*;

View file

@ -1,5 +1,5 @@
mod request; mod params;
mod response; mod response;
pub use request::*; pub use params::*;
pub use response::*; pub use response::*;

View file

@ -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,
} }

View file

@ -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()),
) )
} }

View file

@ -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())

View file

@ -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,
} }