diff options
| author | Ellie Huxtable <ellie@elliehuxtable.com> | 2024-06-24 14:54:54 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-24 14:54:54 +0100 |
| commit | 67d64ec4b368c48188c746f2dba2967ec4615fe5 (patch) | |
| tree | 9da8443d4baa424e99a806cb022d53daa6a8c30e /crates/atuin-server/src/handlers | |
| parent | fix: Some --help comments didn't show properly (#2176) (diff) | |
| download | atuin-67d64ec4b368c48188c746f2dba2967ec4615fe5.zip | |
feat: add user account verification (#2190)
* add verified column to users table
* add database functions to check if verified, or to verify
* getting there
* verification check
* use base64 urlsafe no pad
* add verification client
* clippy
* correct docs
* fix integration tests
Diffstat (limited to 'crates/atuin-server/src/handlers')
| -rw-r--r-- | crates/atuin-server/src/handlers/user.rs | 110 |
1 files changed, 107 insertions, 3 deletions
diff --git a/crates/atuin-server/src/handlers/user.rs b/crates/atuin-server/src/handlers/user.rs index 8e53dd49..7fd1a4a2 100644 --- a/crates/atuin-server/src/handlers/user.rs +++ b/crates/atuin-server/src/handlers/user.rs @@ -12,9 +12,11 @@ use axum::{ Json, }; use metrics::counter; + +use postmark::{reqwest::PostmarkClient, Query}; + use rand::rngs::OsRng; use tracing::{debug, error, info, instrument}; -use uuid::Uuid; use super::{ErrorResponse, ErrorResponseStatus, RespExt}; use crate::router::{AppState, UserAuth}; @@ -25,7 +27,7 @@ use atuin_server_database::{ use reqwest::header::CONTENT_TYPE; -use atuin_common::api::*; +use atuin_common::{api::*, utils::crypto_random_string}; pub fn verify_str(hash: &str, password: &str) -> bool { let arg2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default()); @@ -126,7 +128,8 @@ pub async fn register<DB: Database>( } }; - let token = Uuid::new_v4().as_simple().to_string(); + // 24 bytes encoded as base64 + let token = crypto_random_string::<24>(); let new_session = NewSession { user_id, @@ -175,6 +178,107 @@ pub async fn delete<DB: Database>( Ok(Json(DeleteUserResponse {})) } +#[instrument(skip_all, fields(user.id = user.id))] +pub async fn send_verification<DB: Database>( + UserAuth(user): UserAuth, + state: State<AppState<DB>>, +) -> Result<Json<SendVerificationResponse>, ErrorResponseStatus<'static>> { + let settings = state.0.settings; + + if !settings.mail.enabled { + return Ok(Json(SendVerificationResponse { + email_sent: false, + verified: false, + })); + } + + if user.verified.is_some() { + return Ok(Json(SendVerificationResponse { + email_sent: false, + verified: true, + })); + } + + // TODO: if we ever add another mail provider, can match on them all here. + let postmark_token = if let Some(token) = settings.mail.postmark.token { + token + } else { + return Err(ErrorResponse::reply("mail not configured") + .with_status(StatusCode::INTERNAL_SERVER_ERROR)); + }; + + let db = &state.0.database; + + let verification_token = db + .user_verification_token(user.id) + .await + .expect("Failed to verify"); + + debug!("Generated verification token, emailing user"); + + let client = PostmarkClient::builder() + .base_url("https://api.postmarkapp.com/") + .token(postmark_token) + .build(); + + let req = postmark::api::email::SendEmailRequest::builder() + .from(settings.mail.verification.from) + .subject(settings.mail.verification.subject) + .to(user.email) + .body(postmark::api::Body::text(format!( + "Please run the following command to finalize your Atuin account verification. It is valid for 15 minutes:\n\natuin account verify --token '{}'", + verification_token + ))) + .build(); + + req.execute(&client) + .await + .expect("postmark email request failed"); + + debug!("Email sent"); + + Ok(Json(SendVerificationResponse { + email_sent: true, + verified: false, + })) +} + +#[instrument(skip_all, fields(user.id = user.id))] +pub async fn verify_user<DB: Database>( + UserAuth(user): UserAuth, + state: State<AppState<DB>>, + Json(token_request): Json<VerificationTokenRequest>, +) -> Result<Json<VerificationTokenResponse>, ErrorResponseStatus<'static>> { + let db = state.0.database; + + if user.verified.is_some() { + return Ok(Json(VerificationTokenResponse { verified: true })); + } + + let token = db.user_verification_token(user.id).await.map_err(|e| { + error!("Failed to read user token: {e}"); + + ErrorResponse::reply("Failed to verify").with_status(StatusCode::INTERNAL_SERVER_ERROR) + })?; + + if token_request.token == token { + db.verify_user(user.id).await.map_err(|e| { + error!("Failed to verify user: {e}"); + + ErrorResponse::reply("Failed to verify").with_status(StatusCode::INTERNAL_SERVER_ERROR) + })?; + } else { + info!( + "Incorrect verification token {} vs {}", + token_request.token, token + ); + + return Ok(Json(VerificationTokenResponse { verified: false })); + } + + Ok(Json(VerificationTokenResponse { verified: true })) +} + #[instrument(skip_all, fields(user.id = user.id, change_password))] pub async fn change_password<DB: Database>( UserAuth(mut user): UserAuth, |
