We're implementing a login process for our actix-web learning application. We undertake some general updates to get ready to support login. We then implement a new /api/login
route, which supports both application/x-www-form-urlencoded
and application/json
content types. In this post, we only implement deserialising the submitted request data, then echo some response. We also add a login page via route /ui/login
.
🚀 Please note, complete code for this post can be downloaded from GitHub with:
git clone -b v0.5.0 https://github.com/behai-nguyen/rust_web_01.git
The actix-web learning application mentioned above has been discussed in the following four (4) previous posts:
- Rust web application: MySQL server, sqlx, actix-web and tera.
- Rust: learning actix-web middleware 01.
- Rust: retrofit integration tests to an existing actix-web application.
- Rust: adding actix-session and actix-identity to an existing actix-web application.
The code we're developing in this post is a continuation of the code from the fourth post above. 🚀 To get the code of this fourth post, please use the following command:
git clone -b v0.4.0 https://github.com/behai-nguyen/rust_web_01.git
-- Note the tag v0.4.0
.
As already mentioned in the introduction above, in this post, our main focus of the login process is deserialising both application/x-www-form-urlencoded
and application/json
into a struct
ready to support login. I struggle with this issue a little, I document it as part of my Rust learning journey.
This post introduces a few new modules, some MySQL migration scripts, and a new login HTML page. The updated directory layout for the project is in the screenshot below:
Table of contents
- ❶ Update Rust to use latest actix-cors
- ❷ Add new fields
email
andpassword
to theemployees
table - ❸ Update
src/models.rs
- ❹ New module
src/auth_handlers.rs
which implements routes/ui/login
and/api/login
- ❺ Update
src/lib.rs
- ❻ The new
templates/auth/login.html
- ❼ Testing
❶ Update Rust to the latest version. At the time of this post, the latest version is 1.75.0
. The command to update:
▶️<code>Windows 10:</code> rustup update
▶️<code>Ubuntu 22.10:</code> $ rustup update
We've taken CORS into account when we started out this project in this first post.
I'm not quite certain what'd happened, but all of a sudden, it just rejects requests with message Origin is not allowed to make this request
.
-- Browsers have been updated, perhaps?
Failing to troubleshoot the problem, and seeing that actix-cors is at version 0.7.0
. I update it.
-- It does not work with Rust version 1.74.0
. This new version of actix-cors seems to fix the above request rejection issue.
❷ Update the employees
table, adding new fields email
and password
.
Using the migration tool SQLx CLI, which we've covered in Rust SQLx CLI: database migration with MySQL and PostgreSQL, to update the employees
table.
While inside the new directory migrations/mysql/
, see project directory layout above, create empty migration files 99999999999999_emp_email_pwd.up.sql
and 99999999999999_emp_email_pwd.down.sql
using the command:
▶️<code>Windows 10:</code> sqlx migrate add -r emp_email_pwd
▶️<code>Ubuntu 22.10:</code> $ sqlx migrate add -r emp_email_pwd
Populate the two script files with what we would like to do. Please see their contents on GitHub. To apply, run the below command, it'll take a little while to complete:
▶️<code>Windows 10:</code> sqlx migrate add -r emp_email_pwd
▶️<code>Ubuntu 22.10:</code> $ sqlx migrate add -r emp_email_pwd
❸ Update src/models.rs
to manage new fields employees.email
and employees.password
.
If we run cargo test
now, all integration tests should fail. All integration tests eventually call to get_employees(...)
, which does a select * from employees...
. Since the two new fields've been added to a specific order, field indexes in get_employees(...)
are out of order.
Module src/models.rs
gets the following updates:
-
pub email: String
field added tostruct Employee
. -
pub async fn get_employees(...)
updated to readEmployee.email
field. Other fields' indexes also get updated. - New
pub struct EmployeeLogin
. - New
pub async fn select_employee(...)
, which optionally selects an employee base on exact email match. - New
pub struct LoginSuccess
. - Add
"email": "siamak.bernardeschi.67115@gmail.com"
to existing tests.
Please see the updated src/models.rs
on GitHub. The documentation should be sufficient to help reading the code.
❹ New module src/auth_handlers.rs
, where new login routes /ui/login
and /api/login
are implemented.
● http://0.0.0.0:5000/ui/login
is a GET
route, which just returns the login.html
page as HTML.
● http://0.0.0.0:5000/api/login
is a POST
route. This is effectively the application login handler.
💥 This http://0.0.0.0:5000/api/login
route is the main focus of this post:
-- Its handler method accepts both application/x-www-form-urlencoded
and application/json
content types, and deserialises the byte stream to struct EmployeeLogin
mentioned above.
💥 Please also note that, as already mentioned, in this post, the login process does not do login, if successfully deserialised the submitted data, it'd just echo a confirmation response in the format of the request content type. If failed to deserialise, it'd send back a JSON response which has an error code and a text message.
Examples of valid submitted data for each content type:
✔️ Content type: application/x-www-form-urlencoded
; data: email=chirstian.koblick.10004@gmail.com&password=password
.
✔️ Content type: application/json
; data: {"email": "chirstian.koblick.10004@gmail.com", "password": "password"}
.
Content of src/auth_handlers.rs
#[post("/login")]
pub async fn login(
request: HttpRequest,
body: Bytes
) -> HttpResponse {
...
// Attempts to extract -- deserialising -- request body into EmployeeLogin.
let api_status = extract_employee_login(&body, request.content_type());
// Failed to deserialise request body. Returns the error as is.
if api_status.is_err() {
return HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&api_status.err().unwrap()).unwrap());
}
// Succeeded to deserialise request body.
let emp_login: EmployeeLogin = api_status.unwrap();
...
Note the second parameter body
, which is actix_web::web::Bytes, this is the byte stream presentation of the request body.
As an extractor, actix_web::web::Bytes has been mentioned in section Type-safe information extraction | Other. We're providing our own implementation to do the deserialisation, method extract_employee_login(...)
in new module src/helper/endpoint.rs
.
Content of src/helper/endpoint.rs
pub fn extract_employee_login(
body: &Bytes,
content_type: &str
) -> Result<EmployeeLogin, ApiStatus> {
...
extractors.push(Extractor {
content_type: mime::APPLICATION_WWW_FORM_URLENCODED.to_string(),
handler: |body: &Bytes| -> Result<EmployeeLogin, ApiStatus> {
match from_bytes::<EmployeeLogin>(&body.to_owned().to_vec()) {
Ok(e) => Ok(e),
Err(e) => Err(ApiStatus::new(err_code_500()).set_text(&e.to_string()))
}
}
});
...
extractors.push(Extractor {
content_type: mime::APPLICATION_JSON.to_string(),
handler: |body: &Bytes| -> Result<EmployeeLogin, ApiStatus> {
// From https://stackoverflow.com/a/67340858
match serde_json::from_slice(&body.to_owned()) {
Ok(e) => Ok(e),
Err(e) => Err(ApiStatus::new(err_code_500()).set_text(&e.to_string()))
}
}
});
For application/x-www-form-urlencoded
content type, we call method serde_html_form::from_bytes(...) from (new) crate serde_html_form to deserialise the byte stream to EmployeeLogin
.
-- Cargo.toml
has been updated to include crate serde_html_form.
And for application/json
content type, we call to serde_json::from_slice(...) from the already included serde_json crate to do the work.
These're the essential details of the code. The rest is fairly straightforward, and there's also sufficient documentation to aid the reading of the code.
💥 Please also note that there're also some more new modules, such as src/bh_libs/api_status.rs
and src/helper/messages.rs
, they're very small, self-explanatory and have sufficient documentation where appropriate.
❺ Register new login routes /ui/login
and /api/login
.
Updated src/lib.rs:
pub async fn run(listener: TcpListener) -> Result<Server, std::io::Error> {
...
.service(
web::scope("/ui")
.service(handlers::employees_html1)
.service(handlers::employees_html2)
.service(auth_handlers::login_page)
// .service(auth_handlers::home_page),
)
.service(
web::scope("/api")
.service(auth_handlers::login)
)
.service(
web::resource("/helloemployee/{last_name}/{first_name}")
.wrap(middleware::SayHi)
.route(web::get().to(handlers::hi_first_employee_found))
)
...
❻ The last addition, the new templates/auth/login.html
.
Please note, this login page has only HTML. There is no CSS at all. It looks like a dog's breakfast, but it does work. There is no client-side validations either.
The Login
button POST
s login requests to http://0.0.0.0:5000/api/login
, the content type then is application/x-www-form-urlencoded
.
For application/json
content type, we can use Testfully. (We could also write our own AJAX requests to test.)
❼ As this is not yet the final version of the login process, we're not writing any integration tests for it yet. We'll do so in due course...
⓵ For the time being, we've written some new code and their associated unit tests. We have also written some documentation examples. The full test with the command cargo test
should have all tests pass.
⓶ Manual tests of the new routes.
In the following two successful tests, I run the application server on an Ubuntu 22.10 machine, and run both the login page and Testfully on Windows 10.
Test application/x-www-form-urlencoded
submission via login page:
Test application/json
submission using Testfully:
In this failure test, I run the application server and Testfully on Windows 10. The submitted application/json
data does not have an email
field:
It's been an interesting exercise for me. My understanding of Rust's improved a little. I hope you find the information in this post useful. Thank you for reading and stay safe as always.
✿✿✿
Feature image source:
Top comments (0)