From a6ce34b7de8dabf1fda3c625ff47112e16f52e33 Mon Sep 17 00:00:00 2001
From: Lennart Kloock <lennart.kloock@protonmail.com>
Date: Sun, 22 Oct 2023 00:05:19 +0200
Subject: [PATCH] feat(api): change password endpoint

---
 platform/api/src/api/v1/gql/mutations/auth.rs |  4 +-
 platform/api/src/api/v1/gql/mutations/user.rs | 54 +++++++++++++++++++
 schema.graphql                                | 10 ++++
 3 files changed, 66 insertions(+), 2 deletions(-)

diff --git a/platform/api/src/api/v1/gql/mutations/auth.rs b/platform/api/src/api/v1/gql/mutations/auth.rs
index e5e4c5e3c..50c8fc159 100644
--- a/platform/api/src/api/v1/gql/mutations/auth.rs
+++ b/platform/api/src/api/v1/gql/mutations/auth.rs
@@ -83,12 +83,12 @@ impl AuthMutation {
         .bind(user.id)
         .bind(!user.totp_enabled)
         .bind(expires_at)
-        .fetch_one(&mut *tx)
+        .fetch_one(tx.as_mut())
         .await?;
 
         sqlx::query("UPDATE users SET last_login_at = NOW() WHERE id = $1")
             .bind(user.id)
-            .execute(&mut *tx)
+            .execute(tx.as_mut())
             .await?;
 
         tx.commit().await?;
diff --git a/platform/api/src/api/v1/gql/mutations/user.rs b/platform/api/src/api/v1/gql/mutations/user.rs
index 3c030f157..5a64c88b9 100644
--- a/platform/api/src/api/v1/gql/mutations/user.rs
+++ b/platform/api/src/api/v1/gql/mutations/user.rs
@@ -3,6 +3,7 @@ use bytes::Bytes;
 use prost::Message;
 
 use crate::api::middleware::auth::AuthError;
+use crate::api::v1::gql::validators::PasswordValidator;
 use crate::{
     api::v1::gql::{
         error::{GqlError, Result, ResultExt},
@@ -148,6 +149,59 @@ impl UserMutation {
         Ok(user.into())
     }
 
+    async fn password<'ctx>(
+        &self,
+        ctx: &Context<'_>,
+        #[graphql(desc = "Current password")] current_password: String,
+        #[graphql(desc = "New password", validator(custom = "PasswordValidator"))]
+        new_password: String,
+    ) -> Result<User> {
+        let global = ctx.get_global();
+        let request_context = ctx.get_req_context();
+
+        let auth = request_context
+            .auth()
+            .await?
+            .ok_or(GqlError::Auth(AuthError::NotLoggedIn))?;
+
+        let user = global
+            .user_by_id_loader
+            .load(auth.session.user_id.0)
+            .await
+            .map_err_gql("failed to fetch user")?
+            .map_err_gql(GqlError::NotFound("user"))?;
+
+        if !user.verify_password(&current_password) {
+            return Err(GqlError::InvalidInput {
+                fields: vec!["password"],
+                message: "wrong password",
+            }
+            .into());
+        }
+
+        let mut tx = global.db.begin().await?;
+
+        let user: database::User =
+            sqlx::query_as("UPDATE users SET password_hash = $1 WHERE id = $2 RETURNING *")
+                .bind(database::User::hash_password(&new_password))
+                .bind(user.id)
+                .fetch_one(tx.as_mut())
+                .await?;
+        
+        // Delete all sessions except current
+        sqlx::query("DELETE FROM user_sessions WHERE user_id = $1 AND id != $2")
+            .bind(user.id)
+            .bind(auth.session.id)
+            .execute(tx.as_mut())
+            .await?;
+
+        // TODO: Logout active connections
+
+        tx.commit().await?;
+
+        Ok(user.into())
+    }
+
     /// Follow or unfollow a user.
     async fn follow<'ctx>(
         &self,
diff --git a/schema.graphql b/schema.graphql
index 5842b2e6c..97aba37fa 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -351,6 +351,16 @@ type UserMutation {
 		"""
 		follow: Boolean!
 	): Boolean!
+	password(
+		"""
+		Current password
+		"""
+		currentPassword: String!
+		"""
+		New password
+		"""
+		newPassword: String!
+	): User!
 	twoFa: TwoFaMutation!
 }