diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index e6e7ba0b..679757ec 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -1 +1,31 @@ # Configuration + +Rwf supports file-based and environment-based configuration. The list of configurable options are ever growing, and currently supported features are listed below. + +## Enabling configuration + +To configure Rwf, place a file called `rwf.toml` into the wording directory of your app. During development, this should be the root directory of your Cargo project. At startup, +Rwf will automatically load configuration settings from that file, as they are needed by the application. + +## Available settings + +The configuration file is using the [TOML language](https://toml.io/). If you're not familiar with TOML, it's pretty simple and expressive language commonly used in the world of Rust programming. + +Rwf configuration file is split into multiple sections. The `[general]` section controls various options such as logging settings, and which secret key to use for [encryption](../encryption). The `[database]` +section configures database connection settings, like the database URL, connection pool size, and others. + +### `[general]` + +| Setting | Description | Example | +|---------|-------------|---------| +| `log_queries` | Toggles logging of all SQL queries executed by the [ORM](../models/). | `log_queries = true` | +| `secret_key` | Secret key, encoded using base64, used for [encryption](../encryption). | `secret_key = "..."` | + +#### Secret key + +The secret key is a base64-encoded string of randomly generated data. A valid secret key contains 256 bits of entropy and _must_ be generated using a [_secure_](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) random number generator. + +### `[database]` + +| Setting | Description | Example | +|---------|-------------|---------| diff --git a/docs/docs/controllers/authentication.md b/docs/docs/controllers/authentication.md new file mode 100644 index 00000000..12e1cfd3 --- /dev/null +++ b/docs/docs/controllers/authentication.md @@ -0,0 +1,46 @@ +# Authentication + +Rwf has multiple authentication and authorization mechanisms. Different kinds of authenitcation require their own kinds of user-supplied credentials. The most commonly used mechanism is [Session](../sessions) authentication, which has built-in methods for easy use in [controllers](../). + +## Session authentication + +[Session](../sessions) authentication checks that the user-supplied session cookie is valid (not expired). If that's not the case, the request is either rejected with a `403 - Forbidden` or provided an endpoint to re-authenticate, e.g. using a username and password, with a `302 - Found` redirect. + +### Enable session authentication + +To enable session authentication, it needs to be configured on the controller by implementing the [`auth`](https://docs.rs/rwf/latest/rwf/controller/trait.Controller.html#method.auth) method: + +```rust +use rwf::prelude::*; + +/// A controller that requires authentication. +struct Private { + auth: AuthHandler, +} + +impl Default for Private { + fn default() -> Self { + Private { + // Redirect unauthenitcated requests to the `/login` route. + auth: AuthHandler::new( + SessionAuth::redirect("/login"), + ), + } + } +} + +#[async_trait] +impl Controller for Private { + /// Enable authentication on this controller. + fn auth(&self) -> &AuthHandler { + &self.auth + } + + /* ... */ +} +``` + +## Basic authentication + +HTTP Basic is a form of authentication using a global username and password. It's not particularly secure, but it's good enough to protect an endpoint quickly against random visitors. Enabling basic authentication is as simple +as setting a [`AuthHandler`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.AuthHandler.html) with [`BasicAuth`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.BasicAuth.html) on your [controller](../). See [examples/auth](https://github.com/levkk/rwf/tree/main/examples/auth) for examples on how to do this. diff --git a/docs/docs/controllers/sessions.md b/docs/docs/controllers/sessions.md index ba911948..7432cddf 100644 --- a/docs/docs/controllers/sessions.md +++ b/docs/docs/controllers/sessions.md @@ -1 +1,45 @@ # Sessions + +A session is an [encrypted](../../encryption) [cookie](../cookies) managed by Rwf. It contains a unique identifier for each browser using your web app. All standard-compliant browsers talking to Rwf-powered apps will have a Rwf session set, and should send it back on each request. + +## Check for valid session + +All [controllers](../) can check for the presence of a valid session: + +```rust +let session = request.session(); + +let valid = session + .map(|session| !session.expired()) + .unwrap_or(false); +``` + +Unless the session cookie is set and has been encrypted using the correct algorithm and secret key, calling [`session`](https://docs.rs/rwf/latest/rwf/http/request/struct.Request.html#method.session) will return `None`. + +#### Expired sessions +If the session is expired, it's advisable not to trust its point of origin. While the contents are guaranteed to be accurate, the browser sending the data has not been validated in several weeks (4 weeks, by default). + +The session can be used to privately store custom user-specific data. This allows your web apps to persist sensitive data on the client without using `localStorage` and JavaScript encryption. + +### Session authentication + +Rwf can ensure all requests have valid and current (not expired) sessions. To enable this feature, enable the [`SessionAuth`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.SessionAuth.html) [authentication](../authentication) on your controllers. + +## Store data in session + +Rwf sessions allow you to store arbitrary JSON-encoded data. Since browsers place limits on cookie sizes, this data should be relatively small. To store some data in the session, you can set it on the [response](../response): + +```rust +let session = Session::new( + serde_json::json!({ + "data": "secret_value" + }) +); + +let response = Response::new() + .set_session(session); +``` + +## Renew sessions + +Sessions are automatically renewed on each request. Expired sessions are renewed as well, unless session [authentication](../authentication) is enabled. diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md deleted file mode 100644 index f67eb25e..00000000 --- a/docs/docs/getting-started.md +++ /dev/null @@ -1,102 +0,0 @@ -# Getting started - -Rust Web Framework (Rwf for short) has very few dependencies and is easy to install and use. - -## Install Rust - -If you haven't already, install the Rust compiler and tools from [rust-lang.org](https://rust-lang.org). Rwf doesn't use any nightly or experimental features, -so the stable version of the compiler will work. - -## Create a project - -Rwf can be used inside any Rust binary or library project. If you don't have a project already, you can create one with Cargo: - -```bash -cargo init --bin rwf-web-app -``` - -## Install Rwf - -Rwf has two packages: - -* `rwf` which is the Rust crate[^1] used to build web apps -* `rwf-cli` which is a binary application that helps manage Rust projects built with Rwf - -To install them, run the following while inside the root directory of your Cargo-created project: - -``` -cargo add rwf -cargo install rwf-cli -``` - -[^1]: A "crate" is a Rust package used as a dependency in other packages. It's analogous to "package" in JavaScript or Python. - -## Building the app - -With the packages installed, you're ready to launch your first web app in Rust. Rwf is built using the [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (Model-view-controller) design pattern, -so to get started, let's create a simple controller that will serve the index page (`/`) of your app: - -```rust -use rwf::prelude::*; - -#[derive(Default)] -struct Index; - -#[async_trait] -impl Controller for Index { - async fn handle(&self, request: &Request) -> Result { - Ok(Response::new().html("

My first Rwf app!

")) - } -} -``` - -`rwf::prelude::*` includes the vast majority of types, structs, traits and functions you'll be using when building controllers with Rwf. -Adding this declaration in your source files will make handling imports easier, but it's not required. - -Rwf controllers are defined as Rust structs which implement the [`Controller`](../controllers/) trait. The trait is asynchronous, hence the `#[async_trait]` macro[^2], -and has only one method you need to implement: `async fn handle`. This method -accepts a [`Request`](../controllers/request), and must return a [`Response`](../controllers/response). - -In this example, we are returning HTTP `200 - OK` with the body `

My first Rwf app

`. This is not strictly valid HTML, -but it'll work in all browsers for our demo purposes. - -[^2]: The Rust language support for async traits is still incomplete. The `async_trait` crate helps with writing async traits in an ergonomic way. - -## Launching the server - -Once you have at least one controller, you can add it to the Rwf HTTP server and launch it on the address and port of your choosing: - -```rust -use rwf::http::{self, Server}; - -#[tokio::main] -async fn main() -> Result<(), http::Error> { - // Configure then logger (stderr with colors by default) - Logger::init(); - - Server::new(vec![ - route!("/" => Index), - ]) - .launch("0.0.0.0:8000") - .await -} -``` - -Rwf uses the `log` crate for logging. `Logger::init()` automatically configures it for your app using `env_logger`, but if you prefer, you can configure logging yourself -using the crate of your choosing. - -Launching the server can be done with Cargo: - -``` -cargo run -``` - -Once the server is running, you can visit the index page by pointing your browser to [http://localhost:8000](http://localhost:8000). - -Full code for this is available in GitHub in [examples/quick-start](https://github.com/levkk/rwf/tree/main/examples/quick-start). - -## Learn more - -- [Controllers](../controllers/) -- [Models](../models/) -- [Views](../views/) diff --git a/docs/docs/index.md b/docs/docs/index.md index 7e3c7834..64d557b3 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,3 +1,105 @@ # Introduction -[Rwf](https://github.com/levkk/rwf) is a framework for building web applications in the Rust programming language. +Rust Web Framework (Rwf for short) is set of libraries and tools to build web applications using the Rust programming langauge. It aims to be comprehensive, by providing all features +for you to build modern, fast, and secure web apps, out of the box. + +Rwf has very few dependencies, and is easy to install and use within new or existing Rust applications. + +## Install Rust + +If you haven't already, install the Rust compiler and tools from [rust-lang.org](https://rust-lang.org). Rwf doesn't use any nightly or experimental features, +so the stable version of the compiler will work. + +## Create a project + +Rwf can be used inside any Rust binary or library project. If you don't have a project already, you can create one with Cargo: + +```bash +cargo init --bin rwf-web-app +``` + +## Install Rwf + +Rwf has two packages: + +* `rwf` which is the Rust crate[^1] used to build web apps +* `rwf-cli` which is a binary application that helps manage Rust projects built with Rwf + +To install them, run the following while inside the root directory of your Cargo-created project: + +``` +cargo add rwf +cargo install rwf-cli +``` + +[^1]: A "crate" is a Rust package used as a dependency in other packages. It's analogous to "package" in JavaScript or Python. + +## Building the app + +With the packages installed, you're ready to launch your first web app in Rust. Rwf is built using the [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (Model-view-controller) design pattern, +so to get started, let's create a simple controller that will serve the index page (`/`) of your app: + +```rust +use rwf::prelude::*; + +#[derive(Default)] +struct Index; + +#[async_trait] +impl Controller for Index { + async fn handle(&self, request: &Request) -> Result { + Ok(Response::new().html("

My first Rwf app!

")) + } +} +``` + +`rwf::prelude::*` includes the vast majority of types, structs, traits and functions you'll be using when building controllers with Rwf. +Adding this declaration in your source files will make handling imports easier, but it's not required. + +Rwf controllers are defined as Rust structs which implement the [`Controller`](../controllers/) trait. The trait is asynchronous, hence the `#[async_trait]` macro[^2], +and has only one method you need to implement: `async fn handle`. This method +accepts a [`Request`](../controllers/request), and must return a [`Response`](../controllers/response). + +In this example, we are returning HTTP `200 - OK` with the body `

My first Rwf app

`. This is not strictly valid HTML, +but it'll work in all browsers for our demo purposes. + +[^2]: The Rust language support for async traits is still incomplete. The `async_trait` crate helps with writing async traits in an ergonomic way. + +## Launching the server + +Once you have at least one controller, you can add it to the Rwf HTTP server and launch it on the address and port of your choosing: + +```rust +use rwf::http::{self, Server}; + +#[tokio::main] +async fn main() -> Result<(), http::Error> { + // Configure then logger (stderr with colors by default) + Logger::init(); + + Server::new(vec![ + route!("/" => Index), + ]) + .launch("0.0.0.0:8000") + .await +} +``` + +Rwf uses the `log` crate for logging. `Logger::init()` automatically configures it for your app using `env_logger`, but if you prefer, you can configure logging yourself +using the crate of your choosing. + +Launching the server can be done with Cargo: + +``` +cargo run +``` + +Once the server is running, you can visit the index page by pointing your browser to [http://localhost:8000](http://localhost:8000). + +Full code for this is available in GitHub in [examples/quick-start](https://github.com/levkk/rwf/tree/main/examples/quick-start). + +## Learn more + +- [Controllers](../controllers/) +- [Models](../models/) +- [Views](../views/) diff --git a/rwf/src/config.rs b/rwf/src/config.rs index fd09e719..daa878da 100644 --- a/rwf/src/config.rs +++ b/rwf/src/config.rs @@ -30,6 +30,9 @@ pub enum Error { #[error("config is already loaded")] ConfigLoaded, + + #[error("config not found")] + NoConfig, } /// Global configuration. @@ -172,7 +175,22 @@ impl Default for Config { impl Config { pub fn load() -> Result { let mut config = Config::default(); - let config_file = ConfigFile::load("Rum.toml")?; + let mut config_file = None; + + for name in ["rwf.toml", "Rum.toml", "Rwf.toml"] { + let path = PathBuf::from(name); + if path.exists() { + config_file = Some(ConfigFile::load("Rum.toml")?); + break; + } + + return Err(Error::NoConfig); + } + + let config_file = match config_file { + Some(config_file) => config_file, + None => return Err(Error::NoConfig), + }; let secret_key = config_file.general.secret_key()?; diff --git a/rwf/src/http/cookies.rs b/rwf/src/http/cookies.rs index b0adaa2b..c64f1138 100644 --- a/rwf/src/http/cookies.rs +++ b/rwf/src/http/cookies.rs @@ -64,6 +64,7 @@ impl Cookies { pub fn get_session(&self) -> Result, Error> { let cookie = self.get_private("rwf_session")?; + if let Some(cookie) = cookie { Ok(serde_json::from_str(cookie.value())?) } else {