feat: add sharing endpoints — share, link, revoke, public access
This commit is contained in:
@@ -2,4 +2,5 @@ pub mod albums;
|
||||
pub mod assets;
|
||||
pub mod auth;
|
||||
pub mod health;
|
||||
pub mod sharing;
|
||||
pub mod storage;
|
||||
|
||||
128
crates/presentation/src/handlers/sharing.rs
Normal file
128
crates/presentation/src/handlers/sharing.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use crate::{errors::AppError, extractors::JwtClaims, state::AppState};
|
||||
use api_types::{
|
||||
requests::{GenerateShareLinkRequest, ShareResourceRequest},
|
||||
responses::{ShareLinkResponse, ShareScopeResponse, SharedResourceResponse},
|
||||
};
|
||||
use application::sharing::{
|
||||
AccessSharedResourceQuery, GenerateShareLinkCommand, RevokeShareCommand, ShareResourceCommand,
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use domain::{
|
||||
entities::{LinkAccessLevel, ShareableType, TargetType},
|
||||
errors::DomainError,
|
||||
value_objects::{DateTimeStamp, SystemId},
|
||||
};
|
||||
|
||||
const DEFAULT_ACCESS_LEVEL: &str = "view_only";
|
||||
|
||||
fn parse_shareable_type(s: &str) -> Result<ShareableType, AppError> {
|
||||
match s {
|
||||
"asset" => Ok(ShareableType::Asset),
|
||||
"album" => Ok(ShareableType::Album),
|
||||
"collection" => Ok(ShareableType::Collection),
|
||||
"directory" => Ok(ShareableType::Directory),
|
||||
_ => Err(AppError::from(DomainError::Validation(format!(
|
||||
"Invalid shareable type: {s}"
|
||||
)))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_target_type(s: &str) -> Result<TargetType, AppError> {
|
||||
match s {
|
||||
"user" => Ok(TargetType::User),
|
||||
"group" => Ok(TargetType::Group),
|
||||
_ => Err(AppError::from(DomainError::Validation(format!(
|
||||
"Invalid target type: {s}"
|
||||
)))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_access_level(s: &str) -> Result<LinkAccessLevel, AppError> {
|
||||
match s {
|
||||
"view_only" => Ok(LinkAccessLevel::ViewOnly),
|
||||
"limited_search" => Ok(LinkAccessLevel::LimitedSearch),
|
||||
_ => Err(AppError::from(DomainError::Validation(format!(
|
||||
"Invalid access level: {s}"
|
||||
)))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn share_resource(
|
||||
State(state): State<AppState>,
|
||||
claims: JwtClaims,
|
||||
Json(req): Json<ShareResourceRequest>,
|
||||
) -> Result<(StatusCode, Json<ShareScopeResponse>), AppError> {
|
||||
let shareable_type = parse_shareable_type(&req.shareable_type)?;
|
||||
let target_type = parse_target_type(&req.target_type)?;
|
||||
|
||||
let cmd = ShareResourceCommand {
|
||||
shareable_type,
|
||||
shareable_id: SystemId::from_uuid(req.shareable_id),
|
||||
target_type,
|
||||
target_id: SystemId::from_uuid(req.target_id),
|
||||
role_id: SystemId::from_uuid(req.role_id),
|
||||
created_by: claims.user_id,
|
||||
};
|
||||
let (scope, _target) = state.sharing.share_resource.execute(cmd).await?;
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(ShareScopeResponse::from_domain(&scope)),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn generate_link(
|
||||
State(state): State<AppState>,
|
||||
claims: JwtClaims,
|
||||
Json(req): Json<GenerateShareLinkRequest>,
|
||||
) -> Result<(StatusCode, Json<ShareLinkResponse>), AppError> {
|
||||
let shareable_type = parse_shareable_type(&req.shareable_type)?;
|
||||
let access_level =
|
||||
parse_access_level(req.access_level.as_deref().unwrap_or(DEFAULT_ACCESS_LEVEL))?;
|
||||
|
||||
let expires_at = req.expires_in_hours.map(|h| {
|
||||
DateTimeStamp::from_datetime(chrono::Utc::now() + chrono::Duration::hours(h as i64))
|
||||
});
|
||||
|
||||
let cmd = GenerateShareLinkCommand {
|
||||
shareable_type,
|
||||
shareable_id: SystemId::from_uuid(req.shareable_id),
|
||||
access_level,
|
||||
created_by: claims.user_id,
|
||||
expires_at,
|
||||
max_uses: req.max_uses,
|
||||
};
|
||||
let (_scope, link) = state.sharing.generate_link.execute(cmd).await?;
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(ShareLinkResponse::from_domain(&link)),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn revoke(
|
||||
State(state): State<AppState>,
|
||||
claims: JwtClaims,
|
||||
Path((scope_id,)): Path<(uuid::Uuid,)>,
|
||||
) -> Result<StatusCode, AppError> {
|
||||
let cmd = RevokeShareCommand {
|
||||
scope_id: SystemId::from_uuid(scope_id),
|
||||
revoked_by: claims.user_id,
|
||||
};
|
||||
state.sharing.revoke.execute(cmd).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub async fn access_by_token(
|
||||
State(state): State<AppState>,
|
||||
Path((token,)): Path<(String,)>,
|
||||
) -> Result<Json<SharedResourceResponse>, AppError> {
|
||||
let query = AccessSharedResourceQuery { token };
|
||||
let (scope, access_level) = state.sharing.access.execute(query).await?;
|
||||
Ok(Json(SharedResourceResponse::from_domain(
|
||||
&scope,
|
||||
access_level,
|
||||
)))
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
handlers::{albums, assets, auth, health, storage},
|
||||
handlers::{albums, assets, auth, health, sharing, storage},
|
||||
openapi::openapi_router,
|
||||
state::AppState,
|
||||
};
|
||||
@@ -28,6 +28,11 @@ pub fn api_v1_router() -> Router<AppState> {
|
||||
.route("/assets/{id}", get(assets::get_asset))
|
||||
.route("/assets/{id}/metadata", put(assets::update_metadata))
|
||||
.route("/assets/{id}/file", get(assets::serve_file))
|
||||
// sharing
|
||||
.route("/sharing", post(sharing::share_resource))
|
||||
.route("/sharing/links", post(sharing::generate_link))
|
||||
.route("/sharing/{id}", delete(sharing::revoke))
|
||||
.route("/sharing/access/{token}", get(sharing::access_by_token))
|
||||
// storage
|
||||
.route("/storage/volumes", post(storage::register_volume))
|
||||
.route(
|
||||
|
||||
@@ -4,6 +4,10 @@ use application::{
|
||||
catalog::{GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, UpdateMetadataHandler},
|
||||
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
|
||||
organization::{CreateAlbumHandler, GetAlbumHandler, ManageAlbumEntriesHandler},
|
||||
sharing::{
|
||||
AccessSharedResourceHandler, GenerateShareLinkHandler, RevokeShareHandler,
|
||||
ShareResourceHandler,
|
||||
},
|
||||
storage::{IngestAssetHandler, RegisterLibraryPathHandler, RegisterVolumeHandler},
|
||||
};
|
||||
use domain::ports::TokenIssuer;
|
||||
@@ -37,11 +41,20 @@ pub struct StorageHandlers {
|
||||
pub register_library_path: Arc<RegisterLibraryPathHandler>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SharingHandlers {
|
||||
pub share_resource: Arc<ShareResourceHandler>,
|
||||
pub generate_link: Arc<GenerateShareLinkHandler>,
|
||||
pub revoke: Arc<RevokeShareHandler>,
|
||||
pub access: Arc<AccessSharedResourceHandler>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub identity: IdentityHandlers,
|
||||
pub catalog: CatalogHandlers,
|
||||
pub organization: OrganizationHandlers,
|
||||
pub storage: StorageHandlers,
|
||||
pub sharing: SharingHandlers,
|
||||
pub token_issuer: Arc<dyn TokenIssuer>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user