Skip to content

Commit

Permalink
rest-http: add axum-based server
Browse files Browse the repository at this point in the history
  • Loading branch information
epilys committed May 12, 2023
1 parent 243f4af commit 19860d2
Show file tree
Hide file tree
Showing 13 changed files with 666 additions and 23 deletions.
288 changes: 286 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions rest-http/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
config/local.json
26 changes: 25 additions & 1 deletion rest-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,29 @@ name = "mpot-http"
path = "src/main.rs"

[dependencies]
async-trait = "0.1"
axum = { version = "0.6", features = ["headers"] }
#jsonwebtoken = "8.3"
bcrypt = "0.14"
config = "0.13"
http = "0.2"
lazy_static = "1.4"
log = "0.4"
mailpot = { version = "^0.1", path = "../core" }
tokio = { version = "^1", features = ["full"] }
mailpot-web = { version = "^0.1", path = "../web" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
stderrlog = "^0.5"
thiserror = "1"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.4", features = [
"trace",
"compression-br",
"propagate-header",
"sensitive-headers",
"cors",
] }

[dev-dependencies]
assert-json-diff = "2"
reqwest = { version = "0.11", features = ["json"] }
1 change: 0 additions & 1 deletion rest-http/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# mailpot REST http server

Not implemented.
12 changes: 12 additions & 0 deletions rest-http/config/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"environment": "development",
"server": {
"port": 8080
},
"auth": {
"secret": "secret"
},
"logger": {
"level": "debug"
}
}
6 changes: 6 additions & 0 deletions rest-http/config/production.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"environment": "production",
"logger": {
"level": "info"
}
}
9 changes: 9 additions & 0 deletions rest-http/config/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"environment": "test",
"server": {
"port": 8088
},
"logger": {
"level": "error"
}
}
98 changes: 98 additions & 0 deletions rest-http/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use bcrypt::BcryptError;
use serde_json::json;
use tokio::task::JoinError;

#[derive(thiserror::Error, Debug)]
#[error("...")]
pub enum Error {
#[error("Error parsing ObjectID {0}")]
ParseObjectID(String),

#[error("{0}")]
Authenticate(#[from] AuthenticateError),

#[error("{0}")]
BadRequest(#[from] BadRequest),

#[error("{0}")]
NotFound(#[from] NotFound),

#[error("{0}")]
RunSyncTask(#[from] JoinError),

#[error("{0}")]
HashPassword(#[from] BcryptError),

#[error("{0}")]
System(#[from] mailpot::Error),
}

impl Error {
fn get_codes(&self) -> (StatusCode, u16) {
match *self {
// 4XX Errors
Error::ParseObjectID(_) => (StatusCode::BAD_REQUEST, 40001),
Error::BadRequest(_) => (StatusCode::BAD_REQUEST, 40002),
Error::NotFound(_) => (StatusCode::NOT_FOUND, 40003),
Error::Authenticate(AuthenticateError::WrongCredentials) => {
(StatusCode::UNAUTHORIZED, 40004)
}
Error::Authenticate(AuthenticateError::InvalidToken) => {
(StatusCode::UNAUTHORIZED, 40005)
}
Error::Authenticate(AuthenticateError::Locked) => (StatusCode::LOCKED, 40006),

// 5XX Errors
Error::Authenticate(AuthenticateError::TokenCreation) => {
(StatusCode::INTERNAL_SERVER_ERROR, 5001)
}
Error::RunSyncTask(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5005),
Error::HashPassword(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5006),
Error::System(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5007),
}
}

pub fn bad_request() -> Self {
Error::BadRequest(BadRequest {})
}

pub fn not_found() -> Self {
Error::NotFound(NotFound {})
}
}

impl IntoResponse for Error {
fn into_response(self) -> Response {
let (status_code, code) = self.get_codes();
let message = self.to_string();
let body = Json(json!({ "code": code, "message": message }));

(status_code, body).into_response()
}
}

#[derive(thiserror::Error, Debug)]
#[error("...")]
pub enum AuthenticateError {
#[error("Wrong authentication credentials")]
WrongCredentials,
#[error("Failed to create authentication token")]
TokenCreation,
#[error("Invalid authentication credentials")]
InvalidToken,
#[error("User is locked")]
Locked,
}

#[derive(thiserror::Error, Debug)]
#[error("Bad Request")]
pub struct BadRequest {}

#[derive(thiserror::Error, Debug)]
#[error("Not found")]
pub struct NotFound {}
10 changes: 10 additions & 0 deletions rest-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,13 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

pub use std::{net::SocketAddr, sync::Arc};

pub use axum::Router;
pub use http::header;
pub use log::{debug, info, trace};
pub use mailpot::{models::*, Configuration, Connection};
pub mod errors;
pub mod routes;
pub mod settings;
68 changes: 49 additions & 19 deletions rest-http/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
/*
* This file is part of mailpot
*
* Copyright 2020 - Manos Pitsidianakis
*
* 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 <https://www.gnu.org/licenses/>.
*/
use mailpot_http::{settings::SETTINGS, *};
use tower_http::{
compression::CompressionLayer, cors::CorsLayer, propagate_header::PropagateHeaderLayer,
sensitive_headers::SetSensitiveHeadersLayer,
};

fn main() {}
use crate::routes;

#[tokio::main]
async fn main() {
let app = create_app().await;

let port = SETTINGS.server.port;
let address = SocketAddr::from(([127, 0, 0, 1], port));

info!("Server listening on {}", &address);
axum::Server::bind(&address)
.serve(app.into_make_service())
.await
.expect("Failed to start server");
}
pub async fn create_app() -> Router {
let config_path = std::env::args()
.nth(1)
.expect("Expected configuration file path as first argument.");
stderrlog::new()
.quiet(false)
.verbosity(15)
.show_module_names(true)
.timestamp(stderrlog::Timestamp::Millisecond)
.init()
.unwrap();
let conf = Arc::new(Configuration::from_file(config_path).unwrap());

Router::new()
.with_state(conf.clone())
.merge(Router::new().nest("/v1", Router::new().merge(routes::list::create_route(conf))))
.layer(SetSensitiveHeadersLayer::new(std::iter::once(
header::AUTHORIZATION,
)))
// Compress responses
.layer(CompressionLayer::new())
// Propagate `X-Request-Id`s from requests to responses
.layer(PropagateHeaderLayer::new(header::HeaderName::from_static(
"x-request-id",
)))
// CORS configuration. This should probably be more restrictive in
// production.
.layer(CorsLayer::permissive())
}
107 changes: 107 additions & 0 deletions rest-http/src/routes/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use std::sync::Arc;

pub use axum::extract::{Path, Query, State};
use axum::{
http::StatusCode,
routing::{get, post},
Json, Router,
};
use mailpot_web::{typed_paths::*, ResponseError, RouterExt};
use serde::{Deserialize, Serialize};

use crate::*;

pub fn create_route(conf: Arc<Configuration>) -> Router {
Router::new()
.route("/list/", get(all_lists))
.route("/list/", post(post_list))
.typed_get(get_list)
.with_state(conf)
}

async fn get_list(
ListPath(id): ListPath,
State(state): State<Arc<Configuration>>,
) -> Result<Json<DbVal<MailingList>>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let Some(list) = (match id {
ListPathIdentifier::Pk(id) => db.list(id)?,
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
}) else {
return Err(mailpot_web::ResponseError::new(
"Not found".to_string(),
StatusCode::NOT_FOUND,
));
};
Ok(Json(list))
}

async fn all_lists(
Query(GetRequest {
filter: _,
count,
page,
}): Query<GetRequest>,
State(state): State<Arc<Configuration>>,
) -> Result<Json<GetResponse>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let lists_values = db.lists()?;
let page = page.unwrap_or(0);
let Some(count) = count else {
let mut stmt = db
.connection
.prepare("SELECT count(*) FROM list;")?;
return Ok(Json(GetResponse {
entries: vec![],
total: stmt.query_row([], |row| {
let count: usize = row.get(0)?;
Ok(count)
})?,
start: 0,
}));
};
let offset = page * count;
let res: Vec<_> = lists_values.into_iter().skip(offset).take(count).collect();

Ok(Json(GetResponse {
total: res.len(),
start: offset,
entries: res,
}))
}

async fn post_list(
State(state): State<Arc<Configuration>>,
Json(_body): Json<GetRequest>,
) -> Result<Json<()>, ResponseError> {
let _db = Connection::open_db(Configuration::clone(&state))?;
// let password_hash = list::hash_password(body.password).await?;
// let list = list::new(body.name, body.email, password_hash);
// let list = list::create(list).await?;
// let res = Publiclist::from(list);
//

Ok(Json(()))
}

#[derive(Debug, Serialize, Deserialize)]
enum GetFilter {
Pk(i64),
Address(String),
Id(String),
Name(String),
}

#[derive(Debug, Serialize, Deserialize)]
struct GetRequest {
filter: Option<GetFilter>,
count: Option<usize>,
page: Option<usize>,
}

#[derive(Debug, Serialize, Deserialize)]
struct GetResponse {
entries: Vec<DbVal<MailingList>>,
total: usize,
start: usize,
}
1 change: 1 addition & 0 deletions rest-http/src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod list;
Loading

0 comments on commit 19860d2

Please sign in to comment.