feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1
29
crates/presentation/src/errors.rs
Normal file
29
crates/presentation/src/errors.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
|
||||||
|
use domain::errors::DomainError;
|
||||||
|
use api_types::responses::ErrorResponse;
|
||||||
|
|
||||||
|
pub enum ApiError {
|
||||||
|
Domain(DomainError),
|
||||||
|
Unauthorized,
|
||||||
|
BadRequest(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DomainError> for ApiError {
|
||||||
|
fn from(e: DomainError) -> Self { Self::Domain(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, msg) = match self {
|
||||||
|
Self::Domain(DomainError::NotFound) => (StatusCode::NOT_FOUND, "not found".into()),
|
||||||
|
Self::Domain(DomainError::Unauthorized) => (StatusCode::UNAUTHORIZED, "unauthorized".into()),
|
||||||
|
Self::Domain(DomainError::Forbidden) => (StatusCode::FORBIDDEN, "forbidden".into()),
|
||||||
|
Self::Domain(DomainError::Conflict(m)) => (StatusCode::CONFLICT, m),
|
||||||
|
Self::Domain(DomainError::InvalidInput(m)) => (StatusCode::UNPROCESSABLE_ENTITY, m),
|
||||||
|
Self::Domain(DomainError::Internal(m)) => (StatusCode::INTERNAL_SERVER_ERROR, m),
|
||||||
|
Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()),
|
||||||
|
Self::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
|
||||||
|
};
|
||||||
|
(status, Json(ErrorResponse { error: msg })).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
47
crates/presentation/src/extractors.rs
Normal file
47
crates/presentation/src/extractors.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||||
|
use domain::value_objects::UserId;
|
||||||
|
use crate::{errors::ApiError, state::AppState};
|
||||||
|
|
||||||
|
pub struct AuthUser(pub UserId);
|
||||||
|
pub struct OptionalAuthUser(pub Option<UserId>);
|
||||||
|
|
||||||
|
impl FromRequestParts<AppState> for AuthUser {
|
||||||
|
type Rejection = ApiError;
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, ApiError> {
|
||||||
|
extract_user_id(parts, state).await?
|
||||||
|
.ok_or(ApiError::Unauthorized)
|
||||||
|
.map(AuthUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequestParts<AppState> for OptionalAuthUser {
|
||||||
|
type Rejection = ApiError;
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, ApiError> {
|
||||||
|
Ok(OptionalAuthUser(extract_user_id(parts, state).await?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_user_id(parts: &mut Parts, state: &AppState) -> Result<Option<UserId>, ApiError> {
|
||||||
|
if let Some(auth_header) = parts.headers.get("Authorization") {
|
||||||
|
if let Ok(s) = auth_header.to_str() {
|
||||||
|
if let Some(token) = s.strip_prefix("Bearer ") {
|
||||||
|
return state.auth.validate_token(token).map(Some).map_err(|_| ApiError::Unauthorized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(key_header) = parts.headers.get("X-Api-Key") {
|
||||||
|
if let Ok(raw) = key_header.to_str() {
|
||||||
|
let hash = sha256_hex(raw);
|
||||||
|
if let Ok(Some(key)) = state.api_keys.find_by_hash(&hash).await {
|
||||||
|
return Ok(Some(key.user_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sha256_hex(s: &str) -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
let hash = Sha256::digest(s.as_bytes());
|
||||||
|
hex::encode(hash)
|
||||||
|
}
|
||||||
0
crates/presentation/src/handlers/api_keys.rs
Normal file
0
crates/presentation/src/handlers/api_keys.rs
Normal file
35
crates/presentation/src/handlers/auth.rs
Normal file
35
crates/presentation/src/handlers/auth.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||||
|
use api_types::{requests::{LoginRequest, RegisterRequest}, responses::{AuthResponse, UserResponse}};
|
||||||
|
use application::use_cases::auth::{login, register, LoginInput, RegisterInput};
|
||||||
|
use crate::{errors::ApiError, state::AppState};
|
||||||
|
|
||||||
|
pub fn to_user_response(u: &domain::models::user::User) -> UserResponse {
|
||||||
|
UserResponse {
|
||||||
|
id: u.id.as_uuid(),
|
||||||
|
username: u.username.to_string(),
|
||||||
|
display_name: u.display_name.clone(),
|
||||||
|
bio: u.bio.clone(),
|
||||||
|
avatar_url: u.avatar_url.clone(),
|
||||||
|
header_url: u.header_url.clone(),
|
||||||
|
local: u.local,
|
||||||
|
created_at: u.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_register(State(s): State<AppState>, Json(body): Json<RegisterRequest>) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
let out = register(&*s.users, &*s.hasher, &*s.auth, &*s.events, RegisterInput {
|
||||||
|
username: body.username,
|
||||||
|
email: body.email,
|
||||||
|
password: body.password,
|
||||||
|
}).await?;
|
||||||
|
let resp = AuthResponse { token: out.token, user: to_user_response(&out.user) };
|
||||||
|
Ok((StatusCode::CREATED, Json(resp)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_login(State(s): State<AppState>, Json(body): Json<LoginRequest>) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
let out = login(&*s.users, &*s.hasher, &*s.auth, LoginInput {
|
||||||
|
email: body.email,
|
||||||
|
password: body.password,
|
||||||
|
}).await?;
|
||||||
|
Ok(Json(AuthResponse { token: out.token, user: to_user_response(&out.user) }))
|
||||||
|
}
|
||||||
0
crates/presentation/src/handlers/feed.rs
Normal file
0
crates/presentation/src/handlers/feed.rs
Normal file
7
crates/presentation/src/handlers/mod.rs
Normal file
7
crates/presentation/src/handlers/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod api_keys;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod feed;
|
||||||
|
pub mod notifications;
|
||||||
|
pub mod social;
|
||||||
|
pub mod thoughts;
|
||||||
|
pub mod users;
|
||||||
0
crates/presentation/src/handlers/notifications.rs
Normal file
0
crates/presentation/src/handlers/notifications.rs
Normal file
0
crates/presentation/src/handlers/social.rs
Normal file
0
crates/presentation/src/handlers/social.rs
Normal file
0
crates/presentation/src/handlers/thoughts.rs
Normal file
0
crates/presentation/src/handlers/thoughts.rs
Normal file
15
crates/presentation/src/handlers/users.rs
Normal file
15
crates/presentation/src/handlers/users.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use axum::{extract::{Path, State}, Json};
|
||||||
|
use api_types::{requests::UpdateProfileRequest, responses::UserResponse};
|
||||||
|
use application::use_cases::profile::{get_user_by_username, update_profile};
|
||||||
|
use crate::{errors::ApiError, extractors::AuthUser, handlers::auth::to_user_response, state::AppState};
|
||||||
|
|
||||||
|
pub async fn get_user(State(s): State<AppState>, Path(username): Path<String>) -> Result<Json<UserResponse>, ApiError> {
|
||||||
|
let user = get_user_by_username(&*s.users, &username).await?;
|
||||||
|
Ok(Json(to_user_response(&user)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn patch_profile(State(s): State<AppState>, AuthUser(uid): AuthUser, Json(body): Json<UpdateProfileRequest>) -> Result<Json<UserResponse>, ApiError> {
|
||||||
|
update_profile(&*s.users, &uid, body.display_name, body.bio, body.avatar_url, body.header_url, body.custom_css).await?;
|
||||||
|
let user = s.users.find_by_id(&uid).await?.ok_or(domain::errors::DomainError::NotFound)?;
|
||||||
|
Ok(Json(to_user_response(&user)))
|
||||||
|
}
|
||||||
4
crates/presentation/src/lib.rs
Normal file
4
crates/presentation/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod errors;
|
||||||
|
pub mod extractors;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod state;
|
||||||
21
crates/presentation/src/state.rs
Normal file
21
crates/presentation/src/state.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use domain::ports::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub users: Arc<dyn UserRepository>,
|
||||||
|
pub thoughts: Arc<dyn ThoughtRepository>,
|
||||||
|
pub likes: Arc<dyn LikeRepository>,
|
||||||
|
pub boosts: Arc<dyn BoostRepository>,
|
||||||
|
pub follows: Arc<dyn FollowRepository>,
|
||||||
|
pub blocks: Arc<dyn BlockRepository>,
|
||||||
|
pub tags: Arc<dyn TagRepository>,
|
||||||
|
pub api_keys: Arc<dyn ApiKeyRepository>,
|
||||||
|
pub top_friends: Arc<dyn TopFriendRepository>,
|
||||||
|
pub notifications: Arc<dyn NotificationRepository>,
|
||||||
|
pub remote_actors: Arc<dyn RemoteActorRepository>,
|
||||||
|
pub feed: Arc<dyn FeedRepository>,
|
||||||
|
pub auth: Arc<dyn AuthService>,
|
||||||
|
pub hasher: Arc<dyn PasswordHasher>,
|
||||||
|
pub events: Arc<dyn EventPublisher>,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user