implement /auth/account and /auth/register
This commit is contained in:
parent
a7abc782e4
commit
d3ec3ed422
10 changed files with 298 additions and 26 deletions
|
@ -4,10 +4,17 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7" }
|
argon2 = "0.5.3"
|
||||||
chrono = "0.4"
|
axum = { version = "0.7", features = ["macros", "tracing"] }
|
||||||
diesel = { version = "2.2", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono"] }
|
tower-http = { version = "0.6", features = ["trace"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
diesel = { version = "2.2", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "r2d2"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
r2d2 = "0.8"
|
||||||
|
jsonwebtoken = "9"
|
||||||
|
uuid = { version = "1.10", features = ["v4", "fast-rng"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
|
@ -14,15 +14,19 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use diesel::{Connection, SqliteConnection};
|
use diesel::prelude::*;
|
||||||
|
use diesel::r2d2::ConnectionManager;
|
||||||
|
use diesel::r2d2::Pool;
|
||||||
|
|
||||||
use crate::env::database_url;
|
use crate::env;
|
||||||
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
|
||||||
pub fn establish_connection() -> SqliteConnection {
|
|
||||||
let url = database_url();
|
pub fn create_connection_pool() -> Result<Pool<ConnectionManager<SqliteConnection>>, r2d2::Error> {
|
||||||
SqliteConnection::establish(url).unwrap_or_else(|_| panic!("failed to connect to {}", url))
|
let url = env::database_url();
|
||||||
|
let manager = ConnectionManager::<SqliteConnection>::new(url);
|
||||||
|
Pool::builder().build(manager)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ pub struct LocationCoordinates {
|
||||||
longitude: f64,
|
longitude: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::date_entries)]
|
#[diesel(table_name = schema::date_entries)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct DateEntry {
|
pub struct DateEntry {
|
||||||
|
@ -41,7 +41,7 @@ pub struct DateEntry {
|
||||||
referenced_date: NaiveDateTime,
|
referenced_date: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::entries)]
|
#[diesel(table_name = schema::entries)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
|
@ -58,7 +58,7 @@ pub struct Entry {
|
||||||
date_entry: Option<String>,
|
date_entry: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::heirs)]
|
#[diesel(table_name = schema::heirs)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct Heir {
|
pub struct Heir {
|
||||||
|
@ -69,7 +69,7 @@ pub struct Heir {
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::limits)]
|
#[diesel(table_name = schema::limits)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct Limit {
|
pub struct Limit {
|
||||||
|
@ -78,7 +78,7 @@ pub struct Limit {
|
||||||
max_asset_count: i32,
|
max_asset_count: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::location_entries)]
|
#[diesel(table_name = schema::location_entries)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct LocationEntry {
|
pub struct LocationEntry {
|
||||||
|
@ -94,7 +94,7 @@ impl LocationEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::music_entries)]
|
#[diesel(table_name = schema::music_entries)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct MusicEntry {
|
pub struct MusicEntry {
|
||||||
|
@ -105,7 +105,7 @@ pub struct MusicEntry {
|
||||||
universal_ids: List<UniversalId>,
|
universal_ids: List<UniversalId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::session_keys)]
|
#[diesel(table_name = schema::session_keys)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct SessionKey {
|
pub struct SessionKey {
|
||||||
|
@ -113,7 +113,7 @@ pub struct SessionKey {
|
||||||
user_id: String,
|
user_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable)]
|
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = schema::users)]
|
#[diesel(table_name = schema::users)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use std::env;
|
use std::{env, str::FromStr};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
|
||||||
|
|
||||||
const REQUIRED_ENV_VARIABLES: [&str; 4] = [
|
const REQUIRED_ENV_VARIABLES: [&str; 4] = [
|
||||||
"IDENTITY_API_JWT_SECRET",
|
"IDENTITY_API_JWT_SECRET",
|
||||||
"IDENTITY_API_ASSET_API_ENDPOINT",
|
"IDENTITY_API_ASSET_API_ENDPOINT",
|
||||||
|
@ -81,18 +83,21 @@ pub fn listen_port() -> &'static u16 {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn jwt_secret() -> &'static str {
|
pub fn jwt_secret() -> &'static (EncodingKey, DecodingKey) {
|
||||||
static IDENTITY_API_JWT_SECRET: OnceLock<String> = OnceLock::new();
|
static IDENTITY_API_JWT_SECRET: OnceLock<(EncodingKey, DecodingKey)> = OnceLock::new();
|
||||||
IDENTITY_API_JWT_SECRET.get_or_init(|| {
|
IDENTITY_API_JWT_SECRET.get_or_init(|| {
|
||||||
env::var("IDENTITY_API_JWT_SECRET")
|
let secret = env::var("IDENTITY_API_JWT_SECRET")
|
||||||
.expect("environment variables were not loaded correctly")
|
.expect("environment variables were not loaded correctly");
|
||||||
|
|
||||||
|
(EncodingKey::from_secret(secret.as_bytes()), DecodingKey::from_secret(secret.as_bytes()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn jwt_alg() -> &'static str {
|
pub fn jwt_alg() -> &'static Algorithm {
|
||||||
static IDENTITY_API_JWT_ALG: OnceLock<String> = OnceLock::new();
|
static IDENTITY_API_JWT_ALG: OnceLock<Algorithm> = OnceLock::new();
|
||||||
IDENTITY_API_JWT_ALG.get_or_init(|| {
|
IDENTITY_API_JWT_ALG.get_or_init(|| {
|
||||||
env::var("IDENTITY_API_JWT_ALG").expect("environment variables were not loaded correctly")
|
let algo = env::var("IDENTITY_API_JWT_ALG").expect("environment variables were not loaded correctly");
|
||||||
|
Algorithm::from_str(&algo).expect("invalid JWT algorithm")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
44
identity-api-rs/src/http/extractors/auth.rs
Normal file
44
identity-api-rs/src/http/extractors/auth.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use axum::{async_trait, extract::FromRequestParts, http::{header::AUTHORIZATION, request::Parts, StatusCode}};
|
||||||
|
use jsonwebtoken::Validation;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use tracing::{warn, info};
|
||||||
|
|
||||||
|
use crate::env;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct JwtUser {
|
||||||
|
pub uid: String,
|
||||||
|
pub email: String,
|
||||||
|
pub name: String,
|
||||||
|
pub exp: u64,
|
||||||
|
}
|
||||||
|
pub struct ExtractUser(pub JwtUser);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for ExtractUser
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = (StatusCode, &'static str);
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
if let Some(authorization) = parts.headers.get(AUTHORIZATION) {
|
||||||
|
if let Ok(authorization) = authorization.to_str() {
|
||||||
|
let token = authorization.replacen("Bearer ", "", 1);
|
||||||
|
match jsonwebtoken::decode::<JwtUser>(&token, &env::jwt_secret().1, &Validation::new(*env::jwt_alg())) {
|
||||||
|
Ok(claims) => Ok(Self(claims.claims)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("token couldn't be decoded: {:?}", err);
|
||||||
|
Err((StatusCode::UNAUTHORIZED, "Invalid token"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("invalid authorization header: {:?}", authorization);
|
||||||
|
Err((StatusCode::BAD_REQUEST, "Invalid `AUTHORIZATION` header"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("missin authorization header");
|
||||||
|
Err((StatusCode::BAD_REQUEST, "Missing `AUTHORIZATION` header"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
identity-api-rs/src/http/extractors/mod.rs
Normal file
1
identity-api-rs/src/http/extractors/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod auth;
|
2
identity-api-rs/src/http/mod.rs
Normal file
2
identity-api-rs/src/http/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod extractors;
|
||||||
|
pub mod routes;
|
139
identity-api-rs/src/http/routes/auth.rs
Normal file
139
identity-api-rs/src/http/routes/auth.rs
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use argon2::{Argon2, PasswordHasher, password_hash::{rand_core::OsRng, SaltString}};
|
||||||
|
use axum::{extract::State, http::StatusCode, routing::{get, post}, Json, Router};
|
||||||
|
use chrono::Utc;
|
||||||
|
use diesel::{/*query_dsl::methods::{FindDsl, SelectDsl, FilterDsl},*/ SelectableHelper, RunQueryDsl, ExpressionMethods, QueryDsl, OptionalExtension};
|
||||||
|
use jsonwebtoken::Header;
|
||||||
|
use tracing::{error, info};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::{database::models::User, database::list::List, env, http::extractors::auth::{ExtractUser, JwtUser}, AppState};
|
||||||
|
|
||||||
|
pub fn auth_router() -> Router<AppState> {
|
||||||
|
let router = Router::new()
|
||||||
|
.route("/auth/account", get(account))
|
||||||
|
.route("/auth/register", post(register));
|
||||||
|
|
||||||
|
router
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn account(ExtractUser(jwt_user): ExtractUser, State(state): State<AppState>) -> Result<Json<User>, StatusCode> {
|
||||||
|
use crate::database::schema::users::dsl::users;
|
||||||
|
|
||||||
|
if let Ok(mut conn) = state.pool.get() {
|
||||||
|
let user = users
|
||||||
|
.find(jwt_user.uid)
|
||||||
|
.select(User::as_select())
|
||||||
|
.first(&mut conn);
|
||||||
|
|
||||||
|
if let Ok(user) = user {
|
||||||
|
Ok(Json(user))
|
||||||
|
} else {
|
||||||
|
error!("JWT user does not exist in database");
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("failed to obtain pooled connection");
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct RegisterRequest {
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RegisterResponse {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register(State(state): State<AppState>, Json(req): Json<RegisterRequest>) -> Result<Json<RegisterResponse>, StatusCode> {
|
||||||
|
use crate::database::schema::users::dsl as users;
|
||||||
|
use crate::database::schema::limits::dsl as limits;
|
||||||
|
|
||||||
|
if let Ok(mut conn) = state.pool.get() {
|
||||||
|
let user = users::users
|
||||||
|
.filter(users::email.eq(&req.email))
|
||||||
|
.limit(1)
|
||||||
|
.select(User::as_select())
|
||||||
|
.first(&mut conn)
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
if user.is_err() {
|
||||||
|
error!("failed to retrieve potential existing user from database: {}, error: {:?}", &req.email, user.err());
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.is_ok_and(|v| v.is_some()) {
|
||||||
|
info!("tried to register existing user: {}", &req.email);
|
||||||
|
return Err(StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
let limit_id = Uuid::new_v4().to_string();
|
||||||
|
let result = diesel::insert_into(limits::limits)
|
||||||
|
.values((
|
||||||
|
limits::id.eq(&limit_id),
|
||||||
|
limits::current_asset_count.eq(0),
|
||||||
|
limits::max_asset_count.eq(10),
|
||||||
|
))
|
||||||
|
.execute(&mut conn);
|
||||||
|
|
||||||
|
if result.is_err() {
|
||||||
|
error!("failed to insert into limits: {}, error: {:?}", &req.email, result.err());
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let password_hash = argon2.hash_password(req.password.as_bytes(), &salt);
|
||||||
|
|
||||||
|
if let Ok(password_hash) = password_hash {
|
||||||
|
let user_id = Uuid::new_v4().to_string();
|
||||||
|
let result = diesel::insert_into(users::users)
|
||||||
|
.values((
|
||||||
|
users::id.eq(&user_id),
|
||||||
|
users::created_at.eq(Utc::now().naive_utc()),
|
||||||
|
users::last_connected_at.eq(Utc::now().naive_utc()),
|
||||||
|
users::email.eq(&req.email),
|
||||||
|
users::password.eq(password_hash.to_string()),
|
||||||
|
users::name.eq(&req.name),
|
||||||
|
users::limits.eq(&limit_id),
|
||||||
|
// TODO: Implement diesel::Expression for List
|
||||||
|
users::assets.eq("[]"),
|
||||||
|
))
|
||||||
|
.execute(&mut conn);
|
||||||
|
|
||||||
|
if result.is_err() {
|
||||||
|
error!("failed to insert into users: {}, error: {:?}", req.email, result.err());
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
match jsonwebtoken::encode(&Header::new(*env::jwt_alg()), &JwtUser {
|
||||||
|
uid: user_id,
|
||||||
|
email: req.email,
|
||||||
|
name: req.name,
|
||||||
|
exp: SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.expect("time went backwards")
|
||||||
|
.as_secs() + 180 * 24 * 3600,
|
||||||
|
}, &env::jwt_secret().0) {
|
||||||
|
Ok(token) => Ok(Json(RegisterResponse { token })),
|
||||||
|
Err(err) => {
|
||||||
|
error!("token couldn't be encoded: {:?}", err);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("failed to hash password: {:?}", password_hash.err());
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
error!("failed to obtain pooled connection");
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
1
identity-api-rs/src/http/routes/mod.rs
Normal file
1
identity-api-rs/src/http/routes/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod auth;
|
|
@ -14,11 +14,25 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{extract::{MatchedPath, Request}, response::Response, routing::get, Router};
|
||||||
|
use database::create_connection_pool;
|
||||||
|
use diesel::{r2d2::ConnectionManager, SqliteConnection};
|
||||||
use env::LoadEnvError;
|
use env::LoadEnvError;
|
||||||
|
use http::routes::auth::auth_router;
|
||||||
|
use r2d2::Pool;
|
||||||
|
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
|
||||||
|
use tracing::{info, info_span, warn, error, Span};
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
use tokio::time::Duration;
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
mod env;
|
mod env;
|
||||||
|
mod http;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
pool: Pool<ConnectionManager<SqliteConnection>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@ -29,7 +43,62 @@ async fn main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new().route("/", get(landing));
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||||
|
// axum logs rejections from built-in extractors with the `axum::rejection`
|
||||||
|
// target, at `TRACE` level. `axum::rejection=trace` enables showing those events
|
||||||
|
format!(
|
||||||
|
"{}=debug,tower_http=debug,axum::rejection=trace",
|
||||||
|
env!("CARGO_CRATE_NAME")
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
pool: create_connection_pool().expect("failed to create database connection pool"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(landing))
|
||||||
|
.merge(auth_router())
|
||||||
|
.with_state(state)
|
||||||
|
.layer(
|
||||||
|
TraceLayer::new_for_http()
|
||||||
|
.make_span_with(|request: &Request<_>| {
|
||||||
|
let matched_path = request
|
||||||
|
.extensions()
|
||||||
|
.get::<MatchedPath>()
|
||||||
|
.map(MatchedPath::as_str);
|
||||||
|
|
||||||
|
info_span!(
|
||||||
|
"http_request",
|
||||||
|
method = ?request.method(),
|
||||||
|
matched_path,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_response(|response: &Response, _latency: Duration, _span: &Span| {
|
||||||
|
if response.status().is_client_error() {
|
||||||
|
warn!(
|
||||||
|
"client error: {}",
|
||||||
|
response.status().to_string()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
info!("finished processing request");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_failure(
|
||||||
|
|error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
|
||||||
|
error!(
|
||||||
|
"internal server error: {}",
|
||||||
|
error.to_string(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||||
.await
|
.await
|
||||||
|
|
Loading…
Reference in a new issue