aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-server
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@atuin.sh>2026-01-27 13:56:18 -0800
committerGitHub <noreply@github.com>2026-01-27 13:56:18 -0800
commite2b421c88479857831e938acb311aef5127f38b4 (patch)
tree0ff160c378f1c151ecb30fa0329aafcee72b8d9d /crates/atuin-server
parentchore(deps): cleanup of dep versions (#3106) (diff)
downloadatuin-e2b421c88479857831e938acb311aef5127f38b4.zip
feat: remove user verification functionality (#3108)
<!-- Thank you for making a PR! Bug fixes are always welcome, but if you're adding a new feature or changing an existing one, we'd really appreciate if you open an issue, post on the forum, or drop in on Discord --> ## Checks - [ ] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [ ] I have checked that there are no existing pull requests for the same thing --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Diffstat (limited to '')
-rw-r--r--crates/atuin-server-database/src/lib.rs4
-rw-r--r--crates/atuin-server-database/src/models.rs1
-rw-r--r--crates/atuin-server-postgres/migrations/20260127000000_remove-email-verification.sql2
-rw-r--r--crates/atuin-server-postgres/src/lib.rs105
-rw-r--r--crates/atuin-server-postgres/src/wrappers.rs1
-rw-r--r--crates/atuin-server-sqlite/migrations/20260127000000_remove-email-verification.sql2
-rw-r--r--crates/atuin-server-sqlite/src/lib.rs99
-rw-r--r--crates/atuin-server-sqlite/src/wrappers.rs1
-rw-r--r--crates/atuin-server/Cargo.toml1
-rw-r--r--crates/atuin-server/src/handlers/user.rs105
-rw-r--r--crates/atuin-server/src/router.rs5
-rw-r--r--crates/atuin-server/src/settings.rs29
12 files changed, 22 insertions, 333 deletions
diff --git a/crates/atuin-server-database/src/lib.rs b/crates/atuin-server-database/src/lib.rs
index a4ddf23c..6000a530 100644
--- a/crates/atuin-server-database/src/lib.rs
+++ b/crates/atuin-server-database/src/lib.rs
@@ -107,10 +107,6 @@ pub trait Database: Sized + Clone + Send + Sync + 'static {
async fn get_user_session(&self, u: &User) -> DbResult<Session>;
async fn add_user(&self, user: &NewUser) -> DbResult<i64>;
- async fn user_verified(&self, id: i64) -> DbResult<bool>;
- async fn verify_user(&self, id: i64) -> DbResult<()>;
- async fn user_verification_token(&self, id: i64) -> DbResult<String>;
-
async fn update_user_password(&self, u: &User) -> DbResult<()>;
async fn count_history(&self, user: &User) -> DbResult<i64>;
diff --git a/crates/atuin-server-database/src/models.rs b/crates/atuin-server-database/src/models.rs
index 894ac7f6..b71a9bc9 100644
--- a/crates/atuin-server-database/src/models.rs
+++ b/crates/atuin-server-database/src/models.rs
@@ -32,7 +32,6 @@ pub struct User {
pub username: String,
pub email: String,
pub password: String,
- pub verified: Option<OffsetDateTime>,
}
pub struct Session {
diff --git a/crates/atuin-server-postgres/migrations/20260127000000_remove-email-verification.sql b/crates/atuin-server-postgres/migrations/20260127000000_remove-email-verification.sql
new file mode 100644
index 00000000..15309920
--- /dev/null
+++ b/crates/atuin-server-postgres/migrations/20260127000000_remove-email-verification.sql
@@ -0,0 +1,2 @@
+drop table if exists user_verification_token;
+alter table users drop column if exists verified_at;
diff --git a/crates/atuin-server-postgres/src/lib.rs b/crates/atuin-server-postgres/src/lib.rs
index 54ba2ee8..ce101d8d 100644
--- a/crates/atuin-server-postgres/src/lib.rs
+++ b/crates/atuin-server-postgres/src/lib.rs
@@ -5,7 +5,6 @@ use rand::Rng;
use async_trait::async_trait;
use atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus};
-use atuin_common::utils::crypto_random_string;
use atuin_server_database::models::{History, NewHistory, NewSession, NewUser, Session, User};
use atuin_server_database::{Database, DbError, DbResult, DbSettings};
use futures_util::TryStreamExt;
@@ -13,7 +12,7 @@ use sqlx::Row;
use sqlx::postgres::PgPoolOptions;
use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
-use tracing::{instrument, trace};
+use tracing::instrument;
use uuid::Uuid;
use wrappers::{DbHistory, DbRecord, DbSession, DbUser};
@@ -121,100 +120,18 @@ impl Database for Postgres {
#[instrument(skip_all)]
async fn get_user(&self, username: &str) -> DbResult<User> {
- sqlx::query_as(
- "select id, username, email, password, verified_at from users where username = $1",
- )
- .bind(username)
- .fetch_one(self.read_pool())
- .await
- .map_err(fix_error)
- .map(|DbUser(user)| user)
- }
-
- #[instrument(skip_all)]
- async fn user_verified(&self, id: i64) -> DbResult<bool> {
- let res: (bool,) =
- sqlx::query_as("select verified_at is not null from users where id = $1")
- .bind(id)
- .fetch_one(self.read_pool())
- .await
- .map_err(fix_error)?;
-
- Ok(res.0)
- }
-
- #[instrument(skip_all)]
- async fn verify_user(&self, id: i64) -> DbResult<()> {
- sqlx::query(
- "update users set verified_at = (current_timestamp at time zone 'utc') where id=$1",
- )
- .bind(id)
- .execute(&self.pool)
- .await
- .map_err(fix_error)?;
-
- Ok(())
- }
-
- /// Return a valid verification token for the user
- /// If the user does not have any token, create one, insert it, and return
- /// If the user has a token, but it's invalid, delete it, create a new one, return
- /// If the user already has a valid token, return it
- #[instrument(skip_all)]
- async fn user_verification_token(&self, id: i64) -> DbResult<String> {
- const TOKEN_VALID_MINUTES: i64 = 15;
-
- // First we check if there is a verification token
- let token: Option<(String, sqlx::types::time::OffsetDateTime)> = sqlx::query_as(
- "select token, valid_until from user_verification_token where user_id = $1",
- )
- .bind(id)
- .fetch_optional(&self.pool)
- .await
- .map_err(fix_error)?;
-
- let token = if let Some((token, valid_until)) = token {
- trace!("Token for user {id} valid until {valid_until}");
-
- // We have a token, AND it's still valid
- if valid_until > time::OffsetDateTime::now_utc() {
- token
- } else {
- // token has expired. generate a new one, return it
- let token = crypto_random_string::<24>();
-
- sqlx::query("update user_verification_token set token = $2, valid_until = $3 where user_id=$1")
- .bind(id)
- .bind(&token)
- .bind(time::OffsetDateTime::now_utc() + time::Duration::minutes(TOKEN_VALID_MINUTES))
- .execute(&self.pool)
- .await
- .map_err(fix_error)?;
-
- token
- }
- } else {
- // No token in the database! Generate one, insert it
- let token = crypto_random_string::<24>();
-
- sqlx::query("insert into user_verification_token (user_id, token, valid_until) values ($1, $2, $3)")
- .bind(id)
- .bind(&token)
- .bind(time::OffsetDateTime::now_utc() + time::Duration::minutes(TOKEN_VALID_MINUTES))
- .execute(&self.pool)
- .await
- .map_err(fix_error)?;
-
- token
- };
-
- Ok(token)
+ sqlx::query_as("select id, username, email, password from users where username = $1")
+ .bind(username)
+ .fetch_one(self.read_pool())
+ .await
+ .map_err(fix_error)
+ .map(|DbUser(user)| user)
}
#[instrument(skip_all)]
async fn get_session_user(&self, token: &str) -> DbResult<User> {
sqlx::query_as(
- "select users.id, users.username, users.email, users.password, users.verified_at from users
+ "select users.id, users.username, users.email, users.password from users
inner join sessions
on users.id = sessions.user_id
and sessions.token = $1",
@@ -431,12 +348,6 @@ impl Database for Postgres {
.await
.map_err(fix_error)?;
- sqlx::query("delete from user_verification_token where user_id = $1")
- .bind(u.id)
- .execute(&self.pool)
- .await
- .map_err(fix_error)?;
-
sqlx::query("delete from total_history_count_user where user_id = $1")
.bind(u.id)
.execute(&self.pool)
diff --git a/crates/atuin-server-postgres/src/wrappers.rs b/crates/atuin-server-postgres/src/wrappers.rs
index 0d6a0ee6..cde4134c 100644
--- a/crates/atuin-server-postgres/src/wrappers.rs
+++ b/crates/atuin-server-postgres/src/wrappers.rs
@@ -16,7 +16,6 @@ impl<'a> FromRow<'a, PgRow> for DbUser {
username: row.try_get("username")?,
email: row.try_get("email")?,
password: row.try_get("password")?,
- verified: row.try_get("verified_at")?,
}))
}
}
diff --git a/crates/atuin-server-sqlite/migrations/20260127000000_remove-email-verification.sql b/crates/atuin-server-sqlite/migrations/20260127000000_remove-email-verification.sql
new file mode 100644
index 00000000..15309920
--- /dev/null
+++ b/crates/atuin-server-sqlite/migrations/20260127000000_remove-email-verification.sql
@@ -0,0 +1,2 @@
+drop table if exists user_verification_token;
+alter table users drop column if exists verified_at;
diff --git a/crates/atuin-server-sqlite/src/lib.rs b/crates/atuin-server-sqlite/src/lib.rs
index 83d05ea5..d69258c4 100644
--- a/crates/atuin-server-sqlite/src/lib.rs
+++ b/crates/atuin-server-sqlite/src/lib.rs
@@ -1,10 +1,7 @@
use std::str::FromStr;
use async_trait::async_trait;
-use atuin_common::{
- record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus},
- utils::crypto_random_string,
-};
+use atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus};
use atuin_server_database::{
Database, DbError, DbResult, DbSettings,
models::{History, NewHistory, NewSession, NewUser, Session, User},
@@ -67,9 +64,9 @@ impl Database for Sqlite {
#[instrument(skip_all)]
async fn get_session_user(&self, token: &str) -> DbResult<User> {
sqlx::query_as(
- "select users.id, users.username, users.email, users.password, users.verified_at from users
- inner join sessions
- on users.id = sessions.user_id
+ "select users.id, users.username, users.email, users.password from users
+ inner join sessions
+ on users.id = sessions.user_id
and sessions.token = $1",
)
.bind(token)
@@ -99,14 +96,12 @@ impl Database for Sqlite {
#[instrument(skip_all)]
async fn get_user(&self, username: &str) -> DbResult<User> {
- sqlx::query_as(
- "select id, username, email, password, verified_at from users where username = $1",
- )
- .bind(username)
- .fetch_one(&self.pool)
- .await
- .map_err(fix_error)
- .map(|DbUser(user)| user)
+ sqlx::query_as("select id, username, email, password from users where username = $1")
+ .bind(username)
+ .fetch_one(&self.pool)
+ .await
+ .map_err(fix_error)
+ .map(|DbUser(user)| user)
}
#[instrument(skip_all)]
@@ -142,80 +137,6 @@ impl Database for Sqlite {
}
#[instrument(skip_all)]
- async fn user_verified(&self, id: i64) -> DbResult<bool> {
- let res: (bool,) =
- sqlx::query_as("select verified_at is not null from users where id = $1")
- .bind(id)
- .fetch_one(&self.pool)
- .await
- .map_err(fix_error)?;
-
- Ok(res.0)
- }
-
- #[instrument(skip_all)]
- async fn verify_user(&self, id: i64) -> DbResult<()> {
- sqlx::query(
- "update users set verified_at = (current_timestamp at time zone 'utc') where id=$1",
- )
- .bind(id)
- .execute(&self.pool)
- .await
- .map_err(fix_error)?;
-
- Ok(())
- }
-
- #[instrument(skip_all)]
- async fn user_verification_token(&self, id: i64) -> DbResult<String> {
- const TOKEN_VALID_MINUTES: i64 = 15;
-
- // First we check if there is a verification token
- let token: Option<(String, sqlx::types::time::OffsetDateTime)> = sqlx::query_as(
- "select token, valid_until from user_verification_token where user_id = $1",
- )
- .bind(id)
- .fetch_optional(&self.pool)
- .await
- .map_err(fix_error)?;
-
- let token = if let Some((token, valid_until)) = token {
- // We have a token, AND it's still valid
- if valid_until > time::OffsetDateTime::now_utc() {
- token
- } else {
- // token has expired. generate a new one, return it
- let token = crypto_random_string::<24>();
-
- sqlx::query("update user_verification_token set token = $2, valid_until = $3 where user_id=$1")
- .bind(id)
- .bind(&token)
- .bind(time::OffsetDateTime::now_utc() + time::Duration::minutes(TOKEN_VALID_MINUTES))
- .execute(&self.pool)
- .await
- .map_err(fix_error)?;
-
- token
- }
- } else {
- // No token in the database! Generate one, insert it
- let token = crypto_random_string::<24>();
-
- sqlx::query("insert into user_verification_token (user_id, token, valid_until) values ($1, $2, $3)")
- .bind(id)
- .bind(&token)
- .bind(time::OffsetDateTime::now_utc() + time::Duration::minutes(TOKEN_VALID_MINUTES))
- .execute(&self.pool)
- .await
- .map_err(fix_error)?;
-
- token
- };
-
- Ok(token)
- }
-
- #[instrument(skip_all)]
async fn update_user_password(&self, user: &User) -> DbResult<()> {
sqlx::query(
"update users
diff --git a/crates/atuin-server-sqlite/src/wrappers.rs b/crates/atuin-server-sqlite/src/wrappers.rs
index 3f2262c3..2f1230c2 100644
--- a/crates/atuin-server-sqlite/src/wrappers.rs
+++ b/crates/atuin-server-sqlite/src/wrappers.rs
@@ -15,7 +15,6 @@ impl<'a> FromRow<'a, SqliteRow> for DbUser {
username: row.try_get("username")?,
email: row.try_get("email")?,
password: row.try_get("password")?,
- verified: row.try_get("verified_at")?,
}))
}
}
diff --git a/crates/atuin-server/Cargo.toml b/crates/atuin-server/Cargo.toml
index 915ceb14..ea647f38 100644
--- a/crates/atuin-server/Cargo.toml
+++ b/crates/atuin-server/Cargo.toml
@@ -32,4 +32,3 @@ argon2 = "0.5"
semver = { workspace = true }
metrics-exporter-prometheus = "0.18"
metrics = "0.24"
-postmark = {version= "0.11", features=["reqwest", "reqwest-rustls-tls"]}
diff --git a/crates/atuin-server/src/handlers/user.rs b/crates/atuin-server/src/handlers/user.rs
index 4edd1787..c6fec51e 100644
--- a/crates/atuin-server/src/handlers/user.rs
+++ b/crates/atuin-server/src/handlers/user.rs
@@ -13,8 +13,6 @@ use axum::{
};
use metrics::counter;
-use postmark::{Query, reqwest::PostmarkClient};
-
use rand::rngs::OsRng;
use tracing::{debug, error, info, instrument};
@@ -178,109 +176,6 @@ 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;
- debug!("request to verify user {}", user.username);
-
- 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 = match settings.mail.postmark.token {
- Some(token) => token,
- _ => {
- error!("Failed to verify email: got None for postmark token");
- 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/")
- .server_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,
diff --git a/crates/atuin-server/src/router.rs b/crates/atuin-server/src/router.rs
index 9d4f7d44..0c41d5e6 100644
--- a/crates/atuin-server/src/router.rs
+++ b/crates/atuin-server/src/router.rs
@@ -134,11 +134,6 @@ pub fn router<DB: Database>(database: DB, settings: Settings) -> Router {
.route("/record", get(handlers::record::index))
.route("/record/next", get(handlers::record::next))
.route("/api/v0/me", get(handlers::v0::me::get))
- .route("/api/v0/account/verify", post(handlers::user::verify_user))
- .route(
- "/api/v0/account/send-verification",
- post(handlers::user::send_verification),
- )
.route("/api/v0/record", post(handlers::v0::record::post))
.route("/api/v0/record", get(handlers::v0::record::index))
.route("/api/v0/record/next", get(handlers::v0::record::next))
diff --git a/crates/atuin-server/src/settings.rs b/crates/atuin-server/src/settings.rs
index 98d1d69f..3a612be9 100644
--- a/crates/atuin-server/src/settings.rs
+++ b/crates/atuin-server/src/settings.rs
@@ -8,33 +8,6 @@ use serde::{Deserialize, Serialize};
static EXAMPLE_CONFIG: &str = include_str!("../server.toml");
-#[derive(Default, Clone, Debug, Deserialize, Serialize)]
-pub struct Mail {
- #[serde(alias = "enable")]
- pub enabled: bool,
-
- /// Configuration for the postmark api client
- /// This is what we use for Atuin Cloud, the forum, etc.
- #[serde(default)]
- pub postmark: Postmark,
-
- #[serde(default)]
- pub verification: MailVerification,
-}
-
-#[derive(Default, Clone, Debug, Deserialize, Serialize)]
-pub struct Postmark {
- #[serde(alias = "token")]
- pub token: Option<String>,
-}
-
-#[derive(Default, Clone, Debug, Deserialize, Serialize)]
-pub struct MailVerification {
- #[serde(alias = "enable")]
- pub from: String,
- pub subject: String,
-}
-
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Metrics {
#[serde(alias = "enabled")]
@@ -65,7 +38,6 @@ pub struct Settings {
pub register_webhook_url: Option<String>,
pub register_webhook_username: String,
pub metrics: Metrics,
- pub mail: Mail,
/// Enable legacy sync v1 routes (history-based sync)
/// Set to false to use only the newer record-based sync
@@ -108,7 +80,6 @@ impl Settings {
.set_default("metrics.enable", false)?
.set_default("metrics.host", "127.0.0.1")?
.set_default("metrics.port", 9001)?
- .set_default("mail.enable", false)?
.set_default("sync_v1_enabled", true)?
.add_source(
Environment::with_prefix("atuin")