Creating a Full Server-Side Web App

In the previous chapter, we saw how to build a REST web service using the Actix web framework. A REST web service must be used by a client app in order for it to be useful to us.

In this chapter, we'll see how to build a very small but complete web app using the Actix web framework. We will use HTML code to be formatted in a web browser, JavaScript code to be executed in the same web browser, and the Tera crate to perform HTML templating. This is useful for embedding dynamic data inside HTML pages.

The following topics will be covered in this chapter:

  • Understanding what a classical web app is and what its HTML templates are
  • Using the Tera template engine with Rust and Actix web
  • Using Actix web to handle requests of web pages
  • Handling authentication and authorization in web pages

Technical requirements

To best understand this chapter, you will need to have read the previous chapter. In addition, basic knowledge of HTML and JavaScript is assumed.

The complete source code for this chapter can be found in the Chapter04folder of the repository at https://github.com/PacktPublishing/Rust-2018-Projects.

Definition of a web app

Everyone knows what a web page or a website is, and everyone knows that some web pages are quite static, while others have more dynamic behavior. The definition of a web app, however, is more subtle and controversial.

We will start with an operational definition of a web app; that is, looking at the appearance and behavior of web apps.

For our purposes, a web app is a website that has the following behavior:

  • It appears as one or more web pages in a web browser. On these pages, the user can interact with the page by pressing keys on a keyboard, clicking with a mouse, tapping on a touchscreen, or using another input device. For some user interactions, these web pages send requests to a server and receive data from that site as a response.
  • In the case of a static web page, the data received is always the same for the same request; but for a web app, the data received depends on the current state of the server, which can change with time. Upon receipt of the data, the web page shows other HTML code, either as a new full page or as a portion of the current page.
  • Classic web apps receive HTML code from the server only, so all the browser must do is display the HTML code when it arrives. Modern apps more often receive raw data from the server and use JavaScript code within the browser to create the HTML code that displays the data.

Here, we are going to develop a rather classical web app, as our app receives mainly HTML code from the server. Some JavaScript code will be used to improve the structure of the app.

Understanding the behavior of a web app

When a user navigates to a website by using the address bar of the browser or by clicking on a link in a page, the browser sends an HTTP GET request, with the URI specified in the address field or in the link element, such ashttp://hostname.domainname:8080/dir/file?arg1=value1&arg2=value2.

This address is commonly named Uniform Resource Locator (URL) orUniform Resource Identifier (URI). The difference between these two acronyms is that a URI is something that uniquelyidentifies a resource without necessarily specifyingwhereit can be found; a URL, however, specifies exactly where a resource can be found. In doing this, it also identifies the resource because there can be only one resourcein a single place.

So, every URL is also a URI, but an address can be a URI without being a URL. For example, an address that specifies the pathname of a file is a URL (and also a URI) because it specifies the path to the file. However, an address specifying a filter condition on files is a URI, but not a URL because it does not explicitlyspecify which file satisfies that condition.

The first part of an address (such as http://hostname.domainname:8080), up to the (optional) port number, is needed to route the request to the server process that should handle it. This server must be running on the host computer and it must be waiting for incoming requests addressed at that port; or, as it is usually said, it must be listening on that port.

The subsequent portion of the URI (such as /dir/file) is the so-called path, which always starts with a slash and ends at the first question mark character or at the end of the URI. The possible subsequent part (such as ?arg1=value1&arg2=value2) is the so-called query, which has one or more fields separated by an ampersand. Any field of the query has a name, followed by an equals sign, followed by a value.

When a request is made, the server should reply by sending an HTTP response, which contains the HTML page to display in the browser as its body.

After the display of the initial page, any further interaction usually happens when the user operates on the page by using the keyboard, the mouse, or other input devices.

Notice that the effect of any user actions on a page can be classified in the following ways:

  • No code: Some user actions are handled only by the browser, with no invoked application code. For example, when hovering the mouse over a widget, the mouse cursor shape changes; when typing in a text widget, the text inside that widget changes; and when clicking on a checkbox, the box is selected or deselected. Usually, this behavior is not controlled by the application code.
  • Frontend only: Some user actions (such as the pressing of a key) trigger the execution of the client-side JavaScript code associated with these actions, but no client-server communication is performed and so no server-side code is invoked as a consequence of these user actions. Typically, any push button is associated (using the onclick attribute of the button element) to JavaScript code that is executed any time the user clicks that button. This code could, for example, enable or disable other widgets or copy data from a widget to another widget of the same page.
  • Backend only: Some user actions trigger client-server communication without using any JavaScript code. There are only two examples of these actions:
    • Clicking on a submit input element inside an HTML form element
    • Clicking on an a HTML element, better known as a link
  • Full-stack: Some user actions trigger the execution of the client-side JavaScript code associated with that action. This JavaScript code sends one or more requests to the backend process and receives the responses sent as replies to these requests. The backend process receives the requests and responds properly to them. So, both the client-side application code and server-side application code is run.

Now, let's examine the advantages and disadvantages of these four cases. The no code case is the default one. If the basic behavior of the browser is good enough, there is no need to customize it. Some behavior customization can be performed using HTML or CSS.

The frontend only and the full-stack cases require JavaScript to be supported and enabled in the browser. This was once a problem because some people or platforms couldn't or wouldn't support it. Nowadays, something that wishes to be called a web app, and not simply a web page or website, cannot do so without the use of some kind of client-side processing.

The frontend only case does not interact with the server, and so it may be useful and is recommended for any processes that do not need to send data outside of the current computer or do not need to receive data from another computer. For example, a calculator can be implemented in JavaScript with no communication with a server. However, most web apps need this communication.

The backend only case was the original type of web communication available before JavaScript was invented. It is quite limited, though.

The concept of a link is useful for websites that are meant to be hypertext, not apps. Remember that HT in HTML and in HTTP stands for Hypertext. That was the original purpose of the web, but nowadays, web apps are meant to be general-purpose applications, not just hypertexts.

The concept of a form containing a submit button also limits the interaction to a rigid protocol—some fields are filled in and a button is pressed to send all of the data to the server. The server processes the request and sends back a new page that replaces the current page. In many cases, this can be done, but it is not a very pleasant experience for the user.

The fourth case is called full-stack because, for these apps, there are both application frontend code and application backend code. As the frontend code needs the backend code to work properly, it can be seen as stacked on it.

Notice that any web interaction must have some machine code running on the frontend and some machine code running on the backend. On the frontend, there can be the web browser, the curl utility, or some other kind of HTTP client. On the backend, there can be a web server, such as Internet Information Services (IIS), Apache, or NGINX, or an application that acts as an HTTP server.
So, for any web app, there is client-server communication using the HTTP protocol.
The term full-stack means that, in addition to system software, there is also some application software running on the frontend (acting as an HTTP client) and some application software running on the backend (acting as an HTTP server).

In a typical full-stack application running on a browser, there are no links or forms, just the typical widgets of a GUI. Usually, these widgets are fixed text, editable fields, drop-down lists, check buttons, and push buttons. When the user presses any push button, a request is sent to the server, possibly using the values contained in the widgets, and when the server sends back an HTML page, that page is used to replace the current page or a portion of it.

Project overview

The sample web app that we are going to build has the purpose of managing a list of people contained in a database. It is an extremely simple database as it only has one table with two columns—one for a numeric ID and one for a name. To keep the project simple, the database is actually a vector of struct objects kept in memory; but of course, in a real-world application, it would be stored in a Database Management System (DBMS).

The project will be built in steps, creating four projects that are progressively more complex, that can be downloaded from the GitHub repository linked in the Technical requirements section of this chapter:

  • Thetempl project is a collection of code snippets that shows how to use the Tera template engine for the projects of this chapter.
  • The list project is a simple list of records about people that can be filtered by name. These records are actually contained in the database code and cannot be changed by the user.
  • The crud project contains the features to add, change, and delete people. They are the so-called Create, Retrieve, Update, andDelete (CRUD) basic functions.
  • The auth project adds a login page and ensures that only authorized users can read or change the database. The list of users and their privileges cannot be changed, however.

The templ project, which does not use the Actix web framework, can be compiled in 1 to 3 minutes the first time, and in a few seconds after any changes to the code.

Any of the other projects will take around 3 to 9 minutes to compile the first time, then 8 to 20 seconds after any changes.

When you run any of the preceding projects (except the first one), all you will see is Listening at address 127.0.0.1:8080printed on the console. To view anything more, you will need a web browser.

Using the Tera template engine

Before starting to develop our web app, we will examine the concept of a template engine—in particular, the Tera crate, one of the many template engines available for Rust.

Template engines can have several applications, but they are mostly used for web development.

A typical problem in web development is knowing how to generate HTML code containing some constants parts written by hand and some dynamic parts generated by application code. In general, there are two ways to obtain this kind of effect:

  • You have a programming language source file that contains a lot of statements that print strings to create the desired HTML page. These print statements mix string literals (that is, strings enclosed in quotation marks) and variables formatted as strings. This is what you'd do in Rust if you didn't have a template engine.
  • You write an HTML file containing the desired constant HTML elements and the desired constant text, but it also contains some statements enclosed in specific markers. The evaluation of these statements generates the variable parts of the HTML file. This is what you'd do in PHP, JSP, ASP, and ASP.NET.

However, there is also a compromise, which is to write both application code files and HTML code containing statements to evaluate. You can then choose the best tool for the job. This is the paradigm used by template engines.

Imagine you have some Rust code files and some HTML files that must cooperate with one another. The tool to make the two worlds communicate is a template engine. The HTML files with embedded statements are named templates and the Rust application code calls the template engine functions to manipulate these templates.

Now, let's see the code in the templ example project. The first statement creates an instance of the engine:

 let mut tera_engine = tera::Tera::default();

The second statement loads one simple template into the engine by calling the add_raw_template function:

 tera_engine.add_raw_template(
"id_template", "Identifier: {{id}}.").unwrap();

The first argument is the name that will be used to refer to this template and the second argument is the template itself. It is a normal reference to a string slice, but it contains the {{id}}placeholder. This symbol qualifies it as a Teraexpression. In particular, this expression contains just a Tera variable, but it could contain a more complex expression.

A constant expression is also allowed, such as {{3+5}}, even if there is no point in using constant expressions. A template can contain several expressions or none at all.

Notice that the add_raw_template function is fallible, so unwrap is called on its result. This function, before adding the template received as an argument, analyzes it to see whether it is well-formed. For example, if it read "Identifier: {{id}." (with a missing brace), it would generate an error, and so the call to unwrap would panic.

When you have a Tera template, you can render it; that is, generate a string that replaces the expressions with some specified strings, in a similar way to how a macro processor does.

To evaluate an expression, the Tera engine has to first replace all of the variables used in it with their current value. To do that, a collection of Tera variables—each one associated with its current value—must be created. This collection is named a context. A context is created and populated by the following two statements:

let mut numeric_id = tera::Context::new();
numeric_id.insert("id", &7362);

The first one creates a mutable context and the second one inserts a key-value association into it. Here, the value is a reference to a number, but other types are also allowed as values.

Of course, in a real-world example, the value would be a Rust variable, not a constant.

Now, we can render it:

println!("id_template with numeric_id: [{}]",
tera_engine.render("id_template", &numeric_id).unwrap());

The render method gets a template named "id_template" in the tera_engine object and applies the substitutions specified by the numeric_id context.

This can fail if the specified template is not found, if variables in the template have not been substituted, or if an evaluation has failed for some other reason. If the result is okay, unwrap gets the string. Therefore, it should print the following:

id_template with numeric_id: [Identifier: 7362.]

The next three Rust statements in the example are as follows:

let mut textual_id = tera::Context::new();
textual_id.insert("id", &"ABCD");
println!(
"id_template with textual_id: [{}]",
tera_engine.render("id_template", &textual_id).unwrap()
);

They do the same thing, but with a literal string, showing that the same template variable can be replaced with both a number and a string. The printed line should be as follows:

id_template with textual_id: [Identifier: ABCD.]

The next statement is as follows:

tera_engine
.add_raw_template("person_id_template", "Person id: {{person.id}}")
.unwrap();

It adds a new template to the engine containing the {{person.id}}expression. This Tera dot notation has the same function as the Rust dot notation—it allows us to access a field of a struct. Of course, it only works if thepersonvariable is replaced by an object with anidfield.

So, a Person struct is defined in the following way:

#[derive(serde_derive::Serialize)]
struct Person {
id: u32,
name: String,
}

The struct has an id field but also derives the Serialize trait. This is a requirement for any object that must be passed to a Tera template.

The statement to define the personvariable in the context is as follows:

one_person.insert(
"person",
&Person {
id: 534,
name: "Mary".to_string(),
},
);

So, the printed string will be as follows:

person_id_template with one_person: [Person id: 534]

Now, there is a more complex template:

tera_engine
.add_raw_template(
"possible_person_id_template",
"{%if person%}Id: {{person.id}}
{%else%}No person
{%endif%}",
)
.unwrap();

The template is one-line long, but it has been split into three lines in Rust source code.

In addition to the {{person.id}} expression, there are three markers of another kind; they are Tera statements. Tera statements differ from Tera expressions because they are enclosed by the {% and %} signs, instead of double braces. While Tera expressions are similar to C preprocessor macros (that is, #define), Tera statements are similar to the conditional compilation directives of the C preprocessor (that is, #if, #else, and #endif).

The expression after the if statement is evaluated by the render function. If the expression is not defined or its value is either false, 0, an empty string, or an empty collection, the expression is considered false. The text part—up to the {%else%} statement—is then discarded. Otherwise, the part after that statement, up to the {%endif%} statement, is discarded.

This template is rendered with two different contexts—one in which the person variable is defined and the other in which no variable is defined. The two printedlines are as follows:

possible_person_id_template with one_person: [Id: 534]
possible_person_id_template with empty context: [No person]

In the first case, the id value of the person is printed; in the second case, the No person text is printed.

Then, another complex template is created:

tera_engine
.add_raw_template(
"multiple_person_id_template",
"{%for p in persons%}
Id: {{p.id}};
{%endfor%}",
)
.unwrap();

Here, the template contains two other kinds of statements—{%for p in persons%} and {%endfor%}. They enclose a loop where the newly created p variable iterates over the persons collection, which must belong to the context used by render.

Then, there is the following code:

let mut three_persons = tera::Context::new();
three_persons.insert(
"persons",
&vec![
Person {
id: 534,
name: "Mary".to_string(),
},
Person {
id: 298,
name: "Joe".to_string(),
},
Person {
id: 820,
name: "Ann".to_string(),
},
],
);

This adds a Tera variable named persons to the three_persons Tera context. This variable is a vector containing three people.

Because the persons variable can be iterated, it is possible to evaluate the template, thereby obtaining the following:

multiple_person_id_template with three_persons: [Id: 534;
Id: 298;
Id: 820;
]

Notice that any Id object is in a distinct line because the template contains a new-line character (through the escape sequence); otherwise, they would have been printed in a single line.

So far, we have used templates in string literals. This becomes difficult for long templates, though. Therefore, templates are usually loaded from separate files. This is advisable because the Integrated Development Environment (IDE) can help the developer (if it knows which language it is processing) and so it is better to keep HTML code in files with a .html suffix, CSS code in files with a .css suffix, and so on.

The next statement loads a Tera template from a file:

tera_engine
.add_template_file("templates/templ_id.txt", Some("id_file_template"))
.unwrap();

The first argument of the add_template_file function is the path of the template file, relative to the root of the project. It is good practice to put all the template files in a separate folder or in its subfolders.

The second argument allows us to specify the name of the new template. If the value of that argument is None, the name of the new template is the first argument.

So, the statement is as follows:

println!(
"id_file_template with numeric_id: [{}]",
tera_engine
.render("id_file_template", numeric_id.clone())
.unwrap()
);

This will print the following:

id_file_template with numeric_id: [This file contains one id: 7362.]

The following code will have similar results:

tera_engine
.add_template_file("templates/templ_id.txt", None)
.unwrap();

println!(
"templates/templ_id.txt with numeric_id: [{}]",
tera_engine
.render("templates/templ_id.txt", numeric_id)
.unwrap()
);

Lastly, let's talk about a convenient feature that can be used to load all of the templates with a single statement.

Instead of loading the templates one at a time, where they are needed, it is possible to load all of the templates at once and store them in a global dictionary. This makes them available to the entire module. To do so, it is convenient to use the lazy_static macro, described in Chapter 1, Rust 2018 – Productivity!, to write outside of any function:

lazy_static::lazy_static! {
pub static ref TERA: tera::Tera =
tera::Tera::new("templates/**/*").unwrap();
}

This statement defines the TERAstatic variable as a global template engine. It will be initialized automatically when some Rust code of your app uses it first. This initialization will search all of the files in the specified subtree of folders and will load them, giving each of them the name of the file itself and omitting the name of its folder.

The last feature of the Tera engine to be presented in this section is the include statement. The last line of the templ_names.txt file is the following one:

{% include "footer.txt" %}

It will load the contents of the specified file and will expand it inline, replacing the statement itself. It is similar to the #include directive of the C preprocessor.

A simple list of persons

Now, we can examine the list project. If you run the server in a console and you access the localhost:8080 address from a web browser, you will seethe following pagein the browser:

There is a heading, a label, a text field, a push button, and a table containing a list of three people.

The only thing you can do on this page is type something into the text field and then click on the button to apply the typed text as a filter. For example, if you type l (that is, a lowercase L), only the Hamlet and Othello lines will appear as they are the only two people whose name contains this letter. If the filter is x, the result will be the No personstext as none of the three people has a name containing this letter. The page will look as in the following screenshot:

Before explaining how it all works, let's see the dependencies of this project; that is, the external crates used by it. They are as follows:

  • actix-web: This is the web framework, also used in Chapter 3, Creating a REST Web Service.
  • tera: This is the Tera template engine.
  • serde and serde_derive: These are the serialization crates used by the Tera engine to pass whole struct objects to a template context.
  • lazy_static: This contains the macro to initialize the Tera engine.

Now, let's take a glimpse at the source code.For this project, the src folder contains the following files:

  • main.rs: This is the whole server-side application, excluding the database.
  • db_access.rs: This is the mock database with some mock data.
  • favicon.ico: This is the icon that any website should have as it is automatically downloaded by the browser to display it in the browser tab.

There is also a templates folder, containing the following files:

  • main.html: This is the Tera/HTMLtemplate of the whole web page with an empty body.
  • persons.html: This isthe Tera/HTML template of a partial web page, containing only the body of our web app.
  • main.js: This is the JavaScript code to be included in the HTML page.

Now, let's examine the mechanics of this web app.

When the user navigates to the http://localhost:8080/URI, the browser sends a GET HTTP request (that has only a slash as its path) to our process, with no query and empty body, and it expects an HTML page to be displayed. As described in the previous chapter, the server—using the Actix web framework—can respond to the request if its mainfunction contains the following code:

let server_address = "127.0.0.1:8080";
println!("Listening at address {}", server_address);
let db_conn = web::Data::new(Mutex::new(AppState {
db: db_access::DbConnection::new(),
}));
HttpServer::new(move || {
App::new()
.register_data(db_conn.clone())
.service(
web::resource("/")
.route(web::get().to(get_main)),
)
})
.bind(server_address)?
.run()

Here, we have a web app whose state is only a shared reference to a database connection (that is actually a mock database). This app accepts only one kind of request—those using the root path (/) and the GET method. These requests are routed to the get_main function. The function should return an HTTP response containing the initial HTML page to display.

Here is the body of the get_main function:

let context = tera::Context::new();
HttpResponse::Ok()
.content_type("text/html")
.body(TERA.render("main.html", context).unwrap())

This function does not use the request at all because it always returns the same result.

To return a successful response (that is, with status code 200), the HttpResponse::Ok() function is called. To specify that the body of the response is HTML code, the content_type("text/html") method is called on the response. To specify the content of the body of the response, the body method is called on the response.

The argument of the body function must be a string containing the HTML code to display. It is possible to write all of that code here, as follows:

.body("<!DOCTYPEhtml><html><body><p>Hello</p></body></html>")

However, for more complex pages, it is better to keep all the HTML code in a separate file, with the .html filename extension, and to load the contents of this file into a string to pass as an argument to the body function. This can be done using the following expression:

.body(include_str!("main.html"))

This would work well if the main.htmlfile was static; that is, it wouldn't need to change at runtime. However, this solution would be too limiting for two reasons:

  • We want our initial page to be a dynamic page. It should show the list of people that are in the database when the page is opened.
  • We want our initial page, and also all of the other possible pages, to be composed of several parts: metadata elements, JavaScript routines, styles, a page header, a page central part, and a page footer. All of these parts, except for the central part, are to be shared by all of the pages to avoid repeating them in source code. So, we need to keep these parts in separate files and then splice them together before the HTML page is sent to the browser. In addition, we want to keep JavaScript code in separate files with the .js file extension and style code in separate files with the .css file extension so that our IDE recognizes their language.

A solution to these problems is to use the Tera template engine, which we will see in the next section.

The templates folder

It is best to put all deliverable application text files in the templates folder (or in some of its subfolders). So, this subtree should contain all the HTML, CSS, and JS files, even if, at the moment, they may contain no Tera statements or expressions.

Instead, non-textual files (such as pictures, audio, video, and many others), user-uploaded files, documents that are to be downloaded explicitly, and databases should be kept elsewhere.

The loading of all template files happens at runtime, but usually only once in the process life. The fact that the loading happens at runtime implies that the templates subtree must be deployed and that to deploy a new or changed version of one of those files, a rebuild of the program is not required. The fact that this loading usually happens once in the process life implies that the template engine is quite fast at processing the templates after the first time.

The preceding body statement has the following argument:

TERA.render("main.html", context).unwrap()

This expression renders the template contained in the main.html file using a Tera context contained in the context Rust variable. This kind of variable has been initialized by the tera::Context::new() expression and so it is an empty context.

The HTML file is very small and it has two noteworthy snippets. The first one is as follows:

 <script>
{% include "main.js" %}
</script>

This uses the include Tera statement to incorporate the JavaScript code into the HTML page. Having it incorporated into the server means that no further HTTP requests will be needed to load it. The second snippet is as follows:

<body id="body" onload="getPage('/page/persons')">

This causes the invocation of the getPage JavaScript function as soon as the page is loaded. This function is defined in the main.js file and, as its name suggests, it causes the loading of the specified page.

So, when the user navigates to the root of the website, the server prepares an HTML page containing all the required JavaScript code, but almost no HTML code, and sends it to the browser. As soon as the browser has loaded the empty page, it requests another page, which will become the body of the first page.

This may sound complicated, but you can look at it as the page being split into two parts—the metadata, the scripts, the styles, and possibly the page header and footer are the common parts, which do not change during the session. The central part (which here is the body element, but may also be an inner element) is the variable part, which changes with any click from the user.

By reloading only part of the page, the app has better performance and usability.

Let's look at the contents of the main.js file:

function getPage(uri) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById('body')
.innerHTML = xhttp.responseText;
}
};
xhttp.open('GET', uri, true);
xhttp.send();
}

This code creates an XMLHttpRequest object that, in spite of its name, does not use XML, but it is actually used to send an HTTP request. This object is set to process the response when it arrives by assigning an anonymous function to the onreadystatechangefield. Then, the specified URI is opened with aGETmethod.

When a response arrives, the code checks whether the message is complete (readystate == 4) and valid (state == 200). In this case, the text of the response that is assumed to be valid HTML is assigned as the content of the element that has body as its unique ID.

The last file in the templates folder is the persons.html file. It is a partial HTML file—that is, a file containing HTML elements, but without the <html> element itself—and so its purpose is to be included in another HTML page. This small app has only one page and so it only hasone partial HTML file.

Let's look at some interesting parts of this file. The following is an element to let the user type in some text (a so-called edit box):

 <input id="name_portion" type="text" value="{{partial_name}}"/>

Its initial value—that is, the text that is shown to the user when the page is opened—is a Tera variable. Rust code should assign a value to the variable.

Then, there is the Filter push button:

<button onclick="getPage('/page/persons?partial_name='
+ getElementById('name_portion').value)">Filter</button>

When the user clicks on it and the preceding edit box contains the word Ham, the '/page/persons?partial_name=Ham'argument is passed to the JavaScript getPage functions. So, the function sends the GET request to the backend and replaces the body of the page with whatever is returned by the backend, so long as it is a complete and valid response.

Then, there is the following Tera statement:

{% if persons %}
...
{% else %}
<p>No persons.</p>
{% endif %}

Here, the persons Tera variable is evaluated. According to the Rust program, the variable can only be a collection. If the variable is a non-empty collection, a table is inserted into the HTML page; if instead the variable is not defined or it is an empty collection, the No persons.text will be shown.

Within the HTML code defining the table, there is the following:

{% for p in persons %}
<tr>
<td>{{p.id}}</td>
<td>{{p.name}}</td>
</tr>
{% endfor %}

This is an iteration over the items contained in persons (which we know is non-empty).

In each iteration, the p variable will contain the data of a specific person. This variable is used in two expressions. The first one shows the value of the id field of the variable. The second one shows the value of its name field.

The other Rust handlers

We have only seen the routing and handling of the root of the site—the/ path. This happens when the user opens the page.

There are four other requests that can be sent by the browser to this app:

  • When the root path is accessed, the page loaded by this request automatically sends—using JavaScript code—another request to load the body of the page.
  • When the user presses the Filter button, the frontend should send the text contained in the edit box to the backend, and then the backend should respond by sending back the list of the people satisfying this filter.
  • The browser automatically requests the favicon.ico app icon.
  • Any other requests should be treated as errors.

Actually, the first and second of these requests can be handled in the same way, because the initial state can be generated by a filter where an empty string is specified. So, three different kinds of requests remain.

To route these requests, the following code is inserted into the main function:

.service(
web::resource("/page/persons")
.route(web::get().to(get_page_persons)),
)
.service(
web::resource("/favicon.ico")
.route(web::get().to(get_favicon)),
)
.default_service(web::route().to(invalid_resource))

The first route redirects any GET requests to the /page/persons path to the get_page_persons function. These requests come when the user clicks on the Filter button, but also indirectly when the / path is requested.

The second route redirects anyGETrequests to the /favicon.ico path to the get_faviconfunction. These requests come from the browser when it receives a complete HTML page, not a partial page.

The call todefault_resourceredirects any other requests to the invalid_resourcefunction. These requests cannot come with proper use of the app, but may come under specific conditions or when the user types an unexpected path into the address bar of the browser. For example, this request occurs if you type in http://127.0.0.1:8080/abc.

Now, let's look at the handler's functions.

Theget_page_persons function has two arguments:

  • web::Query<Filter> is used to pass the optional filter condition.
  • web::Data<Mutex<AppState>> is used to pass the database connection.

The parameter of the Query type is defined as follows:

#[derive(Deserialize)]
pub struct Filter {
partial_name: Option<String>,
}

This specifies the possible arguments of the query, which is the part of the URI following the question mark. Here, there is only one argument and it is optional as it is typical of HTTP queries. A possible query is ?partial_name=Jo, but also an empty string is a valid query in this case.

To be able to receive the Filter structure from the request, itmust implement the Deserialize trait.

The body of the get_page_persons function is as follows:

let partial_name = &query.partial_name.clone().unwrap_or_else(|| "".to_string());
let db_conn = &state.lock().unwrap().db;
let person_list = db_conn.get_persons_by_partial_name(&partial_name);
let mut context = tera::Context::new();
context.insert("partial_name", &partial_name);
context.insert("persons", &person_list.collect::<Vec<_>>());
HttpResponse::Ok()
.content_type("text/html")
.body(TERA.render("persons.html", context).unwrap())

The first statement gets the query from the request. If the partial_namefield is defined, it is extracted; otherwise, an empty string is generated.

The second statement extractsthe connection to the databasefrom the shared state.

The third statement uses this connection to get an iterator on the people satisfying the criteria. See the subsection, Implementing the database in the section Building a stateful server in the previous chapter. See the previous chapter to understand these two lines.

Then, an empty Tera context is created and two Tera variables are added to it:

  • partial_name is used to keep the typed characters that otherwise would disappear when the page is reloaded in the edit box.
  • persons is the vector containing the people collected from the database. To make this possible, the Person type must implement the Serialize trait.

Finally, the Tera engine can render the persons.html template using the context, because all the variables used in the template have been defined. The result of this rendering is passed as the body of the successful HTTP response. When the JavaScript code inside the browser receives that HTML code, it will use it to replace the contents of the body of the current page.

Now, let's see the body of the get_favicon function:

HttpResponse::Ok()
.content_type("image/x-icon")
.body(include_bytes!("favicon.ico") as &[u8])

This is simply a successful HTTP response whose content is of the imageHTTP type and the x-iconsubtype, and whose body is a slice of bytes containing the icon. This binary object is constructed at compile time from the bytes contained in thefavicon.icofile. The content of this file is embedded in the executable program, so it is not required to deploy this file.

Finally, let's look at the body of the invalid_resource function:

HttpResponse::NotFound()
.content_type("text/html")
.body("<h2>Invalid request.</h2>")

This is a failing response (as NotFound generates the 404 status code), which should contain a complete HTML page. For simplicity, a straightforward message has been returned.

We have now looked at a very simple web app. Many of the concepts seen in this section will be used in the following sections, where the database will be modified by user actions.

A CRUD application

The web app shown in the previous section allowed us to view filtered data in a single page. If you now run the project in the crud folder, you will see a much more rich and useful web page:

The Id edit box and the Find button to its right are used to open a page that allows you to view or edit the data of a person with a specific ID.

The Name portionedit box and the Filter button to its right are for filtering the table below it, in a similar way as in the list project.

Then, there are two buttons—one for deleting data and one for adding data.

Lastly, there is the filtered table of the people. In this app, the initial state of the database is an empty list of people and so no HTML table is shown.

Let's create some people.

Click on the Add New Person push button. You will see the following window:

This is the page used to create a person and insert them into the database. The Id field is disabled because its value will be generated automatically. To insert a person, type in a name for them—for example, Juliet—and click on the Insert button. The main page will appear again, but with a small table containing only Juliet, preceded by 1 as its ID.

If you repeat these steps, inserting Romeo and Julius, you'll have the results shown in the following picture:

The push buttons near any listed person allow us to open a page related to that person. For example, if the button near Julius is clicked, the following page will appear:

This page is very similar to the page used to insert people, but with the following differences:

  • The Id field now contains a value.
  • The Name field now contains an initial value.
  • Instead of the Insertbutton, now there is an Update button.

If you change the Juliusvalue toJulius Caesar and click onUpdate, you will see the updated list on the main page.

Another way to open the page relating to a single person is to type the ID of that person into the Id field and then click on the Find button. If you click on this button when that field is empty or when it contains a value that no person has as its ID, a red error message appears on the page:

The final feature of this app allows us to delete records. To do that, click on the checkboxes to the left of the lines of the people you want to delete, and then click on the Delete Selected Persons button. The list is immediately updated.

Notice that the database is stored in the memory of the backend process. You will see the same list of people if you close the browser and reopen it or if you open another browser. You can even open the page from another computer, as long as you insert the appropriate name or IP address of the computer where the backend process is running. However, if you terminate the backend process by pressing the Ctrl + C key combination (or in any other way) and then re-run it, all of the browsers will display no people when the page is reloaded.

The JavaScript code

We are now going to look at what makes this project different from the one described in the previous section.

First of all, the main.js file is much larger because it contains three additional functions:

  • sendCommand: This is quite a generic routine used to send HTTP requests to a server and to process the received response asynchronously. It accepts five arguments:
    • method is the HTTP command to use, such as GET,PUT,POST, orDELETE.
    • uri is the path and possible query to send to the server.
    • body is the possible body of the request, used to send data larger than 2 KB.
    • success is a reference to a function that will be called after receiving a successful response (status == 200).
    • failureis a reference to a function that will be called after receiving any failure response (status != 200).
    This function is used to access a REST service as it allows any HTTP method, but it doesn't automatically change the current HTML page. Instead, the getPage function can only use the GET method, but it replaces the current HTML page with the HTML code received.
  • delete_selected_persons: This scans the items whose checkboxes are selected and sends a DELETE command to the server with the /persons?id_list= URI followed by a comma-separated list of the IDs of the selected items. The server should delete these records and return a successful state. If the deletion is successful, this JavaScript function reloads the main page with no filter; otherwise, an error message is shown in a message box and the current page is not changed. It should be called when the Delete Selected Persons button is clicked.
  • savePerson: This receives an HTTP method, which can be POST (to insert) or PUT (to update). It sends a command to the server, using the method received as an argument, and a URI that depends on the method. For a POST request, the URI is /one_person?name=NAME, while for a PUT request, the URI is/one_person?id=ID&name=NAME, wherein ID and NAME are actually the values of the id and name fields of the record to create or update. This function should be called with a POST argument when the Insert button is clicked and with a PUT argument when the Update button is clicked.

Now, let's check the HTML code of the application.

The HTML code

Of course, many HTML elements have been added to the persons.html file to create the additional widgets.

First, there is the <label class="error">{{id_error}}</label>element, used to display error messages caused by theFind button. To correctlyprocess this element,theid_errorTera variable needs to be defined in the current Tera context.

Then, there is the following element:

<div>
<label>Id:</label>
<input id="person_id" type="number">
<button onclick="getPage(
'/page/edit_person/' + getElementById('person_id').value)"
>Find</button>
</div>

When the Find button is clicked, a page is requested at the /page/edit_person/URI, followed by the typed ID.

Then, there are two push buttons:

<div>
<button onclick="delete_selected_persons()">Delete Selected Persons</button>
<button onclick="getPage('/page/new_person')">Add New Person</button>
</div>

The first one simply delegates all the work to the delete_selected_personsfunction, while the second one gets the page at the /page/new_personURI.

Finally, two columns are added to the HTML table containing the list of people. They are found on the left side of the table:

<td><input name="selector" id="{{p.id}}" type="checkbox"/></td>
<td><button onclick="getPage('/page/edit_person/{{p.id}}')">Edit</button></td>

The first column is the checkbox to select the record to delete and the second one is the Edit button. The value of the HTML id attribute of the checkbox element is the {{p.id}}Tera expression, which will be replaced by the ID of the record of the current line. So, this attribute can be used to prepare the request to send it to the server to delete the selected items.

The Edit button will get the page at the /page/edit_person/URI, followed by the ID of the current record.

In addition, there is another HTML partial file, one_person.html. This is the page used both to insert a new person and to view/edit an existing person. Its first part is as follows:

<h1>Person data</h1>
<div>
<label>Id:</label>
<input id="person_id" type="number" value="{{ person_id }}" disabled>
</div>
<div>
<label>Name:</label>
<input id="person_name" type="text" value="{{ person_name }}"/>
</div>

For both of the input elements, the value attribute is set to a Tera expression; for the first one, it is the person_id Tera variable and for the second, it is the person_name Tera variable. When inserting a person, these variables will be empty and when editing a person, these variables will contain the current values of the database fields.

The last part of the file is as follows:

{% if inserting %}
<button onclick="savePerson('POST')">Insert</button>
{% else %}
<button onclick="savePerson('PUT')">Update</button>
{% endif %}
<button onclick="getPage('/page/persons')">Cancel</button>

This page must show the Insert button when it has been opened for inserting a person, and the Update button when it has been opened for viewing or editing a person. So, the inserting Tera variable is used. Its value will be true when in insert mode and false when in edit mode.

Finally, the Cancel button opens the /page/persons page, with no filtering.

That's all we need to know about the templates folder.

The Rust code

In the src folder, both the db_access.rs and the main.rs files have many changes.

The db_access.rs changes

The persons vector is initially empty because users can insert records into it.

The following functions have been added:

  • get_person_by_id: This searches the vector for a person with a specified ID. It returns the person if found or None, otherwise.
  • delete_by_id: This searches the vector for a person with the specified ID; if found, it is removed from the vector and true is returned. Otherwise, false is returned.
  • insert_person: A Person object is received as an argument to insert into the database. However, before inserting it into the vector, its id field is overwritten by a unique ID value. This value is an integer larger than the largest ID present in the vector if the vector is not empty, or 1, otherwise.
  • update_person: This searches the vector for a person that has the specified ID; if found, this person is replaced by the specified person and true is returned. Otherwise, false is returned.

Nothing web-specific is contained in these functions.

The main.rschanges

For the main function, there are many kinds of requests to the route. The new routes are as follows:

.service(
web::resource("/persons")
.route(web::delete().to(delete_persons)),
)
.service(
web::resource("/page/new_person")
.route(web::get().to(get_page_new_person)),
)
.service(
web::resource("/page/edit_person/{id}")
.route(web::get().to(get_page_edit_person)),
)
.service(
web::resource("/one_person")
.route(web::post().to(insert_person))
.route(web::put().to(update_person)),
)

The first route is used to delete the selected people.

The second route is used to get the page to allow the user to insert a new person—that is, the one_person.html page—in insert mode.

The third route is used to get the page to allow the user to view or edit a new person—that is, theone_person.htmlpage—in edit mode.

For the fourth resource, there are two possible routes. Actually, this resource can be accessed using the POST method or the PUT method. The first method is used to insert a new record into the database. The second method is used to update the specified record using the specified data.

Now, let's see the handlers. With respect to the previous project, some of them are new, some are old but have been changed, and some are unmodified.

The new handlers are as follows:

  • delete_persons is used to delete the selected people.
  • get_page_new_person is used to get the page to create a new person.
  • get_page_edit_person is used to get the page to edit an existing person.
  • insert_person is used to insert a new person into the database.
  • update_person is used to update an existing person in the database.

The changed handlers are get_page_persons and invalid_resource. The unmodified handlers are get_main and get_favicon.

These handlers can be grouped into three logical kinds:

  • The ones whose job it is to generate HTML code to replace part of a web page
  • The ones whose job it is to return non-HTML data
  • The ones that do some work and then return status information regarding the job that has been done

The HTML-returning functions are get_main, get_page_persons, get_page_new_person, get_page_edit_person, and invalid_resource. get_favicon is the only data-returning function; the other three are data-manipulating functions.

It is logically possible to have a single handler that first does some work and then returns the HTML page to be shown. However, it is better to separate these logically different features into two distinct functions—first, the function that manipulates data is executed, and then the function that returns the HTML code is run. This separation can happen on the backend or on the frontend.

In this project, it is the frontend that does the separation. First, JavaScript code sends a request to manipulate data (for example, to insert a record in the database) and then, if the operation was successful, some other JavaScript code requests the HTML code to show up next in the browser.

An alternative architecture is to have the following sequence of calls:

  1. The user performs an action on the web page.
  2. That action causes a JavaScript routine to be executed.
  3. That routine sends a request from the browser to the server.
  4. The server routes that request to a backend handler function.
  1. The backend handler first calls a routine to manipulate data and then waits for its completion.
  2. If the backend routine is successful, the backend calls another routine to generate and return the next HTML page to the browser. If the backend routine fails, the backend generates and returns another HTML page to the browser, describing the failure.
  3. The JavaScript routine receives the HTML page and displays it to the user.

Now, let's look at the body of the get_page_edit_person function one piece at a time.

Remember that the purpose of this routine is to generate the HTML code of a web page to edit the name of a person. The current name of the person to edit is to be found in the database and the constant HTML code is to be found in the one_person.html template.

The first five statements define and initialize as many local variables:

let id = &path.0;
let db_conn = &state.lock().unwrap().db;
let mut context = tera::Context::new();
if let Ok(id_n) = id.parse::<u32>() {
if let Some(person) = db_conn.get_person_by_id(id_n) {

The first statement gets the id variable from the path as a string. For this function, the routing was /page/edit_person/{id}, and so the id variable is available to be extracted.

The second statement gets and locks the database connection.

The thirdstatement creates an empty Tera context.

The fourth statement tries to parse the id Rust variable into an integer. If the conversion is successful, the condition of the if statement is satisfied and so the next statement is executed.

The fifth statement searches the database for a person identified by this ID by calling the get_person_by_idmethod.

Now that the required information is available, the Tera context can be filled in:

context.insert("person_id", &id);
context.insert("person_name", &person.name);
context.insert("inserting", &false);

Let's see what the purpose of these variables is:

  • The person_id Tera variable allows us to show the current (disabled) ID of the person on the page.
  • The person_nameTera variable allows us to show the current (editable) name of the person on the page.
  • The insertingTera variable allows us (through a conditional Tera statement) to set the page as an edit page, instead of as an insert page.

Then, we can call the render Tera method with this context to get the HTML page and send the resulting page as the HTML body of the response:

return HttpResponse::Ok()
.content_type("text/html")
.body(TERA.render("one_person.html", context).unwrap());

Here, we have considered the cases where every statement was successful. In cases where the typed ID is not a number or it does not exist in the database, the function carries out the following code. This happens when the user types a wrong number in the Id field of the main page and then clicks on Find:

context.insert("id_error", &"Person id not found");
context.insert("partial_name", &"");
let person_list = db_conn.get_persons_by_partial_name(&"");
context.insert("persons", &person_list.collect::<Vec<_>>());
HttpResponse::Ok()
.content_type("text/html")
.body(TERA.render("persons.html", context).unwrap())

The last line shows that the template we will use is persons.html, so we are going to the main page. The Tera variables of that template are id_error, partial_name, and persons. We want a specific error message in the first variable, nothing as the filter condition, and a list of all the people. This can be obtained by filtering all the people whose name contains an empty string.

When the user presses the Update button, theupdate_person function is called.

This function has the following arguments:

 state: web::Data<Mutex<AppState>>,
query: web::Query<ToUpdate>,

The second is a query using a type defined by the following structure:

#[derive(Deserialize)]
struct ToUpdate {
id: Option<u32>,
name: Option<String>,
}

So, this query allows two optional keywords: id and name. The first keyword must be an integer number. Here are some valid queries:

  • ?id=35&name=Jo
  • ?id=-2
  • ?name=Jo
  • No query

The following are invalid queries for that structure:

  • ?id=x&name=Jo
  • ?id=2.4

Here is the first part of the body of the function:

let db_conn = &mut state.lock().unwrap().db;
let mut updated_count = 0;
let id = query.id.unwrap_or(0);

The first statement gets and locks the database connection.

A count of the records to update is defined by the second statement. This routine can update only one record, and so this count will be 0 or 1 only.

Then, the id variable is extracted from the query, if present and valid, or otherwise, 0 is considered as a substitute.

Notice that because the type of the query variable defines which fields are defined (whether they are optional or required and what is their type), the Actix web framework can perform a strict parsing of the URI query. If the URI query is not valid, the handler is not invoked and the default_service routine will be chosen. On the other side, in the handler, we can be sure that the query is valid.

The last part of the body of the function is as follows:

let name = query.name.clone().unwrap_or_else(|| "".to_string()).clone();
updated_count += if db_conn.update_person(Person { id, name }) {
1
} else {
0
};
updated_count.to_string()

First, the name variable is extracted from the query, or an empty string is considered if that variable is not contained in the query. This name is cloned as the database operations take ownership of their arguments and we cannot yield the ownership of a field of the query.

Then, the update_person method of the database connection is called. This method receives a new Person object constructed with the id and name values that were just extracted. If this method returns true, the count of the processed record is set to 1.

Finally, the count of the processed record is returned as a response.

The other routines are conceptually similar to the one described here.

Handling an application with authentication

All of the features of the previous apps were accessible to everyone that could create an HTTP connection with our server. Usually, a web app should behave differently depending on who is currently using it. Typically, some users are authorized to carry out some important operations, such as adding or updating records, while other users are authorized only to read these records. Sometimes, user-specific data must be recorded.

This opens up the vast world of authentication, authorization, and security.

Let's imagine a simplified scenario. There are two users whose profiles are wired-in to the mock database:

  • joe, whose password is xjoe, can only read the database of people.
  • susan, whose password is xsusan, can read and writethe database of people—that is, she can do what the app in the previous section allowed.

The application starts with a login page. If the user does not insert an existing username and its matching password, they cannot access the other pages. Even if the username and password are valid, the widgets that the user is not authorized for are disabled.

For these situations, some applications create a server-side user session. This may be appropriate to use when there are a limited number of users, but it may overload the server if there are many users. Here, we'll show a solution without server-side sessions.

If you run the auth project and access the site from a browser, you will see the following page:

It shows that there is no current users and two fields allow us to type in a username and password. If you type foo into the User name field and then click on Log in, the red User "foo" not found.message will appear. If you type in susan and then click onLog in, the message will be Invalid password for user "susan".

Instead, if you type in the correct password for that user, xsusan, the following page will appear:

This is the same main page as the crud project, with an added line of widgets: the name of the current user shown in blue and a button to change it. If you click on the Change User button, you go back to the login page. Also, the page to view, edit, or insert a person has the same widgets just under the page heading.

If on the login page you insert joe as the username and xjoe as the password, the following page will appear:

This has the same widgets that appeared for susan, but the Delete Selected Persons button and the Add New Person button now are disabled.

To see how joe sees the people, first, you need to log in as susan, insert some people, and then change the user to joe, because joe cannot insert people. If you do this and then you click on the Edit button of a person, you will see the following page, where the Name field is read-only and the Update button is disabled:

Let's start with understanding the nitty-gritty of the application we just did.

The implementation

This project adds some code with respect to the crud project.

The first difference is in the Cargo.toml file, where the actix-web-httpauth = "0.1" dependency has been added. This crate handles the encoding of the username and password in the HTTP request.

The HTML code

Themain.htmlpage, instead of opening the/page/personsURI, opens /page/login to show the login page,initially. So, this project adds a new TERA template for the login page. This is thelogin.htmlpartial HTML page, shown as follows:


<h1>Login to Persons</h1>
<div>
<span>Current user:</span>
<span id="current_user" class="current-user"></span>
</div>
<hr/>
<label class="error">{{error_message}}</label>
<div>
<label>User name:</label>
<input id="username" type="text">
</div>
<div>
<label>Password:</label>
<input id="password" type="password">
</div>
<button onclick="login()">Log in</button>

Its noteworthy points are underlined: the {{error_message}}Tera variable, the call to login()when the Log in button is clicked, and three elements whose IDs are current_user, username, and password.

Both thepersons.htmlandone_person.htmltemplates have the following section just below the heading:

<div>
<span>Current user: </span>
<span id="current_user" class="current-user"></span>
<button onclick="getPage('/page/login')">Change User</button>
</div>
<hr/>

This will show the current user, or ---, followed by the Change User button. Clicking on this will load the /page/login page.

The app contains four buttons that must be disabled for unauthorized users—two in the persons.html template and two in the one_person.html template. They now contain the following attribute:

{% if not can_write %}disabled{% endif %}

It assumes that the can_write Tera variable is defined as true, or any non-null value, if—and only if—the current user has the authorization to modify the content of the database.

There is also an edit box element in the one_person.html template that must be made read-only for users that are not authorized to change that data; so, it contains the following attribute:

{% if not can_write %}readonly{% endif %}

You should be aware that these checks are not an ultimate security guard. The checks of authorization in frontend software can always be bypassed, and so the ultimate security guards are those performed by the DBMS.

However, it is good to always carry out an early check to make that the user experience is more intuitive and the error messages are helpful.

For example, if an attribute of an entity shouldn't be modifiable by the current user, this constraint can be specified in a solid way using the DBMS.

However, if the user interface allows this kind of change, the user could try to change this value and they will be disappointed when they find out that this change is not allowed.

In addition, when a forbidden change is attempted, an error message is issued by the DBMS. The message is probably not internationalized and makes reference to DBMS concepts such as tables, columns, rows, and the names of objects that are unfamiliar to the user. So, this message can be obscure for the user.

The JavaScript code

The main.js file has the following additions with respect to the crud project.

The username and passwordglobal variables have been added and initialized as empty strings.

The following statement has been added to both thesendCommandfunction and thegetPagefunction:

xhttp.setRequestHeader("Authorization",
"Basic " + btoa(username + ":" + password));

This sets the Authorization header for the HTTP request that is about to be sent. The format of that header is standard HTTP.

In the getPage function, after the statement that assignsthe HTML code that is receivedto the current body, the following three lines are inserted:

var cur_user = document.getElementById('current_user');
if (cur_user)
cur_user.innerHTML = username ? username : '---';

They set the content of the element whose id attribute has current_user as its value if the current page contains such an element. This content is the value of the usernameglobal JavaScript variable if it is defined and not empty, or---, otherwise.

Another addition is the definition of the new login function. Its body is as follows:

username = document.getElementById('username').value;
password = document.getElementById('password').value;
getPage('/page/persons');

This gets the values of the username and password elements of the page and saves them to the global variables with the same names, and then opens the main page. Of course, this shouldonly be called in thelogin.htmlpage as other pages are not likely to have apasswordelement.

The mock database code

The mock database has one more table: users. So, the type of its elements must be defined:

#[derive(Serialize, Clone, Debug)]
pub struct User {
pub username: String,
pub password: String,
pub privileges: Vec<DbPrivilege>,
}

Any user has a username, a password, and a set of privileges. A privilege has a custom type, which is defined in the same file:

#[derive(Serialize, Clone, Copy, PartialEq, Debug)]
pub enum DbPrivilege { CanRead, CanWrite }

Here, there are only two possible privileges: to be able to read the database or to be able to write the database. A real-world system would have more granularity.

The DbConnection struct now alsocontains the usersfield, which is a vector ofUsers. Its content (the records about joe and susan) is specified inline.

The following function has been added:

pub fn get_user_by_username(&self, username: &str) -> Option<&User> {
if let Some(u) = self.users.iter().find(|u| u.username == username) {
Some(u)
}
else { None }
}

This searches the users vector for a user with the specified username. If it is found, it is returned; otherwise, None is returned.

The main function

The main function has just two small changes. The first change is to call data(Config::default().realm("PersonsApp")) on the App object. This invocation is required to get the authentication context from the HTTP requests. It specifies the context using the realm call.

The second change is the addition of the following routing rule:

.service(
web::resource("/page/login")
.route(web::get().to(get_page_login)),
)


This path is used to open the login page. It is used by the main page as the entry point of the app and by the two Change User buttons.

The get_page_login function is the only new handler. It just calls the get_page_login_with_message function, which has a string argument, to be shown as an error message. When this function is called byget_page_login, an empty string is specified as an argument because no error has happened yeton this page. However, this function is called in six otherplaces, where various error messages are specified. The purpose of this function is to go to the login page and display the message received as an argumentin red.

The login page is obviously accessible to every user, as the favicon resource is, but all of the other handlers have been modified to ensure that only authorized users can access those resources. The bodies of the handlers that manipulate data have the following structure:

match check_credentials(auth, &state, DbPrivilege::CanWrite) {
Ok(_) => {
... manipulate data ...
HttpResponse::Ok()
.content_type("text/plain")
.body(result)
},
Err(msg) => get_page_login_with_message(&msg)
}

First, the check_credentialsfunction checks whether the credentials specified by theauthargument identify a user that has the CanWriteprivilege. Only users allowed to write should manipulate the data. For them, the function returns as Ok and so they can change the database and return the result of these changes in a plaintext format.

Users that are not allowed to write are redirected to the login page, which shows the error message returned by check_credentials.

Instead, the bodies of the handlers that get HTML pages have the following structure:

match check_credentials(auth, &state, DbPrivilege::CanRead) {
Ok(privileges) => {
... get path arguments, query arguments, body ...
... get data from the database ...
let mut context = tera::Context::new();
context.insert("can_write",
&privileges.contains(&DbPrivilege::CanWrite));
... insert some other variables in the context ...
return HttpResponse::Ok()
.content_type("text/html")
.body(TERA.render("<template_name>.html", context).unwrap());
},
Err(msg) => get_page_login_with_message(&msg)
}

Here, as is typical, any user that can read the data can also access the web page. In this case, the check_credentials function is successful and it returns the complete set of privileges of that user. Matching these results with the Ok(privileges)pattern causes the privileges of that user to be used to initialize the privileges Rust variable.

If the user has the CanWrite privilege, that information is passed to the can_write Tera variable as a true value and to false, otherwise. In this way, the page can enable or disable the HTML widgets in accordance with the user's privileges.

Finally, let's look at the check_credentials function.

Among its arguments, there is auth: BasicAuth. Thanks to the actix_web_httpauth crate and to the call to data in the main function, this argument allows access to the authorization HTTP header for basic authentication. The objects of the BasicAuth type have the user_id and password methods, which return the optional credential specified by the HTTP client.

These methods are invoked with the following snippet:

if let Some(user) = db_conn.get_user_by_username(auth.user_id()) {
if auth.password().is_some() && &user.password == auth.password().unwrap() {

This code gets the user from the database through their username and checks that the stored password matches the password coming from the browser.

This is quite basic. A real-world system would store an encrypted password; it would encrypt the specified password using the same one-way encryption and it would compare the encrypted strings.

Then, the routine discriminates between the different kinds of errors:

  • The HTTP request does not contain credentials, or the credentials exist but the specified user does not exist in the user's table.
  • The user exists, but the stored password is different from that specified in the received credentials.
  • The credentials are valid, but that user hasn't got the required privileges (for example, they only have the CanRead access but CanWrite is required).

So, we have now covered a simple authenticated web app.

Summary

In this chapter, we have seen how to use the Tera template engine to create text strings or files (not just in HTML format) containing variable parts, conditional sections, repeated sections, and sections included from another file.

Then, we saw how Actix web—together with HTML code, JavaScript code, CSS styles, and the Tera template engine—can be used to create a complete web app with CRUD capabilities, authentication (to prove who is the current user), and authorization (to forbid some operations to the current user).

This project showed us how to create a single application that performs both client-side code and server-side code.

In the next chapter, we will see how to create a client-side web app using WebAssembly technology and the Yew framework.

Questions

  1. What are the possible strategies for creating HTML code containing variable parts?
  2. What is the syntax to embed a Tera expression into a text file?
  3. What is the syntax to embed a Tera statement into a text file?
  4. How are the values of variables in a Tera rendering operation specified?
  5. How can the requests to a web server be classified?
  6. Why may it be useful to split a web page into parts?
  7. Should HTML templates and JavaScript files be deployed separately or are they linked into the executable program?
  8. Which JavaScript object can be used to send HTTP requests?
  9. Where should the current username be stored when the server does not store user sessions?
  10. How are credentials extracted from an HTTP request?

Further reading

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.178.181