handlers: image/reindex/auth via use cases; auth::authenticate replaces ensure_active; check-handler-services guard
All checks were successful
CI / Check / Test (push) Successful in 1h10m22s
All checks were successful
CI / Check / Test (push) Successful in 1h10m22s
This commit is contained in:
@@ -40,4 +40,4 @@ jobs:
|
|||||||
# Layering guards. These live in the Makefile and were previously local-only,
|
# Layering guards. These live in the Makefile and were previously local-only,
|
||||||
# so a PR could go green in CI while violating them.
|
# so a PR could go green in CI while violating them.
|
||||||
- name: guards
|
- name: guards
|
||||||
run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
|
run: make check-appcontext check-handler-deps check-handler-repos check-handler-services check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
# Layering guards. These live in the Makefile and were previously local-only,
|
# Layering guards. These live in the Makefile and were previously local-only,
|
||||||
# so a PR could go green in CI while violating them.
|
# so a PR could go green in CI while violating them.
|
||||||
- name: guards
|
- name: guards
|
||||||
run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
|
run: make check-appcontext check-handler-deps check-handler-repos check-handler-services check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
name: Build & Push Docker Image
|
name: Build & Push Docker Image
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ cd spa && bun install && bun run dev
|
|||||||
make check # the full gate; plain `make` runs it too
|
make check # the full gate; plain `make` runs it too
|
||||||
```
|
```
|
||||||
|
|
||||||
`make check` runs, in order: `fmt-check`, `clippy` (workspace, all targets, `-D warnings`), `test`, `test-federation-off` (workspace tests with default features off), then the layering guards (`check-appcontext`, `check-handler-deps`, `check-handler-repos`, `check-http-adapter-deps`, `check-federation-off-social`, `check-sqlite-is-ap-free`, `check-binaries-are-thin`, `check-federation-off-deps`, `check-nats-isolated`, `check-no-second-web-stack`). CI runs the same steps.
|
`make check` runs, in order: `fmt-check`, `clippy` (workspace, all targets, `-D warnings`), `test`, `test-federation-off` (workspace tests with default features off), then the layering guards (`check-appcontext`, `check-handler-deps`, `check-handler-repos`, `check-handler-services`, `check-http-adapter-deps`, `check-federation-off-social`, `check-sqlite-is-ap-free`, `check-binaries-are-thin`, `check-federation-off-deps`, `check-nats-isolated`, `check-no-second-web-stack`). CI runs the same steps.
|
||||||
|
|
||||||
For the SPA also run:
|
For the SPA also run:
|
||||||
|
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -1,6 +1,6 @@
|
|||||||
.DEFAULT_GOAL := check
|
.DEFAULT_GOAL := check
|
||||||
|
|
||||||
check: fmt-check clippy test test-federation-off check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
|
check: fmt-check clippy test test-federation-off check-appcontext check-handler-deps check-handler-repos check-handler-services check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
|
||||||
@echo "✅ All checks passed"
|
@echo "✅ All checks passed"
|
||||||
|
|
||||||
check-appcontext:
|
check-appcontext:
|
||||||
@@ -30,6 +30,16 @@ check-handler-repos:
|
|||||||
|
|
||||||
HTTP_ADAPTER_ALLOWED_ADAPTERS :=
|
HTTP_ADAPTER_ALLOWED_ADAPTERS :=
|
||||||
|
|
||||||
|
check-handler-services:
|
||||||
|
@hits=$$(find crates/adapters/http-axum/src -name '*.rs' -not -path '*/tests/*' -not -name tests.rs \
|
||||||
|
| xargs perl -0ne 'while (/app_ctx\s*\.\s*services\s*\.\s*([a-z_]+)/g) { print "$$ARGV: services.$$1\n" unless $$1 eq "review_logger" }'); \
|
||||||
|
if [ -n "$$hits" ]; then \
|
||||||
|
echo "❌ http-axum reaches a service port directly (call a use case instead):"; \
|
||||||
|
echo "$$hits"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo "✅ No direct service-port calls in http-axum"
|
||||||
|
|
||||||
check-http-adapter-deps:
|
check-http-adapter-deps:
|
||||||
@deps=$$(awk '/^\[dependencies\]/{f=1;next} /^\[/{f=0} f' crates/adapters/http-axum/Cargo.toml \
|
@deps=$$(awk '/^\[dependencies\]/{f=1;next} /^\[/{f=0} f' crates/adapters/http-axum/Cargo.toml \
|
||||||
| sed -n 's/^\([A-Za-z0-9_-]\{1,\}\)[[:space:]]*=.*/\1/p'); \
|
| sed -n 's/^\([A-Za-z0-9_-]\{1,\}\)[[:space:]]*=.*/\1/p'); \
|
||||||
@@ -167,4 +177,4 @@ dev-spa:
|
|||||||
spa-dev:
|
spa-dev:
|
||||||
@npm --prefix spa run dev
|
@npm --prefix spa run dev
|
||||||
|
|
||||||
.PHONY: check fmt fmt-check clippy test test-federation-off fix dev dev-stop dev-worker dev-spa spa-dev check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
|
.PHONY: check fmt fmt-check clippy test test-federation-off fix dev dev-stop dev-worker dev-spa spa-dev check-appcontext check-handler-deps check-handler-repos check-handler-services check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack
|
||||||
|
|||||||
@@ -163,8 +163,9 @@ pub async fn post_reindex_search(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_admin: crate::extractors::AdminApiUser,
|
_admin: crate::extractors::AdminApiUser,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let event = domain::events::DomainEvent::SearchReindexRequested;
|
match application::search::request_reindex::execute(&state.app_ctx.deps.search.request_reindex)
|
||||||
match state.app_ctx.services.event_publisher.publish(&event).await {
|
.await
|
||||||
|
{
|
||||||
Ok(()) => StatusCode::ACCEPTED.into_response(),
|
Ok(()) => StatusCode::ACCEPTED.into_response(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("failed to publish reindex event: {:?}", e);
|
tracing::error!("failed to publish reindex event: {:?}", e);
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ use domain::{errors::DomainError, value_objects::UserId};
|
|||||||
|
|
||||||
use crate::{errors::ApiError, state::AppState};
|
use crate::{errors::ApiError, state::AppState};
|
||||||
|
|
||||||
async fn ensure_active(state: &AppState, user_id: &UserId) -> Result<(), DomainError> {
|
async fn authenticate(state: &AppState, token: &str) -> Result<UserId, DomainError> {
|
||||||
application::users::ensure_active::execute(&state.app_ctx.deps.users.ensure_active, user_id)
|
application::auth::authenticate::execute(&state.app_ctx.deps.auth.authenticate, token).await
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AuthenticatedUser(pub UserId);
|
pub struct AuthenticatedUser(pub UserId);
|
||||||
@@ -36,13 +35,7 @@ where
|
|||||||
"Missing or invalid auth token".into(),
|
"Missing or invalid auth token".into(),
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
let user_id = app_state
|
let user_id = authenticate(&app_state, token).await?;
|
||||||
.app_ctx
|
|
||||||
.services
|
|
||||||
.auth
|
|
||||||
.validate_token(token)
|
|
||||||
.await?;
|
|
||||||
ensure_active(&app_state, &user_id).await?;
|
|
||||||
Ok(AuthenticatedUser(user_id))
|
Ok(AuthenticatedUser(user_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,24 +67,14 @@ where
|
|||||||
let Some(token) = extract_token_from_cookie(parts) else {
|
let Some(token) = extract_token_from_cookie(parts) else {
|
||||||
return Ok(OptionalCookieUser(None));
|
return Ok(OptionalCookieUser(None));
|
||||||
};
|
};
|
||||||
let user_id = app_state
|
let user_id = match authenticate(&app_state, &token).await {
|
||||||
.app_ctx
|
Ok(id) => Some(id),
|
||||||
.services
|
Err(e) => {
|
||||||
.auth
|
if e.is_transient() {
|
||||||
.validate_token(&token)
|
tracing::warn!("authenticate failed, treating as anonymous: {e:?}");
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
let user_id = match user_id {
|
|
||||||
Some(id) => match ensure_active(&app_state, &id).await {
|
|
||||||
Ok(()) => Some(id),
|
|
||||||
Err(e) => {
|
|
||||||
if e.is_transient() {
|
|
||||||
tracing::warn!("ensure_active failed, treating as anonymous: {e:?}");
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
},
|
None
|
||||||
None => None,
|
}
|
||||||
};
|
};
|
||||||
Ok(OptionalCookieUser(user_id))
|
Ok(OptionalCookieUser(user_id))
|
||||||
}
|
}
|
||||||
@@ -108,14 +91,7 @@ where
|
|||||||
let app_state = AppState::from_ref(state);
|
let app_state = AppState::from_ref(state);
|
||||||
let token = extract_token_from_cookie(parts)
|
let token = extract_token_from_cookie(parts)
|
||||||
.ok_or_else(|| Redirect::to("/login").into_response())?;
|
.ok_or_else(|| Redirect::to("/login").into_response())?;
|
||||||
let user_id = app_state
|
let user_id = authenticate(&app_state, &token)
|
||||||
.app_ctx
|
|
||||||
.services
|
|
||||||
.auth
|
|
||||||
.validate_token(&token)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Redirect::to("/login").into_response())?;
|
|
||||||
ensure_active(&app_state, &user_id)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| match e {
|
.map_err(|e| match e {
|
||||||
DomainError::InfrastructureError(_) => {
|
DomainError::InfrastructureError(_) => {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ pub async fn get_image(
|
|||||||
if key.starts_with("http://") || key.starts_with("https://") {
|
if key.starts_with("http://") || key.starts_with("https://") {
|
||||||
return axum::response::Redirect::temporary(&key).into_response();
|
return axum::response::Redirect::temporary(&key).into_response();
|
||||||
}
|
}
|
||||||
match state.app_ctx.services.object_storage.get(&key).await {
|
match application::images::get_image::execute(&state.app_ctx.deps.images.get_image, &key).await
|
||||||
|
{
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
let mime = infer::get(&bytes)
|
let mime = infer::get(&bytes)
|
||||||
.map(|t| t.mime_type())
|
.map(|t| t.mime_type())
|
||||||
|
|||||||
11
crates/application/src/auth/authenticate.rs
Normal file
11
crates/application/src/auth/authenticate.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use domain::{errors::DomainError, value_objects::UserId};
|
||||||
|
|
||||||
|
use crate::auth::deps::AuthenticateDeps;
|
||||||
|
|
||||||
|
pub async fn execute(deps: &AuthenticateDeps, token: &str) -> Result<UserId, DomainError> {
|
||||||
|
let user_id = deps.auth.validate_token(token).await?;
|
||||||
|
match deps.user.find_by_id(&user_id).await? {
|
||||||
|
Some(_) => Ok(user_id),
|
||||||
|
None => Err(DomainError::Unauthorized("account is not active".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,11 @@ pub struct RegisterAndLoginDeps {
|
|||||||
pub config: AppSettings,
|
pub config: AppSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AuthenticateDeps {
|
||||||
|
pub auth: Arc<dyn AuthService>,
|
||||||
|
pub user: Arc<dyn UserRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct LogoutDeps {
|
pub struct LogoutDeps {
|
||||||
pub refresh_session: Arc<dyn RefreshSessionRepository>,
|
pub refresh_session: Arc<dyn RefreshSessionRepository>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod authenticate;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod deps;
|
pub mod deps;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use domain::ports::{EventPublisher, MediaServerParser, ObjectStorage, PersonEnrichmentClient};
|
use domain::ports::{EventPublisher, MediaServerParser, ObjectStorage, PersonEnrichmentClient};
|
||||||
|
|
||||||
use crate::auth::deps::{LoginDeps, LogoutDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps};
|
use crate::auth::deps::{
|
||||||
|
AuthenticateDeps, LoginDeps, LogoutDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps,
|
||||||
|
};
|
||||||
use crate::diary::deps::{
|
use crate::diary::deps::{
|
||||||
DeleteReviewDeps, EditReviewDeps, ExportDiaryDeps, GetActivityFeedDeps, GetDiaryDeps,
|
DeleteReviewDeps, EditReviewDeps, ExportDiaryDeps, GetActivityFeedDeps, GetDiaryDeps,
|
||||||
GetMovieSocialPageDeps, GetReviewHistoryDeps, GetUserFeedDeps,
|
GetMovieSocialPageDeps, GetReviewHistoryDeps, GetUserFeedDeps,
|
||||||
};
|
};
|
||||||
use crate::goals::deps::{GoalCommandDeps, GoalQueryDeps};
|
use crate::goals::deps::{GoalCommandDeps, GoalQueryDeps};
|
||||||
|
use crate::images::deps::GetImageDeps;
|
||||||
use crate::import::deps::{
|
use crate::import::deps::{
|
||||||
ApplyMappingDeps, ApplyProfileAndMapDeps, ApplyProfileDeps, CreateSessionDeps,
|
ApplyMappingDeps, ApplyProfileAndMapDeps, ApplyProfileDeps, CreateSessionDeps,
|
||||||
DeleteImportProfileDeps, ExecuteImportDeps, GetMappingStageDeps, GetPreviewStageDeps,
|
DeleteImportProfileDeps, ExecuteImportDeps, GetMappingStageDeps, GetPreviewStageDeps,
|
||||||
@@ -23,10 +26,10 @@ use crate::movies::deps::{
|
|||||||
};
|
};
|
||||||
use crate::movies::merge_duplicates::MergeDuplicatesDeps;
|
use crate::movies::merge_duplicates::MergeDuplicatesDeps;
|
||||||
use crate::person::deps::{EnrichPersonDeps, GetPersonDeps};
|
use crate::person::deps::{EnrichPersonDeps, GetPersonDeps};
|
||||||
use crate::search::deps::SearchDeps;
|
use crate::search::deps::{RequestReindexDeps, SearchDeps};
|
||||||
use crate::social::deps::{SocialCommandDeps, SocialQueryDeps};
|
use crate::social::deps::{SocialCommandDeps, SocialQueryDeps};
|
||||||
use crate::users::deps::{
|
use crate::users::deps::{
|
||||||
AuthorizeAdminDeps, EnsureActiveDeps, GetCurrentProfileDeps, GetFederatedProfileDeps,
|
AuthorizeAdminDeps, GetCurrentProfileDeps, GetFederatedProfileDeps,
|
||||||
GetFederatedProfileStatsDeps, GetLocalProfileDeps, GetPageViewerDeps, GetProfileSettingsDeps,
|
GetFederatedProfileStatsDeps, GetLocalProfileDeps, GetPageViewerDeps, GetProfileSettingsDeps,
|
||||||
GetSettingsDeps, GetUsersListDeps, RequestAccountDeletionDeps, ResolveUsernameDeps,
|
GetSettingsDeps, GetUsersListDeps, RequestAccountDeletionDeps, ResolveUsernameDeps,
|
||||||
UpdateProfileDeps, UpdateProfileFieldsDeps, UpdateSettingsDeps,
|
UpdateProfileDeps, UpdateProfileFieldsDeps, UpdateSettingsDeps,
|
||||||
@@ -46,6 +49,7 @@ pub struct AuthGroup {
|
|||||||
pub refresh: RefreshDeps,
|
pub refresh: RefreshDeps,
|
||||||
pub register_and_login: RegisterAndLoginDeps,
|
pub register_and_login: RegisterAndLoginDeps,
|
||||||
pub logout: LogoutDeps,
|
pub logout: LogoutDeps,
|
||||||
|
pub authenticate: AuthenticateDeps,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DiaryGroup {
|
pub struct DiaryGroup {
|
||||||
@@ -64,6 +68,10 @@ pub struct GoalsGroup {
|
|||||||
pub query: GoalQueryDeps,
|
pub query: GoalQueryDeps,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ImagesGroup {
|
||||||
|
pub get_image: GetImageDeps,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ImportGroup {
|
pub struct ImportGroup {
|
||||||
pub create_session: CreateSessionDeps,
|
pub create_session: CreateSessionDeps,
|
||||||
pub apply_mapping: ApplyMappingDeps,
|
pub apply_mapping: ApplyMappingDeps,
|
||||||
@@ -109,6 +117,7 @@ pub struct PersonGroup {
|
|||||||
|
|
||||||
pub struct SearchGroup {
|
pub struct SearchGroup {
|
||||||
pub execute: SearchDeps,
|
pub execute: SearchDeps,
|
||||||
|
pub request_reindex: RequestReindexDeps,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SocialGroup {
|
pub struct SocialGroup {
|
||||||
@@ -125,7 +134,6 @@ pub struct UsersGroup {
|
|||||||
pub get_users_list: GetUsersListDeps,
|
pub get_users_list: GetUsersListDeps,
|
||||||
pub update_profile: UpdateProfileDeps,
|
pub update_profile: UpdateProfileDeps,
|
||||||
pub request_account_deletion: RequestAccountDeletionDeps,
|
pub request_account_deletion: RequestAccountDeletionDeps,
|
||||||
pub ensure_active: EnsureActiveDeps,
|
|
||||||
pub get_current_profile: GetCurrentProfileDeps,
|
pub get_current_profile: GetCurrentProfileDeps,
|
||||||
pub update_profile_fields: UpdateProfileFieldsDeps,
|
pub update_profile_fields: UpdateProfileFieldsDeps,
|
||||||
pub get_settings: GetSettingsDeps,
|
pub get_settings: GetSettingsDeps,
|
||||||
@@ -163,6 +171,7 @@ pub struct Deps {
|
|||||||
pub auth: AuthGroup,
|
pub auth: AuthGroup,
|
||||||
pub diary: DiaryGroup,
|
pub diary: DiaryGroup,
|
||||||
pub goals: GoalsGroup,
|
pub goals: GoalsGroup,
|
||||||
|
pub images: ImagesGroup,
|
||||||
pub import: ImportGroup,
|
pub import: ImportGroup,
|
||||||
pub insights: InsightsGroup,
|
pub insights: InsightsGroup,
|
||||||
pub integrations: IntegrationsGroup,
|
pub integrations: IntegrationsGroup,
|
||||||
|
|||||||
7
crates/application/src/images/deps.rs
Normal file
7
crates/application/src/images/deps.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use domain::ports::ObjectStorage;
|
||||||
|
|
||||||
|
pub struct GetImageDeps {
|
||||||
|
pub object_storage: Arc<dyn ObjectStorage>,
|
||||||
|
}
|
||||||
7
crates/application/src/images/get_image.rs
Normal file
7
crates/application/src/images/get_image.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
use domain::errors::DomainError;
|
||||||
|
|
||||||
|
use crate::images::deps::GetImageDeps;
|
||||||
|
|
||||||
|
pub async fn execute(deps: &GetImageDeps, key: &str) -> Result<Vec<u8>, DomainError> {
|
||||||
|
deps.object_storage.get(key).await
|
||||||
|
}
|
||||||
2
crates/application/src/images/mod.rs
Normal file
2
crates/application/src/images/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod deps;
|
||||||
|
pub mod get_image;
|
||||||
@@ -8,6 +8,7 @@ pub mod worker;
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod diary;
|
pub mod diary;
|
||||||
pub mod goals;
|
pub mod goals;
|
||||||
|
pub mod images;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod insights;
|
pub mod insights;
|
||||||
pub mod integrations;
|
pub mod integrations;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::ports::SearchPort;
|
use domain::ports::{EventPublisher, SearchPort};
|
||||||
|
|
||||||
pub struct SearchDeps {
|
pub struct SearchDeps {
|
||||||
pub search_port: Arc<dyn SearchPort>,
|
pub search_port: Arc<dyn SearchPort>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RequestReindexDeps {
|
||||||
|
pub event_publisher: Arc<dyn EventPublisher>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod deps;
|
pub mod deps;
|
||||||
pub mod execute;
|
pub mod execute;
|
||||||
|
pub mod request_reindex;
|
||||||
|
|||||||
9
crates/application/src/search/request_reindex.rs
Normal file
9
crates/application/src/search/request_reindex.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use domain::{errors::DomainError, events::DomainEvent};
|
||||||
|
|
||||||
|
use crate::search::deps::RequestReindexDeps;
|
||||||
|
|
||||||
|
pub async fn execute(deps: &RequestReindexDeps) -> Result<(), DomainError> {
|
||||||
|
deps.event_publisher
|
||||||
|
.publish(&DomainEvent::SearchReindexRequested)
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -52,10 +52,6 @@ pub struct RequestAccountDeletionDeps {
|
|||||||
pub password_hasher: Arc<dyn PasswordHasher>,
|
pub password_hasher: Arc<dyn PasswordHasher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EnsureActiveDeps {
|
|
||||||
pub user: Arc<dyn UserRepository>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GetCurrentProfileDeps {
|
pub struct GetCurrentProfileDeps {
|
||||||
pub user: Arc<dyn UserRepository>,
|
pub user: Arc<dyn UserRepository>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
use domain::{errors::DomainError, value_objects::UserId};
|
|
||||||
|
|
||||||
use crate::users::deps::EnsureActiveDeps;
|
|
||||||
|
|
||||||
pub async fn execute(deps: &EnsureActiveDeps, user: &UserId) -> Result<(), DomainError> {
|
|
||||||
match deps.user.find_by_id(user).await? {
|
|
||||||
Some(_) => Ok(()),
|
|
||||||
None => Err(DomainError::Unauthorized("account is not active".into())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ pub mod authorize_admin;
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod deps;
|
pub mod deps;
|
||||||
mod diary_filter;
|
mod diary_filter;
|
||||||
pub mod ensure_active;
|
|
||||||
pub mod get_current_profile;
|
pub mod get_current_profile;
|
||||||
pub mod get_federated_profile;
|
pub mod get_federated_profile;
|
||||||
pub mod get_federated_profile_stats;
|
pub mod get_federated_profile_stats;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
mod support;
|
mod support;
|
||||||
|
|
||||||
|
#[path = "auth/authenticate_test.rs"]
|
||||||
|
mod authenticate_test;
|
||||||
#[path = "auth/login_test.rs"]
|
#[path = "auth/login_test.rs"]
|
||||||
mod login_test;
|
mod login_test;
|
||||||
#[path = "auth/logout_test.rs"]
|
#[path = "auth/logout_test.rs"]
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ use domain::{
|
|||||||
value_objects::{Email, UserId},
|
value_objects::{Email, UserId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use application::{
|
use application::auth::{
|
||||||
auth::{deps::RegisterDeps, register},
|
authenticate,
|
||||||
users::{deps::EnsureActiveDeps, ensure_active},
|
deps::{AuthenticateDeps, RegisterDeps},
|
||||||
|
register,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::support::{TestContextBuilder, register_command};
|
use crate::support::{TestContextBuilder, register_command};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_live_user_is_active() {
|
async fn a_live_users_token_authenticates() {
|
||||||
let store = InMemoryStore::new();
|
let store = InMemoryStore::new();
|
||||||
let b = TestContextBuilder::new().with_users(Arc::clone(&store) as _);
|
let b = TestContextBuilder::new().with_users(Arc::clone(&store) as _);
|
||||||
let reg = RegisterDeps {
|
let reg = RegisterDeps {
|
||||||
@@ -33,15 +34,20 @@ async fn a_live_user_is_active() {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.id()
|
.id()
|
||||||
.clone();
|
.clone();
|
||||||
let deps = EnsureActiveDeps {
|
let deps = AuthenticateDeps {
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
user: b.user_repo.clone(),
|
user: b.user_repo.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ensure_active::execute(&deps, &id).await.unwrap();
|
let authenticated = authenticate::execute(&deps, &id.value().to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(authenticated, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_deleted_user_is_rejected() {
|
async fn a_deleted_users_token_is_rejected() {
|
||||||
let store = InMemoryStore::new();
|
let store = InMemoryStore::new();
|
||||||
let b = TestContextBuilder::new().with_users(Arc::clone(&store) as _);
|
let b = TestContextBuilder::new().with_users(Arc::clone(&store) as _);
|
||||||
let reg = RegisterDeps {
|
let reg = RegisterDeps {
|
||||||
@@ -60,23 +66,42 @@ async fn a_deleted_user_is_rejected() {
|
|||||||
.id()
|
.id()
|
||||||
.clone();
|
.clone();
|
||||||
store.mark_deleted(&id, &[]).await.unwrap();
|
store.mark_deleted(&id, &[]).await.unwrap();
|
||||||
let deps = EnsureActiveDeps {
|
let deps = AuthenticateDeps {
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
user: b.user_repo.clone(),
|
user: b.user_repo.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let err = ensure_active::execute(&deps, &id).await.unwrap_err();
|
let err = authenticate::execute(&deps, &id.value().to_string())
|
||||||
|
.await
|
||||||
assert!(matches!(err, DomainError::Unauthorized(_)), "got {err:?}");
|
.unwrap_err();
|
||||||
}
|
|
||||||
|
assert!(matches!(err, DomainError::Unauthorized(_)), "got {err:?}");
|
||||||
#[tokio::test]
|
}
|
||||||
async fn an_unknown_user_is_rejected() {
|
|
||||||
let b = TestContextBuilder::new();
|
#[tokio::test]
|
||||||
let deps = EnsureActiveDeps {
|
async fn an_unknown_users_token_is_rejected() {
|
||||||
user: b.user_repo.clone(),
|
let b = TestContextBuilder::new();
|
||||||
};
|
let deps = AuthenticateDeps {
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
let err = ensure_active::execute(&deps, &UserId::generate())
|
user: b.user_repo.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = authenticate::execute(&deps, &UserId::generate().value().to_string())
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(err, DomainError::Unauthorized(_)), "got {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_invalid_token_is_rejected() {
|
||||||
|
let b = TestContextBuilder::new();
|
||||||
|
let deps = AuthenticateDeps {
|
||||||
|
auth: b.auth_service.clone(),
|
||||||
|
user: b.user_repo.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = authenticate::execute(&deps, "not-a-token")
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
2
crates/application/tests/images.rs
Normal file
2
crates/application/tests/images.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#[path = "images/get_image_test.rs"]
|
||||||
|
mod get_image_test;
|
||||||
76
crates/application/tests/images/get_image_test.rs
Normal file
76
crates/application/tests/images/get_image_test.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use domain::{errors::DomainError, ports::ObjectStorage};
|
||||||
|
|
||||||
|
use application::images::{deps::GetImageDeps, get_image};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Storage {
|
||||||
|
objects: Mutex<HashMap<String, Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ObjectStorage for Storage {
|
||||||
|
async fn store(&self, key: &str, image_bytes: &[u8]) -> Result<String, DomainError> {
|
||||||
|
self.objects
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(key.to_string(), image_bytes.to_vec());
|
||||||
|
Ok(key.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, key: &str) -> Result<Vec<u8>, DomainError> {
|
||||||
|
self.objects
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(key)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| DomainError::NotFound(key.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_stream(
|
||||||
|
&self,
|
||||||
|
_key: &str,
|
||||||
|
) -> Result<futures::stream::BoxStream<'static, Result<bytes::Bytes, DomainError>>, DomainError>
|
||||||
|
{
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, _key: &str) -> Result<(), DomainError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exists(&self, key: &str) -> Result<bool, DomainError> {
|
||||||
|
Ok(self.objects.lock().unwrap().contains_key(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn returns_the_stored_bytes() {
|
||||||
|
let storage = Arc::new(Storage::default());
|
||||||
|
storage.store("posters/1.webp", b"image").await.unwrap();
|
||||||
|
let deps = GetImageDeps {
|
||||||
|
object_storage: storage,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bytes = get_image::execute(&deps, "posters/1.webp").await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(bytes, b"image");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_missing_key_is_not_found() {
|
||||||
|
let deps = GetImageDeps {
|
||||||
|
object_storage: Arc::new(Storage::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = get_image::execute(&deps, "posters/missing.webp")
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(err, DomainError::NotFound(_)));
|
||||||
|
}
|
||||||
@@ -2,3 +2,6 @@ mod support;
|
|||||||
|
|
||||||
#[path = "search/execute_test.rs"]
|
#[path = "search/execute_test.rs"]
|
||||||
mod execute_test;
|
mod execute_test;
|
||||||
|
|
||||||
|
#[path = "search/request_reindex_test.rs"]
|
||||||
|
mod request_reindex_test;
|
||||||
|
|||||||
33
crates/application/tests/search/request_reindex_test.rs
Normal file
33
crates/application/tests/search/request_reindex_test.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
events::DomainEvent,
|
||||||
|
testing::{NoopEventPublisher, RefusingEventPublisher},
|
||||||
|
};
|
||||||
|
|
||||||
|
use application::search::{deps::RequestReindexDeps, request_reindex};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn publishes_a_reindex_request() {
|
||||||
|
let publisher = NoopEventPublisher::new();
|
||||||
|
let deps = RequestReindexDeps {
|
||||||
|
event_publisher: publisher.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
request_reindex::execute(&deps).await.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
publisher.published().as_slice(),
|
||||||
|
[DomainEvent::SearchReindexRequested]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_publish_failure_is_returned() {
|
||||||
|
let deps = RequestReindexDeps {
|
||||||
|
event_publisher: std::sync::Arc::new(RefusingEventPublisher),
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = request_reindex::execute(&deps).await.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(err, DomainError::InfrastructureError(_)));
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@ mod support;
|
|||||||
|
|
||||||
#[path = "users/authorize_admin_test.rs"]
|
#[path = "users/authorize_admin_test.rs"]
|
||||||
mod authorize_admin_test;
|
mod authorize_admin_test;
|
||||||
#[path = "users/ensure_active_test.rs"]
|
|
||||||
mod ensure_active_test;
|
|
||||||
#[path = "users/get_current_profile_test.rs"]
|
#[path = "users/get_current_profile_test.rs"]
|
||||||
mod get_current_profile_test;
|
mod get_current_profile_test;
|
||||||
#[path = "users/get_federated_profile_stats_test.rs"]
|
#[path = "users/get_federated_profile_stats_test.rs"]
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
use application::auth::deps::{
|
use application::auth::deps::{
|
||||||
LoginDeps, LogoutDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps,
|
AuthenticateDeps, LoginDeps, LogoutDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps,
|
||||||
};
|
};
|
||||||
use application::deps::{
|
use application::deps::{
|
||||||
AuthGroup, Deps, DiaryGroup, GoalsGroup, ImportGroup, InsightsGroup, IntegrationsGroup,
|
AuthGroup, Deps, DiaryGroup, GoalsGroup, ImagesGroup, ImportGroup, InsightsGroup,
|
||||||
MoviesGroup, PersonGroup, SearchGroup, SocialGroup, UsersGroup, WatchlistGroup, WorkerDeps,
|
IntegrationsGroup, MoviesGroup, PersonGroup, SearchGroup, SocialGroup, UsersGroup,
|
||||||
WrapupGroup,
|
WatchlistGroup, WorkerDeps, WrapupGroup,
|
||||||
};
|
};
|
||||||
use application::diary::deps::{
|
use application::diary::deps::{
|
||||||
DeleteReviewDeps, EditReviewDeps, ExportDiaryDeps, GetActivityFeedDeps, GetDiaryDeps,
|
DeleteReviewDeps, EditReviewDeps, ExportDiaryDeps, GetActivityFeedDeps, GetDiaryDeps,
|
||||||
GetMovieSocialPageDeps, GetReviewHistoryDeps, GetUserFeedDeps,
|
GetMovieSocialPageDeps, GetReviewHistoryDeps, GetUserFeedDeps,
|
||||||
};
|
};
|
||||||
use application::goals::deps::{GoalCommandDeps, GoalQueryDeps};
|
use application::goals::deps::{GoalCommandDeps, GoalQueryDeps};
|
||||||
|
use application::images::deps::GetImageDeps;
|
||||||
use application::import::deps::{
|
use application::import::deps::{
|
||||||
ApplyMappingDeps, ApplyProfileAndMapDeps, ApplyProfileDeps, CreateSessionDeps,
|
ApplyMappingDeps, ApplyProfileAndMapDeps, ApplyProfileDeps, CreateSessionDeps,
|
||||||
DeleteImportProfileDeps, ExecuteImportDeps, GetMappingStageDeps, GetPreviewStageDeps,
|
DeleteImportProfileDeps, ExecuteImportDeps, GetMappingStageDeps, GetPreviewStageDeps,
|
||||||
@@ -26,10 +27,10 @@ use application::movies::deps::{
|
|||||||
};
|
};
|
||||||
use application::movies::merge_duplicates::MergeDuplicatesDeps;
|
use application::movies::merge_duplicates::MergeDuplicatesDeps;
|
||||||
use application::person::deps::{EnrichPersonDeps, GetPersonDeps};
|
use application::person::deps::{EnrichPersonDeps, GetPersonDeps};
|
||||||
use application::search::deps::SearchDeps;
|
use application::search::deps::{RequestReindexDeps, SearchDeps};
|
||||||
use application::social::deps::{SocialCommandDeps, SocialQueryDeps};
|
use application::social::deps::{SocialCommandDeps, SocialQueryDeps};
|
||||||
use application::users::deps::{
|
use application::users::deps::{
|
||||||
AuthorizeAdminDeps, EnsureActiveDeps, GetCurrentProfileDeps, GetFederatedProfileDeps,
|
AuthorizeAdminDeps, GetCurrentProfileDeps, GetFederatedProfileDeps,
|
||||||
GetFederatedProfileStatsDeps, GetLocalProfileDeps, GetPageViewerDeps, GetProfileSettingsDeps,
|
GetFederatedProfileStatsDeps, GetLocalProfileDeps, GetPageViewerDeps, GetProfileSettingsDeps,
|
||||||
GetSettingsDeps, GetUsersListDeps, RequestAccountDeletionDeps, ResolveUsernameDeps,
|
GetSettingsDeps, GetUsersListDeps, RequestAccountDeletionDeps, ResolveUsernameDeps,
|
||||||
UpdateProfileDeps, UpdateProfileFieldsDeps, UpdateSettingsDeps,
|
UpdateProfileDeps, UpdateProfileFieldsDeps, UpdateSettingsDeps,
|
||||||
@@ -84,6 +85,10 @@ pub fn build_deps(
|
|||||||
logout: LogoutDeps {
|
logout: LogoutDeps {
|
||||||
refresh_session: repos.refresh_session.clone(),
|
refresh_session: repos.refresh_session.clone(),
|
||||||
},
|
},
|
||||||
|
authenticate: AuthenticateDeps {
|
||||||
|
auth: services.auth.clone(),
|
||||||
|
user: repos.user.clone(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
diary: DiaryGroup {
|
diary: DiaryGroup {
|
||||||
delete_review: DeleteReviewDeps {
|
delete_review: DeleteReviewDeps {
|
||||||
@@ -129,6 +134,11 @@ pub fn build_deps(
|
|||||||
stats: repos.stats.clone(),
|
stats: repos.stats.clone(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
images: ImagesGroup {
|
||||||
|
get_image: GetImageDeps {
|
||||||
|
object_storage: services.object_storage.clone(),
|
||||||
|
},
|
||||||
|
},
|
||||||
import: ImportGroup {
|
import: ImportGroup {
|
||||||
create_session: CreateSessionDeps {
|
create_session: CreateSessionDeps {
|
||||||
import_session: repos.import_session.clone(),
|
import_session: repos.import_session.clone(),
|
||||||
@@ -236,6 +246,9 @@ pub fn build_deps(
|
|||||||
execute: SearchDeps {
|
execute: SearchDeps {
|
||||||
search_port: repos.search_port.clone(),
|
search_port: repos.search_port.clone(),
|
||||||
},
|
},
|
||||||
|
request_reindex: RequestReindexDeps {
|
||||||
|
event_publisher: services.event_publisher.clone(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
social: SocialGroup {
|
social: SocialGroup {
|
||||||
command: SocialCommandDeps {
|
command: SocialCommandDeps {
|
||||||
@@ -284,9 +297,6 @@ pub fn build_deps(
|
|||||||
user: repos.user.clone(),
|
user: repos.user.clone(),
|
||||||
password_hasher: services.password_hasher.clone(),
|
password_hasher: services.password_hasher.clone(),
|
||||||
},
|
},
|
||||||
ensure_active: EnsureActiveDeps {
|
|
||||||
user: repos.user.clone(),
|
|
||||||
},
|
|
||||||
get_current_profile: GetCurrentProfileDeps {
|
get_current_profile: GetCurrentProfileDeps {
|
||||||
user: repos.user.clone(),
|
user: repos.user.clone(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ exists), so two admins deleting each other concurrently cannot both succeed; the
|
|||||||
`count_active_admins` check is only the early, friendly refusal (409).
|
`count_active_admins` check is only the early, friendly refusal (409).
|
||||||
|
|
||||||
**Immediate effects, before the purge runs.** Login is refused and tokens are rejected
|
**Immediate effects, before the purge runs.** Login is refused and tokens are rejected
|
||||||
(`ensure_active` in the extractors; the user lookups filter `deleted_at IS NULL`). The user's
|
(`auth::authenticate` in the extractors; the user lookups filter `deleted_at IS NULL`). The user's
|
||||||
webhook tokens stop ingesting at once (`find_by_token_hash` ignores tokens of tombstoned users,
|
webhook tokens stop ingesting at once (`find_by_token_hash` ignores tokens of tombstoned users,
|
||||||
so the endpoint answers 401). The profile
|
so the endpoint answers 401). The profile
|
||||||
and `?embed=true` pages 404. The ActivityPub actor document, inbox, outbox and featured answer
|
and `?embed=true` pages 404. The ActivityPub actor document, inbox, outbox and featured answer
|
||||||
|
|||||||
Reference in New Issue
Block a user