diff --git a/ee/tabby-db/migrations/0014_password-reset.down.sql b/ee/tabby-db/migrations/0014_password-reset.down.sql new file mode 100644 index 000000000000..818ffa3e6bda --- /dev/null +++ b/ee/tabby-db/migrations/0014_password-reset.down.sql @@ -0,0 +1 @@ +DROP TABLE password_reset; diff --git a/ee/tabby-db/migrations/0014_password-reset.up.sql b/ee/tabby-db/migrations/0014_password-reset.up.sql new file mode 100644 index 000000000000..34edbe729cde --- /dev/null +++ b/ee/tabby-db/migrations/0014_password-reset.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE password_reset( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL UNIQUE, + code VARCHAR(36) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT (DATETIME('now')) +); diff --git a/ee/tabby-db/schema.sqlite b/ee/tabby-db/schema.sqlite index a5673c2840df..cdf23331d4b0 100644 Binary files a/ee/tabby-db/schema.sqlite and b/ee/tabby-db/schema.sqlite differ diff --git a/ee/tabby-db/src/lib.rs b/ee/tabby-db/src/lib.rs index 04c5a352ffdf..0ab038d32b6a 100644 --- a/ee/tabby-db/src/lib.rs +++ b/ee/tabby-db/src/lib.rs @@ -14,6 +14,7 @@ mod github_oauth_credential; mod google_oauth_credential; mod invitations; mod job_runs; +mod password_reset; mod path; mod refresh_tokens; mod repositories; diff --git a/ee/tabby-db/src/password_reset.rs b/ee/tabby-db/src/password_reset.rs new file mode 100644 index 000000000000..a1a43f88c89f --- /dev/null +++ b/ee/tabby-db/src/password_reset.rs @@ -0,0 +1,58 @@ +use anyhow::Result; +use chrono::{DateTime, Duration, Utc}; +use sqlx::{prelude::FromRow, query}; +use uuid::Uuid; + +use crate::DbConn; + +#[derive(FromRow)] +pub struct PasswordResetDAO { + pub user_id: i32, + pub code: String, + pub created_at: DateTime, +} + +impl DbConn { + pub async fn create_password_reset(&self, user_id: i32) -> Result { + let code = Uuid::new_v4().to_string(); + let time = Utc::now(); + query!( + "INSERT INTO password_reset (user_id, code, created_at) VALUES ($1, $2, $3) + ON CONFLICT(user_id) DO UPDATE SET code= $2, created_at = $3;", + user_id, + code, + time + ) + .execute(&self.pool) + .await?; + Ok(code) + } + + pub async fn delete_password_reset_by_user_id(&self, user_id: i32) -> Result<()> { + query!("DELETE FROM password_reset WHERE user_id = ?", user_id) + .execute(&self.pool) + .await?; + Ok(()) + } + + pub async fn get_password_reset_by_user_id( + &self, + user_id: i32, + ) -> Result> { + let password_reset = sqlx::query_as( + "SELECT user_id, code, created_at FROM password_reset WHERE user_id = ?;", + ) + .bind(user_id) + .fetch_optional(&self.pool) + .await?; + Ok(password_reset) + } + + pub async fn delete_expired_password_resets(&self) -> Result<()> { + let time = Utc::now() - Duration::hours(1); + query!("DELETE FROM password_reset WHERE created_at < ?", time) + .execute(&self.pool) + .await?; + Ok(()) + } +} diff --git a/ee/tabby-db/src/users.rs b/ee/tabby-db/src/users.rs index 32513ad740f7..432defb3e34c 100644 --- a/ee/tabby-db/src/users.rs +++ b/ee/tabby-db/src/users.rs @@ -191,6 +191,17 @@ impl DbConn { Ok(()) } } + + pub async fn update_user_password(&self, id: i32, password_encrypted: String) -> Result<()> { + query!( + "UPDATE users SET password_encrypted = ? WHERE id = ?", + password_encrypted, + id + ) + .execute(&self.pool) + .await?; + Ok(()) + } } fn generate_auth_token() -> String { diff --git a/ee/tabby-webserver/email_templates/password_reset.html b/ee/tabby-webserver/email_templates/password_reset.html new file mode 100644 index 000000000000..83d374ddcdb0 --- /dev/null +++ b/ee/tabby-webserver/email_templates/password_reset.html @@ -0,0 +1,9 @@ +Reset your Tabby password +--- +You recently requested a password reset for your TabbyML account {{EMAIL}}. Please click the link below to reset your password. + +{{EXTERNAL_URL}}/passwordReset?code={{CODE}} + +Best regards, + +The Tabby Team