-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Service layer: core business logic. App is a collection of services. Services can optionally have underlying implementors (e.g. Firebase as an auth service). - Repo ports: business logic around persistence - Repo adapters: handle read/write of primitive (json) data in e.g. postgres. - Router ports: business logic for handling incoming requests. - Router adapters (e.g. axum): Parse raw requests into technology agnostic requests. Routing adapter -> Router -> Service -> Repo -> Repo adapter. App is a trait/struct that contains the service layer. Each service has it's own repo (if necessary). Routers are top level functions because there's nowhere to keep an instance. The global-ish app instance is the one given to Axum as the app's state. That's how services are called upon when handling a request.
- Loading branch information
Showing
19 changed files
with
1,662 additions
and
102 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,42 @@ | ||
use crate::auth::AuthService; | ||
use crate::{ | ||
auth::AuthService, | ||
user::{UserRepoAdapter, UserService}, | ||
}; | ||
|
||
pub trait App { | ||
pub trait Service: Sync + Send + 'static {} | ||
impl<T: Sync + Send + 'static> Service for T {} | ||
|
||
pub trait App: Sync + Send + 'static { | ||
type Auth: AuthService; | ||
type UserRepo: UserRepoAdapter; | ||
|
||
fn auth(&self) -> &Self::Auth; | ||
|
||
fn user(&self) -> &UserService<Self::UserRepo>; | ||
} | ||
|
||
pub struct NewApp<Auth> | ||
pub struct NewApp<Auth, UserRepo> | ||
where | ||
Auth: AuthService, | ||
UserRepo: UserRepoAdapter, | ||
{ | ||
pub auth: Auth, | ||
pub user: UserService<UserRepo>, | ||
} | ||
|
||
impl<Auth> App for NewApp<Auth> | ||
impl<Auth, UserRepo> App for NewApp<Auth, UserRepo> | ||
where | ||
Auth: AuthService, | ||
UserRepo: UserRepoAdapter, | ||
{ | ||
type Auth = Auth; | ||
type UserRepo = UserRepo; | ||
|
||
fn auth(&self) -> &Self::Auth { | ||
&self.auth | ||
} | ||
|
||
fn user(&self) -> &UserService<Self::UserRepo> { | ||
&self.user | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
mod models; | ||
mod router; | ||
pub mod router_axum; | ||
mod service; | ||
mod service_jwt; | ||
|
||
pub use service::AuthService; | ||
pub use models::*; | ||
pub use service::*; | ||
pub use service_jwt::AuthServiceJwt; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
use serde::{Deserialize, Serialize}; | ||
use validator::Validate; | ||
|
||
#[derive(Debug, thiserror::Error)] | ||
pub enum AuthError { | ||
#[error("Invalid credential")] | ||
InvalidCredential, | ||
} | ||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||
pub struct Token { | ||
pub user_id: String, | ||
pub token: String, | ||
pub expires: u64, | ||
} | ||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Validate)] | ||
pub struct SignUpData { | ||
#[validate(email)] | ||
pub email: String, | ||
#[validate(length(min = 4, max = 64))] | ||
pub password: String, | ||
#[validate(length(min = 1, max = 64))] | ||
pub name: Option<String>, | ||
#[validate(url)] | ||
pub photo_url: Option<String>, | ||
} | ||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Validate)] | ||
pub struct SignInData { | ||
#[validate(email)] | ||
pub email: String, | ||
#[validate(length(min = 4, max = 64))] | ||
pub password: String, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
use crate::{ | ||
user::{NewUser, UserIdentifier}, | ||
App, AppError, | ||
}; | ||
|
||
use super::{AuthService, SignInData, SignUpData, Token}; | ||
|
||
pub async fn sign_up<A: App>(data: SignUpData, app: &A) -> Result<Token, AppError> { | ||
let new_user = NewUser { | ||
email: data.email, | ||
password_hash: app.auth().hash_password(data.password)?, | ||
name: data.name, | ||
photo_url: data.photo_url, | ||
}; | ||
let user = app.user().create_user(new_user).await?; | ||
let token = app.auth().generate_token(user).await?; | ||
Ok(token) | ||
} | ||
|
||
pub async fn sign_in<A: App>(data: SignInData, app: &A) -> Result<Token, AppError> { | ||
let user = app | ||
.user() | ||
.get_user(UserIdentifier::new(None, Some(data.email))?) | ||
.await?; | ||
app.auth() | ||
.verify_password(data.password, &user.password_hash)?; | ||
app.auth().generate_token(user).await | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
use std::sync::Arc; | ||
|
||
use axum::{extract::State, routing::post, Json, Router}; | ||
|
||
use crate::{app::App, AppError}; | ||
|
||
use super::{router, SignInData, SignUpData, Token}; | ||
|
||
pub fn create_router<A: App>() -> Router<Arc<A>> { | ||
Router::new() | ||
.route("/sign-up", post(sign_up)) | ||
.route("/sign-in", post(sign_in)) | ||
} | ||
|
||
async fn sign_up<A: App>( | ||
State(app): State<Arc<A>>, | ||
Json(data): Json<SignUpData>, | ||
) -> Result<Json<Token>, AppError> { | ||
let token = router::sign_up(data, app.as_ref()).await?; | ||
Ok(Json(token)) | ||
} | ||
|
||
async fn sign_in<A: App>( | ||
State(app): State<Arc<A>>, | ||
Json(data): Json<SignInData>, | ||
) -> Result<Json<Token>, AppError> { | ||
let token = router::sign_in(data, app.as_ref()).await?; | ||
Ok(Json(token)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,28 @@ | ||
use axum::async_trait; | ||
use argon2::{ | ||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, | ||
Argon2, | ||
}; | ||
use async_trait::async_trait; | ||
|
||
use crate::{app::Service, user::User, AppError}; | ||
|
||
use super::{AuthError, Token}; | ||
|
||
#[async_trait] | ||
pub trait AuthService { | ||
async fn authenticate(&self, token: &str) -> Result<(), ()>; | ||
pub trait AuthService: Service { | ||
fn hash_password(&self, password: String) -> Result<String, AppError> { | ||
let salt = SaltString::generate(&mut OsRng); | ||
let hashed = Argon2::default() | ||
.hash_password(password.as_bytes(), &salt) | ||
.map_err(|e| AppError::Internal(e.to_string()))? | ||
.to_string(); | ||
Ok(hashed) | ||
} | ||
fn verify_password(&self, password: String, hash: &str) -> Result<(), AppError> { | ||
let parsed = PasswordHash::new(hash).map_err(|e| AppError::Internal(e.to_string()))?; | ||
Argon2::default() | ||
.verify_password(password.as_bytes(), &parsed) | ||
.map_err(|_| AuthError::InvalidCredential.into()) | ||
} | ||
async fn generate_token(&self, user: User) -> Result<Token, AppError>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,14 @@ | ||
use axum::async_trait; | ||
use async_trait::async_trait; | ||
|
||
use super::AuthService; | ||
use crate::{user::User, AppError}; | ||
|
||
use super::{AuthService, Token}; | ||
|
||
pub struct AuthServiceJwt; | ||
|
||
#[async_trait] | ||
impl AuthService for AuthServiceJwt { | ||
async fn authenticate(&self, token: &str) -> Result<(), ()> { | ||
if token == "valid" { | ||
Ok(()) | ||
} else { | ||
Err(()) | ||
} | ||
async fn generate_token(&self, user: User) -> Result<Token, AppError> { | ||
todo!() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
use axum::{ | ||
body::Body, | ||
http::{Response, StatusCode}, | ||
response::IntoResponse, | ||
Json, | ||
}; | ||
use serde_json::json; | ||
|
||
use crate::auth::AuthError; | ||
|
||
#[derive(Debug, thiserror::Error)] | ||
pub enum AppError { | ||
#[error("Authentication failed: {0}")] | ||
Auth(#[from] AuthError), | ||
|
||
#[error("Database error: {0}")] | ||
Database(#[from] sqlx::Error), | ||
|
||
#[error("Not found: {0}")] | ||
NotFound(String), | ||
|
||
#[error("Validation failed: {0}")] | ||
Validation(String), | ||
|
||
#[error("Internal error: {0}")] | ||
Internal(String), | ||
|
||
#[error("Internal error: {0}")] | ||
MalformedData(#[from] serde_json::Error), | ||
} | ||
|
||
impl IntoResponse for AppError { | ||
fn into_response(self) -> Response<Body> { | ||
let (status, error_message) = match self { | ||
AppError::Auth(_) => (StatusCode::UNAUTHORIZED, self.to_string()), | ||
AppError::Database(_) => ( | ||
StatusCode::INTERNAL_SERVER_ERROR, | ||
"Internal server error".to_string(), | ||
), | ||
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), | ||
AppError::Validation(msg) => (StatusCode::BAD_REQUEST, msg), | ||
AppError::Internal(_) => ( | ||
StatusCode::INTERNAL_SERVER_ERROR, | ||
"Internal server error".to_string(), | ||
), | ||
AppError::MalformedData(_) => ( | ||
StatusCode::INTERNAL_SERVER_ERROR, | ||
"Internal server error: Malformed data.".to_string(), | ||
), | ||
}; | ||
|
||
let body = Json(json!({ | ||
"error": error_message, | ||
"code": status.as_u16() | ||
})); | ||
|
||
(status, body).into_response() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
use std::sync::Arc; | ||
|
||
use axum::{response::Html, routing::get, Router}; | ||
use tracing::debug; | ||
|
||
use crate::{ | ||
app::NewApp, | ||
auth::{self, AuthServiceJwt}, | ||
user::{UserRepoPostgres, UserService}, | ||
}; | ||
|
||
pub async fn initialize() { | ||
setup_tracing(); | ||
|
||
let app = NewApp { | ||
auth: AuthServiceJwt, | ||
user: UserService::new(UserRepoPostgres), | ||
}; | ||
|
||
let router = Router::new() | ||
.route("/", get(hello_world)) | ||
.nest("/auth", auth::router_axum::create_router()) | ||
.with_state(Arc::new(app)); | ||
|
||
start_api_server(router).await; | ||
} | ||
|
||
fn setup_tracing() { | ||
#[cfg(not(feature = "lambda"))] | ||
{ | ||
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; | ||
tracing_subscriber::registry() | ||
.with(fmt::layer()) | ||
.with(EnvFilter::from_default_env()) | ||
.init(); | ||
} | ||
#[cfg(feature = "lambda")] | ||
{ | ||
lambda_http::tracing::init_default_subscriber(); | ||
} | ||
} | ||
|
||
async fn start_api_server(router: Router) { | ||
#[cfg(not(feature = "lambda"))] | ||
{ | ||
debug!("DEV MODE"); | ||
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 3000)); | ||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); | ||
axum::serve(listener, router).await.unwrap(); | ||
} | ||
|
||
#[cfg(feature = "lambda")] | ||
{ | ||
debug!("LAMBDA MODE"); | ||
lambda_http::run(router).await.unwrap(); | ||
} | ||
} | ||
|
||
async fn hello_world() -> Html<&'static str> { | ||
Html("<h1>Hello, World!</h1>") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,8 @@ | ||
pub mod app; | ||
pub mod auth; | ||
mod error; | ||
pub mod init; | ||
pub mod user; | ||
|
||
pub fn add(left: usize, right: usize) -> usize { | ||
left + right | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn it_works() { | ||
let result = add(2, 2); | ||
assert_eq!(result, 4); | ||
} | ||
} | ||
pub use app::App; | ||
pub use error::AppError; |
Oops, something went wrong.