20
crates/adapters/http-axum/Cargo.toml
Normal file
20
crates/adapters/http-axum/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "http-axum"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
domain.workspace = true
|
||||
application.workspace = true
|
||||
api-types.workspace = true
|
||||
config.workspace = true
|
||||
web-push-adapter.workspace = true
|
||||
axum.workspace = true
|
||||
tower-http.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
uuid.workspace = true
|
||||
utoipa.workspace = true
|
||||
utoipa-scalar.workspace = true
|
||||
tracing.workspace = true
|
||||
56
crates/adapters/http-axum/src/errors.rs
Normal file
56
crates/adapters/http-axum/src/errors.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
||||
use api_types::errors::ApiValidationError;
|
||||
use application::errors::ApplicationError;
|
||||
use domain::errors::DomainError;
|
||||
|
||||
pub struct ApiError(pub ApplicationError);
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, code, message) = match &self.0 {
|
||||
ApplicationError::Domain(domain_err) => domain_error_response(domain_err),
|
||||
ApplicationError::Validation(msg) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"VALIDATION_ERROR",
|
||||
msg.clone(),
|
||||
),
|
||||
};
|
||||
|
||||
let body = serde_json::json!({ "error": { "code": code, "message": message } });
|
||||
(status, Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApplicationError> for ApiError {
|
||||
fn from(err: ApplicationError) -> Self {
|
||||
Self(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiValidationError> for ApiError {
|
||||
fn from(err: ApiValidationError) -> Self {
|
||||
match err {
|
||||
ApiValidationError::Domain(e) => Self(ApplicationError::Domain(e)),
|
||||
ApiValidationError::Invalid(msg) => Self(ApplicationError::Validation(msg)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomainError> for ApiError {
|
||||
fn from(err: DomainError) -> Self {
|
||||
Self(ApplicationError::Domain(err))
|
||||
}
|
||||
}
|
||||
|
||||
fn domain_error_response(err: &DomainError) -> (StatusCode, &'static str, String) {
|
||||
match err {
|
||||
DomainError::NotFound(msg) => (StatusCode::NOT_FOUND, "NOT_FOUND", msg.clone()),
|
||||
DomainError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, "INVALID_INPUT", msg.clone()),
|
||||
DomainError::Conflict(msg) => (StatusCode::CONFLICT, "CONFLICT", msg.clone()),
|
||||
DomainError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED", msg.clone()),
|
||||
DomainError::Forbidden(msg) => (StatusCode::FORBIDDEN, "FORBIDDEN", msg.clone()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use axum::Json;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::request::Parts;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
pub struct AuthenticatedUser(pub UserId);
|
||||
|
||||
impl IntoResponse for AuthRejection {
|
||||
fn into_response(self) -> Response {
|
||||
let body = serde_json::json!({ "error": self.0 });
|
||||
(StatusCode::UNAUTHORIZED, Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthRejection(String);
|
||||
|
||||
impl<S> FromRequestParts<S> for AuthenticatedUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
AppState: FromRef<S>,
|
||||
{
|
||||
type Rejection = AuthRejection;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let app_state = AppState::from_ref(state);
|
||||
|
||||
let token = extract_bearer_token(parts)
|
||||
.ok_or_else(|| AuthRejection("missing or invalid authorization header".into()))?;
|
||||
|
||||
let user_id = app_state
|
||||
.auth_service
|
||||
.validate_token(&token)
|
||||
.await
|
||||
.map_err(|_| AuthRejection("invalid or expired token".into()))?;
|
||||
|
||||
Ok(AuthenticatedUser(user_id))
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_bearer_token(parts: &Parts) -> Option<String> {
|
||||
let header = parts.headers.get("authorization")?.to_str().ok()?;
|
||||
let token = header.strip_prefix("Bearer ")?;
|
||||
Some(token.to_string())
|
||||
}
|
||||
|
||||
use axum::extract::FromRef;
|
||||
7
crates/adapters/http-axum/src/extractors/mod.rs
Normal file
7
crates/adapters/http-axum/src/extractors/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod authenticated_user;
|
||||
mod multipart;
|
||||
mod path_id;
|
||||
|
||||
pub use authenticated_user::AuthenticatedUser;
|
||||
pub use multipart::{extract_file_bytes, extract_media_upload};
|
||||
pub use path_id::PathId;
|
||||
66
crates/adapters/http-axum/src/extractors/multipart.rs
Normal file
66
crates/adapters/http-axum/src/extractors/multipart.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use axum::extract::Multipart;
|
||||
|
||||
use domain::attachment::{ContentType, MediaUpload};
|
||||
|
||||
use crate::errors::ApiError;
|
||||
|
||||
pub async fn extract_media_upload(mut multipart: Multipart) -> Result<MediaUpload, ApiError> {
|
||||
let mut file_data: Option<Vec<u8>> = None;
|
||||
let mut content_type_str: Option<String> = None;
|
||||
|
||||
while let Some(field) = multipart
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|e| validation_error(format!("invalid multipart data: {e}")))?
|
||||
{
|
||||
match field.name() {
|
||||
Some("file") => {
|
||||
if content_type_str.is_none() {
|
||||
content_type_str = field.content_type().map(|s| s.to_string());
|
||||
}
|
||||
let bytes = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| validation_error(format!("failed to read file: {e}")))?;
|
||||
file_data = Some(bytes.to_vec());
|
||||
}
|
||||
Some("content_type") | Some("contentType") => {
|
||||
let text = field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| validation_error(format!("failed to read content type: {e}")))?;
|
||||
content_type_str = Some(text);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let data = file_data.ok_or_else(|| validation_error("missing 'file' field".into()))?;
|
||||
let content_type_str =
|
||||
content_type_str.ok_or_else(|| validation_error("missing content type".into()))?;
|
||||
let content_type = ContentType::new(content_type_str)?;
|
||||
|
||||
MediaUpload::new(data, content_type).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn extract_file_bytes(mut multipart: Multipart) -> Result<Vec<u8>, ApiError> {
|
||||
while let Some(field) = multipart
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|e| validation_error(format!("invalid multipart data: {e}")))?
|
||||
{
|
||||
if field.name() == Some("file") {
|
||||
let bytes = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| validation_error(format!("failed to read file: {e}")))?;
|
||||
return Ok(bytes.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
Err(validation_error("missing 'file' field".into()))
|
||||
}
|
||||
|
||||
fn validation_error(msg: String) -> ApiError {
|
||||
ApiError(application::errors::ApplicationError::Validation(msg))
|
||||
}
|
||||
38
crates/adapters/http-axum/src/extractors/path_id.rs
Normal file
38
crates/adapters/http-axum/src/extractors/path_id.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use axum::Json;
|
||||
use axum::extract::Path;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
||||
pub struct PathId<T>(pub T);
|
||||
|
||||
pub struct PathIdRejection(String);
|
||||
|
||||
impl IntoResponse for PathIdRejection {
|
||||
fn into_response(self) -> Response {
|
||||
let body = serde_json::json!({ "error": self.0 });
|
||||
(StatusCode::BAD_REQUEST, Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> axum::extract::FromRequestParts<S> for PathId<T>
|
||||
where
|
||||
S: Send + Sync,
|
||||
T: From<uuid::Uuid>,
|
||||
{
|
||||
type Rejection = PathIdRejection;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut axum::http::request::Parts,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let Path(id_str) = Path::<String>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|e| PathIdRejection(format!("invalid path parameter: {e}")))?;
|
||||
|
||||
let uuid: uuid::Uuid = id_str
|
||||
.parse()
|
||||
.map_err(|_| PathIdRejection(format!("invalid UUID: {id_str}")))?;
|
||||
|
||||
Ok(PathId(T::from(uuid)))
|
||||
}
|
||||
}
|
||||
159
crates/adapters/http-axum/src/handlers/activities.rs
Normal file
159
crates/adapters/http-axum/src/handlers/activities.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use api_types::requests::{CreateActivityRequest, RenameActivityRequest, SetCategoryRequest};
|
||||
use api_types::responses::ActivityResponse;
|
||||
use application::activity::use_cases::{
|
||||
archive_activity, create_activity, delete_activity, get_activity, list_activities,
|
||||
rename_activity, set_category,
|
||||
};
|
||||
use domain::activity::ActivityId;
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::extractors::{AuthenticatedUser, PathId};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/activities", tag = "activities", security(("bearer" = [])),
|
||||
request_body = CreateActivityRequest,
|
||||
responses((status = 201, body = ActivityResponse))
|
||||
)]
|
||||
pub async fn handle_create(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Json(body): Json<CreateActivityRequest>,
|
||||
) -> Result<(StatusCode, Json<ActivityResponse>), ApiError> {
|
||||
let cmd = body.into_command(user_id)?;
|
||||
let deps = create_activity::Deps {
|
||||
activities: state.activity_command,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
let activity = create_activity::execute(cmd, &deps).await?;
|
||||
Ok((StatusCode::CREATED, Json(ActivityResponse::from(activity))))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/activities/{id}", tag = "activities", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 200, body = ActivityResponse))
|
||||
)]
|
||||
pub async fn handle_get(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(activity_id): PathId<ActivityId>,
|
||||
) -> Result<Json<ActivityResponse>, ApiError> {
|
||||
let deps = get_activity::Deps {
|
||||
query: state.activity_query,
|
||||
};
|
||||
let activity = get_activity::execute(activity_id, user_id, &deps).await?;
|
||||
Ok(Json(ActivityResponse::from(activity)))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/activities", tag = "activities", security(("bearer" = [])),
|
||||
responses((status = 200, body = Vec<ActivityResponse>))
|
||||
)]
|
||||
pub async fn handle_list(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<Json<Vec<ActivityResponse>>, ApiError> {
|
||||
let deps = list_activities::Deps {
|
||||
query: state.activity_query,
|
||||
};
|
||||
let activities = list_activities::active_only(user_id, &deps).await?;
|
||||
Ok(Json(
|
||||
activities.into_iter().map(ActivityResponse::from).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(patch, path = "/api/v1/activities/{id}/name", tag = "activities", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
request_body = RenameActivityRequest,
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_rename(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(activity_id): PathId<ActivityId>,
|
||||
Json(body): Json<RenameActivityRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let cmd = body.into_command(activity_id)?;
|
||||
let deps = rename_activity::Deps {
|
||||
command: state.activity_command,
|
||||
query: state.activity_query,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
rename_activity::execute(cmd, user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(patch, path = "/api/v1/activities/{id}/category", tag = "activities", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
request_body = SetCategoryRequest,
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_set_category(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(activity_id): PathId<ActivityId>,
|
||||
Json(body): Json<SetCategoryRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let cmd = body.into_command(activity_id)?;
|
||||
let deps = set_category::Deps {
|
||||
command: state.activity_command,
|
||||
query: state.activity_query,
|
||||
};
|
||||
set_category::execute(cmd, user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/activities/{id}/archive", tag = "activities", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_archive(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(activity_id): PathId<ActivityId>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = archive_activity::Deps {
|
||||
command: state.activity_command,
|
||||
query: state.activity_query,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
archive_activity::archive(activity_id, user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/activities/{id}/unarchive", tag = "activities", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_unarchive(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(activity_id): PathId<ActivityId>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = archive_activity::Deps {
|
||||
command: state.activity_command,
|
||||
query: state.activity_query,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
archive_activity::unarchive(activity_id, user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(delete, path = "/api/v1/activities/{id}", tag = "activities", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_delete(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(activity_id): PathId<ActivityId>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = delete_activity::Deps {
|
||||
command: state.activity_command,
|
||||
query: state.activity_query,
|
||||
};
|
||||
delete_activity::execute(activity_id, user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
89
crates/adapters/http-axum/src/handlers/auth.rs
Normal file
89
crates/adapters/http-axum/src/handlers/auth.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use api_types::requests::LoginRequest;
|
||||
use api_types::responses::UserResponse;
|
||||
use application::auth::use_cases::{login, logout, refresh};
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/auth/login", tag = "auth",
|
||||
request_body = LoginRequest,
|
||||
responses((status = 200, description = "Login successful"))
|
||||
)]
|
||||
pub async fn handle_login(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<LoginRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let cmd = body.into_command();
|
||||
let deps = login::Deps {
|
||||
user_query: state.user_query,
|
||||
password_hasher: state.password_hasher,
|
||||
auth_service: state.auth_service,
|
||||
refresh_session_command: state.refresh_session_command,
|
||||
refresh_token_ttl_seconds: state.auth_config.refresh_token_ttl_seconds as i64,
|
||||
};
|
||||
|
||||
let result = login::execute(cmd, &deps).await?;
|
||||
let user_response = UserResponse::from(result.user);
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"accessToken": result.access_token.token(),
|
||||
"refreshToken": result.refresh_token,
|
||||
"expiresAt": result.access_token.expires_at(),
|
||||
"user": user_response,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefreshRequest {
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/auth/refresh", tag = "auth",
|
||||
request_body = RefreshRequest,
|
||||
responses((status = 200, description = "Token refreshed"))
|
||||
)]
|
||||
pub async fn handle_refresh(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<RefreshRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let deps = refresh::Deps {
|
||||
auth_service: state.auth_service,
|
||||
refresh_session_command: state.refresh_session_command,
|
||||
refresh_session_query: state.refresh_session_query,
|
||||
refresh_token_ttl_seconds: state.auth_config.refresh_token_ttl_seconds as i64,
|
||||
};
|
||||
|
||||
let result = refresh::execute(&body.refresh_token, &deps).await?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"accessToken": result.access_token.token(),
|
||||
"refreshToken": result.refresh_token,
|
||||
"expiresAt": result.access_token.expires_at(),
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogoutRequest {
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/auth/logout", tag = "auth",
|
||||
request_body = LogoutRequest,
|
||||
responses((status = 204, description = "Logged out"))
|
||||
)]
|
||||
pub async fn handle_logout(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<LogoutRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = logout::Deps {
|
||||
refresh_session_command: state.refresh_session_command,
|
||||
};
|
||||
logout::execute(&body.refresh_token, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
260
crates/adapters/http-axum/src/handlers/entries.rs
Normal file
260
crates/adapters/http-axum/src/handlers/entries.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use api_types::mappers::correlation_response;
|
||||
use api_types::requests::{
|
||||
CreateEntryRequest, DateRangeParams, ListEntriesParams, ReplaceActivityRequest,
|
||||
UpdateEntryRequest,
|
||||
};
|
||||
use api_types::responses::{
|
||||
BulkActionResponse, CalendarDayResponse, CorrelationResponse, EntryResponse, MoodStatsResponse,
|
||||
};
|
||||
use application::entry::queries::{FilterByActivityQuery, FilterByMoodQuery, MoodStatsQuery};
|
||||
use application::entry::use_cases::{
|
||||
create_entry, delete_entries_by_date_range, delete_entry, filter_by_activity, filter_by_mood,
|
||||
get_activity_correlation, get_calendar, get_entry, get_mood_stats, list_entries,
|
||||
replace_activity, update_entry,
|
||||
};
|
||||
use domain::activity::ActivityId;
|
||||
use domain::entry::{Mood, MoodEntryId};
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::extractors::{AuthenticatedUser, PathId};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/entries", tag = "entries", security(("bearer" = [])),
|
||||
request_body = CreateEntryRequest,
|
||||
responses((status = 201, body = EntryResponse))
|
||||
)]
|
||||
pub async fn handle_create(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Json(body): Json<CreateEntryRequest>,
|
||||
) -> Result<(StatusCode, Json<EntryResponse>), ApiError> {
|
||||
let cmd = body.into_command(user_id, &state.entry_config)?;
|
||||
let deps = create_entry::Deps {
|
||||
entries: state.entry_command,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
let entry = create_entry::execute(cmd, &deps).await?;
|
||||
Ok((StatusCode::CREATED, Json(EntryResponse::from(entry))))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/entries/{id}", tag = "entries", security(("bearer" = [])),
|
||||
params(("id" = String, Path, description = "Entry ID")),
|
||||
responses((status = 200, body = EntryResponse))
|
||||
)]
|
||||
pub async fn handle_get(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(entry_id): PathId<MoodEntryId>,
|
||||
) -> Result<Json<EntryResponse>, ApiError> {
|
||||
let deps = get_entry::Deps {
|
||||
query: state.entry_query,
|
||||
};
|
||||
let entry = get_entry::execute(entry_id, user_id, &deps).await?;
|
||||
Ok(Json(EntryResponse::from(entry)))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/entries", tag = "entries", security(("bearer" = [])),
|
||||
params(ListEntriesParams),
|
||||
responses((status = 200, body = Vec<EntryResponse>))
|
||||
)]
|
||||
pub async fn handle_list(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Query(params): Query<ListEntriesParams>,
|
||||
) -> Result<Json<Vec<EntryResponse>>, ApiError> {
|
||||
let query = params.into_query(user_id)?;
|
||||
let deps = list_entries::Deps {
|
||||
query: state.entry_query,
|
||||
};
|
||||
let entries = list_entries::execute(query, &deps).await?;
|
||||
Ok(Json(entries.into_iter().map(EntryResponse::from).collect()))
|
||||
}
|
||||
|
||||
#[utoipa::path(patch, path = "/api/v1/entries/{id}", tag = "entries", security(("bearer" = [])),
|
||||
params(("id" = String, Path, description = "Entry ID")),
|
||||
request_body = UpdateEntryRequest,
|
||||
responses((status = 200, body = EntryResponse))
|
||||
)]
|
||||
pub async fn handle_update(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(entry_id): PathId<MoodEntryId>,
|
||||
Json(body): Json<UpdateEntryRequest>,
|
||||
) -> Result<Json<EntryResponse>, ApiError> {
|
||||
let cmd = body.into_command(entry_id, &state.entry_config)?;
|
||||
let deps = update_entry::Deps {
|
||||
command: state.entry_command,
|
||||
query: state.entry_query,
|
||||
media_storage: state.media_storage,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
let entry = update_entry::execute(cmd, user_id, &deps).await?;
|
||||
Ok(Json(EntryResponse::from(entry)))
|
||||
}
|
||||
|
||||
#[utoipa::path(delete, path = "/api/v1/entries/{id}", tag = "entries", security(("bearer" = [])),
|
||||
params(("id" = String, Path, description = "Entry ID")),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_delete(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(entry_id): PathId<MoodEntryId>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = delete_entry::Deps {
|
||||
command: state.entry_command,
|
||||
query: state.entry_query,
|
||||
events: state.event_publisher,
|
||||
media_storage: state.media_storage,
|
||||
};
|
||||
delete_entry::execute(entry_id, user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/entries/filter/mood/{mood}", tag = "entries", security(("bearer" = [])),
|
||||
params(("mood" = u8, Path, description = "Mood value 1-5")),
|
||||
responses((status = 200, body = Vec<EntryResponse>))
|
||||
)]
|
||||
pub async fn handle_filter_by_mood(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Path(mood): Path<u8>,
|
||||
) -> Result<Json<Vec<EntryResponse>>, ApiError> {
|
||||
let mood = Mood::try_from(mood)?;
|
||||
let query = FilterByMoodQuery { user_id, mood };
|
||||
let deps = filter_by_mood::Deps {
|
||||
query: state.entry_query,
|
||||
};
|
||||
let entries = filter_by_mood::execute(query, &deps).await?;
|
||||
Ok(Json(entries.into_iter().map(EntryResponse::from).collect()))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/entries/filter/activity/{id}", tag = "entries", security(("bearer" = [])),
|
||||
params(("id" = String, Path, description = "Activity ID")),
|
||||
responses((status = 200, body = Vec<EntryResponse>))
|
||||
)]
|
||||
pub async fn handle_filter_by_activity(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(activity_id): PathId<ActivityId>,
|
||||
) -> Result<Json<Vec<EntryResponse>>, ApiError> {
|
||||
let query = FilterByActivityQuery {
|
||||
user_id,
|
||||
activity_id,
|
||||
};
|
||||
let deps = filter_by_activity::Deps {
|
||||
query: state.entry_query,
|
||||
};
|
||||
let entries = filter_by_activity::execute(query, &deps).await?;
|
||||
Ok(Json(entries.into_iter().map(EntryResponse::from).collect()))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/entries/stats", tag = "entries", security(("bearer" = [])),
|
||||
params(ListEntriesParams),
|
||||
responses((status = 200, body = MoodStatsResponse))
|
||||
)]
|
||||
pub async fn handle_stats(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Query(params): Query<ListEntriesParams>,
|
||||
) -> Result<Json<MoodStatsResponse>, ApiError> {
|
||||
let range = match (params.from, params.to) {
|
||||
(Some(from), Some(to)) => {
|
||||
let from = api_types::mappers::shared::parse_datetime(&from)?;
|
||||
let to = api_types::mappers::shared::parse_datetime(&to)?;
|
||||
Some(domain::entry::DateRange::new(from, to)?)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let query = MoodStatsQuery { user_id, range };
|
||||
let deps = get_mood_stats::Deps {
|
||||
query: state.entry_query,
|
||||
};
|
||||
let stats = get_mood_stats::execute(query, &deps).await?;
|
||||
Ok(Json(MoodStatsResponse::from(stats)))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/entries/calendar", tag = "entries", security(("bearer" = [])),
|
||||
params(DateRangeParams),
|
||||
responses((status = 200, body = Vec<CalendarDayResponse>))
|
||||
)]
|
||||
pub async fn handle_calendar(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Query(params): Query<DateRangeParams>,
|
||||
) -> Result<Json<Vec<CalendarDayResponse>>, ApiError> {
|
||||
let range = params.into_date_range()?;
|
||||
let deps = get_calendar::Deps {
|
||||
query: state.entry_query,
|
||||
};
|
||||
let days = get_calendar::execute(user_id, range, &deps).await?;
|
||||
Ok(Json(
|
||||
days.into_iter().map(CalendarDayResponse::from).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/entries/correlation/{id}", tag = "entries", security(("bearer" = [])),
|
||||
params(("id" = String, Path, description = "Activity ID"), ListEntriesParams),
|
||||
responses((status = 200, body = CorrelationResponse))
|
||||
)]
|
||||
pub async fn handle_activity_correlation(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(activity_id): PathId<ActivityId>,
|
||||
Query(params): Query<ListEntriesParams>,
|
||||
) -> Result<Json<CorrelationResponse>, ApiError> {
|
||||
let range = match (params.from, params.to) {
|
||||
(Some(from), Some(to)) => {
|
||||
let from = api_types::mappers::shared::parse_datetime(&from)?;
|
||||
let to = api_types::mappers::shared::parse_datetime(&to)?;
|
||||
Some(domain::entry::DateRange::new(from, to)?)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let deps = get_activity_correlation::Deps {
|
||||
query: state.entry_query,
|
||||
};
|
||||
let correlation =
|
||||
get_activity_correlation::execute(user_id, activity_id.clone(), range, &deps).await?;
|
||||
Ok(Json(correlation_response(activity_id, correlation)))
|
||||
}
|
||||
|
||||
#[utoipa::path(delete, path = "/api/v1/entries/bulk/delete", tag = "entries", security(("bearer" = [])),
|
||||
params(DateRangeParams),
|
||||
responses((status = 200, body = BulkActionResponse))
|
||||
)]
|
||||
pub async fn handle_delete_by_date_range(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Query(params): Query<DateRangeParams>,
|
||||
) -> Result<Json<BulkActionResponse>, ApiError> {
|
||||
let range = params.into_date_range()?;
|
||||
let deps = delete_entries_by_date_range::Deps {
|
||||
cascade: state.cascade,
|
||||
media_storage: state.media_storage,
|
||||
};
|
||||
let affected_count = delete_entries_by_date_range::execute(user_id, &range, &deps).await?;
|
||||
Ok(Json(BulkActionResponse { affected_count }))
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/entries/bulk/replace-activity", tag = "entries", security(("bearer" = [])),
|
||||
request_body = ReplaceActivityRequest,
|
||||
responses((status = 200, body = BulkActionResponse))
|
||||
)]
|
||||
pub async fn handle_replace_activity(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Json(body): Json<ReplaceActivityRequest>,
|
||||
) -> Result<Json<BulkActionResponse>, ApiError> {
|
||||
let (user_id, old_id, new_id) = body.into_parts(user_id)?;
|
||||
let deps = replace_activity::Deps {
|
||||
entry_command: state.entry_command,
|
||||
activity_query: state.activity_query,
|
||||
};
|
||||
let affected_count = replace_activity::execute(user_id, old_id, new_id, &deps).await?;
|
||||
Ok(Json(BulkActionResponse { affected_count }))
|
||||
}
|
||||
64
crates/adapters/http-axum/src/handlers/import_export.rs
Normal file
64
crates/adapters/http-axum/src/handlers/import_export.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Multipart, State};
|
||||
use axum::http::header;
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
use api_types::responses::ImportResultResponse;
|
||||
use application::export::use_cases::export_user_data;
|
||||
use application::import::commands::ImportCommand;
|
||||
use application::import::use_cases::import_entries;
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::extractors::{AuthenticatedUser, extract_file_bytes};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/data/export", tag = "data", security(("bearer" = [])),
|
||||
responses((status = 200, description = "ZIP archive with user data"))
|
||||
)]
|
||||
pub async fn handle_export(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = export_user_data::Deps {
|
||||
entry_query: state.entry_query,
|
||||
activity_query: state.activity_query,
|
||||
reminder_query: state.reminder_query,
|
||||
media_storage: state.media_storage,
|
||||
exporter: state.export_port.clone(),
|
||||
};
|
||||
let data = export_user_data::execute(user_id, &deps).await?;
|
||||
|
||||
Ok((
|
||||
[
|
||||
(header::CONTENT_TYPE, "application/zip"),
|
||||
(
|
||||
header::CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"k-mood-export.zip\"",
|
||||
),
|
||||
],
|
||||
data,
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/data/import", tag = "data", security(("bearer" = [])),
|
||||
responses((status = 200, body = ImportResultResponse))
|
||||
)]
|
||||
pub async fn handle_import(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
multipart: Multipart,
|
||||
) -> Result<Json<ImportResultResponse>, ApiError> {
|
||||
let data = extract_file_bytes(multipart).await?;
|
||||
|
||||
let cmd = ImportCommand { user_id, data };
|
||||
let deps = import_entries::Deps {
|
||||
source: state.import_source.clone(),
|
||||
entry_command: state.entry_command,
|
||||
entry_query: state.entry_query,
|
||||
activity_command: state.activity_command,
|
||||
activity_query: state.activity_query,
|
||||
preset: state.preset_config,
|
||||
};
|
||||
let result = import_entries::execute(cmd, &deps).await?;
|
||||
Ok(Json(ImportResultResponse::from(result)))
|
||||
}
|
||||
122
crates/adapters/http-axum/src/handlers/media.rs
Normal file
122
crates/adapters/http-axum/src/handlers/media.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Multipart, State};
|
||||
use axum::http::{StatusCode, header};
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
use api_types::responses::MediaIdResponse;
|
||||
use application::media::use_cases::{
|
||||
delete_photo, delete_voice_memo, upload_photo, upload_voice_memo,
|
||||
};
|
||||
use domain::attachment::{PhotoId, VoiceMemoId};
|
||||
use domain::errors::DomainError;
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::extractors::{AuthenticatedUser, PathId, extract_media_upload};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/media/photos", tag = "media", security(("bearer" = [])),
|
||||
responses((status = 201, body = MediaIdResponse))
|
||||
)]
|
||||
pub async fn handle_upload_photo(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(_user_id): AuthenticatedUser,
|
||||
multipart: Multipart,
|
||||
) -> Result<(StatusCode, Json<MediaIdResponse>), ApiError> {
|
||||
let upload = extract_media_upload(multipart).await?;
|
||||
let deps = upload_photo::Deps {
|
||||
storage: state.media_storage,
|
||||
};
|
||||
let photo_id = upload_photo::execute(upload, &deps).await?;
|
||||
Ok((StatusCode::CREATED, Json(MediaIdResponse::from(photo_id))))
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/media/voice-memos", tag = "media", security(("bearer" = [])),
|
||||
responses((status = 201, body = MediaIdResponse))
|
||||
)]
|
||||
pub async fn handle_upload_voice_memo(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(_user_id): AuthenticatedUser,
|
||||
multipart: Multipart,
|
||||
) -> Result<(StatusCode, Json<MediaIdResponse>), ApiError> {
|
||||
let upload = extract_media_upload(multipart).await?;
|
||||
let deps = upload_voice_memo::Deps {
|
||||
storage: state.media_storage,
|
||||
};
|
||||
let voice_memo_id = upload_voice_memo::execute(upload, &deps).await?;
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(MediaIdResponse::from(voice_memo_id)),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/media/photos/{id}", tag = "media",
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 200, description = "Photo binary"))
|
||||
)]
|
||||
pub async fn handle_serve_photo(
|
||||
State(state): State<AppState>,
|
||||
PathId(photo_id): PathId<PhotoId>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let file = state
|
||||
.media_storage
|
||||
.get_photo(&photo_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("photo not found".into()))?;
|
||||
|
||||
Ok((
|
||||
[(header::CONTENT_TYPE, file.content_type.value().to_string())],
|
||||
file.data,
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/media/voice-memos/{id}", tag = "media",
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 200, description = "Voice memo binary"))
|
||||
)]
|
||||
pub async fn handle_serve_voice_memo(
|
||||
State(state): State<AppState>,
|
||||
PathId(voice_memo_id): PathId<VoiceMemoId>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let file = state
|
||||
.media_storage
|
||||
.get_voice_memo(&voice_memo_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("voice memo not found".into()))?;
|
||||
|
||||
Ok((
|
||||
[(header::CONTENT_TYPE, file.content_type.value().to_string())],
|
||||
file.data,
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(delete, path = "/api/v1/media/photos/{id}", tag = "media", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_delete_photo(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(_user_id): AuthenticatedUser,
|
||||
PathId(photo_id): PathId<PhotoId>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = delete_photo::Deps {
|
||||
storage: state.media_storage,
|
||||
};
|
||||
delete_photo::execute(photo_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(delete, path = "/api/v1/media/voice-memos/{id}", tag = "media", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_delete_voice_memo(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(_user_id): AuthenticatedUser,
|
||||
PathId(voice_memo_id): PathId<VoiceMemoId>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = delete_voice_memo::Deps {
|
||||
storage: state.media_storage,
|
||||
};
|
||||
delete_voice_memo::execute(voice_memo_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
8
crates/adapters/http-axum/src/handlers/mod.rs
Normal file
8
crates/adapters/http-axum/src/handlers/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub mod activities;
|
||||
pub mod auth;
|
||||
pub mod entries;
|
||||
pub mod import_export;
|
||||
pub mod media;
|
||||
pub mod push;
|
||||
pub mod reminders;
|
||||
pub mod users;
|
||||
77
crates/adapters/http-axum/src/handlers/push.rs
Normal file
77
crates/adapters/http-axum/src/handlers/push.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use api_types::requests::{PushSubscribeRequest, PushUnsubscribeRequest};
|
||||
use application::push::use_cases::{subscribe, unsubscribe};
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::extractors::AuthenticatedUser;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/push/vapid-key", tag = "push",
|
||||
responses((status = 200, description = "VAPID public key"))
|
||||
)]
|
||||
pub async fn handle_vapid_key(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
if !state.push_config.enabled {
|
||||
return Err(domain::errors::DomainError::NotFound(
|
||||
"push notifications are disabled".into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let public_key = web_push_adapter::WebPushSender::public_key_base64(&state.push_config)?;
|
||||
Ok(Json(serde_json::json!({ "publicKey": public_key })))
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/push/subscribe", tag = "push", security(("bearer" = [])),
|
||||
request_body = PushSubscribeRequest,
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_subscribe(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Json(body): Json<PushSubscribeRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let cmd = body.into_command(user_id);
|
||||
let deps = subscribe::Deps {
|
||||
push_command: state.push_subscription_command,
|
||||
push_query: state.push_subscription_query,
|
||||
};
|
||||
subscribe::execute(cmd, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/push/unsubscribe", tag = "push", security(("bearer" = [])),
|
||||
request_body = PushUnsubscribeRequest,
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_unsubscribe(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(_user_id): AuthenticatedUser,
|
||||
Json(body): Json<PushUnsubscribeRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let cmd = body.into_command();
|
||||
let deps = unsubscribe::Deps {
|
||||
push_command: state.push_subscription_command,
|
||||
};
|
||||
unsubscribe::execute(cmd, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/push/test", tag = "push", security(("bearer" = [])),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_test(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let sender = state
|
||||
.reminder_sender
|
||||
.as_ref()
|
||||
.ok_or_else(|| domain::errors::DomainError::InvalidInput("push not enabled".into()))?;
|
||||
|
||||
sender.send_reminder(&user_id).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
103
crates/adapters/http-axum/src/handlers/reminders.rs
Normal file
103
crates/adapters/http-axum/src/handlers/reminders.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use api_types::requests::{CreateReminderRequest, UpdateReminderRequest};
|
||||
use api_types::responses::ReminderResponse;
|
||||
use application::reminder::use_cases::{
|
||||
create_reminder, delete_reminder, get_reminder, list_reminders, update_reminder,
|
||||
};
|
||||
use domain::reminder::ReminderId;
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::extractors::{AuthenticatedUser, PathId};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/reminders", tag = "reminders", security(("bearer" = [])),
|
||||
request_body = CreateReminderRequest,
|
||||
responses((status = 201, body = ReminderResponse))
|
||||
)]
|
||||
pub async fn handle_create(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Json(body): Json<CreateReminderRequest>,
|
||||
) -> Result<(StatusCode, Json<ReminderResponse>), ApiError> {
|
||||
let cmd = body.into_command(user_id)?;
|
||||
let deps = create_reminder::Deps {
|
||||
reminders: state.reminder_command,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
let reminder = create_reminder::execute(cmd, &deps).await?;
|
||||
Ok((StatusCode::CREATED, Json(ReminderResponse::from(reminder))))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/reminders/{id}", tag = "reminders", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 200, body = ReminderResponse))
|
||||
)]
|
||||
pub async fn handle_get(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(reminder_id): PathId<ReminderId>,
|
||||
) -> Result<Json<ReminderResponse>, ApiError> {
|
||||
let deps = get_reminder::Deps {
|
||||
query: state.reminder_query,
|
||||
};
|
||||
let reminder = get_reminder::execute(reminder_id, user_id, &deps).await?;
|
||||
Ok(Json(ReminderResponse::from(reminder)))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/reminders", tag = "reminders", security(("bearer" = [])),
|
||||
responses((status = 200, body = Vec<ReminderResponse>))
|
||||
)]
|
||||
pub async fn handle_list(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<Json<Vec<ReminderResponse>>, ApiError> {
|
||||
let deps = list_reminders::Deps {
|
||||
query: state.reminder_query,
|
||||
};
|
||||
let reminders = list_reminders::execute(user_id, &deps).await?;
|
||||
Ok(Json(
|
||||
reminders.into_iter().map(ReminderResponse::from).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(patch, path = "/api/v1/reminders/{id}", tag = "reminders", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
request_body = UpdateReminderRequest,
|
||||
responses((status = 200, body = ReminderResponse))
|
||||
)]
|
||||
pub async fn handle_update(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(reminder_id): PathId<ReminderId>,
|
||||
Json(body): Json<UpdateReminderRequest>,
|
||||
) -> Result<Json<ReminderResponse>, ApiError> {
|
||||
let cmd = body.into_command(reminder_id)?;
|
||||
let deps = update_reminder::Deps {
|
||||
command: state.reminder_command,
|
||||
query: state.reminder_query,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
let reminder = update_reminder::execute(cmd, user_id, &deps).await?;
|
||||
Ok(Json(ReminderResponse::from(reminder)))
|
||||
}
|
||||
|
||||
#[utoipa::path(delete, path = "/api/v1/reminders/{id}", tag = "reminders", security(("bearer" = [])),
|
||||
params(("id" = String, Path)),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_delete(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
PathId(reminder_id): PathId<ReminderId>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = delete_reminder::Deps {
|
||||
command: state.reminder_command,
|
||||
query: state.reminder_query,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
delete_reminder::execute(reminder_id, user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
124
crates/adapters/http-axum/src/handlers/users.rs
Normal file
124
crates/adapters/http-axum/src/handlers/users.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use api_types::requests::{ChangePasswordRequest, RegisterRequest, UpdateProfileRequest};
|
||||
use api_types::responses::UserResponse;
|
||||
use application::user::use_cases::{
|
||||
change_password, clear_data, delete_user, get_profile, register, update_profile,
|
||||
};
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::extractors::AuthenticatedUser;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[utoipa::path(post, path = "/api/v1/users/register", tag = "users",
|
||||
request_body = RegisterRequest,
|
||||
responses((status = 201, body = UserResponse))
|
||||
)]
|
||||
pub async fn handle_register(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<RegisterRequest>,
|
||||
) -> Result<(StatusCode, Json<UserResponse>), ApiError> {
|
||||
if !state.auth_config.allow_registration {
|
||||
return Err(
|
||||
domain::errors::DomainError::Forbidden("registration is disabled".into()).into(),
|
||||
);
|
||||
}
|
||||
let cmd = body.into_command()?;
|
||||
let deps = register::Deps {
|
||||
user_command: state.user_command,
|
||||
user_query: state.user_query,
|
||||
activity_command: state.activity_command,
|
||||
password_hasher: state.password_hasher,
|
||||
events: state.event_publisher,
|
||||
preset: state.preset_config,
|
||||
};
|
||||
let user = register::execute(cmd, &deps).await?;
|
||||
Ok((StatusCode::CREATED, Json(UserResponse::from(user))))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/v1/users/me", tag = "users", security(("bearer" = [])),
|
||||
responses((status = 200, body = UserResponse))
|
||||
)]
|
||||
pub async fn handle_get_profile(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let deps = get_profile::Deps {
|
||||
user_query: state.user_query,
|
||||
};
|
||||
let user = get_profile::execute(user_id, &deps).await?;
|
||||
Ok(Json(UserResponse::from(user)))
|
||||
}
|
||||
|
||||
#[utoipa::path(patch, path = "/api/v1/users/me", tag = "users", security(("bearer" = [])),
|
||||
request_body = UpdateProfileRequest,
|
||||
responses((status = 200, body = UserResponse))
|
||||
)]
|
||||
pub async fn handle_update_profile(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Json(body): Json<UpdateProfileRequest>,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let cmd = body.into_command(user_id)?;
|
||||
let deps = update_profile::Deps {
|
||||
user_command: state.user_command,
|
||||
user_query: state.user_query,
|
||||
};
|
||||
let user = update_profile::execute(cmd, &deps).await?;
|
||||
Ok(Json(UserResponse::from(user)))
|
||||
}
|
||||
|
||||
#[utoipa::path(patch, path = "/api/v1/users/me/password", tag = "users", security(("bearer" = [])),
|
||||
request_body = ChangePasswordRequest,
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_change_password(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
Json(body): Json<ChangePasswordRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let cmd = body.into_command(user_id);
|
||||
let deps = change_password::Deps {
|
||||
user_command: state.user_command,
|
||||
user_query: state.user_query,
|
||||
password_hasher: state.password_hasher,
|
||||
};
|
||||
change_password::execute(cmd, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(delete, path = "/api/v1/users/me", tag = "users", security(("bearer" = [])),
|
||||
responses((status = 204))
|
||||
)]
|
||||
pub async fn handle_delete(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = delete_user::Deps {
|
||||
user_query: state.user_query,
|
||||
entry_query: state.entry_query,
|
||||
cascade: state.cascade,
|
||||
media_storage: state.media_storage,
|
||||
events: state.event_publisher,
|
||||
};
|
||||
delete_user::execute(user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(delete, path = "/api/v1/users/me/data", tag = "users", security(("bearer" = [])),
|
||||
responses((status = 204, description = "All user data cleared"))
|
||||
)]
|
||||
pub async fn handle_clear_data(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = clear_data::Deps {
|
||||
entry_query: state.entry_query,
|
||||
cascade: state.cascade,
|
||||
media_storage: state.media_storage,
|
||||
};
|
||||
clear_data::execute(user_id, &deps).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
7
crates/adapters/http-axum/src/lib.rs
Normal file
7
crates/adapters/http-axum/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod errors;
|
||||
pub mod extractors;
|
||||
pub mod handlers;
|
||||
pub mod openapi;
|
||||
pub mod router;
|
||||
pub mod spa;
|
||||
pub mod state;
|
||||
120
crates/adapters/http-axum/src/openapi.rs
Normal file
120
crates/adapters/http-axum/src/openapi.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
||||
use utoipa::{Modify, OpenApi};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
title = "k-mood API",
|
||||
version = "1.0.0",
|
||||
description = "Mood tracking journal API"
|
||||
),
|
||||
modifiers(&SecurityAddon),
|
||||
paths(
|
||||
crate::handlers::auth::handle_login,
|
||||
crate::handlers::auth::handle_refresh,
|
||||
crate::handlers::auth::handle_logout,
|
||||
crate::handlers::entries::handle_create,
|
||||
crate::handlers::entries::handle_get,
|
||||
crate::handlers::entries::handle_list,
|
||||
crate::handlers::entries::handle_update,
|
||||
crate::handlers::entries::handle_delete,
|
||||
crate::handlers::entries::handle_filter_by_mood,
|
||||
crate::handlers::entries::handle_filter_by_activity,
|
||||
crate::handlers::entries::handle_stats,
|
||||
crate::handlers::entries::handle_calendar,
|
||||
crate::handlers::entries::handle_activity_correlation,
|
||||
crate::handlers::entries::handle_delete_by_date_range,
|
||||
crate::handlers::entries::handle_replace_activity,
|
||||
crate::handlers::activities::handle_create,
|
||||
crate::handlers::activities::handle_get,
|
||||
crate::handlers::activities::handle_list,
|
||||
crate::handlers::activities::handle_rename,
|
||||
crate::handlers::activities::handle_set_category,
|
||||
crate::handlers::activities::handle_archive,
|
||||
crate::handlers::activities::handle_unarchive,
|
||||
crate::handlers::activities::handle_delete,
|
||||
crate::handlers::users::handle_register,
|
||||
crate::handlers::users::handle_get_profile,
|
||||
crate::handlers::users::handle_update_profile,
|
||||
crate::handlers::users::handle_change_password,
|
||||
crate::handlers::users::handle_delete,
|
||||
crate::handlers::users::handle_clear_data,
|
||||
crate::handlers::reminders::handle_create,
|
||||
crate::handlers::reminders::handle_get,
|
||||
crate::handlers::reminders::handle_list,
|
||||
crate::handlers::reminders::handle_update,
|
||||
crate::handlers::reminders::handle_delete,
|
||||
crate::handlers::media::handle_upload_photo,
|
||||
crate::handlers::media::handle_upload_voice_memo,
|
||||
crate::handlers::media::handle_serve_photo,
|
||||
crate::handlers::media::handle_serve_voice_memo,
|
||||
crate::handlers::media::handle_delete_photo,
|
||||
crate::handlers::media::handle_delete_voice_memo,
|
||||
crate::handlers::import_export::handle_export,
|
||||
crate::handlers::import_export::handle_import,
|
||||
crate::handlers::push::handle_vapid_key,
|
||||
crate::handlers::push::handle_subscribe,
|
||||
crate::handlers::push::handle_unsubscribe,
|
||||
crate::handlers::push::handle_test,
|
||||
),
|
||||
components(schemas(
|
||||
api_types::requests::CreateEntryRequest,
|
||||
api_types::requests::UpdateEntryRequest,
|
||||
api_types::requests::ListEntriesParams,
|
||||
api_types::requests::DateRangeParams,
|
||||
api_types::requests::ReplaceActivityRequest,
|
||||
api_types::requests::CreateActivityRequest,
|
||||
api_types::requests::RenameActivityRequest,
|
||||
api_types::requests::SetCategoryRequest,
|
||||
api_types::requests::RegisterRequest,
|
||||
api_types::requests::LoginRequest,
|
||||
api_types::requests::UpdateProfileRequest,
|
||||
api_types::requests::ChangePasswordRequest,
|
||||
api_types::requests::CreateReminderRequest,
|
||||
api_types::requests::UpdateReminderRequest,
|
||||
crate::handlers::auth::RefreshRequest,
|
||||
crate::handlers::auth::LogoutRequest,
|
||||
api_types::responses::EntryResponse,
|
||||
api_types::responses::ActivityResponse,
|
||||
api_types::responses::UserResponse,
|
||||
api_types::responses::ReminderResponse,
|
||||
api_types::responses::DayScheduleResponse,
|
||||
api_types::responses::MoodStatsResponse,
|
||||
api_types::responses::MoodFrequency,
|
||||
api_types::responses::CalendarDayResponse,
|
||||
api_types::responses::BulkActionResponse,
|
||||
api_types::responses::CorrelationResponse,
|
||||
api_types::responses::ImportResultResponse,
|
||||
api_types::responses::MediaIdResponse,
|
||||
api_types::requests::PushSubscribeRequest,
|
||||
api_types::requests::PushUnsubscribeRequest,
|
||||
)),
|
||||
tags(
|
||||
(name = "auth", description = "Authentication"),
|
||||
(name = "entries", description = "Mood entries"),
|
||||
(name = "activities", description = "Activity catalog"),
|
||||
(name = "users", description = "User management"),
|
||||
(name = "reminders", description = "Reminder schedules"),
|
||||
(name = "media", description = "Photo and voice memo storage"),
|
||||
(name = "data", description = "Import and export"),
|
||||
(name = "push", description = "Push notifications"),
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
struct SecurityAddon;
|
||||
|
||||
impl Modify for SecurityAddon {
|
||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||
let components = openapi.components.get_or_insert_with(Default::default);
|
||||
components.add_security_scheme(
|
||||
"bearer",
|
||||
SecurityScheme::Http(
|
||||
HttpBuilder::new()
|
||||
.scheme(HttpAuthScheme::Bearer)
|
||||
.bearer_format("JWT")
|
||||
.build(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
172
crates/adapters/http-axum/src/router.rs
Normal file
172
crates/adapters/http-axum/src/router.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::http::HeaderValue;
|
||||
use axum::routing::{delete, get, patch, post};
|
||||
use axum::{Json, Router};
|
||||
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_scalar::{Scalar, Servable};
|
||||
|
||||
use crate::handlers::{activities, auth, entries, import_export, media, push, reminders, users};
|
||||
use crate::openapi::ApiDoc;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub fn build_router(state: AppState) -> Router {
|
||||
let cors = build_cors(&state.server_config.cors);
|
||||
let body_limit = DefaultBodyLimit::max(state.server_config.max_body_size);
|
||||
|
||||
Router::new()
|
||||
.nest("/api/v1", api_routes())
|
||||
.route("/health", get(health))
|
||||
.route("/openapi.json", get(openapi_json))
|
||||
.merge(Scalar::with_url("/docs", ApiDoc::openapi()))
|
||||
.fallback_service(crate::spa::serve_spa(&state.server_config.spa_dir))
|
||||
.layer(body_limit)
|
||||
.layer(cors)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn openapi_json() -> Json<utoipa::openapi::OpenApi> {
|
||||
Json(ApiDoc::openapi())
|
||||
}
|
||||
|
||||
fn build_cors(config: &config::CorsConfig) -> CorsLayer {
|
||||
let layer = CorsLayer::new().allow_methods(Any).allow_headers(Any);
|
||||
|
||||
if config.allow_any_origin {
|
||||
return layer.allow_origin(Any);
|
||||
}
|
||||
|
||||
let origins: Vec<HeaderValue> = config
|
||||
.allowed_origins
|
||||
.iter()
|
||||
.filter_map(|o| o.parse().ok())
|
||||
.collect();
|
||||
|
||||
layer.allow_origin(AllowOrigin::list(origins))
|
||||
}
|
||||
|
||||
async fn health() -> Json<serde_json::Value> {
|
||||
Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
}))
|
||||
}
|
||||
|
||||
fn api_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest("/auth", auth_routes())
|
||||
.nest("/entries", entry_routes())
|
||||
.nest("/activities", activity_routes())
|
||||
.nest("/users", user_routes())
|
||||
.nest("/reminders", reminder_routes())
|
||||
.nest("/media", media_routes())
|
||||
.nest("/push", push_routes())
|
||||
.nest("/data", data_routes())
|
||||
}
|
||||
|
||||
fn auth_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/login", post(auth::handle_login))
|
||||
.route("/refresh", post(auth::handle_refresh))
|
||||
.route("/logout", post(auth::handle_logout))
|
||||
}
|
||||
|
||||
fn entry_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(entries::handle_list).post(entries::handle_create))
|
||||
.route(
|
||||
"/{id}",
|
||||
get(entries::handle_get)
|
||||
.patch(entries::handle_update)
|
||||
.delete(entries::handle_delete),
|
||||
)
|
||||
.route("/stats", get(entries::handle_stats))
|
||||
.route("/calendar", get(entries::handle_calendar))
|
||||
.route("/filter/mood/{mood}", get(entries::handle_filter_by_mood))
|
||||
.route(
|
||||
"/filter/activity/{id}",
|
||||
get(entries::handle_filter_by_activity),
|
||||
)
|
||||
.route(
|
||||
"/correlation/{id}",
|
||||
get(entries::handle_activity_correlation),
|
||||
)
|
||||
.route("/bulk/delete", delete(entries::handle_delete_by_date_range))
|
||||
.route(
|
||||
"/bulk/replace-activity",
|
||||
post(entries::handle_replace_activity),
|
||||
)
|
||||
}
|
||||
|
||||
fn activity_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/",
|
||||
get(activities::handle_list).post(activities::handle_create),
|
||||
)
|
||||
.route(
|
||||
"/{id}",
|
||||
get(activities::handle_get).delete(activities::handle_delete),
|
||||
)
|
||||
.route("/{id}/name", patch(activities::handle_rename))
|
||||
.route("/{id}/category", patch(activities::handle_set_category))
|
||||
.route("/{id}/archive", post(activities::handle_archive))
|
||||
.route("/{id}/unarchive", post(activities::handle_unarchive))
|
||||
}
|
||||
|
||||
fn user_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/register", post(users::handle_register))
|
||||
.route(
|
||||
"/me",
|
||||
get(users::handle_get_profile)
|
||||
.patch(users::handle_update_profile)
|
||||
.delete(users::handle_delete),
|
||||
)
|
||||
.route("/me/password", patch(users::handle_change_password))
|
||||
.route("/me/data", delete(users::handle_clear_data))
|
||||
}
|
||||
|
||||
fn reminder_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/",
|
||||
get(reminders::handle_list).post(reminders::handle_create),
|
||||
)
|
||||
.route(
|
||||
"/{id}",
|
||||
get(reminders::handle_get)
|
||||
.patch(reminders::handle_update)
|
||||
.delete(reminders::handle_delete),
|
||||
)
|
||||
}
|
||||
|
||||
fn media_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/photos", post(media::handle_upload_photo))
|
||||
.route(
|
||||
"/photos/{id}",
|
||||
get(media::handle_serve_photo).delete(media::handle_delete_photo),
|
||||
)
|
||||
.route("/voice-memos", post(media::handle_upload_voice_memo))
|
||||
.route(
|
||||
"/voice-memos/{id}",
|
||||
get(media::handle_serve_voice_memo).delete(media::handle_delete_voice_memo),
|
||||
)
|
||||
}
|
||||
|
||||
fn push_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/vapid-key", get(push::handle_vapid_key))
|
||||
.route("/subscribe", post(push::handle_subscribe))
|
||||
.route("/unsubscribe", post(push::handle_unsubscribe))
|
||||
.route("/test", post(push::handle_test))
|
||||
}
|
||||
|
||||
fn data_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/export", get(import_export::handle_export))
|
||||
.route("/import", post(import_export::handle_import))
|
||||
}
|
||||
5
crates/adapters/http-axum/src/spa.rs
Normal file
5
crates/adapters/http-axum/src/spa.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
|
||||
pub fn serve_spa(spa_dir: &str) -> ServeDir<ServeFile> {
|
||||
ServeDir::new(spa_dir).fallback(ServeFile::new(format!("{spa_dir}/index.html")))
|
||||
}
|
||||
39
crates/adapters/http-axum/src/state.rs
Normal file
39
crates/adapters/http-axum/src/state.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use config::{AuthConfig, EntryConfig, PresetConfig, PushConfig, ServerConfig};
|
||||
use domain::ports::{
|
||||
ActivityCommandPort, ActivityQueryPort, AuthServicePort, CascadeDeletePort, EventPublisherPort,
|
||||
ExportPort, ImportSourcePort, MediaStoragePort, MoodEntryCommandPort, MoodEntryQueryPort,
|
||||
PasswordHasherPort, PushSubscriptionCommandPort, PushSubscriptionQueryPort,
|
||||
RefreshSessionCommandPort, RefreshSessionQueryPort, ReminderCommandPort, ReminderQueryPort,
|
||||
ReminderSenderPort, UserCommandPort, UserQueryPort,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub entry_command: Arc<dyn MoodEntryCommandPort>,
|
||||
pub entry_query: Arc<dyn MoodEntryQueryPort>,
|
||||
pub activity_command: Arc<dyn ActivityCommandPort>,
|
||||
pub activity_query: Arc<dyn ActivityQueryPort>,
|
||||
pub user_command: Arc<dyn UserCommandPort>,
|
||||
pub user_query: Arc<dyn UserQueryPort>,
|
||||
pub reminder_command: Arc<dyn ReminderCommandPort>,
|
||||
pub reminder_query: Arc<dyn ReminderQueryPort>,
|
||||
pub refresh_session_command: Arc<dyn RefreshSessionCommandPort>,
|
||||
pub refresh_session_query: Arc<dyn RefreshSessionQueryPort>,
|
||||
pub cascade: Arc<dyn CascadeDeletePort>,
|
||||
pub auth_service: Arc<dyn AuthServicePort>,
|
||||
pub password_hasher: Arc<dyn PasswordHasherPort>,
|
||||
pub event_publisher: Arc<dyn EventPublisherPort>,
|
||||
pub media_storage: Arc<dyn MediaStoragePort>,
|
||||
pub export_port: Arc<dyn ExportPort>,
|
||||
pub import_source: Arc<dyn ImportSourcePort>,
|
||||
pub push_subscription_command: Arc<dyn PushSubscriptionCommandPort>,
|
||||
pub push_subscription_query: Arc<dyn PushSubscriptionQueryPort>,
|
||||
pub reminder_sender: Option<Arc<dyn ReminderSenderPort>>,
|
||||
pub server_config: ServerConfig,
|
||||
pub entry_config: EntryConfig,
|
||||
pub auth_config: AuthConfig,
|
||||
pub push_config: PushConfig,
|
||||
pub preset_config: PresetConfig,
|
||||
}
|
||||
Reference in New Issue
Block a user