diff --git a/docs-site/content/docs/extras/authentication.md b/docs-site/content/docs/extras/authentication.md index 75cf45000..1fc6f1d98 100644 --- a/docs-site/content/docs/extras/authentication.md +++ b/docs-site/content/docs/extras/authentication.md @@ -37,6 +37,7 @@ $ cargo loco routes [POST] /api/auth/forgot [POST] /api/auth/login [POST] /api/auth/register +[POST] /api/auth/update [POST] /api/auth/reset [GET] /api/auth/verify [GET] /api/auth/current @@ -146,6 +147,24 @@ curl --location --request GET '127.0.0.1:5150/api/auth/current' \ --header 'Authorization: Bearer TOKEN' ``` +#### Update current user + +To update user data in the database, send name, email and password to update in the `update` endpoint. + +##### Example Curl request: + +```sh +curl --location '127.0.0.1:5150/api/auth/update' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer TOKEN' + --data '{ + "name": "new-name", + "email": "new-email@loco.rs", + "password": "new-password" + }' +``` +Loco will check if the new username and email already exist. If so, it will not update and return an error. + ### Creating an Authenticated Endpoint To establish an authenticated endpoint, import `controller::extractor::auth` from the `loco_rs` library and incorporate the auth middleware into the function endpoint parameters. diff --git a/loco-new/base_template/src/controllers/auth.rs b/loco-new/base_template/src/controllers/auth.rs index e66d44842..0276cf918 100644 --- a/loco-new/base_template/src/controllers/auth.rs +++ b/loco-new/base_template/src/controllers/auth.rs @@ -6,6 +6,7 @@ use crate::{ }, views::auth::{CurrentResponse, LoginResponse}, }; +use axum::debug_handler; use loco_rs::prelude::*; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -132,6 +133,26 @@ async fn reset(State(ctx): State, Json(params): Json) - format::json(()) } +/// Updates user data and returns the current user with updated data. +/// Returns an error if username or email already exist. +#[debug_handler] +async fn update( + auth: auth::JWT, + State(ctx): State, + Json(params): Json, +) -> Result { + if users::Model::find_by_email(&ctx.db, ¶ms.email).await.is_ok() { + return Err(Error::Message("Email already exists".to_string())); + } + let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid) + .await? + .into_active_model() + .update_user_data(&ctx.db, params) + .await?; + + format::json(CurrentResponse::new(&user)) +} + /// Creates a user login and returns a token #[debug_handler] async fn login(State(ctx): State, Json(params): Json) -> Result { @@ -266,6 +287,7 @@ pub fn routes() -> Routes { .add("/login", post(login)) .add("/forgot", post(forgot)) .add("/reset", post(reset)) + .add("/update", post(update)) .add("/current", get(current)) .add("/magic-link", post(magic_link)) .add("/magic-link/{token}", get(magic_link_verify)) diff --git a/loco-new/base_template/src/models/users.rs b/loco-new/base_template/src/models/users.rs index e6386d4f7..4321c2bde 100644 --- a/loco-new/base_template/src/models/users.rs +++ b/loco-new/base_template/src/models/users.rs @@ -87,6 +87,23 @@ impl Model { user.ok_or_else(|| ModelError::EntityNotFound) } + /// finds a user by the provided name + /// + /// # Errors + /// + /// When could not find user by the given token or DB query error + pub async fn find_by_name(db: &DatabaseConnection, name: &str) -> ModelResult { + let user = users::Entity::find() + .filter( + model::query::condition() + .eq(users::Column::Name, name) + .build(), + ) + .one(db) + .await?; + user.ok_or_else(|| ModelError::EntityNotFound) + } + /// finds a user by the provided verification token /// /// # Errors @@ -330,6 +347,22 @@ impl ActiveModel { self.update(db).await.map_err(ModelError::from) } + /// Changes the user data and updates it in the database. + /// + /// # Errors + /// + /// when has DB query error + pub async fn update_user_data( + mut self, + db: &DatabaseConnection, + params: RegisterParams, + ) -> ModelResult { + self.name = ActiveValue::set(params.name); + self.email = ActiveValue::set(params.email); + self.reset_password(db, ¶ms.password).await?; + self.update(db).await.map_err(ModelError::from) + } + /// Creates a magic link token for passwordless authentication. /// /// Generates a random token with a specified length and sets an expiration time diff --git a/loco-new/base_template/tests/models/users.rs.t b/loco-new/base_template/tests/models/users.rs.t index ef997e9f5..3a786078b 100644 --- a/loco-new/base_template/tests/models/users.rs.t +++ b/loco-new/base_template/tests/models/users.rs.t @@ -227,6 +227,42 @@ async fn can_reset_password() { assert!(user.verify_password("new-password"), "Password verification failed for new password"); } +#[tokio::test] +#[serial] +async fn can_update_user_data() { + configure_insta!(); + + let boot = boot_test::().await.expect("Failed to boot test application"); + seed::(&boot.app_context).await.expect("Failed to seed database"); + + let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") + .await + .expect("Failed to find user by PID"); + + let update_params = RegisterParams { + name: "new-name".to_string(), + email: "new-email@example.com".to_string(), + password: "new-password".to_string(), + }; + + let result = user + .clone() + .into_active_model() + .update_user_data(&boot.app_context.db, update_params) + .await; + + assert!(result.is_ok(), "Failed to update user data"); + + let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") + .await + .expect("Failed to find user by PID after update"); + + + + assert_eq!(user.name, "new-name", "Expected name to be updated"); + assert_eq!(user.email, "new-email@example.com", "Expected email to be updated"); + assert!(user.verify_password("new-password"), "Password verification failed for new password"); +} #[tokio::test] #[serial] diff --git a/src/mailer/snapshots/loco_rs__mailer__template__tests__can_render_template.snap.new b/src/mailer/snapshots/loco_rs__mailer__template__tests__can_render_template.snap.new new file mode 100644 index 000000000..9ebcd397c --- /dev/null +++ b/src/mailer/snapshots/loco_rs__mailer__template__tests__can_render_template.snap.new @@ -0,0 +1,12 @@ +--- +source: src/mailer/template.rs +assertion_line: 94 +expression: "Template::new(&include_dir!(\"tests/fixtures/email_template/test\")).render(&args)" +--- +Ok( + Content { + subject: "Test Can render test template\r\n", + text: "Welcome to test: Can render test template,\r\n\r\n http://localhost/verify/<%= verifyToken %>\r\n", + html: ";\r\n\r\n\r\n This is a test content\r\n \r\n Some test\r\n \r\n\r\n\r\n\r\n", + }, +)