Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/migrations/202601240027_create_share_mounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS share_mounts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
created_by uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
share_token TEXT NOT NULL,
target_document_id uuid NOT NULL,
target_document_type TEXT NOT NULL CHECK (target_document_type IN ('document','folder')),
target_title TEXT NOT NULL,
permission TEXT NOT NULL CHECK (permission IN ('view','edit')),
parent_folder_id uuid NULL REFERENCES documents(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_share_mounts_workspace_target
ON share_mounts(workspace_id, share_token, target_document_id);

CREATE INDEX IF NOT EXISTS idx_share_mounts_workspace
ON share_mounts(workspace_id);
2 changes: 1 addition & 1 deletion api/openapi/openapi.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions api/src/application/dto/shares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ pub struct ShareItemDto {
pub parent_share_id: Option<Uuid>,
}

#[derive(Debug, Clone)]
pub struct ShareMountDto {
pub id: Uuid,
pub token: String,
pub target_document_id: Uuid,
pub target_document_type: String,
pub target_title: String,
pub permission: String,
pub parent_folder_id: Option<Uuid>,
pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone)]
pub struct ApplicableShareDto {
pub token: String,
Expand Down
28 changes: 28 additions & 0 deletions api/src/application/ports/shares_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ pub struct ShareRow {
pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone)]
pub struct ShareMountRow {
pub id: Uuid,
pub token: String,
pub target_document_id: Uuid,
pub target_document_type: String,
pub target_title: String,
pub permission: String,
pub parent_folder_id: Option<Uuid>,
pub created_at: chrono::DateTime<chrono::Utc>,
}

#[async_trait]
pub trait SharesRepository: Send + Sync {
async fn create_share(
Expand Down Expand Up @@ -59,6 +71,22 @@ pub trait SharesRepository: Send + Sync {
)>,
>; // (share_id, permission, expires_at, shared_id, shared_type)

async fn list_share_mounts(&self, workspace_id: Uuid) -> anyhow::Result<Vec<ShareMountRow>>;

async fn create_share_mount(
&self,
workspace_id: Uuid,
actor_id: Uuid,
token: &str,
target_document_id: Uuid,
target_document_type: &str,
target_title: &str,
permission: &str,
parent_folder_id: Option<Uuid>,
) -> anyhow::Result<ShareMountRow>;

async fn delete_share_mount(&self, workspace_id: Uuid, mount_id: Uuid) -> anyhow::Result<bool>;

async fn get_share_document_meta(
&self,
token: &str,
Expand Down
7 changes: 7 additions & 0 deletions api/src/application/services/markdown/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub struct RenderOptions {
pub theme: Option<String>,
pub features: Option<Vec<String>>,
pub sanitize: Option<bool>,
/// If true, convert soft line breaks (single newlines) into <br> tags
pub hardbreaks: Option<bool>,
/// If provided, rewrite attachment-relative links/images to absolute under /uploads/{doc_id}
pub doc_id: Option<uuid::Uuid>,
/// If provided, prefix absolute URLs with this origin (e.g., https://api.example.com)
Expand Down Expand Up @@ -100,6 +102,11 @@ pub fn render(
}
// Provide data-sourcepos for editor<->preview sync
c_opts.render.sourcepos = true;
// Treat soft line breaks as <br>; default on for "doc" flavor unless explicitly disabled
let hardbreaks = opts
.hardbreaks
.unwrap_or_else(|| matches!(opts.flavor.as_deref(), Some(f) if f.eq_ignore_ascii_case("doc")));
c_opts.render.hardbreaks = hardbreaks;
// Allow HtmlBlock/HtmlInline to pass through; will be sanitized by ammonia afterwards
c_opts.render.unsafe_ = true;

Expand Down
111 changes: 109 additions & 2 deletions api/src/application/services/shares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use uuid::Uuid;

use crate::application::dto::shares::{
ActiveShareItemDto, ApplicableShareDto, CreatedShareDto, ShareBrowseResponseDto,
ShareDocumentDto, ShareItemDto,
ShareDocumentDto, ShareItemDto, ShareMountDto,
};
use crate::application::ports::shares_repository::SharesRepository;
use crate::application::services::errors::ServiceError;
Expand All @@ -15,7 +15,9 @@ use crate::application::use_cases::shares::list_active::ListActiveShares;
use crate::application::use_cases::shares::list_applicable::ListApplicableShares;
use crate::application::use_cases::shares::list_document_shares::ListDocumentShares;
use crate::application::use_cases::shares::validate_share::ValidateShare;
use crate::domain::workspaces::permissions::{PERM_SHARE_CREATE, PERM_SHARE_DELETE, PermissionSet};
use crate::domain::workspaces::permissions::{
PERM_DOC_VIEW, PERM_SHARE_CREATE, PERM_SHARE_DELETE, PermissionSet,
};

pub struct ShareService {
repo: Arc<dyn SharesRepository>,
Expand Down Expand Up @@ -151,6 +153,103 @@ impl ShareService {
})
}

pub async fn save_share_mount(
&self,
workspace_id: Uuid,
actor_id: Uuid,
permissions: &PermissionSet,
token: &str,
parent_folder_id: Option<Uuid>,
) -> Result<ShareMountDto, ServiceError> {
ensure_doc_view_permission(permissions)?;
let resolved = self
.repo
.resolve_share_by_token(token)
.await
.map_err(ServiceError::from)?
.ok_or(ServiceError::NotFound)?;
let (_share_id, permission, expires_at, target_document_id, target_document_type) =
resolved;
if let Some(exp) = expires_at {
if exp < chrono::Utc::now() {
return Err(ServiceError::NotFound);
}
}
let target_title = self
.repo
.validate_share_token(token)
.await
.map_err(ServiceError::from)?
.map(|(_, _, _, title)| title)
.unwrap_or_else(|| "Shared document".to_string());
let row = self
.repo
.create_share_mount(
workspace_id,
actor_id,
token,
target_document_id,
&target_document_type,
&target_title,
&permission,
parent_folder_id,
)
.await
.map_err(|err| match err.to_string().as_str() {
"invalid_parent" => ServiceError::BadRequest("invalid_parent"),
_ => ServiceError::Unexpected(err),
})?;
Ok(ShareMountDto {
id: row.id,
token: row.token,
target_document_id: row.target_document_id,
target_document_type: row.target_document_type,
target_title: row.target_title,
permission: row.permission,
parent_folder_id: row.parent_folder_id,
created_at: row.created_at,
})
}

pub async fn list_share_mounts(
&self,
workspace_id: Uuid,
permissions: &PermissionSet,
) -> Result<Vec<ShareMountDto>, ServiceError> {
ensure_doc_view_permission(permissions)?;
let rows = self
.repo
.list_share_mounts(workspace_id)
.await
.map_err(ServiceError::from)?;
Ok(rows
.into_iter()
.map(|row| ShareMountDto {
id: row.id,
token: row.token,
target_document_id: row.target_document_id,
target_document_type: row.target_document_type,
target_title: row.target_title,
permission: row.permission,
parent_folder_id: row.parent_folder_id,
created_at: row.created_at,
})
.collect())
}

pub async fn delete_share_mount(
&self,
workspace_id: Uuid,
permissions: &PermissionSet,
mount_id: Uuid,
) -> Result<bool, ServiceError> {
ensure_doc_view_permission(permissions)?;
self.repo
.delete_share_mount(workspace_id, mount_id)
.await
.map_err(ServiceError::from)
}

pub async fn share_document_meta(
&self,
token: &str,
Expand Down Expand Up @@ -184,3 +283,11 @@ fn ensure_share_delete_permission(permissions: &PermissionSet) -> Result<(), Ser
Err(ServiceError::Forbidden)
}
}

fn ensure_doc_view_permission(permissions: &PermissionSet) -> Result<(), ServiceError> {
if permissions.allows(PERM_DOC_VIEW) {
Ok(())
} else {
Err(ServiceError::Forbidden)
}
}
5 changes: 5 additions & 0 deletions api/src/bin/export-openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ use utoipa::OpenApi;
shares::validate_share_token,
shares::browse_share,
shares::list_active_shares,
shares::create_share_mount,
shares::list_share_mounts,
shares::delete_share_mount,
shares::list_applicable_shares,
shares::materialize_folder_share,
public::publish_document,
Expand Down Expand Up @@ -158,12 +161,14 @@ use utoipa::OpenApi;
files::UploadFileMultipart,
shares::CreateShareRequest,
shares::CreateShareResponse,
shares::CreateShareMountRequest,
shares::ShareItem,
shares::ShareDocumentResponse,
shares::ShareBrowseTreeItem,
shares::ShareBrowseResponse,
shares::ApplicableShareItem,
shares::ActiveShareItem,
shares::ShareMountItem,
shares::MaterializeResponse,
public::PublishResponse,
public::PublicDocumentSummary,
Expand Down
Loading