Skip to content

maitsarv/actix-simple-bp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Actix simple boilerplate

An Actix 3 server template. Originally based on Actix 2.0 REST server, but has been refactored, updated to Actix 3 and is with extra features.

Motivation for project

Add some extra security on top of the Actix 2.0 REST server example and build a wizard that would allow to easily set up a Actix server.

Features

  • Actix 3.x HTTP Server
  • Multi-Database Support (MySQL)
  • JWT Support
  • Async Caching Layer with a Simple API
  • Public and Secure Static File Service
  • Diesel Database Operations are Non-Blocking
  • Filesystem Organized for Scale
  • .env for Local Development
  • Integrated Application State with a Simple API
  • Lazy Static Config struct
  • Built-in Healthcheck (includes cargo version info)
  • Listeners configured for TDD
  • Custom Errors and HTTP Payload/Json Validation
  • Secure Argon2i Password Hashing
  • CORS Support
  • Unit and Integration Tests
  • Test Coverage Reports
  • Dockerfile for Running the Server in a Container
  • TravisCI Integration

Additional features compared to original project

  • Force connection to TSL (HTTPS).
  • No JWT based session handling. Sessions use secure cookies that are also stored in background.
  • Generate random salt per user password (pull request).
  • Refactoring to change to folder structure.
  • Rate limiting.

Featured Packages

  • Argon2i: Argon2i Password Hasning
  • actix-cors: CORS Support
  • actix-identity: User Authentication
  • actix-redis and redis-async: Async Caching Layer
  • actix-web: Actix Web Server
  • actix-ratelimit: Rate limiting
  • actix-session: Backend session handling
  • derive_more: Error Formatting
  • diesel: ORM that Operates on Several Databases
  • dotenv: Configuration Loader (.env)
  • envy: Deserializes Environment Variables into a Config Struct
  • jsonwebtoken: JWT encoding/decoding
  • kcov: Coverage Analysis
  • listenfd: Listens for Filesystem Changes
  • rayon: Parallelize
  • r2d2: Database Connection Pooling
  • validator: Validates incoming Json
  • rand: Generate random salt string

Installation

Clone the repo and cd into the repo:

git clone https://github.com/maitsarv/actix-simple-bp
cd actix-simple-bp

Copy over the example .env file:

cp .env.example .env

IMPORTANT: Change .env values for your setup, paying special attention to the salt and various keys and ports.

Next, you'll need to install the Diesel CLI:

cargo install diesel_cli

If you run into errors, see http://diesel.rs/guides/getting-started/

Now run the migrations via the Diesel CLI:

diesel migration run

Running the Server

To startup the server:

cargo run

Autoreloading

To startup the server and autoreload on code changes:

systemfd --no-pid -s http::3000 -- cargo watch -x run

Tests

Integration tests are in the /src/tests folder. There are helper functions to make testing the API straightforward. For example, if we want to test the GET /api/v1/user route:

  use crate::tests::helpers::tests::assert_get;

  #[test]
  async fn test_get_users() {
      assert_get("/api/v1/user").await;
  }

Using the Actix test server, the request is sent and the response is asserted for a successful response:

assert!(response.status().is_success());

Similarly, to test a POST route:

use crate::handlers::user::CreateUserRequest;
use crate::tests::helpers::tests::assert_post;

#[test]
async fn test_create_user() {
    let params = CreateUserRequest {
        first_name: "Satoshi".into(),
        last_name: "Nakamoto".into(),
        email: "satoshi@nakamotoinstitute.org".into(),
    };
    assert_post("/api/v1/user", params).await;
}

Running Tests

To run all of the tests:

cargo test

Docker

To build a Docker image of the application:

docker build -t rust_actix_example .

Once the image is built, you can run the container in port 3000:

docker run -it --rm --env-file=.env.docker -p 3000:3000 --name rust_actix_example rust_actix_example

Public Static Files

Static files are served up from the /static folder. Directory listing is turned off. Index files are supported (index.html).

Example:

curl -X GET http://127.0.0.1:3000/test.html

Secure Static Files

To serve static files to authenticated users only, place them in the /static-secure folder. These files are referenced using the root-level /secure path.

Example:

curl -X GET http://127.0.0.1:3000/secure/test.html

Application State

A shared, mutable hashmap is automatically added to the server. To invoke this data in a handler, simply add data: AppState<'_, String> to the function signature.

Helper Functions

get<T>(data: AppState<T>, key: &str) -> Option<T>

Retrieves a copy of the entry in application state by key.

Example:

use create::state::get;

pub async fn handle(data: AppState<'_, String>) -> impl Responder {
  let key = "SOME_KEY";
  let value = get(data, key);
  assert_eq!(value, Some("123".to_string()));
}

set<T>(data: AppState<T>, key: &str, value: T) -> Option<T>

Inserts or updates an entry in application state.

Example:

use create::state::set;

pub async fn handle(data: AppState<'_, String>) -> impl Responder {
  let key = "SOME_KEY";
  let value = set(data, key, "123".into());
  assert_eq!(value, None)); // if this is an insert
  assert_eq!(value, Some("123".to_string())); // if this is an update
}

delete<T>(data: AppState<T>, key: &str) -> Option<T>

Deletes an entry in application state by key.

Example:

use create::state::get;

pub async fn handle(data: AppState<'_, String>) -> impl Responder {
  let key = "SOME_KEY";
  let value = delete(data, key);
  assert_eq!(value, None);
}

Application Cache

Asynchronous access to redis is automatically added to the server if a value is provided for the REDIS_URL environment variable. To invoke this data in a handler, simply add cache: Cache to the function signature.

Helper Functions

get(cache: Cache, key: &str) -> Result<String, ApiError>

Retrieves a copy of the entry in the application cache by key.

Example:

use crate::server_helpers:::cache::{get, Cache};

pub async fn handle(cache: Cache) -> impl Responder {
  let key = "SOME_KEY";
  let value = get(cache, key).await?;
  assert_eq!(value, "123");
}

set(cache: Cache, key: &str, value: &str) -> Result<String, ApiError>

Inserts or updates an entry in the application cache.

Example:

use crate::server_helpers:::cache::{set, Cache};

pub async fn handle(cache: Cache) -> impl Responder {
  let key = "SOME_KEY";
  set(cache, key, "123").await?;
}

delete(cache: Cache, key: &str) -> Result<String, ApiError>

Deletes an entry in the application cache by key.

Example:

use crate::server_helpers:::cache::{delete, Cache};

pub async fn handle(cache: Cache) -> impl Responder {
  let key = "SOME_KEY";
  delete(cache, key).await?;
}

Non-Blocking Diesel Database Operations

When accessing a database via Diesel, operations block the main server thread. This blocking can be mitigated by running the blocking code in a thread pool from within the handler.

Example:

pub async fn get_user(
    user_id: Path<Uuid>,
    pool: Data<PoolType>,
) -> Result<Json<UserResponse>, ApiError> {
    let user = block(move || find(&pool, *user_id)).await?;
    respond_json(user)
}

Blocking errors are automatically converted into ApiErrors to keep the api simple:

impl From<BlockingError<ApiError>> for ApiError {
    fn from(error: BlockingError<ApiError>) -> ApiError {
        match error {
            BlockingError::Error(api_error) => api_error,
            BlockingError::Canceled => ApiError::BlockingError("Thread blocking error".into()),
        }
    }
}

Endpoints

Healthcheck

Determine if the system is healthy.

GET /health

Response

{
  "status": "ok",
  "version": "0.1.0"
}

Example:

curl -X GET http://127.0.0.1:3000/health

Login

POST /api/v1/auth/login

Request

Param Type Description Required Validations
email String The user's email address yes valid email address
password String The user's password yes at least 6 characters
{
  "email": "torvalds@transmeta.com",
  "password": "123456"
}

Response

Header

HTTP/1.1 200 OK
content-length: 118
content-type: application/json
set-cookie: auth=COOKIE_VALUE_HERE; HttpOnly; Path=/; Max-Age=1200
date: Tue, 15 Oct 2019 02:04:54 GMT

Json Body

{
  "id": "0c419802-d1ef-47d6-b8fa-c886a23d61a7",
  "first_name": "Linus",
  "last_name": "Torvalds",
  "email": "torvalds@transmeta.com"
}

When sending subsequent requests, create a header variable cookie with the value auth=COOKIE_VALUE_HERE

Logout

GET /api/v1/auth/logout

Response

200 OK

Example:

curl -X GET http://127.0.0.1:3000/api/v1/auth/logout

Get All Users

GET /api/v1/user

Response

[
  {
    "id": "a421a56e-8652-4da6-90ee-59dfebb9d1b4",
    "first_name": "Satoshi",
    "last_name": "Nakamoto",
    "email": "satoshi@nakamotoinstitute.org"
  },
  {
    "id": "c63d285b-7794-4419-bfb7-86d7bb3ff17d",
    "first_name": "Barbara",
    "last_name": "Liskov",
    "email": "bliskov@substitution.org"
  }
]

Example:

curl -X GET http://127.0.0.1:3000/api/v1/user

Get a User

GET /api/v1/user/{id}

Request

Param Type Description
id Uuid The user's id

Response

{
  "id": "a421a56e-8652-4da6-90ee-59dfebb9d1b4",
  "first_name": "Satoshi",
  "last_name": "Nakamoto",
  "email": "satoshi@nakamotoinstitute.org"
}

Example:

curl -X GET http://127.0.0.1:3000/api/v1/user/a421a56e-8652-4da6-90ee-59dfebb9d1b4

Response - Not Found

404 Not Found

{
  "errors": ["User c63d285b-7794-4419-bfb7-86d7bb3ff17a not found"]
}

Create a User

POST /api/v1/user

Request

Param Type Description Required Validations
first_name String The user's first name yes at least 3 characters
last_name String The user's last name yes at least 3 characters
email String The user's email address yes valid email address
{
  "first_name": "Linus",
  "last_name": "Torvalds",
  "email": "torvalds@transmeta.com"
}

Response

{
  "id": "0c419802-d1ef-47d6-b8fa-c886a23d61a7",
  "first_name": "Linus",
  "last_name": "Torvalds",
  "email": "torvalds@transmeta.com"
}

Example:

curl -X POST \
  http://127.0.0.1:3000/api/v1/user \
  -H 'Content-Type: application/json' \
  -d '{
    "first_name": "Linus",
    "last_name": "Torvalds",
    "email": "torvalds@transmeta.com"
}'

Response - Validation Errors

422 Unprocessable Entity

{
  "errors": [
    "first_name is required and must be at least 3 characters",
    "last_name is required and must be at least 3 characters",
    "email must be a valid email"
  ]
}

Update a User

PUT /api/v1/{id}

Request

Path

Param Type Description
id Uuid The user's id

Body

Param Type Description Required Validations
first_name String The user's first name yes at least 3 characters
last_name String The user's last name yes at least 3 characters
email String The user's email address yes valid email address
{
  "first_name": "Linus",
  "last_name": "Torvalds",
  "email": "torvalds@transmeta.com"
}

Response

{
  "id": "0c419802-d1ef-47d6-b8fa-c886a23d61a7",
  "first_name": "Linus",
  "last_name": "Torvalds",
  "email": "torvalds@transmeta.com"
}

Example:

curl -X PUT \
  http://127.0.0.1:3000/api/v1/user/0c419802-d1ef-47d6-b8fa-c886a23d61a7 \
  -H 'Content-Type: application/json' \
  -d '{
    "first_name": "Linus",
    "last_name": "Torvalds",
    "email": "torvalds@transmeta.com"
}'

Response - Validation Errors

422 Unprocessable Entity

{
  "errors": [
    "first_name is required and must be at least 3 characters",
    "last_name is required and must be at least 3 characters",
    "email must be a valid email"
  ]
}

Response - Not Found

404 Not Found

{
  "errors": ["User 0c419802-d1ef-47d6-b8fa-c886a23d61a7 not found"]
}

Delete a User

DELETE /api/v1/user/{id}

Request

Param Type Description
id Uuid The user's id

Response

{
  "id": "a421a56e-8652-4da6-90ee-59dfebb9d1b4",
  "first_name": "Satoshi",
  "last_name": "Nakamoto",
  "email": "satoshi@nakamotoinstitute.org"
}

Response

200 OK

Example:

curl -X DELETE http://127.0.0.1:3000/api/v1/user/a421a56e-8652-4da6-90ee-59dfebb9d1b4

Response - Not Found

404 Not Found

{
  "errors": ["User c63d285b-7794-4419-bfb7-86d7bb3ff17a not found"]
}

License

This project is licensed under:

About

Actix server boilerplate

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages