Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7c6169e
Initial commit
floscodes Sep 11, 2025
31b8513
chore(docs): add update user feature to docs site
floscodes Sep 11, 2025
287f5d8
chore: check if username and email already exist in update fn
floscodes Sep 12, 2025
3bb6bd9
Merge branch 'master' into feat/update-user-data-endpoint
kaplanelad Sep 21, 2025
763e501
fix(auth.rs): bring StatusCode into scope and fix spelling mistake
floscodes Sep 25, 2025
71e8920
fix(auth.rs): impl find_by_name for Model to find user by provided name
floscodes Sep 25, 2025
32f0280
fix(base_template): fix spelling mistake
floscodes Sep 26, 2025
0a0b25b
fix(base_template): refactor update fn
floscodes Sep 26, 2025
b65b2c7
style(base_template): refactor update fn in auth.rs
floscodes Sep 26, 2025
66003b6
Merge branch 'loco-rs:master' into feat/update-user-data-endpoint
floscodes Oct 8, 2025
fe3f2f7
Merge branch 'master' into feat/update-user-data-endpoint
kaplanelad Oct 20, 2025
b3bf82e
Merge branch 'master' into feat/update-user-data-endpoint
kaplanelad Oct 22, 2025
10e2a17
chore: allow identical usernames
floscodes Oct 22, 2025
ee8121b
Merge branch 'loco-rs:master' into feat/update-user-data-endpoint
floscodes Oct 28, 2025
76e4171
Merge branch 'master' into feat/update-user-data-endpoint
kaplanelad Dec 12, 2025
208b83d
Merge branch 'loco-rs:master' into feat/update-user-data-endpoint
floscodes Dec 15, 2025
4913f44
Merge branch 'loco-rs:master' into feat/update-user-data-endpoint
floscodes Jan 2, 2026
786a907
fix: update_user_data method and test
floscodes Jan 2, 2026
e9fecf5
Merge branch 'loco-rs:master' into feat/update-user-data-endpoint
floscodes Feb 26, 2026
8e4195c
Merge branch 'master' into feat/update-user-data-endpoint
kaplanelad Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs-site/content/docs/extras/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions loco-new/base_template/src/controllers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -132,6 +133,26 @@ async fn reset(State(ctx): State<AppContext>, Json(params): Json<ResetParams>) -
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<AppContext>,
Json(params): Json<RegisterParams>,
) -> Result<Response> {
if users::Model::find_by_email(&ctx.db, &params.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<AppContext>, Json(params): Json<LoginParams>) -> Result<Response> {
Expand Down Expand Up @@ -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))
Expand Down
33 changes: 33 additions & 0 deletions loco-new/base_template/src/models/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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
Expand Down Expand Up @@ -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<Model> {
self.name = ActiveValue::set(params.name);
self.email = ActiveValue::set(params.email);
self.reset_password(db, &params.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
Expand Down
36 changes: 36 additions & 0 deletions loco-new/base_template/tests/models/users.rs.t
Original file line number Diff line number Diff line change
Expand Up @@ -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::<App>().await.expect("Failed to boot test application");
seed::<App>(&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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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: ";<html>\r\n\r\n<body>\r\n This is a test content\r\n <a href=\"http://localhost:/verify/1111-2222-3333-4444\">\r\n Some test\r\n </a>\r\n</body>\r\n\r\n</html>\r\n",
},
)
Loading