Skip to content

Commit

Permalink
Merge pull request #18 from TheAwiteb/13-todo-CRUD
Browse files Browse the repository at this point in the history
Todos CRUDS
  • Loading branch information
TheAwiteb authored Feb 15, 2023
2 parents 6c6f746 + 709241e commit d7567b4
Show file tree
Hide file tree
Showing 46 changed files with 1,374 additions and 191 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ jobs:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: 1.65.0
override: true
components: rustfmt, clippy,
- uses: extractions/setup-just@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
45 changes: 38 additions & 7 deletions .justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,70 @@ _default:

# Build the RESTful API
build:
cargo +stable build --all-features --verbose
cargo +1.65.0 build --all-features --verbose

# Run register tests
_register_tests:
dotenv cargo +stable test -j 1 --all-features tests::register -- --test-threads 1
dotenv cargo +1.65.0 test -j 1 --all-features tests::register:: -- --test-threads 1

# Run login tests
_login_tests:
dotenv cargo +stable test -j 1 --all-features tests::login -- --test-threads 1
dotenv cargo +1.65.0 test -j 1 --all-features tests::login:: -- --test-threads 1

# Run revoke tests
_revoke_tests:
dotenv cargo +stable test -j 1 --all-features tests::revoke -- --test-threads 1
dotenv cargo +1.65.0 test -j 1 --all-features tests::revoke:: -- --test-threads 1

# Run create todo tests
_create_todo_tests:
dotenv cargo +1.65.0 test -j 1 --all-features tests::todo::create_todo:: -- --test-threads 1

# Run list todo tests
_list_todo_tests:
dotenv cargo +1.65.0 test -j 1 --all-features tests::todo::list_todo:: -- --test-threads 1

# Run get todo tests
_get_todo_tests:
dotenv cargo +1.65.0 test -j 1 --all-features tests::todo::get_todo:: -- --test-threads 1

# Run delete todo tests
_delete_todo_tests:
dotenv cargo +1.65.0 test -j 1 --all-features tests::todo::delete_todo:: -- --test-threads 1

# Run delete todos tests
_delete_todos_tests:
dotenv cargo +1.65.0 test -j 1 --all-features tests::todo::delete_todos:: -- --test-threads 1

# Run update todo tests
_update_todo_tests:
dotenv cargo +1.65.0 test -j 1 --all-features tests::todo::update_todo:: -- --test-threads 1

# Run the tests
tests:
# Clean the database
echo > db.sqlite3
# Run the tests sequentially, because they are not independent
just _register_tests
just _login_tests
just _revoke_tests
just _create_todo_tests
just _list_todo_tests
just _get_todo_tests
just _delete_todo_tests
just _delete_todos_tests
just _update_todo_tests

# Format everything
fmt:
cargo +stable fmt --all --verbose
cargo +1.65.0 fmt --all --verbose

# Check the format of everything
fmt-check:
cargo +stable fmt --all --check --verbose
cargo +1.65.0 fmt --all --check --verbose

# Run Rust linter (clippy)
linter:
cargo +stable clippy --workspace --examples --all-features --verbose
cargo +1.65.0 clippy --workspace --examples --all-features --verbose

# Run the CI
ci: build && fmt-check linter tests
Expand Down
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ publish = false
description = "A RESTful API for a todo list, written in Rust."
license = "MIT"
authors = ["Awiteb <https://github.com/TheAwiteb>"]
rust-version = "1.65"

[dependencies]
entity = { path = "entity" }
Expand All @@ -30,6 +31,7 @@ hex = "= 0.4.3"
utoipa = { version = "= 2.3.0", features = ["actix_extras"] }
utoipa-swagger-ui = { version = "= 2.0.1", features = ["actix-web"] }
actix-extensible-rate-limit = {version = "= 0.2.1", default-features = false, features = ["dashmap"]}
uuid = {version = "1.3.0", features = ["serde", "v4"]}

[dev-dependencies]
actix-http = "= 3.2.2"
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RESTful Todo API with [Actix](https://actix.rs) and [SeaORM](https://www.sea-ql.
</div>

### Prerequisites
- [Rust](https://www.rust-lang.org/tools/install)
- [Rust](https://www.rust-lang.org/tools/install) (Minimum Supported Rust Version: 1.65.0)

### Usage
Clone the repository and run the following commands:
Expand All @@ -23,8 +23,8 @@ RUST_LOG=debug cargo run
```

### Documentation
- The API documentation is available at `{HOST}:{PORT}/docs/swagger/` (default: [http://localhost:8080/docs/swagger](http://localhost:8080/docs/swagger/))
- The OpenAPI specification is available at `{HOST}:{PORT}/docs/openapi.json` (default: [http://localhost:8080/docs/openapi.json](http://localhost:8080/docs/openapi.json))
- The API documentation is available at `{HOST}:{PORT}/docs/swagger/` (default: <http://localhost:8080/docs/swagger/>)
- The OpenAPI specification is available at `{HOST}:{PORT}/docs/openapi.json` (default: <http://localhost:8080/docs/openapi.json>)

### Environment variables
Rename the `.env.example` file to `.env` and change the values to your needs. Empty default means that the variable is required.
Expand All @@ -44,13 +44,13 @@ Rename the `.env.example` file to `.env` and change the values to your needs. Em

### Testing
#### Prerequisites
- [dotenv cli](https://pypi.org/project/python-dotenv/)
- [dotenv-cli](https://pypi.org/project/dotenv-cli/)
- [just](https://github.com/casey/just)
```bash
just tests
```
### Development
For development you need to install [just](https://github.com/casey/just) and [dotenv cli](https://pypi.org/project/python-dotenv/).
For development you need to install [just](https://github.com/casey/just) and [dotenv-cli](https://pypi.org/project/dotenv-cli/).
With `just` you can run all needed commands with one command, type `just` folloing by the command you want to run.<br>
Available commands:
- `just build` to build the RESTful API
Expand Down
7 changes: 5 additions & 2 deletions entity/src/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
#[sea_orm(rs_type = "String", db_type = "String(Some(1))")]
#[serde(rename_all = "lowercase")]
/// The todo status
pub enum Status {
/// Completed todo
Expand Down Expand Up @@ -39,12 +40,12 @@ impl FromStr for Status {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
match s.to_lowercase().trim() {
"completed" => Ok(Self::Completed),
"pending" => Ok(Self::Pending),
"progress" => Ok(Self::Progress),
"cancelled" => Ok(Self::Cancelled),
_ => Err(format!("The status `{s}` is invalid")),
_ => Err(format!("The status `{s}` is invalid, expected `completed`, `pending`, `progress` or `cancelled`")),
}
}
}
Expand All @@ -54,6 +55,8 @@ impl FromStr for Status {
pub struct Model {
#[sea_orm(primary_key)]
pub id: u32,
#[sea_orm(unique_key)]
pub uuid: Uuid,
pub user_id: u32,
pub title: String,
pub status: Status,
Expand Down
2 changes: 2 additions & 0 deletions migration/src/m20221112_051333_create_todo_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ impl MigrationTrait for Migration {
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Todo::Uuid).uuid().not_null().unique_key())
.col(ColumnDef::new(Todo::UserId).big_unsigned().not_null())
.col(ColumnDef::new(Todo::Title).string().not_null())
.col(ColumnDef::new(Todo::Status).string().not_null())
Expand All @@ -38,6 +39,7 @@ impl MigrationTrait for Migration {
enum Todo {
Table,
Id,
Uuid,
UserId,
Title,
Status,
Expand Down
11 changes: 9 additions & 2 deletions src/api_docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@ use utoipa::{Modify, OpenApi};
crate::auth::register::register,
// Todo routes
crate::todo::create::create,
crate::todo::list::list,
crate::todo::get_todo::get_todo,
crate::todo::delete_todo::delete_todo,
crate::todo::delete_todos::delete_todos,
crate::todo::update::update_todo,
),
components (
schemas (
// General schemas
crate::schemas::errors::ErrorSchema,
crate::schemas::message::MessageSchema,
// Auth schemas
crate::schemas::auth::LoginSchema,
crate::schemas::user::UserSchema,
crate::schemas::auth::RegisterSchema,
// Todo schemas
crate::schemas::todo::CreateTodoSchema,
crate::schemas::todo::TodoContentSchema,
crate::schemas::todo::TodoScheam,
crate::schemas::todo::TodoListSchema,
crate::schemas::todo::TodoListMetaSchema,
)
),
tags(
Expand Down
6 changes: 3 additions & 3 deletions src/auth/login.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use actix_web::{post, web, Responder};
use sea_orm::DatabaseConnection;

use crate::schemas::{auth::LoginSchema, errors::ErrorSchema};
use crate::schemas::{auth::LoginSchema, message::MessageSchema};

/// Login a user
///
Expand All @@ -15,8 +15,8 @@ use crate::schemas::{auth::LoginSchema, errors::ErrorSchema};
status = 200, description = "Login successfully and return a new token", body = UserSchema
),
(
status = 400, description = "The username or password is incorrect", body = ErrorSchema,
example = json!(ErrorSchema::new(400, "The username or password is incorrect"))
status = 400, description = "The username or password is incorrect", body = MessageSchema,
example = json!(MessageSchema::new(400, "The username or password is incorrect"))
),
),
tag = "Auth"
Expand Down
6 changes: 3 additions & 3 deletions src/auth/register.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use actix_web::{http::StatusCode, post, web, Responder};
use sea_orm::DatabaseConnection;

use crate::schemas::{auth::RegisterSchema, errors::ErrorSchema};
use crate::schemas::{auth::RegisterSchema, message::MessageSchema};

/// Register a new user, will return the new token for the user.
///
Expand All @@ -15,8 +15,8 @@ use crate::schemas::{auth::RegisterSchema, errors::ErrorSchema};
status = 201, description = "Register successfully and return a new token", body = UserSchema
),
(
status = 400, description = "The username is not unique", body = ErrorSchema,
example = json!(ErrorSchema::new(400, "Username `Awiteb` already exists"))
status = 400, description = "The username is not unique", body = MessageSchema,
example = json!(MessageSchema::new(400, "Username `Awiteb` already exists"))
),
),
tag = "Auth"
Expand Down
4 changes: 2 additions & 2 deletions src/auth/revoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ use entity::user::Model as UserModel;
use sea_orm::{ActiveModelTrait, DatabaseConnection, IntoActiveModel, Set};

use crate::auth::utils as auth_utils;
use crate::errors::{Result as TodoResult, TodoError as TodoErrorTrait};
use crate::errors::{ErrorTrait, Result as ApiResult};
use crate::schemas::user::UserSchema;

/// Revoke a token by user, will return the new token
pub async fn revoke_token(db: &DatabaseConnection, user: UserModel) -> TodoResult<UserSchema> {
pub async fn revoke_token(db: &DatabaseConnection, user: UserModel) -> ApiResult<UserSchema> {
let mut user = user.into_active_model();
user.token_created_at = Set(Utc::now().naive_utc().timestamp());
user.save(db)
Expand Down
18 changes: 9 additions & 9 deletions src/auth/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use sha2::{Digest, Sha256};

use crate::auth::traits::ClaimsHelper;
use crate::errors::TodoError as TodoErrorTrait;
use crate::errors::{Error as TodoError, Result as TodoResult};
use crate::errors::ErrorTrait;
use crate::errors::{Error as ApiError, Result as ApiResult};

/// Hash given data by sha256 algorithm.
pub fn hash_function(data: &str) -> String {
Expand All @@ -20,7 +20,7 @@ pub fn hash_function(data: &str) -> String {
/// ### Arguments
/// * `user_id` - The id of the user
/// * `created_date` - The timestamp of the token created, to add it to JWT payload
pub fn generate_token(user_id: u32, created_date: i64) -> TodoResult<String> {
pub fn generate_token(user_id: u32, created_date: i64) -> ApiResult<String> {
let secret = std::env::var("SECRET_KEY").expect("SECRET_KEY must be set");
let str_id = user_id.to_string();
let str_timestamp = created_date.to_string();
Expand All @@ -42,7 +42,7 @@ pub fn generate_token(user_id: u32, created_date: i64) -> TodoResult<String> {
/// - User not found
/// - Token is invalid
/// - Token is was revoked
pub async fn get_user_by_token(db: &DatabaseConnection, token: &str) -> TodoResult<UserModel> {
pub async fn get_user_by_token(db: &DatabaseConnection, token: &str) -> ApiResult<UserModel> {
let secret = std::env::var("SECRET_KEY").expect("SECRET_KEY must be set");
let key: Hmac<Sha256> = Hmac::new_from_slice(secret.as_bytes()).key_creation_err()?;
let claims = jwt::VerifyWithKey::<Token<Header, BTreeMap<String, String>, _>>::verify_with_key(
Expand All @@ -58,7 +58,7 @@ pub async fn get_user_by_token(db: &DatabaseConnection, token: &str) -> TodoResu
.incorrect_user_err()?;

if claims.get_created_at() != user.token_created_at {
Err(TodoError::Forbidden("Token has been revoked".to_owned()))
Err(ApiError::Forbidden("Token has been revoked".to_owned()))
} else {
Ok(user)
}
Expand All @@ -69,9 +69,9 @@ pub async fn get_user_by_username_and_password(
db: &DatabaseConnection,
username: &str,
password: &str,
) -> TodoResult<UserModel> {
) -> ApiResult<UserModel> {
if username.is_empty() || password.is_empty() {
return Err(TodoError::BAdRequest(
return Err(ApiError::BAdRequest(
"Invalid username or password, must be not empty".to_owned(),
));
}
Expand All @@ -91,7 +91,7 @@ pub async fn get_user_by_username_and_password(
}

/// Extract the token from the request header.
pub fn extract_token(req: &HttpRequest) -> TodoResult<String> {
pub fn extract_token(req: &HttpRequest) -> ApiResult<String> {
req.headers()
.get("Authorization")
.map(|token| token.to_str().map(|token| token.strip_prefix("Bearer ")))
Expand All @@ -102,7 +102,7 @@ pub fn extract_token(req: &HttpRequest) -> TodoResult<String> {
}

/// Return user model by given request
pub async fn req_auth(req: HttpRequest, db: &DatabaseConnection) -> TodoResult<UserModel> {
pub async fn req_auth(req: HttpRequest, db: &DatabaseConnection) -> ApiResult<UserModel> {
let token = extract_token(&req)?;

get_user_by_token(db, &token).await
Expand Down
Loading

0 comments on commit d7567b4

Please sign in to comment.