diff --git a/migrations/2025-06-03-103311_remove_email_tokens/down.sql b/migrations/2025-06-03-103311_remove_email_tokens/down.sql deleted file mode 100644 index e8f0350..0000000 --- a/migrations/2025-06-03-103311_remove_email_tokens/down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- This file should undo anything in `up.sql` -CREATE TABLE email_tokens ( - token VARCHAR(64) NOT NULL, - user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), - created_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (token, user_uuid) -); diff --git a/migrations/2025-06-03-103311_remove_email_tokens/up.sql b/migrations/2025-06-03-103311_remove_email_tokens/up.sql deleted file mode 100644 index b41afe5..0000000 --- a/migrations/2025-06-03-103311_remove_email_tokens/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -DROP TABLE email_tokens; diff --git a/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql b/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql deleted file mode 100644 index 009d9e4..0000000 --- a/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- This file should undo anything in `up.sql` -CREATE TABLE password_reset_tokens ( - token VARCHAR(64) NOT NULL, - user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), - created_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (token, user_uuid) -); diff --git a/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql b/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql deleted file mode 100644 index 181d7c5..0000000 --- a/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -DROP TABLE password_reset_tokens; diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index 444266c..4373a82 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -26,11 +26,13 @@ struct Query { /// #[get("/reset-password")] pub async fn get(query: web::Query, data: web::Data) -> Result { + let mut conn = data.pool.get().await?; + if let Ok(password_reset_token) = - PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await + PasswordResetToken::get_with_identifier(&mut conn, query.identifier.clone()).await { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { - password_reset_token.delete(&data).await?; + password_reset_token.delete(&mut conn).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), @@ -72,8 +74,15 @@ pub async fn post( reset_password: web::Json, data: web::Data, ) -> Result { + let mut conn = data.pool.get().await?; + let password_reset_token = - PasswordResetToken::get(&data, reset_password.token.clone()).await?; + PasswordResetToken::get(&mut conn, reset_password.token.clone()).await?; + + if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(24) { + password_reset_token.delete(&mut conn).await?; + return Ok(HttpResponse::Gone().finish()); + } password_reset_token .set_password(&data, reset_password.password.clone()) diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index e596500..0f23649 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -46,15 +46,20 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; - let email_token = EmailToken::get(&data, me.uuid).await?; + let email_token = EmailToken::get(&mut conn, me.uuid).await?; if query.token != email_token.token { return Ok(HttpResponse::Unauthorized().finish()); } + if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(24) { + email_token.delete(&mut conn).await?; + return Ok(HttpResponse::Gone().finish()); + } + me.verify_email(&mut conn).await?; - email_token.delete(&data).await?; + email_token.delete(&mut conn).await?; Ok(HttpResponse::Ok().finish()) } @@ -85,9 +90,9 @@ pub async fn post(req: HttpRequest, data: web::Data) -> Result Duration::hours(1) { - email_token.delete(&data).await?; + email_token.delete(&mut conn).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs index f55de8c..e458cf7 100644 --- a/src/objects/email_token.rs +++ b/src/objects/email_token.rs @@ -1,13 +1,19 @@ use chrono::Utc; +use diesel::{ + ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, dsl::now, + insert_into, +}; +use diesel_async::RunQueryDsl; use lettre::message::MultiPart; -use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Data, error::Error, utils::generate_refresh_token}; +use crate::{Conn, Data, error::Error, schema::email_tokens, utils::generate_refresh_token}; use super::Me; -#[derive(Serialize, Deserialize)] +#[derive(Selectable, Queryable)] +#[diesel(table_name = email_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] pub struct EmailToken { user_uuid: Uuid, pub token: String, @@ -15,8 +21,13 @@ pub struct EmailToken { } impl EmailToken { - pub async fn get(data: &Data, user_uuid: Uuid) -> Result { - let email_token = serde_json::from_str(&data.get_cache_key(format!("{}_email_verify", user_uuid)).await?)?; + pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { + use email_tokens::dsl; + let email_token = dsl::email_tokens + .filter(dsl::user_uuid.eq(user_uuid)) + .select(EmailToken::as_select()) + .get_result(conn) + .await?; Ok(email_token) } @@ -25,14 +36,17 @@ impl EmailToken { pub async fn new(data: &Data, me: Me) -> Result<(), Error> { let token = generate_refresh_token()?; - let email_token = EmailToken { - user_uuid: me.uuid, - token: token.clone(), - // TODO: Check if this can be replaced with something built into valkey - created_at: Utc::now() - }; + let mut conn = data.pool.get().await?; - data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400).await?; + use email_tokens::dsl; + insert_into(email_tokens::table) + .values(( + dsl::user_uuid.eq(me.uuid), + dsl::token.eq(&token), + dsl::created_at.eq(now), + )) + .execute(&mut conn) + .await?; let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; @@ -53,8 +67,13 @@ impl EmailToken { Ok(()) } - pub async fn delete(&self, data: &Data) -> Result<(), Error> { - data.del_cache_key(format!("{}_email_verify", self.user_uuid)).await?; + pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { + use email_tokens::dsl; + delete(email_tokens::table) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .filter(dsl::token.eq(&self.token)) + .execute(conn) + .await?; Ok(()) } diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs index 0376d88..e3c7bca 100644 --- a/src/objects/password_reset_token.rs +++ b/src/objects/password_reset_token.rs @@ -4,21 +4,23 @@ use argon2::{ }; use chrono::Utc; use diesel::{ - ExpressionMethods, QueryDsl, update, + ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, dsl::now, + insert_into, update, }; use diesel_async::RunQueryDsl; use lettre::message::MultiPart; -use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Data, + Conn, Data, error::Error, - schema::users, + schema::{password_reset_tokens, users}, utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier}, }; -#[derive(Serialize, Deserialize)] +#[derive(Selectable, Queryable)] +#[diesel(table_name = password_reset_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] pub struct PasswordResetToken { user_uuid: Uuid, pub token: String, @@ -26,22 +28,29 @@ pub struct PasswordResetToken { } impl PasswordResetToken { - pub async fn get(data: &Data, token: String) -> Result { - let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(format!("{}", token)).await?)?; - let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?; + pub async fn get(conn: &mut Conn, token: String) -> Result { + use password_reset_tokens::dsl; + let password_reset_token = dsl::password_reset_tokens + .filter(dsl::token.eq(token)) + .select(PasswordResetToken::as_select()) + .get_result(conn) + .await?; Ok(password_reset_token) } pub async fn get_with_identifier( - data: &Data, + conn: &mut Conn, identifier: String, ) -> Result { - let mut conn = data.pool.get().await?; + let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; - let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - - let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?; + use password_reset_tokens::dsl; + let password_reset_token = dsl::password_reset_tokens + .filter(dsl::user_uuid.eq(user_uuid)) + .select(PasswordResetToken::as_select()) + .get_result(conn) + .await?; Ok(password_reset_token) } @@ -63,14 +72,15 @@ impl PasswordResetToken { .get_result(&mut conn) .await?; - let password_reset_token = PasswordResetToken { - user_uuid, - token: token.clone(), - created_at: Utc::now(), - }; - - data.set_cache_key(format!("{}_password_reset", user_uuid), password_reset_token, 86400).await?; - data.set_cache_key(token.clone(), user_uuid, 86400).await?; + use password_reset_tokens::dsl; + insert_into(password_reset_tokens::table) + .values(( + dsl::user_uuid.eq(user_uuid), + dsl::token.eq(&token), + dsl::created_at.eq(now), + )) + .execute(&mut conn) + .await?; let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; @@ -134,12 +144,16 @@ impl PasswordResetToken { data.mail_client.send_mail(email).await?; - self.delete(&data).await + self.delete(&mut conn).await } - pub async fn delete(&self, data: &Data) -> Result<(), Error> { - data.del_cache_key(format!("{}_password_reset", &self.user_uuid)).await?; - data.del_cache_key(format!("{}", &self.token)).await?; + pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { + use password_reset_tokens::dsl; + delete(password_reset_tokens::table) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .filter(dsl::token.eq(&self.token)) + .execute(conn) + .await?; Ok(()) } diff --git a/src/schema.rs b/src/schema.rs index aaef9c1..09ea7a3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -31,6 +31,15 @@ diesel::table! { } } +diesel::table! { + email_tokens (token, user_uuid) { + #[max_length = 64] + token -> Varchar, + user_uuid -> Uuid, + created_at -> Timestamptz, + } +} + diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -80,6 +89,15 @@ diesel::table! { } } +diesel::table! { + password_reset_tokens (token, user_uuid) { + #[max_length = 64] + token -> Varchar, + user_uuid -> Uuid, + created_at -> Timestamptz, + } +} + diesel::table! { refresh_tokens (token) { #[max_length = 64] @@ -137,6 +155,7 @@ diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> users (uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid)); diesel::joinable!(channels -> guilds (guild_uuid)); +diesel::joinable!(email_tokens -> users (user_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); diesel::joinable!(guilds -> users (owner_uuid)); @@ -145,6 +164,7 @@ diesel::joinable!(invites -> guilds (guild_uuid)); diesel::joinable!(invites -> users (user_uuid)); diesel::joinable!(messages -> channels (channel_uuid)); diesel::joinable!(messages -> users (user_uuid)); +diesel::joinable!(password_reset_tokens -> users (user_uuid)); diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(role_members -> guild_members (member_uuid)); diesel::joinable!(roles -> guilds (guild_uuid)); @@ -153,11 +173,13 @@ diesel::allow_tables_to_appear_in_same_query!( access_tokens, channel_permissions, channels, + email_tokens, guild_members, guilds, instance_permissions, invites, messages, + password_reset_tokens, refresh_tokens, role_members, roles,