feat: move password reset tokens to valkey
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful

Also just as useless to keep in DB
This commit is contained in:
Radical 2025-06-03 11:03:52 +00:00
parent b223dff4ba
commit 419f37b108
5 changed files with 37 additions and 62 deletions

View file

@ -0,0 +1,7 @@
-- 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)
);

View file

@ -0,0 +1,2 @@
-- Your SQL goes here
DROP TABLE password_reset_tokens;

View file

@ -26,13 +26,11 @@ struct Query {
/// ///
#[get("/reset-password")] #[get("/reset-password")]
pub async fn get(query: web::Query<Query>, data: web::Data<Data>) -> Result<HttpResponse, Error> { pub async fn get(query: web::Query<Query>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let mut conn = data.pool.get().await?;
if let Ok(password_reset_token) = if let Ok(password_reset_token) =
PasswordResetToken::get_with_identifier(&mut conn, query.identifier.clone()).await PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await
{ {
if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) {
password_reset_token.delete(&mut conn).await?; password_reset_token.delete(&data).await?;
} else { } else {
return Err(Error::TooManyRequests( return Err(Error::TooManyRequests(
"Please allow 1 hour before sending a new email".to_string(), "Please allow 1 hour before sending a new email".to_string(),
@ -74,15 +72,8 @@ pub async fn post(
reset_password: web::Json<ResetPassword>, reset_password: web::Json<ResetPassword>,
data: web::Data<Data>, data: web::Data<Data>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let mut conn = data.pool.get().await?;
let password_reset_token = let password_reset_token =
PasswordResetToken::get(&mut conn, reset_password.token.clone()).await?; PasswordResetToken::get(&data, 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 password_reset_token
.set_password(&data, reset_password.password.clone()) .set_password(&data, reset_password.password.clone())

View file

@ -4,23 +4,21 @@ use argon2::{
}; };
use chrono::Utc; use chrono::Utc;
use diesel::{ use diesel::{
ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, dsl::now, ExpressionMethods, QueryDsl, update,
insert_into, update,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use lettre::message::MultiPart; use lettre::message::MultiPart;
use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, Data,
error::Error, error::Error,
schema::{password_reset_tokens, users}, schema::users,
utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier}, utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier},
}; };
#[derive(Selectable, Queryable)] #[derive(Serialize, Deserialize)]
#[diesel(table_name = password_reset_tokens)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct PasswordResetToken { pub struct PasswordResetToken {
user_uuid: Uuid, user_uuid: Uuid,
pub token: String, pub token: String,
@ -28,29 +26,22 @@ pub struct PasswordResetToken {
} }
impl PasswordResetToken { impl PasswordResetToken {
pub async fn get(conn: &mut Conn, token: String) -> Result<PasswordResetToken, Error> { pub async fn get(data: &Data, token: String) -> Result<PasswordResetToken, Error> {
use password_reset_tokens::dsl; let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(format!("{}", token)).await?)?;
let password_reset_token = dsl::password_reset_tokens let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?;
.filter(dsl::token.eq(token))
.select(PasswordResetToken::as_select())
.get_result(conn)
.await?;
Ok(password_reset_token) Ok(password_reset_token)
} }
pub async fn get_with_identifier( pub async fn get_with_identifier(
conn: &mut Conn, data: &Data,
identifier: String, identifier: String,
) -> Result<PasswordResetToken, Error> { ) -> Result<PasswordResetToken, Error> {
let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; let mut conn = data.pool.get().await?;
use password_reset_tokens::dsl; let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?;
let password_reset_token = dsl::password_reset_tokens
.filter(dsl::user_uuid.eq(user_uuid)) let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?;
.select(PasswordResetToken::as_select())
.get_result(conn)
.await?;
Ok(password_reset_token) Ok(password_reset_token)
} }
@ -72,15 +63,14 @@ impl PasswordResetToken {
.get_result(&mut conn) .get_result(&mut conn)
.await?; .await?;
use password_reset_tokens::dsl; let password_reset_token = PasswordResetToken {
insert_into(password_reset_tokens::table) user_uuid,
.values(( token: token.clone(),
dsl::user_uuid.eq(user_uuid), created_at: Utc::now(),
dsl::token.eq(&token), };
dsl::created_at.eq(now),
)) data.set_cache_key(format!("{}_password_reset", user_uuid), password_reset_token, 86400).await?;
.execute(&mut conn) data.set_cache_key(token.clone(), user_uuid, 86400).await?;
.await?;
let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?;
@ -144,16 +134,12 @@ impl PasswordResetToken {
data.mail_client.send_mail(email).await?; data.mail_client.send_mail(email).await?;
self.delete(&mut conn).await self.delete(&data).await
} }
pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { pub async fn delete(&self, data: &Data) -> Result<(), Error> {
use password_reset_tokens::dsl; data.del_cache_key(format!("{}_password_reset", &self.user_uuid)).await?;
delete(password_reset_tokens::table) data.del_cache_key(format!("{}", &self.token)).await?;
.filter(dsl::user_uuid.eq(self.user_uuid))
.filter(dsl::token.eq(&self.token))
.execute(conn)
.await?;
Ok(()) Ok(())
} }

View file

@ -80,15 +80,6 @@ diesel::table! {
} }
} }
diesel::table! {
password_reset_tokens (token, user_uuid) {
#[max_length = 64]
token -> Varchar,
user_uuid -> Uuid,
created_at -> Timestamptz,
}
}
diesel::table! { diesel::table! {
refresh_tokens (token) { refresh_tokens (token) {
#[max_length = 64] #[max_length = 64]
@ -154,7 +145,6 @@ diesel::joinable!(invites -> guilds (guild_uuid));
diesel::joinable!(invites -> users (user_uuid)); diesel::joinable!(invites -> users (user_uuid));
diesel::joinable!(messages -> channels (channel_uuid)); diesel::joinable!(messages -> channels (channel_uuid));
diesel::joinable!(messages -> users (user_uuid)); diesel::joinable!(messages -> users (user_uuid));
diesel::joinable!(password_reset_tokens -> users (user_uuid));
diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(refresh_tokens -> users (uuid));
diesel::joinable!(role_members -> guild_members (member_uuid)); diesel::joinable!(role_members -> guild_members (member_uuid));
diesel::joinable!(roles -> guilds (guild_uuid)); diesel::joinable!(roles -> guilds (guild_uuid));
@ -168,7 +158,6 @@ diesel::allow_tables_to_appear_in_same_query!(
instance_permissions, instance_permissions,
invites, invites,
messages, messages,
password_reset_tokens,
refresh_tokens, refresh_tokens,
role_members, role_members,
roles, roles,