14 Commits
da125c6d99
...
95e5c47c69
30 changed files with 1300 additions and 897 deletions
-
1.env
-
625Cargo.lock
-
23Cargo.toml
-
37Makefile.toml
-
25README.md
-
104src/actions/users.rs
-
65src/errors/domain_error.rs
-
72src/lib.rs
-
90src/main.rs
-
2src/middlewares.rs
-
276src/middlewares/csrf.rs
-
4src/models.rs
-
22src/models/api_response.rs
-
14src/models/errors.rs
-
235src/models/users.rs
-
3src/routes/auth.rs
-
142src/routes/users.rs
-
126src/services/user_service.rs
-
5src/types.rs
-
6src/utils.rs
-
28src/utils/extra.rs
-
34src/utils/ops.rs
-
2src/utils/regex.rs
-
0static/dummy.js
-
1static/test.js
-
22templates/hello.hbs
-
92tests/integration/common/mod.rs
-
48tests/integration/main.rs
-
21tests/integration/misc.rs
-
44tests/integration/users.rs
625
Cargo.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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"] |
@ -1,3 +1 @@ |
|||||
pub mod csrf;
|
|
||||
|
|
||||
pub use self::csrf::*;
|
|
@ -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());
|
|
||||
// }
|
|
||||
// }
|
|
@ -1,4 +1,4 @@ |
|||||
pub mod users;
|
pub mod users;
|
||||
pub use self::users::*;
|
pub use self::users::*;
|
||||
pub mod errors;
|
|
||||
pub use self::errors::*;
|
|
||||
|
pub mod api_response;
|
||||
|
pub use self::api_response::*;
|
@ -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)
|
||||
|
}
|
||||
|
}
|
@ -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,
|
|
||||
}
|
|
@ -1,27 +1,234 @@ |
|||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||
|
|
||||
use crate::schema::users;
|
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 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,
|
pub created_at: chrono::NaiveDateTime,
|
||||
}
|
}
|
||||
|
|
||||
#[derive(Debug, Clone, Insertable, Deserialize, Validate)]
|
|
||||
|
#[derive(Debug, Clone, Insertable, Deserialize)]
|
||||
#[table_name = "users"]
|
#[table_name = "users"]
|
||||
pub struct NewUser {
|
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
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
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, Serialize, Deserialize, Queryable)]
|
|
||||
pub struct UserDto {
|
|
||||
pub name: String,
|
|
||||
pub registration_date: chrono::NaiveDateTime,
|
|
||||
|
#[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);
|
||||
|
}
|
||||
}
|
}
|
@ -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
|
||||
|
// // }
|
||||
// }
|
// }
|
||||
}
|
|
@ -1,3 +1,4 @@ |
|||||
use diesel::prelude::*;
|
|
||||
use diesel::r2d2::{self, ConnectionManager};
|
use diesel::r2d2::{self, ConnectionManager};
|
||||
pub type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
|
||||
|
pub type DbPool = r2d2::Pool<
|
||||
|
ConnectionManager<diesel_tracing::sqlite::InstrumentedSqliteConnection>,
|
||||
|
>;
|
@ -1,6 +1,4 @@ |
|||||
pub mod auth;
|
pub mod auth;
|
||||
pub mod regexs;
|
|
||||
|
pub mod regex;
|
||||
pub use self::auth::*;
|
pub use self::auth::*;
|
||||
pub use self::regexs::*;
|
|
||||
pub mod ops;
|
|
||||
pub use self::ops::*;
|
|
||||
|
pub use self::regex::*;
|
@ -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
|
|
||||
}
|
|
@ -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
|
|
||||
})
|
|
||||
}
|
|
||||
}
|
|
@ -1 +0,0 @@ |
|||||
var x = 1 |
|
@ -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> |
|
@ -1,47 +1,3 @@ |
|||||
mod common;
|
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;
|
@ -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());
|
||||
|
}
|
||||
|
}
|
@ -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()
|
||||
|
)
|
||||
|
);
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue