14 Commits

  1. 1
      .env
  2. 625
      Cargo.lock
  3. 23
      Cargo.toml
  4. 37
      Makefile.toml
  5. 25
      README.md
  6. 104
      src/actions/users.rs
  7. 65
      src/errors/domain_error.rs
  8. 72
      src/lib.rs
  9. 90
      src/main.rs
  10. 2
      src/middlewares.rs
  11. 276
      src/middlewares/csrf.rs
  12. 4
      src/models.rs
  13. 22
      src/models/api_response.rs
  14. 14
      src/models/errors.rs
  15. 235
      src/models/users.rs
  16. 3
      src/routes/auth.rs
  17. 150
      src/routes/users.rs
  18. 128
      src/services/user_service.rs
  19. 5
      src/types.rs
  20. 6
      src/utils.rs
  21. 28
      src/utils/extra.rs
  22. 34
      src/utils/ops.rs
  23. 2
      src/utils/regex.rs
  24. 0
      static/dummy.js
  25. 1
      static/test.js
  26. 22
      templates/hello.hbs
  27. 110
      tests/integration/common/mod.rs
  28. 48
      tests/integration/main.rs
  29. 21
      tests/integration/misc.rs
  30. 44
      tests/integration/users.rs

1
.env

@ -5,4 +5,5 @@ ACTIX_DEMO_RUST_LOG=debug
ACTIX_DEMO_TEST_RUST_LOG=debug
ACTIX_DEMO_HTTP_HOST=127.0.0.1
ACTIX_DEMO_HASH_COST=8
ACTIX_DEMO_LOGGER_FORMAT=pretty

625
Cargo.lock
File diff suppressed because it is too large
View File

23
Cargo.toml

@ -11,23 +11,15 @@ actix-files = "0.5.0"
actix-http = "2.2.0"
bytes = "1.0.1"
futures = "0.3.14"
log = "0.4.14"
env_logger = "0.8.3"
serde_json = "1.0.64"
# json = "0.12.4"
# listenfd = "0.3.3"
dotenv = "0.15.0"
r2d2 = "0.8.9"
validator = "0.13.0"
validator_derive = "0.13.0"
# jsonwebtoken = "7.2.0"
actix-identity = "0.3.1"
actix-web-httpauth = "0.5.1"
rand = "0.8.3"
nanoid = "0.4.0"
bcrypt = "0.9.0"
timeago = "0.3.0"
# comp = "0.2.1"
regex = "1.4.5"
lazy_static = "1.4.0"
lazy-regex = "0.1.4"
@ -36,6 +28,21 @@ derive-new = "0.5.9"
diesel_migrations = "1.4.0"
actix-threadpool = "0.3.3"
envy = "0.4"
tracing = { version = "0.1" }
tracing-log = "0.1.2"
tracing-subscriber = { version = "0.2.18", features = ["fmt", "registry", "env-filter"] }
tracing-futures = "0.2.5"
tracing-actix-web = "0.2.1"
tracing-bunyan-formatter = "0.2.4"
diesel-tracing = { version = "0.1.4", features = ["sqlite"] }
validators = "0.22.5"
diesel-derive-newtype = "0.1"
derive_more = "0.99.13"
tracing-appender = "0.1.2"
[dependencies.validators-derive]
version = "0.22.5"
features = ["serde"]
[dependencies.build-info]
version = "=0.0.23"

37
Makefile.toml

@ -0,0 +1,37 @@
[tasks.watch]
install_crate = "watch"
command = "cargo"
args = ["watch", "-x", "run"]
[tasks.format]
install_crate = "rustfmt"
command = "cargo"
args = ["fmt", "--all"]
[tasks.format-check]
install_crate = "rustfmt"
command = "cargo"
args = ["fmt", "--all", "--", "--check"]
[tasks.clippy-check]
install_crate = "clippy"
command = "cargo"
args = ["clippy", "--all", "--", "-D", "warnings"]
[tasks.lint-check]
dependencies = ["format-check", "clippy-check"]
[tasks.compile]
command = "cargo"
args = ["build"]
[tasks.test]
command = "cargo"
args = ["test", "--lib"]
[tasks.it-test]
command = "cargo"
args = ["test", "--test", "integration"]
[tasks.stage]
dependencies = ["lint-check", "compile", "test", "it-test"]

25
README.md

@ -5,7 +5,7 @@ Testing out the Rust framework Actix-Web to create a JSON API CRUD Web App.
### Get Users
```
curl -X GET http://localhost:7800/api/users/get/1
curl -X GET http://localhost:7800/api/users
```
```
@ -16,7 +16,7 @@ curl -X GET http://localhost:7800/api/users/get/1
```
```
curl -X GET http://localhost:7800/api/users/get
curl -X GET http://localhost:7800/api/users
```
```
@ -40,8 +40,8 @@ curl -X GET http://localhost:7800/api/users/get
```
curl -H "content-type: application/json" \
-X POST \
-i http://localhost:7800/do_registration \
-X PUT \
-i http://localhost:7800/api/users \
--data '{"name":"user4","password":"test"}'
```
@ -66,19 +66,10 @@ curl -H "content-type: application/json" \
]
```
### DTO Validation
```
curl -H "content-type: application/json" \
-X POST \
-i http://localhost:7800/do_registration \
--data '{"name":"abc","password":"test"}' # min length for name is 4
```
```
ValidationErrors({"name": Field([ValidationError { code: "length", message: None, params: {"value": String("abc"), "min": Number(4), "max": Number(10)} }])})
```
## Memory Usage
Memory usage as compared to interpreted languages was my primary motivation for looking into rust as a backend language. As of writing, the demo app uses less than 50MB of memory.
## License
AGPLv3

104
src/actions/users.rs

@ -1,69 +1,99 @@
use diesel::prelude::*;
use crate::errors;
use crate::models;
use crate::models::{self, Pagination, UserId, Username};
use crate::{errors, models::Password};
use bcrypt::{hash, verify, DEFAULT_COST};
use validators::prelude::*;
pub fn find_user_by_uid(
uid: i32,
conn: &SqliteConnection,
) -> Result<Option<models::UserDto>, errors::DomainError> {
uid: &UserId,
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<Option<models::User>, errors::DomainError> {
use crate::schema::users::dsl::*;
let maybe_user = users
.select((name, created_at))
.select(users::all_columns())
.find(uid)
.first::<models::UserDto>(conn)
.first::<models::User>(conn)
.optional();
Ok(maybe_user?)
}
pub fn _find_user_by_name(
user_name: String,
conn: &SqliteConnection,
) -> Result<Option<models::UserDto>, errors::DomainError> {
let maybe_user = query::_get_user_by_name(&user_name)
.first::<models::UserDto>(conn)
user_name: Username,
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<Option<models::User>, errors::DomainError> {
use crate::schema::users::dsl::*;
let maybe_user = query::_get_user_by_name()
.filter(name.eq(user_name))
.first::<models::User>(conn)
.optional();
Ok(maybe_user?)
}
pub fn get_all(
conn: &SqliteConnection,
) -> Result<Vec<models::UserDto>, errors::DomainError> {
// def findAll(userId: Long, limit: Int, offset: Int) = db.run {
// for {
// comments <- query.filter(_.creatorId === userId)
// .sortBy(_.createdAt)
// .drop(offset).take(limit)
// .result
// numberOfComments <- query.filter(_.creatorId === userId).length.result
// } yield PaginatedResult(
// totalCount = numberOfComments,
// entities = comments.toList,
// hasNextPage = numberOfComments - (offset + limit) > 0
// )
// }
pub fn get_all_users(
pagination: &Pagination,
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<Vec<models::User>, errors::DomainError> {
Ok(query::_paginate_result(&pagination).load::<models::User>(conn)?)
}
pub fn search_users(
query: &str,
pagination: &Pagination,
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<Vec<models::User>, errors::DomainError> {
use crate::schema::users::dsl::*;
Ok(users
.select((name, created_at))
.load::<models::UserDto>(conn)?)
Ok(query::_paginate_result(&pagination)
.filter(name.like(format!("%{}%", query)))
.load::<models::User>(conn)?)
}
/// Run query using Diesel to insert a new database row and return the result.
pub fn insert_new_user(
nu: models::NewUser,
conn: &SqliteConnection,
) -> Result<models::UserDto, errors::DomainError> {
// It is common when using Diesel with Actix web to import schema-related
// modules inside a function's scope (rather than the normal module's scope)
// to prevent import collisions and namespace pollution.
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
hash_cost: Option<u32>,
) -> Result<models::User, errors::DomainError> {
use crate::schema::users::dsl::*;
let nu = {
let mut nu2 = nu;
nu2.password = hash(&nu2.password, DEFAULT_COST)?;
let hash =
hash(&nu2.password.as_str(), hash_cost.unwrap_or(DEFAULT_COST))?;
nu2.password = Password::parse_string(hash).map_err(|err| {
errors::DomainError::new_field_validation_error(err.to_string())
})?;
nu2
};
diesel::insert_into(users).values(&nu).execute(conn)?;
let user =
query::_get_user_by_name(&nu.name).first::<models::UserDto>(conn)?;
let user = query::_get_user_by_name()
.filter(name.eq(nu.name.as_str()))
.first::<models::User>(conn)?;
Ok(user)
}
//TODO: Add newtype here
pub fn verify_password(
user_name: &str,
given_password: &str,
conn: &SqliteConnection,
conn: &impl diesel::Connection<Backend = diesel::sqlite::Sqlite>,
) -> Result<bool, errors::DomainError> {
use crate::schema::users::dsl::*;
let password_hash = users
@ -74,7 +104,8 @@ pub fn verify_password(
}
mod query {
use diesel::prelude::*;
use super::*;
use diesel::sql_types::Integer;
use diesel::sql_types::Text;
use diesel::sql_types::Timestamp;
use diesel::sqlite::Sqlite;
@ -83,12 +114,19 @@ mod query {
type Query<'a, B, T> = crate::schema::users::BoxedQuery<'a, B, T>;
pub fn _get_user_by_name(
user_name: &str,
) -> Query<Sqlite, (Text, Timestamp)> {
) -> Query<'static, Sqlite, (Integer, Text, Text, Timestamp)> {
use crate::schema::users::dsl::*;
users.into_boxed()
}
pub fn _paginate_result(
pagination: &Pagination,
) -> Query<'static, Sqlite, (Integer, Text, Text, Timestamp)> {
use crate::schema::users::dsl::*;
users
.select((name, created_at))
.filter(name.eq(user_name))
.order_by(created_at)
.offset(pagination.calc_offset().as_uint().into())
.limit(pagination.limit.as_uint().into())
.into_boxed()
}
}

65
src/errors/domain_error.rs

@ -3,7 +3,7 @@ use bcrypt::BcryptError;
use custom_error::custom_error;
// use derive_more::Display;
// use diesel::result::DatabaseErrorKind;
use crate::models::errors::*;
use crate::models::api_response::*;
use std::convert::From;
// impl From<DBError> for DomainError {
@ -24,6 +24,7 @@ use std::convert::From;
custom_error! { #[derive(new)] pub DomainError
PwdHashError {source: BcryptError} = "Failed to hash password",
FieldValidationError {message: String} = "Failed to validate one or more fields",
DbError {source: diesel::result::Error} = "Database error",
DbPoolError {source: r2d2::Error} = "Failed to get connection from pool",
PasswordError {cause: String} = "Failed to validate password - {cause}",
@ -37,56 +38,40 @@ impl ResponseError for DomainError {
let err = self;
match self {
DomainError::PwdHashError { source: _ } => {
HttpResponse::InternalServerError().json(ErrorModel {
// error_code: 500,
success: false,
reason: err.to_string(),
})
HttpResponse::InternalServerError()
.json(ApiResponse::failure(err.to_string()))
}
DomainError::DbError { source: _ } => {
error!("{}", err);
HttpResponse::InternalServerError().json(ErrorModel {
// error_code: 500,
success: false,
reason: "Error in database".to_owned(),
})
let _ = tracing::error!("{}", err);
HttpResponse::InternalServerError()
.json(ApiResponse::failure("Error in database".to_owned()))
}
DomainError::DbPoolError { source: _ } => {
error!("{}", err);
HttpResponse::InternalServerError().json(ErrorModel {
// error_code: 500,
success: false,
reason: "Error getting database pool".to_owned(),
})
let _ = tracing::error!("{}", err);
HttpResponse::InternalServerError().json(ApiResponse::failure(
"Error getting database pool".to_owned(),
))
}
DomainError::PasswordError { cause: _ } => {
HttpResponse::BadRequest().json(ErrorModel {
// error_code: 400,
success: false,
reason: err.to_string(),
})
HttpResponse::BadRequest()
.json(ApiResponse::failure(err.to_string()))
}
DomainError::EntityDoesNotExistError { message: _ } => {
HttpResponse::Accepted().json(ErrorModel {
// error_code: 400,
success: false,
reason: err.to_string(),
})
HttpResponse::NotFound()
.json(ApiResponse::failure(err.to_string()))
}
DomainError::ThreadPoolError { message: _ } => {
error!("{}", err);
HttpResponse::InternalServerError().json(ErrorModel {
// error_code: 400,
success: false,
reason: "Thread pool error occurred".to_owned(),
})
let _ = tracing::error!("{}", err);
HttpResponse::InternalServerError().json(ApiResponse::failure(
"Thread pool error occurred".to_owned(),
))
}
DomainError::AuthError { message: _ } => {
HttpResponse::Accepted().json(ErrorModel {
// error_code: 400,
success: false,
reason: err.to_string(),
})
DomainError::AuthError { message: _ } => HttpResponse::Forbidden()
.json(ApiResponse::failure(err.to_string())),
DomainError::FieldValidationError { message: _ } => {
let _ = tracing::error!("{}", err);
HttpResponse::BadRequest()
.json(ApiResponse::failure(err.to_string()))
}
}
}

72
src/lib.rs

@ -4,7 +4,9 @@ extern crate diesel;
#[macro_use]
extern crate derive_new;
#[macro_use]
extern crate log;
extern crate validators_derive;
#[macro_use]
extern crate diesel_derive_newtype;
mod actions;
mod errors;
@ -18,25 +20,36 @@ mod utils;
use actix_files as fs;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{cookie::SameSite, middleware, web, App, HttpServer};
use actix_web::{middleware::Logger, web::ServiceConfig};
use actix_web::web::ServiceConfig;
use actix_web::{cookie::SameSite, web, App, HttpServer};
use rand::Rng;
use serde::Deserialize;
use std::io;
use tracing_actix_web::TracingLogger;
use types::DbPool;
build_info::build_info!(pub fn get_build_info);
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum LoggerFormat {
Json,
Pretty,
}
#[derive(Deserialize, Debug, Clone)]
pub struct EnvConfig {
pub database_url: String,
pub http_host: String,
#[serde(default = "default_hash_cost")]
pub hash_cost: u8,
pub hash_cost: u32,
pub logger_format: LoggerFormat,
}
#[derive(Deserialize, Debug, Clone)]
pub struct AppConfig {
pub hash_cost: u8,
pub hash_cost: u32,
}
#[derive(Clone)]
@ -45,7 +58,7 @@ pub struct AppData {
pub pool: DbPool,
}
pub fn default_hash_cost() -> u8 {
pub fn default_hash_cost() -> u32 {
8
}
@ -54,23 +67,35 @@ pub fn configure_app(app_data: AppData) -> Box<dyn Fn(&mut ServiceConfig)> {
cfg.data(app_data.clone())
.service(
web::scope("/api")
.service(routes::users::get_user)
.service(routes::users::get_all_users)
.service(web::scope("/get").route(
.service(
web::scope("/users")
.route("", web::get().to(routes::users::get_users))
.route(
"/search",
web::get().to(routes::users::search_users),
)
.route("", web::put().to(routes::users::add_user))
.route(
"/{user_id}",
web::get().to(routes::users::get_user),
),
)
.route(
"/build-info",
web::get().to(routes::misc::build_info_req),
)),
),
)
// .route("/api/users/get", web::get().to(user_controller.get_user.into()))
.service(web::scope("/api/public")) // public endpoint - not implemented yet
.service(routes::auth::login)
.service(routes::auth::logout)
.service(routes::auth::index)
.service(routes::users::add_user)
// .service(routes::users::add_user)
.service(fs::Files::new("/", "./static"));
})
}
//TODO: capture the panic in this method
pub fn id_service(
private_key: &[u8],
) -> actix_identity::IdentityService<CookieIdentityPolicy> {
@ -82,18 +107,29 @@ pub fn id_service(
)
}
pub fn app_logger() -> Logger {
middleware::Logger::default()
}
pub async fn run(addr: String, app_data: AppData) -> std::io::Result<()> {
info!("Starting server at {}", addr);
pub async fn run(addr: String, app_data: AppData) -> io::Result<()> {
let bi = get_build_info();
let _ = tracing::info!(
"Starting {} {}",
bi.crate_info.name,
bi.crate_info.version
);
println!(
r#"
__ .__ .___
_____ _____/ |_|__|__ ___ __| _/____ _____ ____
\__ \ _/ ___\ __\ \ \/ / ______ / __ |/ __ \ / \ / _ \
/ __ \\ \___| | | |> < /_____/ / /_/ \ ___/| Y Y ( <_> )
(____ /\___ >__| |__/__/\_ \ \____ |\___ >__|_| /\____/
\/ \/ \/ \/ \/ \/
"#
);
let private_key = rand::thread_rng().gen::<[u8; 32]>();
let app = move || {
App::new()
.configure(configure_app(app_data.clone()))
.wrap(id_service(&private_key))
.wrap(app_logger())
.wrap(TracingLogger)
};
HttpServer::new(app).bind(addr)?.run().await
}

90
src/main.rs

@ -1,9 +1,17 @@
#![forbid(unsafe_code)]
use actix_demo::{AppConfig, AppData, EnvConfig};
use diesel::{r2d2::ConnectionManager, SqliteConnection};
use env_logger::Env;
use actix_demo::{AppConfig, AppData, EnvConfig, LoggerFormat};
use diesel::r2d2::ConnectionManager;
use diesel_tracing::sqlite::InstrumentedSqliteConnection;
use io::ErrorKind;
use std::io;
use tracing::subscriber::set_global_default;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_log::LogTracer;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::{
layer::SubscriberExt, EnvFilter, FmtSubscriber, Registry,
};
#[actix_web::main]
async fn main() -> io::Result<()> {
@ -14,16 +22,6 @@ async fn main() -> io::Result<()> {
)
})?;
let _ = env_logger::try_init_from_env(
Env::default().filter("ACTIX_DEMO_RUST_LOG"),
)
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set up env logger: {:?}", err),
)
})?;
let env_config = envy::prefixed("ACTIX_DEMO_")
.from_env::<EnvConfig>()
.map_err(|err| {
@ -33,8 +31,12 @@ async fn main() -> io::Result<()> {
)
})?;
//bind guard to variable instead of _
let _guard = setup_logger(env_config.logger_format)?;
let connspec = &env_config.database_url;
let manager = ConnectionManager::<SqliteConnection>::new(connspec);
let manager =
ConnectionManager::<InstrumentedSqliteConnection>::new(connspec);
let pool = r2d2::Pool::builder().build(manager).map_err(|err| {
io::Error::new(
ErrorKind::Other,
@ -68,3 +70,63 @@ async fn main() -> io::Result<()> {
actix_demo::run(format!("{}:7800", env_config.http_host), app_data).await
}
pub fn setup_logger(format: LoggerFormat) -> io::Result<WorkerGuard> {
let env_filter =
EnvFilter::try_from_env("ACTIX_DEMO_RUST_LOG").map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set up env filter: {:?}", err),
)
})?;
let (non_blocking, _guard) =
tracing_appender::non_blocking(std::io::stdout());
let _ = LogTracer::init().map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set up log tracer: {:?}", err),
)
})?;
let bi = actix_demo::get_build_info();
let _ = match format {
LoggerFormat::Json => {
let formatting_layer = BunyanFormattingLayer::new(
format!("actix-demo-{}", bi.crate_info.version),
// Output the formatted spans to non-blocking writer
non_blocking,
);
let subscriber = Registry::default()
.with(env_filter)
.with(JsonStorageLayer)
.with(formatting_layer);
let _ = set_global_default(subscriber).map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set subscriber: {:?}", err),
)
})?;
}
LoggerFormat::Pretty => {
let subscriber = FmtSubscriber::builder()
.pretty()
.with_span_events(FmtSpan::NEW)
.with_span_events(FmtSpan::CLOSE)
.with_env_filter(env_filter)
.with_writer(non_blocking)
.with_thread_names(true)
.finish();
let _ = set_global_default(subscriber).map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set subscriber: {:?}", err),
)
})?;
}
};
Ok(_guard)
}

2
src/middlewares.rs

@ -1,3 +1 @@
pub mod csrf;
pub use self::csrf::*;

276
src/middlewares/csrf.rs

@ -1,276 +0,0 @@
// //! A filter for cross-site request forgery (CSRF).
// //!
// //! This middleware is stateless and [based on request
// //! headers](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers).
// //!
// //! By default requests are allowed only if one of these is true:
// //!
// //! * The request method is safe (`GET`, `HEAD`, `OPTIONS`). It is the
// //! applications responsibility to ensure these methods cannot be used to
// //! execute unwanted actions. Note that upgrade requests for websockets are
// //! also considered safe.
// //! * The `Origin` header (added automatically by the browser) matches one
// //! of the allowed origins.
// //! * There is no `Origin` header but the `Referer` header matches one of
// //! the allowed origins.
// //!
// //! Use [`CsrfFilter::allow_xhr()`](struct.CsrfFilter.html#method.allow_xhr)
// //! if you want to allow requests with unprotected methods via
// //! [CORS](../cors/struct.Cors.html).
// //!
// //! # Example
// //!
// //! ```
// //! # extern crate actix_web;
// //! use actix_web::middleware::csrf;
// //! use actix_web::{http, App, HttpRequest, HttpResponse};
// //!
// //! fn handle_post(_: &HttpRequest) -> &'static str {
// //! "This action should only be triggered with requests from the same site"
// //! }
// //!
// //! fn main() {
// //! let app = App::new()
// //! .middleware(
// //! csrf::CsrfFilter::new().allowed_origin("https://www.example.com"),
// //! )
// //! .resource("/", |r| {
// //! r.method(http::Method::GET).f(|_| HttpResponse::Ok());
// //! r.method(http::Method::POST).f(handle_post);
// //! })
// //! .finish();
// //! }
// //! ```
// //!
// //! In this example the entire application is protected from CSRF.
// use std::borrow::Cow;
// use std::collections::HashSet;
// use bytes::Bytes;
// use error::{ResponseError, Result};
// use http::{header, HeaderMap, HttpTryFrom, Uri};
// use httprequest::HttpRequest;
// use httpresponse::HttpResponse;
// use middleware::{Middleware, Started};
// use server::Request;
// /// Potential cross-site request forgery detected.
// #[derive(Debug, Fail)]
// pub enum CsrfError {
// /// The HTTP request header `Origin` was required but not provided.
// #[fail(display = "Origin header required")]
// MissingOrigin,
// /// The HTTP request header `Origin` could not be parsed correctly.
// #[fail(display = "Could not parse Origin header")]
// BadOrigin,
// /// The cross-site request was denied.
// #[fail(display = "Cross-site request denied")]
// CsrDenied,
// }
// impl ResponseError for CsrfError {
// fn error_response(&self) -> HttpResponse {
// HttpResponse::Forbidden().body(self.to_string())
// }
// }
// fn uri_origin(uri: &Uri) -> Option<String> {
// match (
// uri.scheme_part(),
// uri.host(),
// uri.port_part().map(|port| port.as_u16()),
// ) {
// (Some(scheme), Some(host), Some(port)) => Some(format!("{}://{}:{}", scheme, host, port)),
// (Some(scheme), Some(host), None) => Some(format!("{}://{}", scheme, host)),
// _ => None,
// }
// }
// fn origin(headers: &HeaderMap) -> Option<Result<Cow<str>, CsrfError>> {
// headers
// .get(header::ORIGIN)
// .map(|origin| {
// origin
// .to_str()
// .map_err(|_| CsrfError::BadOrigin)
// .map(|o| o.into())
// })
// .or_else(|| {
// headers.get(header::REFERER).map(|referer| {
// Uri::try_from(Bytes::from(referer.as_bytes()))
// .ok()
// .as_ref()
// .and_then(uri_origin)
// .ok_or(CsrfError::BadOrigin)
// .map(|o| o.into())
// })
// })
// }
// /// A middleware that filters cross-site requests.
// ///
// /// To construct a CSRF filter:
// ///
// /// 1. Call [`CsrfFilter::build`](struct.CsrfFilter.html#method.build) to
// /// start building.
// /// 2. [Add](struct.CsrfFilterBuilder.html#method.allowed_origin) allowed
// /// origins.
// /// 3. Call [finish](struct.CsrfFilterBuilder.html#method.finish) to retrieve
// /// the constructed filter.
// ///
// /// # Example
// ///
// /// ```
// /// use actix_web::middleware::csrf;
// /// use actix_web::App;
// ///
// /// # fn main() {
// /// let app = App::new()
// /// .middleware(csrf::CsrfFilter::new().allowed_origin("https://www.example.com"));
// /// # }
// /// ```
// #[derive(Default)]
// pub struct CsrfFilter {
// origins: HashSet<String>,
// allow_xhr: bool,
// allow_missing_origin: bool,
// allow_upgrade: bool,
// }
// impl CsrfFilter {
// /// Start building a `CsrfFilter`.
// pub fn new() -> CsrfFilter {
// CsrfFilter {
// origins: HashSet::new(),
// allow_xhr: false,
// allow_missing_origin: false,
// allow_upgrade: false,
// }
// }
// /// Add an origin that is allowed to make requests. Will be verified
// /// against the `Origin` request header.
// pub fn allowed_origin<T: Into<String>>(mut self, origin: T) -> CsrfFilter {
// self.origins.insert(origin.into());
// self
// }
// /// Allow all requests with an `X-Requested-With` header.
// ///
// /// A cross-site attacker should not be able to send requests with custom
// /// headers unless a CORS policy whitelists them. Therefore it should be
// /// safe to allow requests with an `X-Requested-With` header (added
// /// automatically by many JavaScript libraries).
// ///
// /// This is disabled by default, because in Safari it is possible to
// /// circumvent this using redirects and Flash.
// ///
// /// Use this method to enable more lax filtering.
// pub fn allow_xhr(mut self) -> CsrfFilter {
// self.allow_xhr = true;
// self
// }
// /// Allow requests if the expected `Origin` header is missing (and
// /// there is no `Referer` to fall back on).
// ///
// /// The filter is conservative by default, but it should be safe to allow
// /// missing `Origin` headers because a cross-site attacker cannot prevent
// /// the browser from sending `Origin` on unprotected requests.
// pub fn allow_missing_origin(mut self) -> CsrfFilter {
// self.allow_missing_origin = true;
// self
// }
// /// Allow cross-site upgrade requests (for example to open a WebSocket).
// pub fn allow_upgrade(mut self) -> CsrfFilter {
// self.allow_upgrade = true;
// self
// }
// fn validate(&self, req: &Request) -> Result<(), CsrfError> {
// let is_upgrade = req.headers().contains_key(header::UPGRADE);
// let is_safe = req.method().is_safe() && (self.allow_upgrade || !is_upgrade);
// if is_safe || (self.allow_xhr && req.headers().contains_key("x-requested-with")) {
// Ok(())
// } else if let Some(header) = origin(req.headers()) {
// match header {
// Ok(ref origin) if self.origins.contains(origin.as_ref()) => Ok(()),
// Ok(_) => Err(CsrfError::CsrDenied),
// Err(err) => Err(err),
// }
// } else if self.allow_missing_origin {
// Ok(())
// } else {
// Err(CsrfError::MissingOrigin)
// }
// }
// }
// impl<S> Middleware<S> for CsrfFilter {
// fn start(&self, req: &HttpRequest<S>) -> Result<Started> {
// self.validate(req)?;
// Ok(Started::Done)
// }
// }
// #[cfg(test)]
// mod tests {
// use super::*;
// use http::Method;
// use test::TestRequest;
// #[test]
// fn test_safe() {
// let csrf = CsrfFilter::new().allowed_origin("https://www.example.com");
// let req = TestRequest::with_header("Origin", "https://www.w3.org")
// .method(Method::HEAD)
// .finish();
// assert!(csrf.start(&req).is_ok());
// }
// #[test]
// fn test_csrf() {
// let csrf = CsrfFilter::new().allowed_origin("https://www.example.com");
// let req = TestRequest::with_header("Origin", "https://www.w3.org")
// .method(Method::POST)
// .finish();
// assert!(csrf.start(&req).is_err());
// }
// #[test]
// fn test_referer() {
// let csrf = CsrfFilter::new().allowed_origin("https://www.example.com");
// let req =
// TestRequest::with_header("Referer", "https://www.example.com/some/path?query=param")
// .method(Method::POST)
// .finish();
// assert!(csrf.start(&req).is_ok());
// }
// #[test]
// fn test_upgrade() {
// let strict_csrf = CsrfFilter::new().allowed_origin("https://www.example.com");
// let lax_csrf = CsrfFilter::new()
// .allowed_origin("https://www.example.com")
// .allow_upgrade();
// let req = TestRequest::with_header("Origin", "https://cswsh.com")
// .header("Connection", "Upgrade")
// .header("Upgrade", "websocket")
// .method(Method::GET)
// .finish();
// assert!(strict_csrf.start(&req).is_err());
// assert!(lax_csrf.start(&req).is_ok());
// }
// }

4
src/models.rs

@ -1,4 +1,4 @@
pub mod users;
pub use self::users::*;
pub mod errors;
pub use self::errors::*;
pub mod api_response;
pub use self::api_response::*;

22
src/models/api_response.rs

@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, new)]
pub struct ApiResponse<T> {
success: bool,
response: T,
}
impl<T: Serialize> ApiResponse<T> {
pub fn is_success(&self) -> bool {
self.success
}
pub fn response(&self) -> &T {
&self.response
}
pub fn successful(response: T) -> ApiResponse<T> {
ApiResponse::new(true, response)
}
pub fn failure(response: T) -> ApiResponse<T> {
ApiResponse::new(false, response)
}
}

14
src/models/errors.rs

@ -1,14 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, new)]
pub struct JsonErrorModel<'a> {
status_code: i16,
pub line: String,
pub reason: &'a str,
}
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, new)]
pub struct ErrorModel {
// pub error_code: i16,
pub success: bool,
pub reason: String,
}

235
src/models/users.rs

@ -1,27 +1,234 @@
use serde::{Deserialize, Serialize};
use crate::schema::users;
use crate::utils::regexs;
use validator_derive::*;
use crate::utils::regex;
use derive_more::{Display, Into};
use std::convert::TryFrom;
use std::{convert::TryInto, str::FromStr};
use validators::prelude::*;
#[derive(Debug, Clone, Queryable, Identifiable, Deserialize)]
///newtype to constrain id to positive int values
///
///sqlite does not allow u32 for primary keys
#[derive(
Debug,
Clone,
Eq,
Hash,
PartialEq,
Deserialize,
Display,
Into,
Serialize,
DieselNewType,
)]
#[serde(try_from = "u32", into = "u32")]
pub struct UserId(i32);
impl From<UserId> for u32 {
fn from(s: UserId) -> u32 {
//this should be safe to unwrap since our newtype
//does not allow negative values
s.0.try_into().unwrap()
}
}
impl FromStr for UserId {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(num) = s.parse::<u32>() {
num.try_into()
.map_err(|err| {
format!("negative values are not allowed: {}", err)
})
.map(UserId)
} else {
Err("expected unsigned int, received string".to_owned())
}
}
}
impl TryFrom<u32> for UserId {
type Error = String;
fn try_from(value: u32) -> Result<Self, Self::Error> {
value
.try_into()
.map_err(|err| format!("error while converting user_id: {}", err))
.map(UserId)
}
}
#[derive(Validator, Debug, Clone, DieselNewType)]
#[validator(regex(regex::USERNAME_REG))]
pub struct Username(String);
impl Username {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Validator, Debug, Clone, DieselNewType)]
#[validator(line(char_length(max = 200)))]
pub struct Password(String);
impl Password {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Queryable, Identifiable)]
#[table_name = "users"]
pub struct User {
pub id: i32,
pub name: String,
pub password: String,
pub id: UserId,
pub name: Username,
#[serde(skip_serializing)]
pub password: Password,
pub created_at: chrono::NaiveDateTime,
}
#[derive(Debug, Clone, Insertable, Deserialize, Validate)]
#[derive(Debug, Clone, Insertable, Deserialize)]
#[table_name = "users"]
pub struct NewUser {
#[validate(regex = "regexs::USERNAME_REG", length(min = 4, max = 10))]
pub name: String,
pub password: String,
pub name: Username,
#[serde(skip_serializing)]
pub password: Password,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(try_from = "u16")]
pub struct PaginationOffset(u16);
impl PaginationOffset {
pub fn as_uint(&self) -> u16 {
self.0
}
}
impl TryFrom<u16> for PaginationOffset {
type Error = String;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value <= 2500 {
Ok(PaginationOffset(value))
} else {
Err("Failed to validate".to_owned())
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(try_from = "u16")]
pub struct PaginationLimit(u16);
impl PaginationLimit {
pub fn as_uint(&self) -> u16 {
self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Queryable)]
pub struct UserDto {
pub name: String,
pub registration_date: chrono::NaiveDateTime,
impl TryFrom<u16> for PaginationLimit {
type Error = String;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value <= 50 {
Ok(PaginationLimit(value))
} else {
Err("Failed to validate".to_owned())
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(try_from = "u16")]
pub struct PaginationPage(u16);
impl PaginationPage {
pub fn as_uint(&self) -> u16 {
self.0
}
}
impl TryFrom<u16> for PaginationPage {
type Error = String;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value <= 50 {
Ok(PaginationPage(value))
} else {
Err("Failed to validate".to_owned())
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Pagination {
pub page: PaginationPage,
pub limit: PaginationLimit,
}
impl Pagination {
pub fn calc_offset(&self) -> PaginationOffset {
let res = self.page.as_uint() * self.limit.as_uint();
PaginationOffset::try_from(res).unwrap()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct SearchQueryString(String);
impl SearchQueryString {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct SearchQuery {
pub q: SearchQueryString,
// pub pagination: Pagination
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn user_model_refinement_test() {
//yes I had been watching a lot of star wars lately
let mb_user = serde_json::from_str::<User>(
r#"{"id":1,"name":"chewbacca","password":"aeqfq3fq","created_at":"2021-05-12T12:37:56"}"#,
);
// println!("{:?}", mb_user);
assert_eq!(mb_user.is_ok(), true);
let mb_user = serde_json::from_str::<User>(
r#"{"id":1,"name":"chew-bacca","password":"aeqfq3fq","created_at":"2021-05-12T12:37:56"}"#,
);
assert_eq!(mb_user.is_ok(), true);
let mb_user = serde_json::from_str::<User>(
r#"{"id":1,"name":"chew.bacca","password":"aeqfq3fq","created_at":"2021-05-12T12:37:56"}"#,
);
assert_eq!(mb_user.is_ok(), false);
let mb_user = serde_json::from_str::<User>(
r#"{"id":-1,"name":"chewbacca","password":"aeqfq3fq","created_at":"2021-05-12T12:37:56"}"#,
);
assert_eq!(mb_user.is_ok(), false);
let mb_user = serde_json::from_str::<User>(
r#"{"id":1,"name":"ch","password":"aeqfq3fq","created_at":"2021-05-12T12:37:56"}"#,
);
assert_eq!(mb_user.is_ok(), false);
let mb_user = serde_json::from_str::<User>(
r#"{"id":1,"name":"chaegw;eaef","password":"aeqfq3fq","created_at":"2021-05-12T12:37:56"}"#,
);
assert_eq!(mb_user.is_ok(), false);
let mb_user = serde_json::from_str::<User>(
r#"{"id":1,"name":"chaegw_eaef","password":"aeqfq3fq","created_at":"2021-05-12T12:37:56"}"#,
);
assert_eq!(mb_user.is_ok(), false);
}
#[test]
fn pagination_refinement_test() {
let mb_pag =
serde_json::from_str::<Pagination>(r#"{"limit":5,"page":5}"#);
// println!("{:?}", mb_pag);
assert_eq!(mb_pag.is_ok(), true);
let mb_pag =
serde_json::from_str::<Pagination>(r#"{"limit":51,"page":5}"#);
assert_eq!(mb_pag.is_ok(), false);
let mb_pag =
serde_json::from_str::<Pagination>(r#"{"limit":5,"page":51}"#);
assert_eq!(mb_pag.is_ok(), false);
}
}

3
src/routes/auth.rs

@ -38,6 +38,7 @@ pub async fn login(
response
}
//TODO: fix the response
#[get("/logout")]
pub async fn logout(
id: Identity,
@ -45,7 +46,7 @@ pub async fn logout(
) -> Result<HttpResponse, Error> {
let maybe_identity = id.identity();
let response = if let Some(identity) = maybe_identity {
info!("Logging out {user}", user = identity);
let _ = tracing::info!("Logging out {user}", user = identity);
id.forget();
HttpResponse::Found().header("location", "/").finish()
} else {

150
src/routes/users.rs

@ -1,28 +1,37 @@
use actix_web::{get, post, web, HttpResponse};
use actix_web::{web, HttpResponse};
use crate::services::UserService;
use crate::{actions, models};
use crate::{
actions,
models::{self, ApiResponse, Pagination, SearchQuery, UserId},
};
use crate::{errors::DomainError, AppData};
use actix_web::error::ResponseError;
use validator::Validate;
/// Finds user by UID.
#[get("/get/users/{user_id}")]
#[tracing::instrument(
level = "debug",
skip(app_data),
fields(
user_id = %user_id.0
)
)]
pub async fn get_user(
app_data: web::Data<AppData>,
user_id: web::Path<i32>,
user_id: web::Path<UserId>,
) -> Result<HttpResponse, DomainError> {
let u_id = user_id.into_inner();
let u_id2 = u_id.clone();
let _ = tracing::info!("Getting user with id {}", u_id);
// use web::block to offload blocking Diesel code without blocking server thread
let res = web::block(move || {
let pool = &app_data.pool;
let conn = pool.get()?;
actions::find_user_by_uid(u_id, &conn)
actions::find_user_by_uid(&u_id2, &conn)
})
.await
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
let _ = tracing::trace!("{:?}", res);
if let Some(user) = res {
Ok(HttpResponse::Ok().json(user))
Ok(HttpResponse::Ok().json(ApiResponse::successful(user)))
} else {
let err = DomainError::new_entity_does_not_exist_error(format!(
"No user found with uid: {}",
@ -32,89 +41,80 @@ pub async fn get_user(
}
}
#[get("/get/users/{user_id}")]
pub async fn get_user2(
user_service: web::Data<dyn UserService>,
user_id: web::Path<i32>,
// #[get("/get/users/{user_id}")]
// pub async fn get_user2(
// user_service: web::Data<dyn UserService>,
// user_id: web::Path<i32>,
// ) -> Result<HttpResponse, DomainError> {
// let u_id = user_id.into_inner();
// let user = user_service.find_user_by_uid(u_id)?;
// if let Some(user) = user {
// Ok(HttpResponse::Ok().json(user))
// } else {
// let err = DomainError::new_entity_does_not_exist_error(format!(
// "No user found with uid: {}",
// u_id
// ));
// Err(err)
// }
// }
#[tracing::instrument(level = "debug", skip(app_data))]
pub async fn get_users(
app_data: web::Data<AppData>,
pagination: web::Query<Pagination>,
) -> Result<HttpResponse, DomainError> {
let u_id = user_id.into_inner();
let user = user_service.find_user_by_uid(u_id)?;
if let Some(user) = user {
Ok(HttpResponse::Ok().json(user))
} else {
let err = DomainError::new_entity_does_not_exist_error(format!(
"No user found with uid: {}",
u_id
));
Err(err)
}
let _ = tracing::info!("Paginated users request");
let users = web::block(move || {
let pool = &app_data.pool;
let conn = pool.get()?;
let p: Pagination = pagination.into_inner();
actions::get_all_users(&p, &conn)
})
.await
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
let _ = tracing::trace!("{:?}", users);
Ok(HttpResponse::Ok().json(ApiResponse::successful(users)))
}
#[get("/get/users")]
pub async fn get_all_users(
#[tracing::instrument(level = "debug", skip(app_data))]
pub async fn search_users(
app_data: web::Data<AppData>,
query: web::Query<SearchQuery>,
pagination: web::Query<Pagination>,
) -> Result<HttpResponse, DomainError> {
// use web::block to offload blocking Diesel code without blocking server thread
let _ = tracing::info!("Search users request");
let users = web::block(move || {
let pool = &app_data.pool;
let conn = pool.get()?;
actions::get_all(&conn)
let p: Pagination = pagination.into_inner();
actions::search_users(query.q.as_str(), &p, &conn)
})
.await
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
debug!("{:?}", users);
let _ = tracing::trace!("{:?}", users);
if !users.is_empty() {
Ok(HttpResponse::Ok().json(users))
} else {
Err(DomainError::new_entity_does_not_exist_error(
"No users available".to_owned(),
))
}
Ok(HttpResponse::Ok().json(ApiResponse::successful(users)))
}
//TODO: Add refinement here
/// Inserts new user with name defined in form.
#[post("/do_registration")]
/// Inserts a new user
#[tracing::instrument(level = "debug", skip(app_data))]
pub async fn add_user(
app_data: web::Data<AppData>,
form: web::Json<models::NewUser>,
) -> Result<HttpResponse, impl ResponseError> {
// use web::block to offload blocking Diesel code without blocking server thread
let res = match form.0.validate() {
Ok(_) => web::block(move || {
let pool = &app_data.pool;
let conn = pool.get()?;
actions::insert_new_user(form.0, &conn)
})
.await
.map(|user| {
debug!("{:?}", user);
HttpResponse::Created().json(user)
}),
Err(e) => {
// let err = e.to_string();
// web::block(move || {
// Err(crate::errors::DomainError::new_generic_error(err))
// })
// .await
) -> Result<HttpResponse, DomainError> {
let user = web::block(move || {
let pool = &app_data.pool;
let conn = pool.get()?;
actions::insert_new_user(form.0, &conn, Some(app_data.config.hash_cost))
})
.await
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
// let res2 =
// crate::errors::DomainError::new_generic_error(e.to_string());
// Err(res2)
// let res2 = crate::errors::DomainError::GenericError {
// cause: e.to_string(),
// };
// Err(res2)
let res = HttpResponse::BadRequest().body(e.to_string());
// .json(models::ErrorModel::new(
// 40,
// "Error registering user due to validation errors",
// ));
Ok(res)
}
};
let _ = tracing::trace!("{:?}", user);
res
Ok(HttpResponse::Created().json(ApiResponse::successful(user)))
}

128
src/services/user_service.rs

@ -1,76 +1,76 @@
use crate::{actions, errors, models, types::DbPool};
// use crate::{actions, errors, models, types::DbPool};
pub trait UserService {
fn find_user_by_uid(
&self,
uid: i32,
) -> Result<Option<models::UserDto>, errors::DomainError>;
fn _find_user_by_name(
&self,
user_name: String,
) -> Result<Option<models::UserDto>, errors::DomainError>;
// pub trait UserService {
// fn find_user_by_uid(
// &self,
// uid: i32,
// ) -> Result<Option<models::UserDto>, errors::DomainError>;
// fn _find_user_by_name(
// &self,
// user_name: String,
// ) -> Result<Option<models::UserDto>, errors::DomainError>;
fn get_all(&self) -> Result<Vec<models::UserDto>, errors::DomainError>;
// fn get_all(&self) -> Result<Vec<models::UserDto>, errors::DomainError>;
fn insert_new_user(
&self,
nu: models::NewUser,
) -> Result<models::UserDto, errors::DomainError>;
// fn insert_new_user(
// &self,
// nu: models::NewUser,
// ) -> Result<models::UserDto, errors::DomainError>;
// fn woot(&self) -> i32;
// // fn woot(&self) -> i32;
fn verify_password(
&self,
user_name: &str,
given_password: &str,
) -> Result<bool, errors::DomainError>;
}
// fn verify_password(
// &self,
// user_name: &str,
// given_password: &str,
// ) -> Result<bool, errors::DomainError>;
// }
#[derive(Clone)]
pub struct UserServiceImpl {
pub pool: DbPool,
}
// #[derive(Clone)]
// pub struct UserServiceImpl {
// pub pool: DbPool,
// }
impl UserService for UserServiceImpl {
fn find_user_by_uid(
&self,
uid: i32,
) -> Result<Option<models::UserDto>, errors::DomainError> {
let conn = self.pool.get()?;
actions::find_user_by_uid(uid, &conn)
}
// impl UserService for UserServiceImpl {
// fn find_user_by_uid(
// &self,
// uid: i32,
// ) -> Result<Option<models::UserDto>, errors::DomainError> {
// let conn = self.pool.get()?;
// actions::find_user_by_uid(uid, &conn)
// }
fn _find_user_by_name(
&self,
user_name: String,
) -> Result<Option<models::UserDto>, errors::DomainError> {
let conn = self.pool.get()?;
actions::_find_user_by_name(user_name, &conn)
}
// fn _find_user_by_name(
// &self,
// user_name: String,
// ) -> Result<Option<models::UserDto>, errors::DomainError> {
// let conn = self.pool.get()?;
// actions::_find_user_by_name(user_name, &conn)
// }
fn get_all(&self) -> Result<Vec<models::UserDto>, errors::DomainError> {
let conn = self.pool.get()?;
actions::get_all(&conn)
}
// fn get_all(&self) -> Result<Vec<models::UserDto>, errors::DomainError> {
// let conn = self.pool.get()?;
// actions::get_all(&conn)
// }
fn insert_new_user(
&self,
nu: models::NewUser,
) -> Result<models::UserDto, errors::DomainError> {
let conn = self.pool.get()?;
actions::insert_new_user(nu, &conn)
}
// fn insert_new_user(
// &self,
// nu: models::NewUser,
// ) -> Result<models::UserDto, errors::DomainError> {
// let conn = self.pool.get()?;
// actions::insert_new_user(nu, &conn, Some(8))
// }
fn verify_password(
&self,
user_name: &str,
given_password: &str,
) -> Result<bool, errors::DomainError> {
let conn = self.pool.get()?;
actions::verify_password(user_name, given_password, &conn)
}
// fn verify_password(
// &self,
// user_name: &str,
// given_password: &str,
// ) -> Result<bool, errors::DomainError> {
// let conn = self.pool.get()?;
// actions::verify_password(user_name, given_password, &conn)
// }
// async fn woot(&self) -> i32 {
// 1
// }
}
// // async fn woot(&self) -> i32 {
// // 1
// // }
// }

5
src/types.rs

@ -1,3 +1,4 @@
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
pub type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
pub type DbPool = r2d2::Pool<
ConnectionManager<diesel_tracing::sqlite::InstrumentedSqliteConnection>,
>;

6
src/utils.rs

@ -1,6 +1,4 @@
pub mod auth;
pub mod regexs;
pub mod regex;
pub use self::auth::*;
pub use self::regexs::*;
pub mod ops;
pub use self::ops::*;
pub use self::regex::*;

28
src/utils/extra.rs

@ -1,28 +0,0 @@
#[derive(Debug, Serialize, Deserialize)]
struct MyObj {
name: String,
// number: i32,
}
#[get("/{id}/{name}")]
async fn index(info: web::Path<(u32, String)>) -> Result<HttpResponse, Error> {
let (id, name) = (info.0, info.1.clone());
let template = models::CardTemplate {
title: "My Title",
body: name,
num: id,
};
template
.call()
.map(|body| HttpResponse::Ok().content_type("text/html").body(body))
.map_err(|_| {
error::ErrorInternalServerError("Error while parsing template")
})
}
/// This handler uses json extractor
#[post("/extractor")]
async fn extract_my_obj(item: web::Json<MyObj>) -> HttpResponse {
debug!("model: {:?}", item);
HttpResponse::Ok().json(item.0) // <- send response
}

34
src/utils/ops.rs

@ -1,34 +0,0 @@
use std::fmt::Display;
pub trait LogErrorResult<T, E> {
fn log_err(self) -> Result<T, E>;
}
impl<T, E: Display> LogErrorResult<T, E> for Result<T, E> {
fn log_err(self) -> Result<T, E> {
self.map_err(|err| {
error!("{}", err.to_string());
err
})
}
}
trait ResultOps<T, E> {
fn tap<U, F: FnOnce(T) -> U>(self, op: F) -> Result<T, E>;
fn tap_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, E>;
}
impl<T: Clone, E: Clone> ResultOps<T, E> for Result<T, E> {
fn tap<U, F: FnOnce(T) -> U>(self, op: F) -> Result<T, E> {
self.map(|x| {
op(x.clone());
x
})
}
fn tap_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, E> {
self.map_err(|err| {
op(err.clone());
err
})
}
}

2
src/utils/regexs.rs → src/utils/regex.rs

@ -2,5 +2,5 @@ use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
pub static ref USERNAME_REG: Regex =
Regex::new(r"^([a-z\d]+-)*[a-z\d]+$").unwrap();
Regex::new(r"^([a-z\d]+-)*[a-z\d]+{5,35}$").unwrap();
}

0
static/dummy.js

1
static/test.js

@ -1 +0,0 @@
var x = 1

22
templates/hello.hbs

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
Hello {{body}}! Number input was: {{num}}
</div>
<h1>BTW... ZA WARUDO</h1>
</div>
<div class="app"></div>
<script src="/test.js" defer></script>
</body>
</html>

110
tests/integration/common/mod.rs

@ -1,59 +1,93 @@
extern crate actix_demo;
use actix_demo::{AppConfig, AppData};
use actix_demo::{AppConfig, AppData, EnvConfig};
use actix_web::test;
use actix_web::App;
use diesel::SqliteConnection;
use diesel::r2d2::{self, ConnectionManager};
use env_logger::Env;
use std::io;
use std::io::ErrorKind;
use tracing::subscriber::set_global_default;
use tracing_actix_web::TracingLogger;
use tracing_log::LogTracer;
use tracing_subscriber::fmt::{format::FmtSpan, Subscriber as FmtSubscriber};
use tracing_subscriber::{layer::SubscriberExt, EnvFilter};
use actix_demo::configure_app;
use actix_http::Request;
use actix_web::{dev as ax_dev, Error as AxError};
pub async fn test_app() -> impl ax_dev::Service<
Request = Request,
Response = ax_dev::ServiceResponse<impl ax_dev::MessageBody>,
Error = AxError,
pub async fn test_app() -> io::Result<
impl ax_dev::Service<
Request = Request,
Response = ax_dev::ServiceResponse<impl ax_dev::MessageBody>,
Error = AxError,
>,
> {
let _ = dotenv::dotenv()
let _ = dotenv::dotenv().map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set up env: {:?}", err),
)
})?;
let _ = envy::prefixed("ACTIX_DEMO_")
.from_env::<EnvConfig>()
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set up env: {:?}", err),
format!("Failed to parse config: {:?}", err),
)
})
.unwrap();
let _ = env_logger::builder()
.is_test(true)
.parse_env(Env::default().filter("ACTIX_DEMO_TEST_RUST_LOG"))
.try_init();
})?;
let connspec = ":memory:";
let manager = ConnectionManager::<SqliteConnection>::new(connspec);
let pool = r2d2::Pool::builder()
.build(manager)
.map_err(|err| {
let env_filter =
EnvFilter::try_from_env("ACTIX_DEMO_RUST_LOG").map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to create pool: {:?}", err),
format!("Failed to set up env logger: {:?}", err),
)
})
.unwrap();
})?;
let _ = LogTracer::init().map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set up log tracer: {:?}", err),
)
});
let subscriber = FmtSubscriber::builder()
.pretty()
.with_test_writer()
.with_span_events(FmtSpan::NEW)
.with_span_events(FmtSpan::CLOSE)
.finish()
.with(env_filter);
let _ = set_global_default(subscriber).map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set subscriber: {:?}", err),
)
});
let connspec = ":memory:";
let manager = ConnectionManager::<
diesel_tracing::sqlite::InstrumentedSqliteConnection,
>::new(connspec);
let pool = r2d2::Pool::builder().build(manager).map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to create pool: {:?}", err),
)
})?;
let _ = {
let conn = &pool
.get()
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to get connection: {:?}", err),
)
})
.unwrap();
let conn = &pool.get().map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to get connection: {:?}", err),
)
})?;
let migrations_dir = diesel_migrations::find_migrations_directory()
.map_err(|err| {
@ -61,8 +95,7 @@ pub async fn test_app() -> impl ax_dev::Service<
ErrorKind::Other,
format!("Error finding migrations dir: {:?}", err),
)
})
.unwrap();
})?;
let _ = diesel_migrations::run_pending_migrations_in_directory(
conn,
&migrations_dir,
@ -73,17 +106,16 @@ pub async fn test_app() -> impl ax_dev::Service<
ErrorKind::Other,
format!("Error running migrations: {:?}", err),
)
})
.unwrap();
})?;
};
test::init_service(
Ok(test::init_service(
App::new()
.configure(configure_app(AppData {
config: AppConfig { hash_cost: 8 },
pool,
}))
.wrap(actix_web::middleware::Logger::default()),
.wrap(TracingLogger),
)
.await
.await)
}

48
tests/integration/main.rs

@ -1,47 +1,3 @@
mod common;
#[cfg(test)]
mod tests {
use super::*;
use actix_demo::models::ErrorModel;
use actix_web::dev::Service as _;
use actix_web::http::StatusCode;
use actix_web::test;
#[actix_rt::test]
async fn get_users_api_should_return_error_message_if_no_users_exist() {
let req = test::TestRequest::get().uri("/api/get/users").to_request();
let resp = common::test_app().await.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let body: ErrorModel = test::read_body_json(resp).await;
assert_eq!(
body,
ErrorModel {
success: false,
reason: "Entity does not exist - No users available".to_owned()
}
);
log::debug!("{:?}", body);
}
#[actix_rt::test]
async fn get_user_api_should_return_error_message_if_user_with_id_does_not_exist(
) {
let req = test::TestRequest::get()
.uri("/api/get/users/1")
.to_request();
let resp = common::test_app().await.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let body: ErrorModel = test::read_body_json(resp).await;
assert_eq!(
body,
ErrorModel {
success: false,
reason: "Entity does not exist - No user found with uid: 1"
.to_owned()
}
);
log::debug!("{:?}", body);
}
}
mod misc;
mod users;

21
tests/integration/misc.rs

@ -0,0 +1,21 @@
use crate::common;
use actix_demo::get_build_info;
#[cfg(test)]
mod tests {
use super::*;
use actix_web::dev::Service as _;
use actix_web::http::StatusCode;
use actix_web::test;
#[actix_rt::test]
async fn get_build_info_should_succeed() {
let req = test::TestRequest::get().uri("/api/build-info").to_request();
let resp = common::test_app().await.unwrap().call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body: build_info::BuildInfo = test::read_body_json(resp).await;
let _ = tracing::debug!("{:?}", body);
assert_eq!(body, *get_build_info());
}
}

44
tests/integration/users.rs

@ -0,0 +1,44 @@
use crate::common;
#[cfg(test)]
mod tests {
use super::*;
use actix_demo::models::ApiResponse;
use actix_web::dev::Service as _;
use actix_web::http::StatusCode;
use actix_web::test;
mod get_users_api {
use super::*;
#[actix_rt::test]
async fn should_return_empty_array_if_no_users_exist() {
let req = test::TestRequest::get()
.uri("/api/users?page=0&limit=2")
.to_request();
let resp =
common::test_app().await.unwrap().call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body: ApiResponse<Vec<_>> = test::read_body_json(resp).await;
let _ = tracing::debug!("{:?}", body);
assert_eq!(body, ApiResponse::successful(vec![1; 0]));
}
#[actix_rt::test]
async fn should_return_error_message_if_user_with_id_does_not_exist() {
let req = test::TestRequest::get().uri("/api/users/1").to_request();
let resp =
common::test_app().await.unwrap().call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let body: ApiResponse<String> = test::read_body_json(resp).await;
let _ = tracing::debug!("{:?}", body);
assert_eq!(
body,
ApiResponse::failure(
"Entity does not exist - No user found with uid: 1"
.to_owned()
)
);
}
}
}
Loading…
Cancel
Save