Skip to content

Commit

Permalink
Merge pull request #169 from Hirevo/refactor/axum
Browse files Browse the repository at this point in the history
Move to `axum` and `tokio` stack
  • Loading branch information
Hirevo authored Sep 7, 2023
2 parents 09e497b + ae8941c commit 9a6c637
Show file tree
Hide file tree
Showing 62 changed files with 1,781 additions and 2,524 deletions.
1,325 changes: 316 additions & 1,009 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ members = [
"crates/alexandrie-rendering",
"helpers/syntect-dump",
]

[workspace.dependencies]
tokio = "1.32.0"
serde = "1.0.160"
thiserror = "1.0.40"
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ RUN \
fi && \
if [ "${DATABASE}" = "postgres" ]; then \
apt install -y libpq-dev; \
cargo install diesel_cli --no-default-features --features "postgres"; \
cargo install --locked diesel_cli --no-default-features --features "postgres"; \
fi && \
if [ "${DATABASE}" = "mysql" ]; then \
apt install -y default-libmysqlclient-dev; \
cargo install diesel_cli --no-default-features --features "mysql"; \
cargo install --locked diesel_cli --no-default-features --features "mysql"; \
fi

WORKDIR /alexandrie
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ Things yet to do
How to build
------------

Alexandrie is built using [**Tide**][Tide] and offers multiple options to be used as its database.
Alexandrie is built using [**Axum**][Axum] and offers multiple options to be used as its database.

The current minimum supported Rust version for Alexandrie is `1.68` (on stable), so make sure to check if your local Rust version is adequate by running `rustc -V`.

To build, you can run `cargo build [--release]`.

[Tide]: https://github.com/http-rs/tide
[Axum]: https://github.com/tokio-rs/axum

Before running it, you need to configure your instance in the `alexandrie.toml` file.

Expand Down
2 changes: 1 addition & 1 deletion alexandrie.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ login_required = false

[frontend.sessions]
cookie_name = "alexandrie.sid"
secret = "YOU_REALLY_SHOULD_CHANGE_THIS_BEFORE_DEPLOYING"
secret = "YOU_REALLY_SHOULD_CHANGE_THIS_BEFORE_DEPLOYING_THIS_TO_PRODUCTION"

[frontend.assets]
path = "assets"
Expand Down
2 changes: 1 addition & 1 deletion crates/alexandrie-rendering/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0"

[dependencies]
# file formats
serde = { version = "1.0.160", features = ["derive"] }
serde = { workspace = true, features = ["derive"] }

# README rendering
syntect = "5.0.0"
Expand Down
8 changes: 4 additions & 4 deletions crates/alexandrie-storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ license = "MIT OR Apache-2.0"

[dependencies]
# async runtime
async-std = { version = "1.12.0", features = ["tokio1"], optional = true }
tokio = { workspace = true, optional = true }

# data types
semver = { version = "1.0.17", features = ["serde"] }

# file formats
serde = { version = "1.0.160", features = ["derive"] }
serde = { workspace = true, features = ["derive"] }

# error handling
thiserror = "1.0.40"
thiserror = { workspace = true }

# S3 crate storage
rusoto_core = { version = "0.48.0", optional = true }
rusoto_s3 = { version = "0.48.0", optional = true }

[features]
default = []
s3 = ["dep:async-std", "dep:rusoto_core", "dep:rusoto_s3"]
s3 = ["dep:tokio", "dep:rusoto_core", "dep:rusoto_s3"]
4 changes: 1 addition & 3 deletions crates/alexandrie-storage/src/s3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ use std::convert::TryFrom;
use std::fmt;
use std::io::{self, Read};

use async_std::task;

use rusoto_core::Region;
use rusoto_s3::{GetObjectOutput, GetObjectRequest, PutObjectRequest, S3Client, StreamingBody, S3};
use semver::Version;
Expand Down Expand Up @@ -59,7 +57,7 @@ impl S3Storage {
key,
..Default::default()
};
Ok(task::block_on(self.client.get_object(request))?)
Ok(tokio::task::block_on(self.client.get_object(request))?)
}

// NOTE: S3 requests can succeed but then give us a body of `None`. I'm not sure
Expand Down
25 changes: 15 additions & 10 deletions crates/alexandrie/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ alexandrie-storage = { path = "../alexandrie-storage", version = "0.1.0" }
alexandrie-rendering = { path = "../alexandrie-rendering", version = "0.1.0" }

# core
tide = { version = "0.16.0", default-features = false, features = ["h1-server", "sessions"] }
clap = { version = "4.2.2", features = ["string"] }
tokio = { workspace = true, features = ["rt-multi-thread", "fs", "macros"] }
axum = { version = "0.6.19", features = ["http2", "headers"] }
axum-extra = "0.7.5"
axum-sessions = "0.5.0"

# command-line interface
clap = { version = "4.2.2", features = ["string", "derive"] }

# session handling
async-session = "3.0.0"
Expand All @@ -32,6 +37,7 @@ async-session = "3.0.0"
url = "2.3.1"
semver = { version = "1.0.17", features = ["serde"] }
chrono = { version = "0.4.24", features = ["serde"] }
bytes = "1.4.0"

# file formats
serde = { version = "1.0.160", features = ["derive"] }
Expand All @@ -55,10 +61,13 @@ tantivy = "0.20"
tantivy-analysis-contrib = { version = "0.9", default-features = false, features = ["commons"] }

# async primitives
async-std = { version = "1.12.0", features = ["attributes", "tokio1"] }
futures-util = "0.3.28"
tower = "0.4.13"
tower-http = { version = "0.4.1", features = ["trace", "fs"] }

# error handling
thiserror = "1.0.40"
thiserror = { workspace = true }
anyhow = "1.0.72"

# README rendering
flate2 = "1.0.25"
Expand All @@ -75,12 +84,8 @@ once_cell = { version = "1.17.1", optional = true }
regex = { version = "1.7.3", optional = true }

# logs
log = "0.4.17"
slog = "2.7.0"
slog-stdlog = "4.1.1"
slog-scope = "4.4.0"
slog-term = "2.9.0"
slog-async = "2.7.0"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }

[features]
default = ["frontend", "sqlite"]
Expand Down
52 changes: 19 additions & 33 deletions crates/alexandrie/src/api/account/login.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use std::num::NonZeroU32;
use std::sync::Arc;

use axum::extract::State;
use axum::Json;
use diesel::prelude::*;
use ring::digest as hasher;
use ring::pbkdf2;
use serde::{Deserialize, Serialize};
use tide::{Request, StatusCode};

use crate::config::AppState;
use crate::db::models::NewAuthorToken;
use crate::db::schema::*;
use crate::error::ApiError;
use crate::utils;
use crate::State;
use crate::utils::auth::api::Auth;

/// Request body for this route.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -28,28 +32,20 @@ pub struct ResponseBody {
}

/// Route to log in to an account.
pub async fn post(mut req: Request<State>) -> tide::Result {
let state = req.state().clone();
pub async fn post(
State(state): State<Arc<AppState>>,
maybe_author: Option<Auth>,
Json(body): Json<RequestBody>,
) -> Result<Json<ResponseBody>, ApiError> {
let db = &state.db;

//? Is the author logged in ?
let author = if let Some(headers) = req.header(utils::auth::AUTHORIZATION_HEADER) {
let header = headers.last().to_string();
db.run(move |conn| utils::checks::get_author(conn, header))
.await
} else {
None
};
if author.is_some() {
return Ok(utils::response::error(
StatusCode::Unauthorized,
"please log out first to register as a new author",
if maybe_author.is_some() {
return Err(ApiError::msg(
"please log out first to login as a new author",
));
}

//? Parse request body.
let body: RequestBody = req.body_json().await?;

let transaction = db.transaction(move |conn| {
//? Get the users' salt and expected hash.
let results = salts::table
Expand All @@ -63,10 +59,7 @@ pub async fn post(mut req: Request<State>) -> tide::Result {
let (author_id, encoded_salt, encoded_expected_hash) = match results {
Some((author_id, salt, Some(passwd))) => (author_id, salt, passwd),
_ => {
return Ok(utils::response::error(
StatusCode::Forbidden,
"invalid email/password combination.",
));
return Err(ApiError::msg("invalid email/password combination."));
}
};

Expand All @@ -77,10 +70,7 @@ pub async fn post(mut req: Request<State>) -> tide::Result {
let (decoded_salt, decoded_expected_hash) = match decode_results {
Ok(results) => results,
Err(_) => {
return Ok(utils::response::error(
StatusCode::InternalServerError,
"an author already exists for this email.",
));
return Err(ApiError::msg("an author already exists for this email."));
}
};

Expand Down Expand Up @@ -112,10 +102,7 @@ pub async fn post(mut req: Request<State>) -> tide::Result {
};

if !password_match {
return Ok(utils::response::error(
StatusCode::Forbidden,
"invalid email/password combination.",
));
return Err(ApiError::msg("invalid email/password combination."));
}

//? Generate new registry token.
Expand Down Expand Up @@ -143,9 +130,8 @@ pub async fn post(mut req: Request<State>) -> tide::Result {
.select(author_tokens::token)
.first(conn)?;

let response = ResponseBody { token };
Ok(utils::response::json(&response))
Ok(Json(ResponseBody { token }))
});

transaction.await
transaction.await.map_err(ApiError::from)
}
45 changes: 17 additions & 28 deletions crates/alexandrie/src/api/account/register.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use std::num::NonZeroU32;
use std::sync::Arc;

use axum::extract::{Json, State};
use diesel::dsl as sql;
use diesel::prelude::*;
use ring::digest as hasher;
use ring::pbkdf2;
use ring::rand::{SecureRandom, SystemRandom};
use serde::{Deserialize, Serialize};
use tide::{Request, StatusCode};

use crate::config::AppState;
use crate::db::models::{NewAuthor, NewAuthorToken, NewSalt};
use crate::db::schema::*;
use crate::error::ApiError;
use crate::utils;
use crate::State;
use crate::utils::auth::api::Auth;

/// Request body for this route.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand All @@ -32,39 +35,26 @@ pub struct ResponseBody {
}

/// Route to register a new account.
pub async fn post(mut req: Request<State>) -> tide::Result {
let state = req.state().clone();
let db = &state.db;

pub async fn post(
State(state): State<Arc<AppState>>,
maybe_author: Option<Auth>,
Json(body): Json<RequestBody>,
) -> Result<Json<ResponseBody>, ApiError> {
//? Is the author already logged in ?
let author = if let Some(headers) = req.header(utils::auth::AUTHORIZATION_HEADER) {
let header = headers.last().to_string();
db.run(move |conn| utils::checks::get_author(conn, header))
.await
} else {
None
};
if author.is_some() {
return Ok(utils::response::error(
StatusCode::Unauthorized,
if maybe_author.is_some() {
return Err(ApiError::msg(
"please log out first to register as a new author",
));
}

//? Parse request body.
let body: RequestBody = req.body_json().await?;

let transaction = db.transaction(move |conn| {
let transaction = state.db.transaction(move |conn| {
//? Does the user already exist ?
let already_exists = sql::select(sql::exists(
authors::table.filter(authors::email.eq(body.email.as_str())),
))
.get_result(conn)?;
if already_exists {
return Ok(utils::response::error(
StatusCode::Forbidden,
"an author already exists for this email.",
));
return Err(ApiError::msg("an author already exists for this email."));
}

//? First rounds of PBKDF2 (5_000 rounds, it corresponds to what the frontend does, cf. `wasm-pbkdf2` sub-crate).
Expand Down Expand Up @@ -145,11 +135,10 @@ pub async fn post(mut req: Request<State>) -> tide::Result {
.values(new_author_token)
.execute(conn)?;

let response = ResponseBody {
Ok(Json(ResponseBody {
token: String::from(token),
};
Ok(utils::response::json(&response))
}))
});

transaction.await
transaction.await.map_err(ApiError::from)
}
Loading

0 comments on commit 9a6c637

Please sign in to comment.