From e00bf57fbeb06535c41ec2c094f3f755761bf22b Mon Sep 17 00:00:00 2001 From: realaravinth Date: Sun, 14 Aug 2022 14:47:47 +0530 Subject: [PATCH] feat: file uploads --- .github/workflows/linux.yml | 6 +- Cargo.toml | 12 ++ src/api/mod.rs | 17 +++ src/api/v1/files.rs | 264 ++++++++++++++++++++++++++++++++++++ src/api/v1/meta.rs | 133 ++++++++++++++++++ src/api/v1/mod.rs | 76 +++++++++++ src/ctx.rs | 89 ++++++++++++ src/errors.rs | 189 ++++++++++++++++++++++++++ src/main.rs | 120 ++++++++++++++++ src/routes.rs | 21 +++ 10 files changed, 924 insertions(+), 3 deletions(-) create mode 100644 src/api/mod.rs create mode 100644 src/api/v1/files.rs create mode 100644 src/api/v1/meta.rs create mode 100644 src/api/v1/mod.rs create mode 100644 src/ctx.rs create mode 100644 src/errors.rs create mode 100644 src/main.rs create mode 100644 src/routes.rs diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index e84dd3f..012ded7 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -22,7 +22,7 @@ jobs: name: ${{ matrix.version }} - x86_64-unknown-linux-gnu runs-on: ubuntu-latest - services: + #services: # postgres: # image: postgres # env: @@ -125,8 +125,8 @@ jobs: if: matrix.version == 'stable' && (github.repository == 'realaravinth/dumbserve') run: make doc env: - POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" - MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}" + # POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" + # MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}" GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value COMPILED_DATE: "2021-07-21" diff --git a/Cargo.toml b/Cargo.toml index 1f758f3..2be8744 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,20 @@ serde = { version = "1", features=["derive"]} tokio = { version = "1.20.1", features = ["fs"]} uuid = { version = "1", features = ["v4"] } sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] } +actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" } +derive_builder = "0.11.2" +argon2-creds = { branch = "master", git = "https://github.com/realaravinth/argon2-creds"} +config = "0.11" +derive_more = "0.99.17" +url = { version = "2.2.2", features = ["serde"]} +serde_json = "1" + [build-dependencies] serde_json = "1" sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline"] } + +[dev-dependencies] +actix-rt = "2.7.0" +base64 = "0.13.0" diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..5aa8f74 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +pub mod v1; diff --git a/src/api/v1/files.rs b/src/api/v1/files.rs new file mode 100644 index 0000000..2a53024 --- /dev/null +++ b/src/api/v1/files.rs @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_multipart::Multipart; +use actix_web::HttpMessage; +use actix_web::{web, Error, HttpRequest, HttpResponse, Responder}; +use actix_web_httpauth::middleware::HttpAuthentication; +use futures_util::TryStreamExt as _; +use serde::{Deserialize, Serialize}; +use tokio::fs; +use tokio::io::AsyncWriteExt; + +use super::httpauth; +use super::SignedInUser; +use super::API_V1_ROUTES; +use crate::AppCtx; + +pub mod routes { + use super::*; + #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] + pub struct Files { + pub delete_dir: &'static str, + pub upload_file: &'static str, + pub index: &'static str, + } + impl Files { + pub const fn new() -> Self { + Self { + delete_dir: "/api/v1/files/delete", + upload_file: "/api/v1/files/upload", + index: "/api/v1/files/", + } + } + } +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(delete_dir); + cfg.service(upload_file); + cfg.service(index); +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] +struct Dir { + path: String, +} + +#[actix_web_codegen_const_routes::delete( + path = "API_V1_ROUTES.files.delete_dir", + wrap = "HttpAuthentication::basic(httpauth)" +)] +async fn delete_dir( + req: HttpRequest, + ctx: AppCtx, + payload: web::Json, +) -> Result { + let path = { + let ext = req.extensions(); + let user = ext.get::().unwrap().clone(); + ctx.settings.files.get_path(&user.0, &payload.path) + }; + + if path.exists() { + if path.is_dir() { + fs::remove_dir_all(path).await?; + Ok(HttpResponse::Ok().into()) + } else { + Ok(HttpResponse::BadRequest().body("Path is not dir".to_string())) + } + } else { + Ok(HttpResponse::NotFound().body("dir not found".to_string())) + } +} + +#[actix_web_codegen_const_routes::post( + path = "API_V1_ROUTES.files.upload_file", + wrap = "HttpAuthentication::basic(httpauth)" +)] +async fn upload_file( + ctx: AppCtx, + mut payload: Multipart, + req: HttpRequest, + query: web::Query, +) -> Result { + let path = { + let ext = req.extensions(); + let user = ext.get::().unwrap().clone(); + ctx.settings.files.get_path(&user.0, &query.path) + }; + if !path.exists() { + fs::create_dir_all(&path).await?; + } + + // iterate over multipart stream + while let Some(mut field) = payload.try_next().await? { + // A multipart/form-data stream has to contain `content_disposition` + let content_disposition = field.content_disposition(); + + let filename = content_disposition.get_filename(); + + if filename.is_none() { + return Ok(HttpResponse::BadRequest().body("Filename is not present".to_string())); + } + let filename = filename.unwrap(); + let filepath = path.join(filename); + + let mut f = fs::File::create(filepath).await?; + + // Field in turn is stream of *Bytes* object + while let Some(chunk) = field.try_next().await? { + f.write_all(&chunk).await? + } + } + + Ok(HttpResponse::Ok().into()) +} +#[actix_web_codegen_const_routes::get( + path = "API_V1_ROUTES.files.index", + wrap = "HttpAuthentication::basic(httpauth)" +)] +async fn index() -> HttpResponse { + let html = r#" + Upload Test + +
+ + +
+ + "#; + + HttpResponse::Ok().body(html) +} + +#[cfg(test)] +pub mod tests { + use actix_web::{ + http::{header, StatusCode}, + test, App, + }; + + use super::*; + use crate::*; + + #[actix_rt::test] + async fn index_works() { + // const USERNAME: &str = "index_works"; + // const PASSWORD: &str = "23k4j;123k4j1;l23kj4"; + let settings = Settings::new().unwrap(); + let creds = settings.files.creds.get(0).unwrap().clone(); + let auth = format!( + "Basic {}", + base64::encode(format!("{}:{}", creds.username, creds.password)) + ); + + // let settings = Settings::new().unwrap(); + let ctx = AppCtx::new(crate::ctx::Ctx::new(&settings).await); + let app = test::init_service( + App::new() + .app_data(ctx.clone()) + .configure(crate::routes::services), + ) + .await; + + let index_resp = test::call_service( + &app, + test::TestRequest::get() + .append_header((header::AUTHORIZATION, auth)) + .uri(API_V1_ROUTES.files.index) + .to_request(), + ) + .await; + assert_eq!(index_resp.status(), StatusCode::OK); + } + + #[actix_rt::test] + async fn delete_dir_works() { + // const USERNAME: &str = "index_works"; + // const PASSWORD: &str = "23k4j;123k4j1;l23kj4"; + let settings = Settings::new().unwrap(); + let creds = settings.files.creds.get(0).unwrap().clone(); + let auth = format!( + "Basic {}", + base64::encode(format!("{}:{}", creds.username.clone(), creds.password)) + ); + + const TEST_DIR_NAME: &str = "test-delete_dir_works"; + const TEST_FILE_NAME: &str = "test-delete_dir_works--file"; + const TEST_NON_EXIST_DIR: &str = "test-delete_dir_works--no-exist"; + + let test_dir = settings.files.get_path(&creds.username, TEST_DIR_NAME); + if !test_dir.exists() { + tokio::fs::create_dir_all(&test_dir).await.unwrap(); + } + + let test_file = settings.files.get_path(&creds.username, TEST_FILE_NAME); + if !test_file.exists() { + let mut f = tokio::fs::File::create(test_file).await.unwrap(); + f.write_all(b"foo").await.unwrap(); + } + + let ctx = AppCtx::new(crate::ctx::Ctx::new(&settings).await); + let app = test::init_service( + App::new() + .app_data(ctx.clone()) + .configure(crate::routes::services), + ) + .await; + + let mut payload = Dir { + path: TEST_FILE_NAME.into(), + }; + + let delete_dir_resp = test::call_service( + &app, + test::TestRequest::delete() + .append_header((header::AUTHORIZATION, auth.clone())) + .set_json(&payload) + .uri(API_V1_ROUTES.files.delete_dir) + .to_request(), + ) + .await; + assert_eq!(delete_dir_resp.status(), StatusCode::BAD_REQUEST); + + payload.path = TEST_NON_EXIST_DIR.into(); + let delete_dir_resp = test::call_service( + &app, + test::TestRequest::delete() + .append_header((header::AUTHORIZATION, auth.clone())) + .set_json(&payload) + .uri(API_V1_ROUTES.files.delete_dir) + .to_request(), + ) + .await; + assert_eq!(delete_dir_resp.status(), StatusCode::NOT_FOUND); + + payload.path = TEST_DIR_NAME.into(); + let delete_dir_resp = test::call_service( + &app, + test::TestRequest::delete() + .append_header((header::AUTHORIZATION, auth)) + .set_json(&payload) + .uri(API_V1_ROUTES.files.delete_dir) + .to_request(), + ) + .await; + assert_eq!(delete_dir_resp.status(), StatusCode::OK); + + assert!(!test_dir.exists()); + } +} diff --git a/src/api/v1/meta.rs b/src/api/v1/meta.rs new file mode 100644 index 0000000..96e6cd6 --- /dev/null +++ b/src/api/v1/meta.rs @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use actix_web::{web, HttpResponse, Responder}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::AppCtx; +use crate::{GIT_COMMIT_HASH, VERSION}; + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +pub struct BuildDetails { + pub version: &'static str, + pub git_commit_hash: &'static str, + pub source_code: String, +} + +pub mod routes { + pub struct Meta { + pub build_details: &'static str, + pub health: &'static str, + } + + impl Meta { + pub const fn new() -> Self { + Self { + build_details: "/api/v1/meta/build", + health: "/api/v1/meta/health", + } + } + } +} + +/// emits build details of the bninary +#[actix_web_codegen_const_routes::get(path = "crate::API_V1_ROUTES.meta.build_details")] +async fn build_details(ctx: AppCtx) -> impl Responder { + let build = BuildDetails { + version: VERSION, + git_commit_hash: GIT_COMMIT_HASH, + source_code: ctx.source_code.clone(), + }; + HttpResponse::Ok().json(build) +} + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +/// Health check return datatype +pub struct Health { + db: bool, +} + +/// checks all components of the system +#[actix_web_codegen_const_routes::get(path = "crate::API_V1_ROUTES.meta.health")] +async fn health() -> impl Responder { + // let mut resp_builder = HealthBuilder::default(); + + // resp_builder.db(data.db.ping().await); + + HttpResponse::Ok() //.json(resp_builder.build().unwrap()) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(build_details); + cfg.service(health); +} + +#[cfg(test)] +pub mod tests { + use actix_web::{http::StatusCode, test, App}; + + use crate::api::v1::services; + use crate::*; + + #[actix_rt::test] + async fn build_details_works() { + let settings = Settings::new().unwrap(); + let ctx = AppCtx::new(crate::ctx::Ctx::new(&settings).await); + let app = test::init_service(App::new().app_data(ctx.clone()).configure(services)).await; + + let resp = test::call_service( + &app, + test::TestRequest::get() + .uri(API_V1_ROUTES.meta.build_details) + .to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + } + + // #[actix_rt::test] + // async fn health_works_pg() { + // let data = crate::tests::pg::get_data().await; + // health_works(data).await; + // } + // + // #[actix_rt::test] + // async fn health_works_maria() { + // let data = crate::tests::maria::get_data().await; + // health_works(data).await; + // } + // + // pub async fn health_works(data: ArcCtx) { + // println!("{}", API_V1_ROUTES.meta.health); + // let data = &data; + // let app = get_app!(data).await; + // + // let resp = test::call_service( + // &app, + // test::TestRequest::get() + // .uri(API_V1_ROUTES.meta.health) + // .to_request(), + // ) + // .await; + // assert_eq!(resp.status(), StatusCode::OK); + // + // let health_resp: Health = test::read_body_json(resp).await; + // assert!(health_resp.db); + // assert_eq!(health_resp.redis, Some(true)); + // } +} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs new file mode 100644 index 0000000..a24e1df --- /dev/null +++ b/src/api/v1/mod.rs @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::dev::ServiceRequest; +use actix_web::web; +use actix_web::Error; +use actix_web::HttpMessage; +use actix_web_httpauth::extractors::basic::BasicAuth; + +pub mod files; +pub mod meta; + +use crate::errors::*; +use crate::AppCtx; +use crate::SETTINGS; + +pub const API_V1_ROUTES: routes::Routes = routes::Routes::new(); + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SignedInUser(String); + +pub async fn httpauth( + req: ServiceRequest, + credentials: BasicAuth, +) -> Result { + let _ctx: &AppCtx = req.app_data().unwrap(); + let username = credentials.user_id(); + let password = credentials.password().unwrap(); + if SETTINGS.files.authenticate(username, password) { + { + let mut ext = req.extensions_mut(); + ext.insert(SignedInUser(username.to_string())); + } + Ok(req) + } else { + let e = Error::from(ServiceError::Unauthorized); + Err((e, req)) + } +} + +pub fn services(cfg: &mut web::ServiceConfig) { + files::services(cfg); + meta::services(cfg); +} + +pub mod routes { + use crate::api::v1::files::routes::Files; + use crate::api::v1::meta::routes::Meta; + + pub struct Routes { + pub files: Files, + pub meta: Meta, + } + + impl Routes { + pub const fn new() -> Self { + Self { + files: Files::new(), + meta: Meta::new(), + } + } + } +} diff --git a/src/ctx.rs b/src/ctx.rs new file mode 100644 index 0000000..c2ee996 --- /dev/null +++ b/src/ctx.rs @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +//! App data: database connections, etc. +use std::sync::Arc; +use std::thread; + +use argon2_creds::{Config, ConfigBuilder, PasswordPolicy}; + +//use crate::errors::ServiceResult; +use crate::settings::Settings; +/// App data +pub struct Ctx { + // /// database ops defined by db crates + // pub db: BoxDB, + /// credential management configuration + pub creds: Config, + /// app settings + pub settings: Settings, + pub source_code: String, +} + +impl Ctx { + pub fn get_creds() -> Config { + ConfigBuilder::default() + .username_case_mapped(true) + .profanity(true) + .blacklist(true) + .password_policy(PasswordPolicy::default()) + .build() + .unwrap() + } + + #[cfg(not(tarpaulin_include))] + /// create new instance of app data + pub async fn new(s: &Settings) -> ArcCtx { + let creds = Self::get_creds(); + let c = creds.clone(); + + #[allow(unused_variables)] + let init = thread::spawn(move || { + log::info!("Initializing credential manager"); + c.init(); + log::info!("Initialized credential manager"); + }); + + //let db = match s.database.database_type { + // crate::settings::DBType::Maria => db::maria::get_data(Some(s.clone())).await, + // crate::settings::DBType::Postgres => db::pg::get_data(Some(s.clone())).await, + //}; + + #[cfg(not(debug_assertions))] + init.join().unwrap(); + + let source_code = { + let mut url = s.source_code.clone(); + if !url.ends_with('/') { + url.push('/'); + } + let mut base = url::Url::parse(&url).unwrap(); + base = base.join("tree/").unwrap(); + base = base.join(crate::GIT_COMMIT_HASH).unwrap(); + base.into() + }; + + let data = Ctx { + creds, + // db, + settings: s.clone(), + source_code, + }; + + Arc::new(data) + } +} + +pub type ArcCtx = Arc; diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..d3f0954 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use std::convert::From; + +use argon2_creds::errors::CredsError; +//use db_core::errors::DBError; +use actix_web::http; +use actix_web::http::StatusCode; +use actix_web::HttpResponse; +use actix_web::HttpResponseBuilder; +use actix_web::ResponseError; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; +use tokio::sync::oneshot::error::RecvError; +use url::ParseError; + +//#[derive(Debug, Display, Error)] +//pub struct DBErrorWrapper(DBError); +// +//impl std::cmp::PartialEq for DBErrorWrapper { +// fn eq(&self, other: &Self) -> bool { +// format!("{}", self.0) == format!("{}", other.0) +// } +//} +// +#[derive(Debug, Display, PartialEq, Eq, Error)] +#[cfg(not(tarpaulin_include))] +#[allow(dead_code)] +pub enum ServiceError { + #[display(fmt = "unauthorized")] + Unauthorized, + + #[display(fmt = "internal server error")] + InternalServerError, + + #[display( + fmt = "This server is is closed for registration. Contact admin if this is unexpecter" + )] + ClosedForRegistration, + + #[display(fmt = "The value you entered for email is not an email")] //405j + NotAnEmail, + #[display(fmt = "The value you entered for URL is not a URL")] //405j + NotAUrl, + + #[display(fmt = "Wrong password")] + WrongPassword, + + /// when the value passed contains profainity + #[display(fmt = "Can't allow profanity in usernames")] + ProfainityError, + /// when the value passed contains blacklisted words + /// see [blacklist](https://github.com/shuttlecraft/The-Big-Username-Blacklist) + #[display(fmt = "Username contains blacklisted words")] + BlacklistError, + /// when the value passed contains characters not present + /// in [UsernameCaseMapped](https://tools.ietf.org/html/rfc8265#page-7) + /// profile + #[display(fmt = "username_case_mapped violation")] + UsernameCaseMappedError, + + #[display(fmt = "Passsword too short")] + PasswordTooShort, + #[display(fmt = "Username too long")] + PasswordTooLong, + #[display(fmt = "Passwords don't match")] + PasswordsDontMatch, + + /// when the a username is already taken + #[display(fmt = "Username not available")] + UsernameTaken, + + /// email is already taken + #[display(fmt = "Email not available")] + EmailTaken, + // #[display(fmt = "{}", _0)] + // DBError(DBErrorWrapper), +} + +#[derive(Serialize, Deserialize)] +#[cfg(not(tarpaulin_include))] +pub struct ErrorToResponse { + pub error: String, +} + +#[cfg(not(tarpaulin_include))] +impl ResponseError for ServiceError { + #[cfg(not(tarpaulin_include))] + fn error_response(&self) -> HttpResponse { + HttpResponseBuilder::new(self.status_code()) + .append_header(( + http::header::CONTENT_TYPE, + "application/json; charset=UTF-8", + )) + .body( + serde_json::to_string(&ErrorToResponse { + error: self.to_string(), + }) + .unwrap(), + ) + } + + #[cfg(not(tarpaulin_include))] + fn status_code(&self) -> StatusCode { + match self { + ServiceError::ClosedForRegistration => StatusCode::FORBIDDEN, + ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + ServiceError::NotAUrl => StatusCode::BAD_REQUEST, + ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, + ServiceError::WrongPassword => StatusCode::UNAUTHORIZED, + ServiceError::Unauthorized => StatusCode::UNAUTHORIZED, + + ServiceError::ProfainityError => StatusCode::BAD_REQUEST, + ServiceError::BlacklistError => StatusCode::BAD_REQUEST, + ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST, + + ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST, + ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST, + ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST, + + ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, + ServiceError::EmailTaken => StatusCode::BAD_REQUEST, + // ServiceError::DBError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: CredsError) -> ServiceError { + match e { + CredsError::UsernameCaseMappedError => ServiceError::UsernameCaseMappedError, + CredsError::ProfainityError => ServiceError::ProfainityError, + CredsError::BlacklistError => ServiceError::BlacklistError, + CredsError::NotAnEmail => ServiceError::NotAnEmail, + CredsError::Argon2Error(_) => ServiceError::InternalServerError, + CredsError::PasswordTooLong => ServiceError::PasswordTooLong, + CredsError::PasswordTooShort => ServiceError::PasswordTooShort, + } + } +} + +//impl From for ServiceError { +// #[cfg(not(tarpaulin_include))] +// fn from(e: DBError) -> ServiceError { +// println!("from conversin: {}", e); +// match e { +// DBError::UsernameTaken => ServiceError::UsernameTaken, +// DBError::SecretTaken => ServiceError::InternalServerError, +// DBError::EmailTaken => ServiceError::EmailTaken, +// DBError::AccountNotFound => ServiceError::AccountNotFound, +// _ => ServiceError::DBError(DBErrorWrapper(e)), +// } +// } +//} + +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(_: ParseError) -> ServiceError { + ServiceError::NotAUrl + } +} + +#[cfg(not(tarpaulin_include))] +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: RecvError) -> Self { + log::error!("{:?}", e); + ServiceError::InternalServerError + } +} + +#[cfg(not(tarpaulin_include))] +pub type ServiceResult = std::result::Result; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..87c0910 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::env; + +use actix_files::Files; +use actix_web::http::StatusCode; +use actix_web::web::JsonConfig; +use actix_web::{error::InternalError, middleware, App, HttpServer}; +use log::info; + +use lazy_static::lazy_static; + +mod api; +mod ctx; +//mod db; +//mod docs; +#[cfg(not(tarpaulin_include))] +mod errors; +//#[macro_use] +//mod pages; +//#[macro_use] +mod routes; +mod settings; +//mod static_assets; +//#[cfg(test)] +//#[macro_use] +//mod tests; +// +pub use crate::ctx::Ctx; +//pub use crate::static_assets::static_files::assets::*; +pub use api::v1::API_V1_ROUTES; +//pub use docs::DOCS; +//pub use pages::routes::ROUTES as PAGES; +pub use settings::Settings; +//use static_assets::FileMap; + +lazy_static! { + pub static ref SETTINGS: Settings= Settings::new().unwrap(); +// pub static ref S: String = env::var("S").unwrap(); +// pub static ref FILES: FileMap = FileMap::new(); +// pub static ref JS: &'static str = +// FILES.get("./static/cache/bundle/bundle.js").unwrap(); +// pub static ref CSS: &'static str = +// FILES.get("./static/cache/bundle/css/main.css").unwrap(); +// pub static ref MOBILE_CSS: &'static str = +// FILES.get("./static/cache/bundle/css/mobile.css").unwrap(); +} + +pub const COMPILED_DATE: &str = env!("COMPILED_DATE"); +pub const GIT_COMMIT_HASH: &str = env!("GIT_HASH"); +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +pub const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); +pub const PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); + +pub const CACHE_AGE: u32 = 604800; + +use ctx::ArcCtx; +pub type AppCtx = actix_web::web::Data; + +#[actix_web::main] +#[cfg(not(tarpaulin_include))] +async fn main() -> std::io::Result<()> { + env::set_var("RUST_LOG", "info"); + + pretty_env_logger::init(); + + info!( + "{}: {}.\nFor more information, see: {}\nBuild info:\nVersion: {} commit: {}", + PKG_NAME, PKG_DESCRIPTION, PKG_HOMEPAGE, VERSION, GIT_COMMIT_HASH + ); + + let settings = Settings::new().unwrap(); + let ctx = Ctx::new(&settings).await; + let ctx = actix_web::web::Data::new(ctx); + + let ip = settings.server.get_ip(); + println!("Starting server on: http://{ip}"); + + HttpServer::new(move || { + App::new() + .wrap(middleware::Logger::default()) + .wrap( + middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")), + ) + .wrap(middleware::Compress::default()) + .app_data(ctx.clone()) + .wrap(middleware::NormalizePath::new( + middleware::TrailingSlash::Trim, + )) + .app_data(get_json_err()) + .configure(routes::services) + .service(Files::new("/", "./tmp").show_files_listing()) + }) + .bind(ip)? + .run() + .await +} + +#[cfg(not(tarpaulin_include))] +pub fn get_json_err() -> JsonConfig { + JsonConfig::default().error_handler(|err, _| { + //debug!("JSON deserialization error: {:?}", &err); + InternalError::new(err, StatusCode::BAD_REQUEST).into() + }) +} diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..7920a1d --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::web; + +pub fn services(cfg: &mut web::ServiceConfig) { + crate::api::v1::services(cfg); +}