├── src ├── db │ ├── mod.rs │ └── postgres │ │ └── mod.rs ├── regex │ ├── mod.rs │ └── user.rs ├── middlewares │ ├── mod.rs │ └── auth.rs ├── controllers │ ├── system │ │ ├── mod.rs │ │ └── user │ │ │ ├── mod.rs │ │ │ ├── data.rs │ │ │ └── handler.rs │ ├── customer │ │ ├── mod.rs │ │ └── index.rs │ ├── global │ │ ├── mod.rs │ │ └── user │ │ │ ├── mod.rs │ │ │ ├── data.rs │ │ │ ├── handler_protected.rs │ │ │ └── handler.rs │ ├── internal │ │ ├── mod.rs │ │ └── user │ │ │ ├── mod.rs │ │ │ ├── data.rs │ │ │ └── handler.rs │ └── mod.rs ├── assets │ ├── mod.rs │ └── user_verification_email.rs ├── utils │ ├── mod.rs │ ├── validator.rs │ ├── http_body.rs │ ├── cli.rs │ ├── mailer.rs │ ├── paginate.rs │ ├── error.rs │ └── auth.rs ├── routes │ ├── admin │ │ ├── mod.rs │ │ └── user.rs │ ├── global │ │ ├── mod.rs │ │ └── user │ │ │ ├── mod.rs │ │ │ └── protected_route.rs │ ├── superadmin │ │ ├── mod.rs │ │ └── user.rs │ ├── system │ │ ├── mod.rs │ │ └── user.rs │ ├── customer │ │ ├── index.rs │ │ └── mod.rs │ └── mod.rs └── main.rs ├── .gitignore ├── .dockerignore ├── migrations ├── global.sql ├── user_status.sql ├── user_roles.sql └── users.sql ├── .env.development ├── .env.production ├── .Dockerfile ├── docker-compose.yml ├── Cargo.toml ├── README.md └── sqlx-data.json /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod postgres; -------------------------------------------------------------------------------- /src/regex/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; -------------------------------------------------------------------------------- /src/middlewares/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; -------------------------------------------------------------------------------- /src/controllers/system/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode -------------------------------------------------------------------------------- /src/controllers/customer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod index; -------------------------------------------------------------------------------- /src/controllers/global/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | -------------------------------------------------------------------------------- /src/controllers/internal/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; -------------------------------------------------------------------------------- /src/assets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user_verification_email; -------------------------------------------------------------------------------- /src/controllers/system/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod handler; -------------------------------------------------------------------------------- /src/controllers/internal/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | pub mod data; -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .env 3 | Cargo.lock 4 | .vscode 5 | migrations/ 6 | .Dockerfile 7 | README.md -------------------------------------------------------------------------------- /src/controllers/global/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod handler; 3 | pub mod handler_protected; -------------------------------------------------------------------------------- /src/controllers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod customer; 2 | pub mod internal; 3 | pub mod global; 4 | pub mod system; -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod http_body; 3 | pub mod paginate; 4 | pub mod validator; 5 | pub mod auth; 6 | pub mod cli; 7 | pub mod mailer; -------------------------------------------------------------------------------- /src/controllers/system/user/data.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize}; 2 | 3 | #[derive(Deserialize)] 4 | pub struct UserVerificationQuery{ 5 | pub token: String 6 | } -------------------------------------------------------------------------------- /migrations/global.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION trigger_set_timestamp() 2 | RETURNS TRIGGER AS $$ 3 | BEGIN 4 | NEW.updated_at = NOW(); 5 | RETURN NEW; 6 | END; 7 | $$ LANGUAGE plpgsql; -------------------------------------------------------------------------------- /src/db/postgres/mod.rs: -------------------------------------------------------------------------------- 1 | use sqlx::{PgPool}; 2 | use std::env::var; 3 | use crate::utils::error::ApiError; 4 | 5 | pub async fn create_connection()->Result{ 6 | Ok(PgPool::connect(&var("DATABASE_URL")?).await?) 7 | } -------------------------------------------------------------------------------- /src/routes/admin/mod.rs: -------------------------------------------------------------------------------- 1 | mod user; 2 | 3 | use actix_web::web; 4 | 5 | pub fn routes(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/admin") 8 | .configure(user::routes) 9 | ); 10 | } -------------------------------------------------------------------------------- /src/routes/global/mod.rs: -------------------------------------------------------------------------------- 1 | mod user; 2 | 3 | use actix_web::web; 4 | 5 | pub fn routes(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/global") 8 | .configure(user::routes) 9 | ); 10 | } -------------------------------------------------------------------------------- /src/routes/superadmin/mod.rs: -------------------------------------------------------------------------------- 1 | mod user; 2 | 3 | use actix_web::web; 4 | 5 | pub fn routes(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/superadmin") 8 | .configure(user::routes) 9 | ); 10 | } -------------------------------------------------------------------------------- /src/routes/system/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | 3 | use actix_web::web; 4 | 5 | pub fn routes(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/system") 8 | .configure(user::routes) 9 | ); 10 | } -------------------------------------------------------------------------------- /src/routes/customer/index.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | use crate::controllers::customer::index::*; 3 | 4 | pub fn routes(cfg: &mut web::ServiceConfig) { 5 | cfg.service( 6 | web::scope("/customer") 7 | .route("/main", web::get().to(main)) 8 | ); 9 | } -------------------------------------------------------------------------------- /src/controllers/customer/index.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | web::{Json} 3 | }; 4 | use crate::utils::{ 5 | http_body::Message, 6 | error::ApiError 7 | }; 8 | 9 | pub async fn main()->Result,ApiError>{ 10 | Ok(Json(Message{ 11 | msg:"OK" 12 | })) 13 | } -------------------------------------------------------------------------------- /src/routes/system/user.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | 3 | use crate::controllers::system::user::handler::*; 4 | 5 | pub fn routes(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/user") 8 | .route("/verify/email", web::get().to(verify_email)) 9 | ); 10 | } -------------------------------------------------------------------------------- /src/routes/customer/mod.rs: -------------------------------------------------------------------------------- 1 | mod index; 2 | 3 | use actix_web::web; 4 | 5 | use crate::middlewares::auth::{Auth,AuthType}; 6 | 7 | pub fn routes(cfg: &mut web::ServiceConfig) { 8 | cfg.service( 9 | web::scope("/customer") 10 | .wrap(Auth{classification:AuthType::JWT(vec!["customer".to_string()])}) 11 | .configure(index::routes) 12 | ); 13 | } -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | ENV=development 2 | 3 | DATABASE_URL=postgres://:@/ 4 | 5 | JWT_ISSUER=http://localhost 6 | JWT_SECRET=your_jwt_secret_key 7 | 8 | SESSION_LENGTH=86400 9 | 10 | API_KEY=your_api_key 11 | 12 | MAIL_VERIFICATION_URL=http://localhost:8080/system/user/verify/email?token= 13 | 14 | MAILER_USERNAME=your_system_email 15 | MAILER_PASSWORD=your_system_email_password -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | ENV=production 2 | 3 | DATABASE_URL=postgres://:@/ 4 | 5 | JWT_ISSUER=http://localhost 6 | JWT_SECRET=your_jwt_secret_key 7 | 8 | SESSION_LENGTH=86400 9 | 10 | API_KEY=your_api_key 11 | 12 | MAIL_VERIFICATION_URL=http://localhost:8080/system/user/verify/email?token= 13 | 14 | MAILER_USERNAME=your_system_email 15 | MAILER_PASSWORD=your_system_email_password -------------------------------------------------------------------------------- /src/routes/admin/user.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web::{get,ServiceConfig,scope}; 2 | use crate::controllers::internal::user::handler::*; 3 | use crate::middlewares::auth::{Auth,AuthType}; 4 | 5 | pub fn routes(cfg: &mut ServiceConfig) { 6 | cfg.service( 7 | scope("/user") 8 | .wrap(Auth{classification:AuthType::JWT(vec!["admin".to_string()])}) 9 | .route("/all", get().to(all)) 10 | ); 11 | } -------------------------------------------------------------------------------- /src/routes/superadmin/user.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web::{ServiceConfig,scope,post}; 2 | use crate::controllers::internal::user::handler::*; 3 | use crate::middlewares::auth::{Auth,AuthType}; 4 | 5 | pub fn routes(cfg: &mut ServiceConfig) { 6 | cfg.service( 7 | scope("/user") 8 | .wrap(Auth{classification:AuthType::JWT(vec!["superadmin".to_string()])}) 9 | .route("/create", post().to(create)) 10 | ); 11 | } -------------------------------------------------------------------------------- /src/utils/validator.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::error::ApiError; 2 | use actix_web::web::Json; 3 | use validator::{Validate}; 4 | use serde_json::{json,value::Value}; 5 | 6 | pub fn validate_input( 7 | params: &Json 8 | ) -> Result<(), ApiError>{ 9 | match params.validate() { 10 | Ok(()) => Ok(()), 11 | Err(error) => Err(ApiError::BadRequest( Value::to_string(&json!(error.field_errors())))) 12 | } 13 | } -------------------------------------------------------------------------------- /.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.47.0 as cargo-build 2 | 3 | WORKDIR /usr/src/code 4 | 5 | COPY . . 6 | 7 | RUN cargo build --release 8 | 9 | FROM gcr.io/distroless/cc-debian10 10 | 11 | COPY --from=cargo-build /usr/src/code/target/release/rust-app /usr/local/bin/rust-app 12 | COPY --from=cargo-build /lib/x86_64-linux-gnu/libz.so.1 /lib/x86_64-linux-gnu/libz.so.1 13 | 14 | WORKDIR /usr/src/code 15 | COPY .env.production .env.production 16 | 17 | CMD ["rust-app"] -------------------------------------------------------------------------------- /src/assets/user_verification_email.rs: -------------------------------------------------------------------------------- 1 | use typed_html::{html,dom::*,text}; 2 | 3 | pub fn create_mail(link:String)->String{ 4 | let doc: DOMTree = html!( 5 | 6 | 7 | "Account Verification" 8 | 9 | 10 |

{text!("Click here to verify your account : {}",link)}

11 | 12 | 13 | ); 14 | 15 | doc.to_string() 16 | } -------------------------------------------------------------------------------- /migrations/user_status.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS user_status( 2 | id SERIAL PRIMARY KEY NOT NULL, 3 | status VARCHAR(255) NOT NULL, 4 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 5 | updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 6 | ); 7 | 8 | CREATE TRIGGER updated_at 9 | BEFORE UPDATE ON user_status 10 | FOR EACH ROW 11 | EXECUTE PROCEDURE trigger_set_timestamp(); 12 | 13 | INSERT INTO user_status(status) VALUES ('unverified'); 14 | INSERT INTO user_status(status) VALUES ('verified'); -------------------------------------------------------------------------------- /src/routes/global/user/mod.rs: -------------------------------------------------------------------------------- 1 | mod protected_route; 2 | use actix_web::web; 3 | 4 | use crate::controllers::global::user::handler::*; 5 | 6 | 7 | pub fn routes(cfg: &mut web::ServiceConfig) { 8 | cfg.service( 9 | web::scope("/user") 10 | .route("/login/web", web::post().to(web_login)) 11 | .route("/signup", web::post().to(sign_up)) 12 | .route("/signup/check/email", web::post().to(check_email_exist)) 13 | .configure(protected_route::routes) 14 | ); 15 | } -------------------------------------------------------------------------------- /migrations/user_roles.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS user_roles( 2 | id SERIAL PRIMARY KEY NOT NULL, 3 | role VARCHAR(255) NOT NULL, 4 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 5 | updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 6 | ); 7 | 8 | CREATE TRIGGER updated_at 9 | BEFORE UPDATE ON user_roles 10 | FOR EACH ROW 11 | EXECUTE PROCEDURE trigger_set_timestamp(); 12 | 13 | INSERT INTO user_roles(role) VALUES ('superadmin'); 14 | INSERT INTO user_roles(role) VALUES ('admin'); 15 | INSERT INTO user_roles(role) VALUES ('customer'); -------------------------------------------------------------------------------- /src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | mod customer; 2 | mod admin; 3 | mod global; 4 | mod superadmin; 5 | mod system; 6 | 7 | use actix_web::web; 8 | 9 | use crate::middlewares::auth::{Auth,AuthType}; 10 | 11 | pub fn routes(cfg: &mut web::ServiceConfig) { 12 | cfg.service( 13 | web::scope("") 14 | .wrap(Auth{classification:AuthType::APIKEY}) 15 | .configure(customer::routes) 16 | .configure(admin::routes) 17 | .configure(superadmin::routes) 18 | .configure(global::routes) 19 | .configure(system::routes) 20 | ); 21 | } -------------------------------------------------------------------------------- /src/utils/http_body.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize,Deserialize}; 2 | use actix_web::HttpRequest; 3 | use serde_json::Value as JsonValue; 4 | use super::error::ApiError; 5 | 6 | #[derive(Deserialize,Serialize)] 7 | pub struct MessageWithData { 8 | pub msg: &'static str, 9 | pub data: T 10 | } 11 | 12 | #[derive(Deserialize,Serialize)] 13 | pub struct Message { 14 | pub msg: &'static str 15 | } 16 | 17 | pub fn get_data_from_middleware(req:&HttpRequest)->Result{ 18 | let header_extension = req.head().extensions(); 19 | Ok(header_extension.get::().unwrap().clone()) 20 | } -------------------------------------------------------------------------------- /migrations/users.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS users( 2 | id BIGSERIAL PRIMARY KEY NOT NULL, 3 | user_role_id INT NOT NULL, 4 | user_status_id INT NOT NULL, 5 | email VARCHAR(255) NOT NULL UNIQUE, 6 | phone_number VARCHAR(255) NOT NULL UNIQUE, 7 | password CHAR(60) NOT NULL, 8 | last_logged_in TIMESTAMPTZ NOT NULL, 9 | name VARCHAR(255), 10 | date_of_birth DATE, 11 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 12 | updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 13 | ); 14 | 15 | CREATE TRIGGER updated_at 16 | BEFORE UPDATE ON users 17 | FOR EACH ROW 18 | EXECUTE PROCEDURE trigger_set_timestamp(); -------------------------------------------------------------------------------- /src/utils/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Arg, App}; 2 | use dotenv::from_filename; 3 | 4 | pub fn read_cli(){ 5 | let app = App::new("rust-app") 6 | .arg(Arg::with_name("environment") 7 | .short("e") 8 | .long("env") 9 | .value_name("ENV_NAME") 10 | .help("Load env file") 11 | .takes_value(true) 12 | ) 13 | .get_matches(); 14 | 15 | let env = app.value_of("environment").unwrap_or("production"); 16 | 17 | match env{ 18 | "development" | "dev" => { from_filename(".env.development").expect("load env error") ;}, 19 | "production" | "prod" | _ => { from_filename(".env.production").expect("load env error") ;} 20 | } 21 | } -------------------------------------------------------------------------------- /src/routes/global/user/protected_route.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | 3 | use crate::controllers::global::user::handler_protected::*; 4 | use crate::middlewares::auth::{Auth,AuthType}; 5 | 6 | pub fn routes(cfg: &mut web::ServiceConfig) { 7 | cfg.service( 8 | web::scope("/protected") 9 | .wrap(Auth{ 10 | classification:AuthType::JWT(vec![ 11 | "customer".to_string(), 12 | "admin".to_string() 13 | ]) 14 | }) 15 | .route("/profile", web::get().to(get_profile)) 16 | .route("/profile/update", web::post().to(update_profile)) 17 | .route("/password/change",web::post().to(change_password)) 18 | ); 19 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{App,HttpServer,web}; 2 | 3 | mod routes; 4 | mod controllers; 5 | mod utils; 6 | mod db; 7 | mod regex; 8 | mod middlewares; 9 | mod assets; 10 | 11 | #[macro_use] 12 | extern crate validator_derive; 13 | 14 | #[derive(Debug)] 15 | pub struct AppState{ 16 | db_postgres: sqlx::PgPool 17 | } 18 | 19 | #[actix_rt::main] 20 | async fn main() -> std::io::Result<()> { 21 | utils::cli::read_cli(); 22 | 23 | let postgres_session = db::postgres::create_connection().await.unwrap(); 24 | 25 | HttpServer::new(move || 26 | App::new() 27 | .app_data(web::Data::new(AppState{ 28 | db_postgres: postgres_session.clone() 29 | })) 30 | .configure(routes::routes) 31 | ) 32 | .workers(num_cpus::get()) 33 | .bind("0.0.0.0:8080")? 34 | .run() 35 | .await 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/mailer.rs: -------------------------------------------------------------------------------- 1 | use lettre::{transport::smtp::authentication::Credentials,header::ContentType, SmtpTransport,Message, Transport}; 2 | use crate::utils::{ 3 | error::ApiError 4 | }; 5 | use std::env::var; 6 | 7 | pub fn send(recipient:String,subject:String,body:String)->Result<(),ApiError>{ 8 | let email = Message::builder() 9 | .from(format!("Do Not Reply <{}>",var("MAILER_USERNAME")?).parse().unwrap()) 10 | .to(recipient.parse().unwrap()) 11 | .subject(subject).header(ContentType::html()) 12 | .body(body)?; 13 | 14 | let creds = Credentials::new( 15 | var("MAILER_USERNAME")?, 16 | var("MAILER_PASSWORD")? 17 | ); 18 | 19 | let mailer = SmtpTransport::relay("smtp.gmail.com")?.credentials(creds).build(); 20 | 21 | match mailer.send(&email){ 22 | Ok(_)=> Ok(()), 23 | Err(e)=> Err(ApiError::InternalServerErrorWithMessage(e.to_string())) 24 | } 25 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | pg: 4 | image: postgres 5 | container_name: pg 6 | restart: unless-stopped 7 | networks: 8 | - backbone 9 | ports: 10 | - 5432:5432 11 | environment: 12 | - POSTGRES_PASSWORD=passport 13 | pg-admin: 14 | image: dpage/pgadmin4 15 | container_name: pg-admin 16 | restart: unless-stopped 17 | networks: 18 | - backbone 19 | ports: 20 | - 5433:80 21 | environment: 22 | - PGADMIN_DEFAULT_EMAIL=root@example.com 23 | - PGADMIN_DEFAULT_PASSWORD=passport 24 | # rust: 25 | # image: rust-app 26 | # container_name: rust 27 | # restart: unless-stopped 28 | # networks: 29 | # - backbone 30 | # ports: 31 | # - 8081:8080 32 | networks: 33 | backbone: 34 | driver: bridge 35 | -------------------------------------------------------------------------------- /src/controllers/system/user/handler.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | web::{Query,Data}, 3 | HttpResponse, 4 | http::{header::SET_COOKIE,HeaderValue} 5 | }; 6 | use sqlx::query; 7 | 8 | use crate::utils::{ 9 | error::ApiError, 10 | auth::{decode_and_authenticate,create_auth_cookie}, 11 | }; 12 | 13 | pub async fn verify_email( 14 | req_query:Query, 15 | state:Data 16 | ) -> Result{ 17 | let (decoded,_) = decode_and_authenticate(req_query.token.as_str(), &state).await?; 18 | 19 | query!( 20 | "UPDATE users SET user_status_id = ( 21 | SELECT id FROM user_status WHERE status = 'verified' 22 | ) WHERE id = $1;", 23 | decoded.id 24 | ).execute(&state.db_postgres).await?; 25 | 26 | let auth_cookie = create_auth_cookie(decoded.id, &state).await; 27 | 28 | Ok(HttpResponse::Ok().set_header( 29 | SET_COOKIE, 30 | HeaderValue::from_str(&auth_cookie)? 31 | ).finish()) 32 | } -------------------------------------------------------------------------------- /src/utils/paginate.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize}; 2 | 3 | #[derive(Deserialize)] 4 | pub struct QueryPagination{ 5 | page: Option, 6 | limit: Option, 7 | order: Option 8 | } 9 | 10 | impl QueryPagination{ 11 | pub fn get_page(&self)->i64{ 12 | match self.page { 13 | Some(page) => page, 14 | None => 1i64 15 | } 16 | } 17 | pub fn get_limit(&self)->i64{ 18 | match self.limit { 19 | Some(limit) => limit, 20 | None => 20i64 21 | } 22 | } 23 | pub fn get_order(&self)->String{ 24 | match &self.order { 25 | Some(order) => { 26 | let input = order.to_uppercase(); 27 | if input != "ASC" || input != "DESC" { return "DESC".to_string(); } 28 | return order.to_string(); 29 | }, 30 | None => { 31 | return "DESC".to_string(); 32 | } 33 | }; 34 | } 35 | pub fn get_offset(&self)->i64{ 36 | (self.get_page() - 1i64)*self.get_limit() 37 | } 38 | } -------------------------------------------------------------------------------- /src/regex/user.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use lazy_static::lazy_static; 3 | use fancy_regex::{Regex as FancyRegex}; 4 | use validator::ValidationError; 5 | 6 | lazy_static! { 7 | pub static ref PHONE_NUMBER: Regex = Regex::new(r"^(08|\+)[0-9]{7,16}$").unwrap(); 8 | pub static ref NAME: Regex = Regex::new(r"[a-zA-Z0-9 ]{3,30}$").unwrap(); 9 | pub static ref ADDRESS: Regex = Regex::new(r"[a-zA-Z0-9 ,.]{8,50}$").unwrap(); 10 | static ref PASSWORD: FancyRegex = FancyRegex::new(r#"^(?=.{6,30})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()_+=/\-|\\{}\[\]\:\;\"\',.<>?`~]).*$"#).unwrap(); 11 | pub static ref USER_ROLE: Regex = Regex::new(r#"(\bsuperadmin\b|\badmin\b|\bcustomer\b)"#).unwrap(); 12 | pub static ref DATE: Regex = Regex::new(r#"\d{4}-(0?[1-9]|1[0-2])-([0-2]?[1-9]|[1-3][01])$"#).unwrap(); 13 | } 14 | 15 | pub fn validate_password(password:&str)-> Result<(), ValidationError>{ 16 | let check = PASSWORD.is_match(password).unwrap(); 17 | 18 | if check == false{ 19 | return Err(ValidationError::new("Password requirement not satisfied")); 20 | } 21 | 22 | Ok(()) 23 | } -------------------------------------------------------------------------------- /src/controllers/internal/user/data.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize,Deserialize}; 2 | use chrono::{Utc,DateTime,naive::NaiveDate}; 3 | use validator::Validate; 4 | 5 | use crate::regex::user::{PHONE_NUMBER,NAME,USER_ROLE,DATE,validate_password}; 6 | 7 | #[derive(Serialize)] 8 | pub struct User{ 9 | pub id: i64, 10 | pub user_role_id: i32, 11 | pub user_status_id: i32, 12 | pub last_logged_in: DateTime, 13 | pub email: String, 14 | pub phone_number: String, 15 | pub password: String, 16 | pub name: Option, 17 | pub date_of_birth: Option, 18 | pub created_at: DateTime, 19 | pub updated_at: DateTime 20 | } 21 | 22 | #[derive(Deserialize,Validate)] 23 | pub struct CreateUser{ 24 | #[validate(regex(path = "USER_ROLE"))]pub user_role: String, 25 | #[validate(email)]pub email: String, 26 | #[validate(custom = "validate_password")]pub password: String, 27 | #[validate(regex(path = "PHONE_NUMBER"))]pub phone_number: String, 28 | #[validate(regex(path = "NAME"))]pub name: Option, 29 | #[validate(regex(path = "DATE"))]pub date_of_birth: Option, 30 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-app" 3 | version = "0.1.0" 4 | authors = ["chris"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | actix = "0.10.0" 11 | actix-http = "2.0.0" 12 | actix-rt = "1.1.1" 13 | actix-service = "1" 14 | actix-web = "3.0.2" 15 | bcrypt = "0.8" 16 | chrono = {version = "0", features = ["serde"]} 17 | clap = "2.33.0" 18 | derive_more = "0" 19 | dotenv = "0" 20 | fancy-regex = "0.3.5" 21 | futures = "0.3.5" 22 | jsonwebtoken = "7" 23 | lazy_static = "1" 24 | lettre = "0.10.0-alpha.2" 25 | num_cpus="1.13" 26 | regex = "1" 27 | serde = {version = "1", features = ["derive"] } 28 | serde_json = "1" 29 | sqlx = {version = "0.4.0-beta.1", features = [ "json", "postgres","chrono","macros","offline"]} 30 | time = "0.2.16" 31 | typed-html={ git = "https://github.com/bodil/typed-html#4c13ecca" } 32 | validator = "0.10" 33 | validator_derive = "0.10" 34 | 35 | [profile.dev] 36 | opt-level = 0 37 | debug = true 38 | debug-assertions = true 39 | overflow-checks = true 40 | lto = false 41 | panic = 'unwind' 42 | incremental = true 43 | codegen-units = 16 44 | rpath = false 45 | 46 | [profile.release] 47 | opt-level = 3 48 | debug = false 49 | debug-assertions = false 50 | overflow-checks = false 51 | lto = true 52 | panic = 'unwind' 53 | incremental = false 54 | codegen-units = 1 55 | rpath = false -------------------------------------------------------------------------------- /src/controllers/global/user/data.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize,Serialize}; 2 | use validator::Validate; 3 | use chrono::{DateTime,Utc,NaiveDate}; 4 | 5 | use crate::regex::user::{PHONE_NUMBER,NAME,validate_password,DATE}; 6 | 7 | #[derive(Deserialize,Validate)] 8 | pub struct LoginRequestBody{ 9 | #[validate(email)]pub email: String, 10 | pub password: String, 11 | } 12 | 13 | #[derive(Deserialize,Validate)] 14 | pub struct SignUp{ 15 | #[validate(email)]pub email: String, 16 | #[validate(custom = "validate_password")]pub password: String, 17 | #[validate(regex(path = "PHONE_NUMBER"))]pub phone_number: String, 18 | #[validate(regex(path = "NAME"))]pub name: Option, 19 | } 20 | 21 | #[derive(Deserialize,Validate)] 22 | pub struct CheckEmailExist{ 23 | #[validate(email)]pub email: String 24 | } 25 | 26 | #[derive(Deserialize,Validate)] 27 | pub struct UpdateProfile{ 28 | #[validate(regex(path = "PHONE_NUMBER"))]pub phone_number: Option, 29 | #[validate(regex(path = "NAME"))]pub name: Option, 30 | #[validate(regex(path = "DATE"))]pub date_of_birth: Option, 31 | } 32 | 33 | #[derive(Serialize)] 34 | pub struct GetProfile{ 35 | pub phone_number: String, 36 | pub email: String, 37 | pub user_status: String, 38 | pub name: Option, 39 | pub date_of_birth: Option, 40 | pub created_at: DateTime 41 | } 42 | 43 | #[derive(Deserialize,Validate)] 44 | pub struct ChangePassword{ 45 | pub previous_password:String, 46 | #[validate(custom = "validate_password")]pub new_password: String 47 | } -------------------------------------------------------------------------------- /src/controllers/internal/user/handler.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | web::{Json,Query,Data} 3 | }; 4 | use sqlx::{query_as,query}; 5 | use bcrypt::{hash}; 6 | 7 | use crate::{ 8 | utils::{ 9 | error::ApiError, 10 | http_body::{MessageWithData,Message}, 11 | paginate, 12 | validator::validate_input 13 | } 14 | }; 15 | 16 | use super::data::{User,CreateUser}; 17 | 18 | pub async fn all( 19 | query:Query, 20 | state:Data, 21 | )->Result>>,ApiError>{ 22 | let data = query_as!(User, 23 | "SELECT * FROM users ORDER BY $1 OFFSET $2 LIMIT $3;", 24 | format!("{} {}","created_at",query.get_order()), 25 | query.get_offset(), 26 | query.get_limit() 27 | ).fetch_all(&state.db_postgres).await?; 28 | 29 | Ok(Json(MessageWithData{ 30 | msg: "ok", 31 | data 32 | })) 33 | } 34 | 35 | pub async fn create( 36 | body:Json, 37 | state:Data 38 | )->Result,ApiError>{ 39 | validate_input(&body)?; 40 | 41 | let dob_splitted:Vec = body.date_of_birth.clone().unwrap().split("-").into_iter().map(|x|->u32{ 42 | x.parse::().unwrap() 43 | }).collect(); 44 | 45 | query!( 46 | "INSERT INTO users( 47 | user_role_id, 48 | user_status_id, 49 | email, 50 | phone_number, 51 | password, 52 | name, 53 | date_of_birth, 54 | last_logged_in 55 | ) 56 | VALUES ( 57 | (SELECT id FROM user_roles WHERE role = $1), 58 | (SELECT id FROM user_status WHERE status = 'verified'), 59 | $2,$3,$4,$5,$6,$7 60 | ); 61 | ", 62 | &body.user_role, 63 | &body.email, 64 | &body.phone_number, 65 | hash(&body.password,6)?, 66 | body.name.clone().unwrap(), 67 | chrono::naive::NaiveDate::from_ymd(dob_splitted[0] as i32,dob_splitted[1],dob_splitted[2]), 68 | chrono::Utc::now() 69 | ).execute(&state.db_postgres).await?; 70 | 71 | Ok(Json(Message{ 72 | msg:"OK" 73 | })) 74 | } -------------------------------------------------------------------------------- /src/utils/error.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | HttpResponse, 3 | error::{ResponseError}, 4 | http 5 | }; 6 | use derive_more::Display; 7 | use serde::{Serialize,Deserialize}; 8 | use std::{ 9 | convert::From, 10 | env::VarError, 11 | }; 12 | use sqlx::error::Error as SqlxError; 13 | use bcrypt::BcryptError; 14 | use jsonwebtoken::errors::Error as JwtError; 15 | use lettre::error::Error as lettre_error; 16 | use lettre::transport::smtp::Error as lettre_smtp_error; 17 | 18 | #[derive(Debug, Display, PartialEq)] 19 | pub enum ApiError{ 20 | BadRequest(String), 21 | Unauthorized(String), 22 | Forbidden(String), 23 | NotFound(String), 24 | Conflict(String), 25 | InternalServerError, 26 | InternalServerErrorWithMessage(String), 27 | } 28 | 29 | #[derive(Debug, Deserialize, Serialize)] 30 | pub struct ErrorResponse { 31 | msg: T, 32 | } 33 | 34 | impl ResponseError for ApiError { 35 | fn error_response(&self) -> HttpResponse { 36 | match self { 37 | ApiError::BadRequest(msg) => HttpResponse::BadRequest().json(ErrorResponse{msg}), 38 | ApiError::Unauthorized(msg) => HttpResponse::Unauthorized().json(ErrorResponse{msg}), 39 | ApiError::Forbidden(msg) => HttpResponse::Forbidden().json(ErrorResponse{msg}), 40 | ApiError::NotFound(msg) => HttpResponse::NotFound().json(ErrorResponse{msg}), 41 | ApiError::Conflict(msg) => HttpResponse::Conflict().json(ErrorResponse{msg}), 42 | ApiError::InternalServerErrorWithMessage(msg) => HttpResponse::InternalServerError().json(ErrorResponse{msg}), 43 | ApiError::InternalServerError => HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR) 44 | } 45 | } 46 | } 47 | 48 | impl From for ApiError { 49 | fn from(error: VarError) -> Self { 50 | println!("{}",error.to_string()); 51 | ApiError::InternalServerError 52 | } 53 | } 54 | 55 | impl From for ApiError { 56 | fn from(error: http::header::InvalidHeaderValue) -> Self { 57 | println!("{}",error.to_string()); 58 | ApiError::InternalServerError 59 | } 60 | } 61 | 62 | impl From for ApiError { 63 | fn from(error: JwtError) -> Self { 64 | println!("{}",error.to_string()); 65 | ApiError::InternalServerError 66 | } 67 | } 68 | 69 | impl From for ApiError { 70 | fn from(error: SqlxError) -> Self { 71 | ApiError::InternalServerErrorWithMessage(error.to_string()) 72 | } 73 | } 74 | 75 | impl From for ApiError { 76 | fn from(error: BcryptError) -> Self { 77 | ApiError::InternalServerErrorWithMessage(error.to_string()) 78 | } 79 | } 80 | 81 | impl From for ApiError { 82 | fn from(error: lettre_error) -> Self { 83 | println!("{}",error.to_string()); 84 | ApiError::InternalServerError 85 | } 86 | } 87 | 88 | impl From for ApiError { 89 | fn from(error: lettre_smtp_error) -> Self { 90 | println!("{}",error.to_string()); 91 | ApiError::InternalServerError 92 | } 93 | } -------------------------------------------------------------------------------- /src/controllers/global/user/handler_protected.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | web::{Data,Json}, 3 | HttpRequest 4 | }; 5 | use sqlx::{query,query_as}; 6 | use bcrypt::{hash,verify}; 7 | use chrono::{Utc}; 8 | 9 | use crate::utils::{ 10 | http_body::{Message,MessageWithData,get_data_from_middleware}, 11 | error::ApiError, 12 | validator::validate_input 13 | }; 14 | 15 | use super::data::{UpdateProfile,GetProfile,ChangePassword}; 16 | 17 | pub async fn get_profile( 18 | state:Data, 19 | req:HttpRequest 20 | ) ->Result>,ApiError>{ 21 | let jwt_decoded = get_data_from_middleware(&req)?; 22 | 23 | let data:GetProfile = query_as!( 24 | GetProfile, 25 | "SELECT 26 | users.phone_number phone_number, 27 | users.email email, 28 | users.date_of_birth date_of_birth, 29 | users.created_at created_at, 30 | users.name, 31 | user_status.status user_status 32 | FROM users JOIN user_status 33 | ON users.user_status_id = user_status.id 34 | WHERE users.id = $1; 35 | ", 36 | jwt_decoded["id"].as_i64() 37 | ).fetch_one(&state.db_postgres).await?; 38 | 39 | Ok(Json(MessageWithData{ 40 | msg:"OK", 41 | data 42 | })) 43 | } 44 | 45 | pub async fn update_profile( 46 | body:Json, 47 | state:Data, 48 | req:HttpRequest 49 | )-> Result,ApiError>{ 50 | validate_input(&body)?; 51 | 52 | let jwt_decoded = get_data_from_middleware(&req)?; 53 | 54 | let date_of_birth:Option = if let Some(dob) = &body.date_of_birth { 55 | let splitted:Vec = dob.split("-").into_iter().map(|x|->u32{ 56 | x.parse::().unwrap() 57 | }).collect(); 58 | 59 | Some(chrono::naive::NaiveDate::from_ymd(splitted[0] as i32,splitted[1],splitted[2])) 60 | } else { 61 | None 62 | }; 63 | 64 | query!( 65 | "UPDATE users SET 66 | name = COALESCE($1,name), 67 | date_of_birth = COALESCE($2,date_of_birth), 68 | phone_number = COALESCE($3,phone_number) 69 | WHERE id = $4; 70 | ", 71 | body.name.clone(),date_of_birth,body.phone_number.clone(),jwt_decoded["id"].clone().as_i64() 72 | ).execute(&state.db_postgres).await?; 73 | 74 | Ok(Json(Message{ 75 | msg:"OK" 76 | })) 77 | } 78 | 79 | pub async fn change_password( 80 | body:Json, 81 | state:Data, 82 | req:HttpRequest 83 | )->Result,ApiError>{ 84 | validate_input(&body)?; 85 | 86 | let jwt_decoded = get_data_from_middleware(&req)?; 87 | 88 | let data_user = query!( 89 | "SELECT id,password FROM users WHERE id = $1;", 90 | jwt_decoded["id"].as_i64() 91 | ).fetch_one(&state.db_postgres).await?; 92 | 93 | let verify_old_password = verify(&body.previous_password, data_user.password.as_str())?; 94 | 95 | if !verify_old_password{ 96 | return Err(ApiError::Forbidden("Old password does not match!".to_string())); 97 | } 98 | 99 | query!( 100 | "UPDATE users SET password=$1,last_logged_in=$2 WHERE id = $3;", 101 | hash(&body.new_password, 6u32)?, 102 | Utc::now(), 103 | jwt_decoded["id"].as_i64() 104 | ).execute(&state.db_postgres).await?; 105 | 106 | Ok(Json(Message{ 107 | msg:"OK" 108 | })) 109 | } -------------------------------------------------------------------------------- /src/controllers/global/user/handler.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | web::{Json,Data}, 3 | http::{header::SET_COOKIE,HeaderValue}, 4 | HttpResponse, 5 | }; 6 | use sqlx::query; 7 | use bcrypt::{verify,hash}; 8 | use std::env::var; 9 | 10 | use crate::utils::{ 11 | error::ApiError, 12 | http_body::Message, 13 | validator::validate_input, 14 | auth::{create_auth_cookie,new as create_jwt}, 15 | mailer 16 | }; 17 | use crate::assets::user_verification_email; 18 | 19 | use super::data::{LoginRequestBody,SignUp,CheckEmailExist}; 20 | 21 | pub async fn web_login( 22 | body:Json, 23 | state: Data, 24 | )-> Result{ 25 | validate_input(&body)?; 26 | 27 | let data_user = query!( 28 | "SELECT id,password FROM users WHERE email=$1;", 29 | &body.email 30 | ).fetch_one(&state.db_postgres).await; 31 | 32 | if let Err(_) = data_user{ 33 | return Err(ApiError::NotFound("Email not registered".to_string())); 34 | } 35 | 36 | let data_user_unwrapped = data_user?; 37 | 38 | if verify(&body.password, &data_user_unwrapped.password)? == false { 39 | return Err(ApiError::Unauthorized("Wrong password".to_string())); 40 | } 41 | 42 | let auth_cookie = create_auth_cookie(data_user_unwrapped.id, &state).await; 43 | 44 | Ok(HttpResponse::Ok().set_header( 45 | SET_COOKIE, 46 | HeaderValue::from_str(&auth_cookie)? 47 | ).finish()) 48 | } 49 | 50 | pub async fn check_email_exist( 51 | body:Json, 52 | state:Data 53 | )-> Result,ApiError>{ 54 | validate_input(&body)?; 55 | 56 | let data = query!( 57 | "SELECT id FROM users WHERE email = $1", 58 | &body.email 59 | ).fetch_one(&state.db_postgres).await; 60 | 61 | if let Ok(_) = data { 62 | return Err(ApiError::Conflict("email already exist".to_string())); 63 | } 64 | 65 | Ok(Json(Message{ 66 | msg:"OK" 67 | })) 68 | } 69 | 70 | pub async fn sign_up( 71 | body:Json, 72 | state:Data 73 | )->Result,ApiError>{ 74 | validate_input(&body)?; 75 | 76 | let data_user_exist = query!( 77 | "SELECT id FROM users WHERE email = $1;", 78 | &body.email 79 | ).fetch_one(&state.db_postgres).await; 80 | 81 | if let Ok(_) = data_user_exist { 82 | return Err(ApiError::Conflict("Email already registered".to_string())); 83 | } 84 | 85 | let data_user = query!( 86 | "INSERT INTO users( 87 | user_role_id, 88 | user_status_id, 89 | email, 90 | phone_number, 91 | password, 92 | name, 93 | last_logged_in 94 | ) 95 | VALUES ( 96 | (SELECT id FROM user_roles WHERE role = 'customer'), 97 | (SELECT id FROM user_status WHERE status = 'unverified'), 98 | $1,$2,$3,$4,$5 99 | ) RETURNING id; 100 | ", 101 | &body.email, 102 | &body.phone_number, 103 | hash(&body.password,6)?, 104 | body.name.clone().unwrap(), 105 | chrono::Utc::now() 106 | ).fetch_one(&state.db_postgres).await?; 107 | 108 | let url_to_send = format!("{}{}",var("MAIL_VERIFICATION_URL")?,create_jwt(data_user.id as i64, &state).await?); 109 | 110 | mailer::send(body.email.clone(), "Verfication".to_string(), user_verification_email::create_mail(url_to_send))?; 111 | 112 | Ok(Json(Message{ 113 | msg:"OK" 114 | })) 115 | } -------------------------------------------------------------------------------- /src/utils/auth.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | cookie::{Cookie,SameSite}, 3 | web::Data, 4 | }; 5 | use std::{ 6 | ops::Add, 7 | env::var 8 | }; 9 | use jsonwebtoken::{encode, decode as dec, Header, Validation, EncodingKey, DecodingKey,errors::ErrorKind::{InvalidToken,InvalidSignature,ExpiredSignature}}; 10 | use serde::{Serialize, Deserialize}; 11 | use sqlx::{query,query_as}; 12 | use chrono::{DateTime,Utc}; 13 | 14 | use super::error::ApiError; 15 | 16 | #[derive(Debug, Serialize, Deserialize)] 17 | pub struct Claims { 18 | pub id: i64, 19 | pub exp: i64, 20 | iat:i64, 21 | iss:String 22 | } 23 | 24 | pub async fn new (id:i64,state:&Data)-> Result{ 25 | let now =chrono::Utc::now(); 26 | let token = encode(&Header::default(), &Claims{ 27 | id, 28 | exp: now.timestamp() + var("SESSION_LENGTH")?.parse::().unwrap(), 29 | iat: now.timestamp(), 30 | iss: var("JWT_ISSUER")? 31 | }, &EncodingKey::from_secret(var("JWT_SECRET")?.as_ref()))?; 32 | 33 | query!( 34 | "UPDATE users SET last_logged_in = $1 where id = $2;", 35 | now,id 36 | ).execute(&state.db_postgres).await?; 37 | 38 | Ok(token) 39 | } 40 | 41 | pub async fn create_auth_cookie(id:i64,state:&Data)->String{ 42 | let mut auth_cookie = Cookie::new("Authorization", new(id, state).await.unwrap()); 43 | if var("ENV").unwrap() == "production".to_string() {auth_cookie.set_secure(true);} 44 | auth_cookie.set_same_site(SameSite::Strict); 45 | auth_cookie.set_http_only(true); 46 | auth_cookie.set_expires(time::OffsetDateTime::now_utc().add(time::Duration::seconds(var("SESSION_LENGTH").unwrap().parse::().unwrap()))); 47 | auth_cookie.set_path("/"); 48 | auth_cookie.to_string() 49 | } 50 | 51 | pub struct User{ 52 | last_logged_in:DateTime, 53 | user_role:String, 54 | status:String, 55 | } 56 | 57 | pub async fn decode_and_authenticate (token: &str,state:&Data) -> Result<(Claims,User),ApiError>{ 58 | match dec::(token, &DecodingKey::from_secret(var("JWT_SECRET")?.as_ref()), &Validation::default()) { 59 | Ok(data) =>{ 60 | let data_user:User = query_as!( 61 | User, 62 | "SELECT 63 | user_status.status status, 64 | users.last_logged_in last_logged_in, 65 | user_roles.role user_role 66 | FROM users 67 | JOIN user_roles ON users.user_role_id = user_roles.id 68 | JOIN user_status ON users.user_status_id = user_status.id 69 | WHERE users.id = $1;", 70 | data.claims.id 71 | ).fetch_one(&state.db_postgres).await?; 72 | 73 | if data_user.last_logged_in.timestamp() > data.claims.iat || Utc::now().timestamp() > data.claims.exp { 74 | return Err(ApiError::Unauthorized("Session Expired".to_string())); 75 | } 76 | 77 | Ok((data.claims,data_user)) 78 | }, 79 | Err(e) => match e.kind(){ 80 | InvalidToken | InvalidSignature => Err(ApiError::BadRequest("Invalid Token".to_string())), 81 | ExpiredSignature => Err(ApiError::Unauthorized("Session Expired".to_string())), 82 | _ => Err(ApiError::InternalServerError) 83 | } 84 | } 85 | } 86 | 87 | pub async fn decode_with_user_role(authorized_role:Vec, token:&str, state:&Data) -> Result{ 88 | let (decoded,data_user) = decode_and_authenticate(token,state).await?; 89 | 90 | let find_role = authorized_role.into_iter().position(|x| x==data_user.user_role); 91 | 92 | if data_user.user_role != "superadmin" && (data_user.status != "verified" || find_role == None) { 93 | return Err(ApiError::Unauthorized("Unauthorized".to_string())); 94 | } 95 | 96 | Ok(decoded) 97 | } -------------------------------------------------------------------------------- /src/middlewares/auth.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::task::{Context, Poll}; 3 | use std::env::var; 4 | use actix_service::{Service, Transform}; 5 | use actix_web::{ 6 | HttpMessage, 7 | dev::{ServiceRequest,ServiceResponse}, 8 | Error, 9 | web::Data 10 | }; 11 | use futures::future::{ok, Ready}; 12 | use futures::Future; 13 | use futures::executor::block_on; 14 | use serde_json::{Value as JsonValue}; 15 | 16 | use crate::utils::{ 17 | error::ApiError, 18 | auth::decode_with_user_role 19 | }; 20 | 21 | #[derive(Clone)] 22 | pub enum AuthType{ 23 | JWT(Vec), 24 | APIKEY 25 | } 26 | 27 | pub struct Auth{ 28 | pub classification: AuthType 29 | } 30 | 31 | impl Transform for Auth 32 | where 33 | S: Service, Error = Error>, 34 | S::Future: 'static, 35 | B: 'static, 36 | { 37 | type Request = ServiceRequest; 38 | type Response = ServiceResponse; 39 | type Error = Error; 40 | type InitError = (); 41 | type Transform = AuthMiddleware; 42 | type Future = Ready>; 43 | 44 | fn new_transform(&self, service: S) -> Self::Future { 45 | ok(AuthMiddleware { service,classification:self.classification.clone() }) 46 | } 47 | } 48 | 49 | pub struct AuthMiddleware { 50 | service: S, 51 | classification:AuthType 52 | } 53 | 54 | impl Service for AuthMiddleware 55 | where 56 | S: Service, Error = Error>, 57 | S::Future: 'static, 58 | B: 'static, 59 | { 60 | type Request = ServiceRequest; 61 | type Response = ServiceResponse; 62 | type Error = Error; 63 | type Future = Pin>>>; 64 | 65 | fn poll_ready(&mut self, cx: &mut Context) -> Poll> { 66 | self.service.poll_ready(cx) 67 | } 68 | 69 | fn call(&mut self, req: ServiceRequest) -> Self::Future { 70 | match &self.classification{ 71 | AuthType::JWT(role)=>{ 72 | let auth_cookie = req.cookie("Authorization"); 73 | 74 | if auth_cookie == None{ 75 | return Box::pin(async { 76 | Ok(req.error_response(ApiError::Unauthorized("not logged in".to_string()))) 77 | }); 78 | } 79 | 80 | let auth_cookie_unwrapped = auth_cookie.unwrap(); 81 | 82 | let jwt_token = auth_cookie_unwrapped.value(); 83 | 84 | let decoded = block_on(async{ 85 | decode_with_user_role(role.clone(),jwt_token,&req.app_data::>().unwrap()).await 86 | }); 87 | 88 | if let Err(error) = decoded{ 89 | return Box::pin(async { 90 | Ok(req.error_response(error)) 91 | }); 92 | } 93 | 94 | req.extensions_mut().insert::(serde_json::from_str( 95 | &format!("{{ \"id\":{} }}",decoded.unwrap().id) 96 | ).unwrap()); 97 | } 98 | AuthType::APIKEY=>{ 99 | if let Some(key) = req.headers().get("x-api-key"){ 100 | if key.to_str().unwrap() != var("API_KEY").unwrap() { 101 | return Box::pin(async { 102 | Ok(req.error_response(ApiError::Unauthorized("wrong auth".to_string()))) 103 | }); 104 | } 105 | } 106 | else{ 107 | return Box::pin(async { 108 | Ok(req.error_response(ApiError::Unauthorized("wrong auth".to_string()))) 109 | }); 110 | } 111 | } 112 | } 113 | 114 | let fut = self.service.call(req); 115 | 116 | Box::pin(async move { 117 | let res = fut.await?; 118 | Ok(res) 119 | }) 120 | } 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-web-backend 2 | web backend written in Rust language, for learning purposes 3 | 4 | ## tech stack: 5 | - PostgreSQL for database (https://hub.docker.com/_/postgres) 6 | - Actix-web for routing (https://github.com/actix/actix-web) 7 | - sqlx for sql object mapper (https://github.com/launchbadge/sqlx) 8 | - jsonwebtoken for user authentication 9 | - bcrypt for password hash 10 | 11 | ## prerequsites: 12 | - Install docker on your machine 13 | - Install PostgreSQL using docker-compose: 14 | ```bash 15 | docker-compose up -d 16 | ``` 17 | 18 | ## to run in development mode: 19 | - modify ENV files, .env is needed for sqlx. Specify the `DATABASE_URL` according to your PostgreSQL 20 | - create migrate database using pg admin 21 | - execute: 22 | ```bash 23 | cargo run -- -e dev 24 | ``` 25 | 26 | ## preparing for deployment: 27 | to avoid sqlx compile time check, follow this steps: 28 | - clone https://github.com/launchbadge/sqlx 29 | - execute: 30 | ```bash 31 | cargo install --path ./sqlx-cli 32 | #after install, move to this project folder 33 | cargo sqlx prepare 34 | #sqlx-data.json will be generated, you can change the `DATABASE_URL` to real address of the database when deployed 35 | ``` 36 | 37 | ## to deploy: 38 | - modify ENV files, `DATABASE_URL` on .env must be deleted 39 | - execute: 40 | ```bash 41 | docker build -t rust-app -f .Dockerfile . 42 | docker-compose up -d 43 | ``` 44 | 45 | ## pre-built API: 46 | - check email exist (customer) 47 | ```bash 48 | curl --location --request POST 'localhost:8080/global/user/signup/check/email' \ 49 | --header 'x-api-key: your_api_key' \ 50 | --header 'Content-Type: application/json' \ 51 | --data-raw '{ 52 | "email": "example@example.com" 53 | }' 54 | ``` 55 | 56 | - sign up (customer) 57 | ```bash 58 | curl --location --request POST 'localhost:8080/global/user/signup' \ 59 | --header 'x-api-key: your_api_key' \ 60 | --header 'Content-Type: application/json' \ 61 | --data-raw '{ 62 | "email": "example@example.com" , 63 | "password":"AlphaNum3ricW!thSpecialChar" , 64 | "phone_number": "+628123456789" , 65 | "name": "valid name" 66 | }' 67 | ``` 68 | 69 | - user login (superadmin,admin,customer) 70 | ```bash 71 | curl --location --request POST 'localhost:8080/global/user/login/web' \ 72 | --header 'x-api-key: your_api_key' \ 73 | --header 'Content-Type: application/json' \ 74 | --data-raw '{ 75 | "email":"example@example.com", 76 | "password":"AlphaNum3ricW!thSpecialChar" 77 | }' 78 | ``` 79 | 80 | - get all user (superadmin,admin) 81 | ```bash 82 | curl --location --request GET 'localhost:8080/admin/user/all' \ 83 | --header 'x-api-key: your_api_key' \ 84 | --header 'Cookie: Authorization=jwt token acquired after login' 85 | ``` 86 | 87 | - create new user (superadmin) 88 | ```bash 89 | curl --location --request POST 'localhost:8080/superadmin/user/create' \ 90 | --header 'x-api-key: your_api_key' \ 91 | --header 'Content-Type: application/json' \ 92 | --header 'Cookie: Authorization=jwt token acquired after login' \ 93 | --data-raw '{ 94 | "user_role": "customer" , 95 | "email": "example@example.com" , 96 | "password":"AlphaNum3ricW!thSpecialChar" , 97 | "phone_number": "+628123456789" , 98 | "name": "valid name" 99 | "date_of_birth": "1970-01-01" 100 | }' 101 | ``` 102 | 103 | - verify user email (system) 104 | ```bash 105 | curl --location --request GET 'localhost:8080/system/user/verify/email?token=jwt token sent to user email, front end need to pass the token to back end' \ 106 | --header 'x-api-key: your_api_key' \ 107 | ``` 108 | 109 | - get user self profile (superadmin,admin,customer) 110 | ```bash 111 | curl --location --request GET 'localhost:8080/global/user/protected/profile' \ 112 | --header 'x-api-key: your_api_key' \ 113 | --header 'Cookie: Authorization=jwt token acquired after login' 114 | ``` 115 | 116 | - update user self profile (superadmin,admin,customer) 117 | ```bash 118 | curl --location --request POST 'localhost:8080/global/user/protected/profile/update' \ 119 | --header 'x-api-key: your_api_key' \ 120 | --header 'Content-Type: application/json' \ 121 | --header 'Cookie: Authorization=jwt token acquired after login' \ 122 | --data-raw '{ 123 | "phone_number":"+628123456789", 124 | "date_of_birth":"2005-10-10, 125 | "name":"valid name" 126 | }' 127 | ``` 128 | 129 | - change password (superadmin,admin,customer) 130 | ```bash 131 | curl --location --request POST 'localhost:8080/global/user/protected/password/change' \ 132 | --header 'x-api-key: your_api_key' \ 133 | --header 'Content-Type: application/json' \ 134 | --header 'Cookie: Authorization=jwt token acquired after login' \ 135 | --data-raw '{ 136 | "previous_password":"OldPa55word-", 137 | "new_password":"N3wPassWord-" 138 | }' 139 | ``` -------------------------------------------------------------------------------- /sqlx-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": "PostgreSQL", 3 | "312bf8c09b67889cbf759597a0878421422aeb9ca3b4efe96d7f2c2fd78bca18": { 4 | "query": "INSERT INTO users(\n user_role_id,\n user_status_id,\n email,\n phone_number,\n password,\n name,\n last_logged_in\n )\n VALUES (\n (SELECT id FROM user_roles WHERE role = 'customer'),\n (SELECT id FROM user_status WHERE status = 'unverified'),\n $1,$2,$3,$4,$5\n ) RETURNING id;\n ", 5 | "describe": { 6 | "columns": [ 7 | { 8 | "ordinal": 0, 9 | "name": "id", 10 | "type_info": "Int8" 11 | } 12 | ], 13 | "parameters": { 14 | "Left": [ 15 | "Varchar", 16 | "Varchar", 17 | "Bpchar", 18 | "Varchar", 19 | "Timestamptz" 20 | ] 21 | }, 22 | "nullable": [ 23 | false 24 | ] 25 | } 26 | }, 27 | "31ad99ad63643077da03218c3ae36e85be54972219d82886d228432776d01dea": { 28 | "query": "UPDATE users SET last_logged_in = $1 where id = $2;", 29 | "describe": { 30 | "columns": [], 31 | "parameters": { 32 | "Left": [ 33 | "Timestamptz", 34 | "Int8" 35 | ] 36 | }, 37 | "nullable": [] 38 | } 39 | }, 40 | "33c6952aff129e1dc84594318a8e86040b344b758ae7f9aaffa098931c5155dd": { 41 | "query": "SELECT id,password FROM users WHERE id = $1;", 42 | "describe": { 43 | "columns": [ 44 | { 45 | "ordinal": 0, 46 | "name": "id", 47 | "type_info": "Int8" 48 | }, 49 | { 50 | "ordinal": 1, 51 | "name": "password", 52 | "type_info": "Bpchar" 53 | } 54 | ], 55 | "parameters": { 56 | "Left": [ 57 | "Int8" 58 | ] 59 | }, 60 | "nullable": [ 61 | false, 62 | false 63 | ] 64 | } 65 | }, 66 | "4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068": { 67 | "query": "SELECT id FROM users WHERE email = $1", 68 | "describe": { 69 | "columns": [ 70 | { 71 | "ordinal": 0, 72 | "name": "id", 73 | "type_info": "Int8" 74 | } 75 | ], 76 | "parameters": { 77 | "Left": [ 78 | "Text" 79 | ] 80 | }, 81 | "nullable": [ 82 | false 83 | ] 84 | } 85 | }, 86 | "6932a426efced0e2f70729ef16772e780f22411c8a175434be17bb52481287db": { 87 | "query": "SELECT * FROM users ORDER BY $1 OFFSET $2 LIMIT $3;", 88 | "describe": { 89 | "columns": [ 90 | { 91 | "ordinal": 0, 92 | "name": "id", 93 | "type_info": "Int8" 94 | }, 95 | { 96 | "ordinal": 1, 97 | "name": "user_role_id", 98 | "type_info": "Int4" 99 | }, 100 | { 101 | "ordinal": 2, 102 | "name": "user_status_id", 103 | "type_info": "Int4" 104 | }, 105 | { 106 | "ordinal": 3, 107 | "name": "email", 108 | "type_info": "Varchar" 109 | }, 110 | { 111 | "ordinal": 4, 112 | "name": "phone_number", 113 | "type_info": "Varchar" 114 | }, 115 | { 116 | "ordinal": 5, 117 | "name": "password", 118 | "type_info": "Bpchar" 119 | }, 120 | { 121 | "ordinal": 6, 122 | "name": "last_logged_in", 123 | "type_info": "Timestamptz" 124 | }, 125 | { 126 | "ordinal": 7, 127 | "name": "name", 128 | "type_info": "Varchar" 129 | }, 130 | { 131 | "ordinal": 8, 132 | "name": "date_of_birth", 133 | "type_info": "Date" 134 | }, 135 | { 136 | "ordinal": 9, 137 | "name": "created_at", 138 | "type_info": "Timestamptz" 139 | }, 140 | { 141 | "ordinal": 10, 142 | "name": "updated_at", 143 | "type_info": "Timestamptz" 144 | } 145 | ], 146 | "parameters": { 147 | "Left": [ 148 | "Text", 149 | "Int8", 150 | "Int8" 151 | ] 152 | }, 153 | "nullable": [ 154 | false, 155 | false, 156 | false, 157 | false, 158 | false, 159 | false, 160 | false, 161 | true, 162 | true, 163 | false, 164 | false 165 | ] 166 | } 167 | }, 168 | "a3096df49e69b167723a871309e22ebaa2f8d59234cf4d7023aec526f82040db": { 169 | "query": "SELECT \n user_status.status status,\n users.last_logged_in last_logged_in, \n user_roles.role user_role \n FROM users \n JOIN user_roles ON users.user_role_id = user_roles.id \n JOIN user_status ON users.user_status_id = user_status.id\n WHERE users.id = $1;", 170 | "describe": { 171 | "columns": [ 172 | { 173 | "ordinal": 0, 174 | "name": "status", 175 | "type_info": "Varchar" 176 | }, 177 | { 178 | "ordinal": 1, 179 | "name": "last_logged_in", 180 | "type_info": "Timestamptz" 181 | }, 182 | { 183 | "ordinal": 2, 184 | "name": "user_role", 185 | "type_info": "Varchar" 186 | } 187 | ], 188 | "parameters": { 189 | "Left": [ 190 | "Int8" 191 | ] 192 | }, 193 | "nullable": [ 194 | false, 195 | false, 196 | false 197 | ] 198 | } 199 | }, 200 | "a591facd21601ca7858b32567e4c434a4f6f4b9a0e7684d74550ab8a95078514": { 201 | "query": "UPDATE users SET password=$1,last_logged_in=$2 WHERE id = $3;", 202 | "describe": { 203 | "columns": [], 204 | "parameters": { 205 | "Left": [ 206 | "Bpchar", 207 | "Timestamptz", 208 | "Int8" 209 | ] 210 | }, 211 | "nullable": [] 212 | } 213 | }, 214 | "b6856e965f8089c28797e7a40adfc8f5d6f83f04768e752c5cd1635b3d28c255": { 215 | "query": "SELECT id,password FROM users WHERE email=$1;", 216 | "describe": { 217 | "columns": [ 218 | { 219 | "ordinal": 0, 220 | "name": "id", 221 | "type_info": "Int8" 222 | }, 223 | { 224 | "ordinal": 1, 225 | "name": "password", 226 | "type_info": "Bpchar" 227 | } 228 | ], 229 | "parameters": { 230 | "Left": [ 231 | "Text" 232 | ] 233 | }, 234 | "nullable": [ 235 | false, 236 | false 237 | ] 238 | } 239 | }, 240 | "be3ce47f8d8e13cf6d6832e252ae42eae57382eb127dc7939db8e8a95614f333": { 241 | "query": "SELECT id FROM users WHERE email = $1;", 242 | "describe": { 243 | "columns": [ 244 | { 245 | "ordinal": 0, 246 | "name": "id", 247 | "type_info": "Int8" 248 | } 249 | ], 250 | "parameters": { 251 | "Left": [ 252 | "Text" 253 | ] 254 | }, 255 | "nullable": [ 256 | false 257 | ] 258 | } 259 | }, 260 | "bf4965a28b69c24106c29b26ca630411339b0f4f0e1e8f1508df7df19bbc3926": { 261 | "query": "UPDATE users SET \n name = COALESCE($1,name),\n date_of_birth = COALESCE($2,date_of_birth),\n phone_number = COALESCE($3,phone_number)\n WHERE id = $4;\n ", 262 | "describe": { 263 | "columns": [], 264 | "parameters": { 265 | "Left": [ 266 | "Varchar", 267 | "Date", 268 | "Varchar", 269 | "Int8" 270 | ] 271 | }, 272 | "nullable": [] 273 | } 274 | }, 275 | "dbdc385cd55f0a1373c137717fb89bbd3f8628a73a4a4167e2c3bf76c93be96f": { 276 | "query": "SELECT \n users.phone_number phone_number,\n users.email email,\n users.date_of_birth date_of_birth,\n users.created_at created_at,\n users.name,\n user_status.status user_status\n FROM users JOIN user_status \n ON users.user_status_id = user_status.id \n WHERE users.id = $1;\n ", 277 | "describe": { 278 | "columns": [ 279 | { 280 | "ordinal": 0, 281 | "name": "phone_number", 282 | "type_info": "Varchar" 283 | }, 284 | { 285 | "ordinal": 1, 286 | "name": "email", 287 | "type_info": "Varchar" 288 | }, 289 | { 290 | "ordinal": 2, 291 | "name": "date_of_birth", 292 | "type_info": "Date" 293 | }, 294 | { 295 | "ordinal": 3, 296 | "name": "created_at", 297 | "type_info": "Timestamptz" 298 | }, 299 | { 300 | "ordinal": 4, 301 | "name": "name", 302 | "type_info": "Varchar" 303 | }, 304 | { 305 | "ordinal": 5, 306 | "name": "user_status", 307 | "type_info": "Varchar" 308 | } 309 | ], 310 | "parameters": { 311 | "Left": [ 312 | "Int8" 313 | ] 314 | }, 315 | "nullable": [ 316 | false, 317 | false, 318 | true, 319 | false, 320 | true, 321 | false 322 | ] 323 | } 324 | }, 325 | "e72f1a80ffda67c3017aad06cc351c49a2d64cbddad9e2595d1be027839d7d73": { 326 | "query": "UPDATE users SET user_status_id = (\n SELECT id FROM user_status WHERE status = 'verified'\n ) WHERE id = $1;", 327 | "describe": { 328 | "columns": [], 329 | "parameters": { 330 | "Left": [ 331 | "Int8" 332 | ] 333 | }, 334 | "nullable": [] 335 | } 336 | }, 337 | "f1e31b336c7cbf8e780c9ee4910d82db7e2bf8d864dae03b4c2930fab2b04e60": { 338 | "query": "INSERT INTO users(\n user_role_id,\n user_status_id,\n email,\n phone_number,\n password,\n name,\n date_of_birth,\n last_logged_in\n )\n VALUES (\n (SELECT id FROM user_roles WHERE role = $1),\n (SELECT id FROM user_status WHERE status = 'verified'),\n $2,$3,$4,$5,$6,$7\n );\n ", 339 | "describe": { 340 | "columns": [], 341 | "parameters": { 342 | "Left": [ 343 | "Text", 344 | "Varchar", 345 | "Varchar", 346 | "Bpchar", 347 | "Varchar", 348 | "Date", 349 | "Timestamptz" 350 | ] 351 | }, 352 | "nullable": [] 353 | } 354 | } 355 | } --------------------------------------------------------------------------------