multi tenancy part 1
This commit is contained in:
+273
-121
@@ -18,7 +18,7 @@ use tracing::{debug, error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::folders::gather_descendant_folder_ids;
|
||||
use crate::auth::AuthenticatedUser;
|
||||
use crate::auth::TenantScopedConn;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::jobs::{enqueue_job, JOB_ANALYZE_DOCUMENT};
|
||||
use crate::models::{
|
||||
@@ -372,10 +372,13 @@ pub struct AssetObjectsQuery {
|
||||
pub async fn list_documents(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<DocumentListQuery>,
|
||||
user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<DocumentResponse>>> {
|
||||
let mut conn = state.db()?;
|
||||
|
||||
let DocumentListQuery {
|
||||
folder_id,
|
||||
include_deleted,
|
||||
@@ -385,7 +388,9 @@ pub async fn list_documents(
|
||||
correspondents,
|
||||
} = params;
|
||||
|
||||
let mut docs_query = documents::table.into_boxed();
|
||||
let mut docs_query = documents::table
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.into_boxed();
|
||||
|
||||
if !include_deleted {
|
||||
docs_query = docs_query.filter(documents::deleted_at.is_null());
|
||||
@@ -414,7 +419,7 @@ pub async fn list_documents(
|
||||
|
||||
match (folder_id, include_descendants) {
|
||||
(Some(folder_id), true) => {
|
||||
let descendant_ids = gather_descendant_folder_ids(&mut conn, folder_id)?;
|
||||
let descendant_ids = gather_descendant_folder_ids(&mut conn, tenant_id, folder_id)?;
|
||||
docs_query = docs_query.filter(documents::folder_id.eq_any(descendant_ids));
|
||||
}
|
||||
(Some(folder_id), false) => {
|
||||
@@ -598,7 +603,7 @@ pub async fn list_documents(
|
||||
let mut correspondents_map = load_correspondents_for_documents(&mut conn, &doc_ids)?;
|
||||
drop(conn);
|
||||
|
||||
let primary_versions = load_primary_assets(&state, &docs).await?;
|
||||
let primary_versions = load_primary_assets(&state, tenant_id, &docs).await?;
|
||||
let mut response = Vec::with_capacity(doc_ids.len());
|
||||
for doc in docs {
|
||||
let tags = tags_map.get(&doc.id).cloned();
|
||||
@@ -606,7 +611,7 @@ pub async fn list_documents(
|
||||
let current_version = primary_versions.get(&doc.id).cloned();
|
||||
response.push(to_document_response(
|
||||
&state,
|
||||
user.user_id,
|
||||
user_id,
|
||||
doc,
|
||||
tags,
|
||||
correspondents,
|
||||
@@ -620,11 +625,17 @@ pub async fn list_documents(
|
||||
pub async fn get_document(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<DocumentDetailResponse>> {
|
||||
let mut conn = state.db()?;
|
||||
|
||||
let doc: Document = documents::table.find(document_id).first(&mut conn)?;
|
||||
let doc: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)?;
|
||||
if doc.deleted_at.is_some() {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
@@ -638,13 +649,13 @@ pub async fn get_document(
|
||||
let version_id = current_version.id;
|
||||
drop(conn);
|
||||
|
||||
let assets = load_asset_responses(&state, version_id).await?;
|
||||
let assets = load_asset_responses(&state, tenant_id, version_id).await?;
|
||||
let version_response = to_version_response(current_version, true);
|
||||
|
||||
Ok(Json(DocumentDetailResponse {
|
||||
document: to_document_response(
|
||||
&state,
|
||||
user.user_id,
|
||||
user_id,
|
||||
doc,
|
||||
tags_map.get(&document_id).cloned(),
|
||||
correspondents_map.remove(&document_id).unwrap_or_default(),
|
||||
@@ -655,7 +666,9 @@ pub async fn get_document(
|
||||
|
||||
pub async fn upload_document(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
tenant_id, user_id, ..
|
||||
}: TenantScopedConn,
|
||||
mut multipart: Multipart,
|
||||
) -> AppResult<(StatusCode, Json<DocumentDetailResponse>)> {
|
||||
let mut file_bytes: Option<Vec<u8>> = None;
|
||||
@@ -733,7 +746,7 @@ pub async fn upload_document(
|
||||
metadata,
|
||||
};
|
||||
|
||||
let outcome = match process_upload(&state, request, user.user_id).await {
|
||||
let outcome = match process_upload(&state, request, tenant_id, user_id).await {
|
||||
Ok(outcome) => {
|
||||
info!(
|
||||
document_id = %outcome.detail.document.id,
|
||||
@@ -759,18 +772,25 @@ pub async fn upload_document(
|
||||
}
|
||||
|
||||
pub async fn request_document_assets(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
Query(query): Query<AssetRequestQuery>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<StatusCode> {
|
||||
let mut conn = state.db()?;
|
||||
let document: Document = documents::table.find(document_id).first(&mut conn)?;
|
||||
let document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)?;
|
||||
if document.deleted_at.is_some() {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
|
||||
enqueue_job(
|
||||
&mut conn,
|
||||
tenant_id,
|
||||
JOB_ANALYZE_DOCUMENT,
|
||||
json!({
|
||||
"document_id": document_id,
|
||||
@@ -785,13 +805,15 @@ pub async fn request_document_assets(
|
||||
}
|
||||
|
||||
pub async fn reanalyze_all_documents(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<(StatusCode, Json<BulkReanalyzeResponse>)> {
|
||||
let mut conn = state.db()?;
|
||||
|
||||
let targets: Vec<(Uuid, Uuid)> = documents::table
|
||||
.filter(documents::deleted_at.is_null())
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.select((documents::id, documents::current_version_id))
|
||||
.load(&mut conn)?;
|
||||
|
||||
@@ -799,6 +821,7 @@ pub async fn reanalyze_all_documents(
|
||||
for (document_id, version_id) in targets {
|
||||
enqueue_job(
|
||||
&mut conn,
|
||||
tenant_id,
|
||||
JOB_ANALYZE_DOCUMENT,
|
||||
json!({
|
||||
"document_id": document_id,
|
||||
@@ -815,7 +838,11 @@ pub async fn reanalyze_all_documents(
|
||||
}
|
||||
|
||||
pub async fn reanalyze_selected_documents(
|
||||
State(state): State<AppState>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<BulkReanalyzeSelectionRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkReanalyzeResponse>)> {
|
||||
let BulkReanalyzeSelectionRequest {
|
||||
@@ -830,11 +857,10 @@ pub async fn reanalyze_selected_documents(
|
||||
document_ids.sort();
|
||||
document_ids.dedup();
|
||||
|
||||
let mut conn = state.db()?;
|
||||
|
||||
let targets: Vec<(Uuid, Uuid)> = documents::table
|
||||
.filter(documents::id.eq_any(&document_ids))
|
||||
.filter(documents::deleted_at.is_null())
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.select((documents::id, documents::current_version_id))
|
||||
.load(&mut conn)?;
|
||||
|
||||
@@ -848,6 +874,7 @@ pub async fn reanalyze_selected_documents(
|
||||
for (document_id, version_id) in targets {
|
||||
enqueue_job(
|
||||
&mut conn,
|
||||
tenant_id,
|
||||
JOB_ANALYZE_DOCUMENT,
|
||||
json!({
|
||||
"document_id": document_id,
|
||||
@@ -866,9 +893,16 @@ pub async fn reanalyze_selected_documents(
|
||||
pub async fn list_document_assets(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<DocumentAssetResponse>>> {
|
||||
let mut conn = state.db()?;
|
||||
let document: Document = documents::table.find(document_id).first(&mut conn)?;
|
||||
let document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)?;
|
||||
if document.deleted_at.is_some() {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
@@ -876,7 +910,7 @@ pub async fn list_document_assets(
|
||||
let version_id = document.current_version_id;
|
||||
drop(conn);
|
||||
|
||||
let assets = load_asset_responses(&state, version_id).await?;
|
||||
let assets = load_asset_responses(&state, tenant_id, version_id).await?;
|
||||
Ok(Json(assets))
|
||||
}
|
||||
|
||||
@@ -884,11 +918,15 @@ pub async fn get_document_asset(
|
||||
State(state): State<AppState>,
|
||||
Path(asset_id): Path<Uuid>,
|
||||
Query(query): Query<AssetObjectsQuery>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<DocumentAssetDetailResponse>> {
|
||||
let mut conn = state.db()?;
|
||||
|
||||
let asset: DocumentAsset = match document_assets::table
|
||||
.find(asset_id)
|
||||
.filter(document_assets::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)
|
||||
.optional()?
|
||||
{
|
||||
@@ -911,6 +949,7 @@ pub async fn get_document_asset(
|
||||
|
||||
let objects: Vec<DocumentAssetObject> = document_asset_objects::table
|
||||
.filter(document_asset_objects::asset_id.eq(asset_id))
|
||||
.filter(document_asset_objects::tenant_id.eq(tenant_id))
|
||||
.filter(document_asset_objects::ordinal.ge(start))
|
||||
.filter(document_asset_objects::ordinal.le(end))
|
||||
.order(document_asset_objects::ordinal.asc())
|
||||
@@ -951,9 +990,16 @@ pub async fn get_document_asset(
|
||||
pub async fn download_document(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<DocumentDownloadResponse>> {
|
||||
let mut conn = state.db()?;
|
||||
let doc: Document = documents::table.find(document_id).first(&mut conn)?;
|
||||
let doc: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)?;
|
||||
if doc.deleted_at.is_some() {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
@@ -989,9 +1035,12 @@ pub async fn download_with_token(
|
||||
.verify_download_token(&token)
|
||||
.map_err(|_| AppError::unauthorized())?;
|
||||
|
||||
let mut conn = state.db()?;
|
||||
let mut conn = state.db_for_tenant(claims.tenant_id)?;
|
||||
|
||||
let doc: Document = documents::table.find(claims.doc_id).first(&mut conn)?;
|
||||
let doc: Document = documents::table
|
||||
.find(claims.doc_id)
|
||||
.filter(documents::tenant_id.eq(claims.tenant_id))
|
||||
.first(&mut conn)?;
|
||||
if doc.deleted_at.is_some() {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
@@ -1004,6 +1053,7 @@ pub async fn download_with_token(
|
||||
let has_active_refresh: bool = select(exists(
|
||||
refresh_dsl::refresh_tokens
|
||||
.filter(refresh_dsl::user_id.eq(claims.user_id))
|
||||
.filter(refresh_dsl::tenant_id.eq(claims.tenant_id))
|
||||
.filter(refresh_dsl::revoked_at.is_null())
|
||||
.filter(refresh_dsl::expires_at.gt(now)),
|
||||
))
|
||||
@@ -1028,29 +1078,42 @@ pub async fn download_with_token(
|
||||
}
|
||||
|
||||
pub async fn delete_document(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<impl IntoResponse> {
|
||||
let mut conn = state.db()?;
|
||||
let now = Utc::now().naive_utc();
|
||||
diesel::update(documents::table.find(document_id))
|
||||
.set((
|
||||
documents::deleted_at.eq(Some(now)),
|
||||
documents::updated_at.eq(now),
|
||||
))
|
||||
.execute(&mut conn)?;
|
||||
diesel::update(
|
||||
documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set((
|
||||
documents::deleted_at.eq(Some(now)),
|
||||
documents::updated_at.eq(now),
|
||||
))
|
||||
.execute(&mut conn)?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub async fn update_document(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<UpdateDocumentRequest>,
|
||||
) -> AppResult<Json<DocumentDetailResponse>> {
|
||||
let mut conn = state.db()?;
|
||||
|
||||
let mut document: Document = documents::table.find(document_id).first(&mut conn)?;
|
||||
let mut document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)?;
|
||||
if document.deleted_at.is_some() {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
@@ -1074,7 +1137,11 @@ pub async fn update_document(
|
||||
let now = Utc::now().naive_utc();
|
||||
let new_filename = filename_with_retained_extension(&title, &document.filename);
|
||||
|
||||
let update_result = diesel::update(documents::table.find(document_id)).set((
|
||||
let target = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id));
|
||||
|
||||
let update_result = diesel::update(target).set((
|
||||
documents::title.eq(&title),
|
||||
documents::filename.eq(&new_filename),
|
||||
documents::updated_at.eq(now),
|
||||
@@ -1090,7 +1157,10 @@ pub async fn update_document(
|
||||
Err(err) => return Err(AppError::from(err)),
|
||||
}
|
||||
|
||||
document = documents::table.find(document_id).first(&mut conn)?;
|
||||
document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)?;
|
||||
}
|
||||
|
||||
let current_version: DocumentVersion = document_versions::table
|
||||
@@ -1102,13 +1172,13 @@ pub async fn update_document(
|
||||
let version_id = current_version.id;
|
||||
drop(conn);
|
||||
|
||||
let assets = load_asset_responses(&state, version_id).await?;
|
||||
let assets = load_asset_responses(&state, tenant_id, version_id).await?;
|
||||
let version_response = to_version_response(current_version, true);
|
||||
|
||||
Ok(Json(DocumentDetailResponse {
|
||||
document: to_document_response(
|
||||
&state,
|
||||
user.user_id,
|
||||
user_id,
|
||||
document,
|
||||
tags_map.get(&document_id).cloned(),
|
||||
correspondents_map.remove(&document_id).unwrap_or_default(),
|
||||
@@ -1120,26 +1190,39 @@ pub async fn update_document(
|
||||
pub async fn move_document(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<MoveDocumentRequest>,
|
||||
) -> AppResult<impl IntoResponse> {
|
||||
if let Some(folder_id) = payload.folder_id {
|
||||
ensure_folder_exists(&state, folder_id)?;
|
||||
ensure_folder_exists(&state, tenant_id, folder_id)?;
|
||||
}
|
||||
|
||||
let mut conn = state.db()?;
|
||||
let now = Utc::now().naive_utc();
|
||||
diesel::update(documents::table.find(document_id))
|
||||
.set((
|
||||
documents::folder_id.eq(payload.folder_id),
|
||||
documents::updated_at.eq(now),
|
||||
))
|
||||
.execute(&mut conn)?;
|
||||
diesel::update(
|
||||
documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set((
|
||||
documents::folder_id.eq(payload.folder_id),
|
||||
documents::updated_at.eq(now),
|
||||
))
|
||||
.execute(&mut conn)?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub async fn bulk_move_documents(
|
||||
State(state): State<AppState>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<BulkMoveRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkMoveResponse>)> {
|
||||
let BulkMoveRequest {
|
||||
@@ -1155,13 +1238,12 @@ pub async fn bulk_move_documents(
|
||||
document_ids.dedup();
|
||||
|
||||
if let Some(target_folder) = folder_id {
|
||||
ensure_folder_exists(&state, target_folder)?;
|
||||
ensure_folder_exists(&state, tenant_id, target_folder)?;
|
||||
}
|
||||
|
||||
let mut conn = state.db()?;
|
||||
|
||||
let existing: Vec<(Uuid, Option<NaiveDateTime>)> = documents::table
|
||||
.filter(documents::id.eq_any(&document_ids))
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.select((documents::id, documents::deleted_at))
|
||||
.load(&mut conn)?;
|
||||
|
||||
@@ -1176,20 +1258,28 @@ pub async fn bulk_move_documents(
|
||||
}
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
let updated = diesel::update(documents::table.filter(documents::id.eq_any(&document_ids)))
|
||||
.set((
|
||||
documents::folder_id.eq(folder_id),
|
||||
documents::updated_at.eq(now),
|
||||
))
|
||||
.execute(&mut conn)?;
|
||||
let updated = diesel::update(
|
||||
documents::table
|
||||
.filter(documents::id.eq_any(&document_ids))
|
||||
.filter(documents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set((
|
||||
documents::folder_id.eq(folder_id),
|
||||
documents::updated_at.eq(now),
|
||||
))
|
||||
.execute(&mut conn)?;
|
||||
|
||||
Ok((StatusCode::OK, Json(BulkMoveResponse { updated })))
|
||||
}
|
||||
|
||||
pub async fn assign_correspondents(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<AssignCorrespondentsRequest>,
|
||||
) -> AppResult<impl IntoResponse> {
|
||||
if payload.assignments.is_empty() {
|
||||
@@ -1199,11 +1289,13 @@ pub async fn assign_correspondents(
|
||||
let (normalized_pairs, correspondents_vec, roles_vec) =
|
||||
normalize_correspondent_assignments(&payload.assignments)?;
|
||||
let replace = payload.replace;
|
||||
let user_id = user.user_id;
|
||||
let user_id_val = user_id;
|
||||
|
||||
let mut conn = state.db()?;
|
||||
conn.transaction::<(), AppError, _>(|conn| {
|
||||
let document: Document = documents::table.find(document_id).first(conn)?;
|
||||
let document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first(conn)?;
|
||||
if document.deleted_at.is_some() {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
@@ -1211,6 +1303,7 @@ pub async fn assign_correspondents(
|
||||
if !correspondents_vec.is_empty() {
|
||||
let existing: Vec<Correspondent> = correspondents::table
|
||||
.filter(correspondents::id.eq_any(&correspondents_vec))
|
||||
.filter(correspondents::tenant_id.eq(tenant_id))
|
||||
.load(conn)?;
|
||||
if existing.len() != correspondents_vec.len() {
|
||||
return Err(AppError::bad_request(
|
||||
@@ -1224,6 +1317,7 @@ pub async fn assign_correspondents(
|
||||
let deleted = diesel::delete(
|
||||
document_correspondents::table
|
||||
.filter(document_correspondents::document_id.eq(document_id))
|
||||
.filter(document_correspondents::tenant_id.eq(tenant_id))
|
||||
.filter(document_correspondents::role.eq_any(&roles_vec)),
|
||||
)
|
||||
.execute(conn)?;
|
||||
@@ -1238,7 +1332,8 @@ pub async fn assign_correspondents(
|
||||
document_id,
|
||||
correspondent_id: *correspondent_id,
|
||||
role: role.clone(),
|
||||
assigned_by: Some(user_id),
|
||||
assigned_by: Some(user_id_val),
|
||||
tenant_id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1253,9 +1348,13 @@ pub async fn assign_correspondents(
|
||||
}
|
||||
|
||||
if changed {
|
||||
diesel::update(documents::table.find(document_id))
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)?;
|
||||
diesel::update(
|
||||
documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1265,8 +1364,12 @@ pub async fn assign_correspondents(
|
||||
}
|
||||
|
||||
pub async fn bulk_assign_correspondents(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<BulkCorrespondentsRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkCorrespondentResponse>)> {
|
||||
if payload.document_ids.is_empty() {
|
||||
@@ -1283,12 +1386,11 @@ pub async fn bulk_assign_correspondents(
|
||||
let (normalized_pairs, correspondents_vec, roles_vec) =
|
||||
normalize_correspondent_assignments(&payload.assignments)?;
|
||||
let action = payload.action;
|
||||
let user_id = user.user_id;
|
||||
|
||||
let mut conn = state.db()?;
|
||||
let user_id_val = user_id;
|
||||
let (assigned, removed) = conn.transaction::<(usize, usize), AppError, _>(|conn| {
|
||||
let docs: Vec<(Uuid, Option<NaiveDateTime>)> = documents::table
|
||||
.filter(documents::id.eq_any(&document_ids))
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.select((documents::id, documents::deleted_at))
|
||||
.load(conn)?;
|
||||
|
||||
@@ -1307,6 +1409,7 @@ pub async fn bulk_assign_correspondents(
|
||||
if !correspondents_vec.is_empty() {
|
||||
let existing: Vec<Correspondent> = correspondents::table
|
||||
.filter(correspondents::id.eq_any(&correspondents_vec))
|
||||
.filter(correspondents::tenant_id.eq(tenant_id))
|
||||
.load(conn)?;
|
||||
if existing.len() != correspondents_vec.len() {
|
||||
return Err(AppError::bad_request(
|
||||
@@ -1322,6 +1425,7 @@ pub async fn bulk_assign_correspondents(
|
||||
removed = diesel::delete(
|
||||
document_correspondents::table
|
||||
.filter(document_correspondents::document_id.eq_any(&document_ids))
|
||||
.filter(document_correspondents::tenant_id.eq(tenant_id))
|
||||
.filter(document_correspondents::role.eq_any(&roles_vec)),
|
||||
)
|
||||
.execute(conn)?;
|
||||
@@ -1334,7 +1438,8 @@ pub async fn bulk_assign_correspondents(
|
||||
document_id: *doc_id,
|
||||
correspondent_id: *correspondent_id,
|
||||
role: role.clone(),
|
||||
assigned_by: Some(user_id),
|
||||
assigned_by: Some(user_id_val),
|
||||
tenant_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1349,9 +1454,13 @@ pub async fn bulk_assign_correspondents(
|
||||
};
|
||||
|
||||
if assigned > 0 || removed > 0 {
|
||||
diesel::update(documents::table.filter(documents::id.eq_any(&document_ids)))
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)?;
|
||||
diesel::update(
|
||||
documents::table
|
||||
.filter(documents::id.eq_any(&document_ids))
|
||||
.filter(documents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
Ok((assigned, removed))
|
||||
@@ -1371,6 +1480,7 @@ pub async fn bulk_assign_correspondents(
|
||||
removed += diesel::delete(
|
||||
document_correspondents::table
|
||||
.filter(document_correspondents::document_id.eq_any(&document_ids))
|
||||
.filter(document_correspondents::tenant_id.eq(tenant_id))
|
||||
.filter(document_correspondents::role.eq(role.as_str()))
|
||||
.filter(document_correspondents::correspondent_id.eq_any(&ids)),
|
||||
)
|
||||
@@ -1379,9 +1489,13 @@ pub async fn bulk_assign_correspondents(
|
||||
}
|
||||
|
||||
if removed > 0 {
|
||||
diesel::update(documents::table.filter(documents::id.eq_any(&document_ids)))
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)?;
|
||||
diesel::update(
|
||||
documents::table
|
||||
.filter(documents::id.eq_any(&document_ids))
|
||||
.filter(documents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
Ok((0, removed))
|
||||
@@ -1396,9 +1510,13 @@ pub async fn bulk_assign_correspondents(
|
||||
}
|
||||
|
||||
pub async fn remove_correspondent(
|
||||
State(state): State<AppState>,
|
||||
Path((document_id, correspondent_id)): Path<(Uuid, Uuid)>,
|
||||
Query(query): Query<CorrespondentRoleQuery>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<impl IntoResponse> {
|
||||
let role = normalize_role(&query.role);
|
||||
if role.is_empty() {
|
||||
@@ -1411,8 +1529,10 @@ pub async fn remove_correspondent(
|
||||
)));
|
||||
}
|
||||
|
||||
let mut conn = state.db()?;
|
||||
let document: Document = documents::table.find(document_id).first(&mut conn)?;
|
||||
let document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)?;
|
||||
if document.deleted_at.is_some() {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
@@ -1420,6 +1540,7 @@ pub async fn remove_correspondent(
|
||||
let deleted = diesel::delete(
|
||||
document_correspondents::table
|
||||
.filter(document_correspondents::document_id.eq(document_id))
|
||||
.filter(document_correspondents::tenant_id.eq(tenant_id))
|
||||
.filter(document_correspondents::correspondent_id.eq(correspondent_id))
|
||||
.filter(document_correspondents::role.eq(&role)),
|
||||
)
|
||||
@@ -1429,33 +1550,41 @@ pub async fn remove_correspondent(
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
|
||||
diesel::update(documents::table.find(document_id))
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(&mut conn)?;
|
||||
diesel::update(
|
||||
documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(&mut conn)?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub async fn assign_tags(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<AssignTagsRequest>,
|
||||
) -> AppResult<impl IntoResponse> {
|
||||
if payload.tag_ids.is_empty() {
|
||||
return Err(AppError::bad_request("tag_ids must not be empty"));
|
||||
}
|
||||
|
||||
let mut conn = state.db()?;
|
||||
|
||||
// Ensure document exists
|
||||
documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.first::<Document>(&mut conn)?;
|
||||
|
||||
// Ensure tags exist
|
||||
let existing_tags: Vec<Tag> = tags::table
|
||||
.filter(tags::id.eq_any(&payload.tag_ids))
|
||||
.filter(tags::tenant_id.eq(tenant_id))
|
||||
.load(&mut conn)?;
|
||||
if existing_tags.len() != payload.tag_ids.len() {
|
||||
return Err(AppError::bad_request("one or more tags do not exist"));
|
||||
@@ -1467,7 +1596,8 @@ pub async fn assign_tags(
|
||||
.map(|tag_id| NewDocumentTag {
|
||||
document_id,
|
||||
tag_id: *tag_id,
|
||||
assigned_by: Some(user.user_id),
|
||||
assigned_by: Some(user_id),
|
||||
tenant_id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1480,8 +1610,12 @@ pub async fn assign_tags(
|
||||
}
|
||||
|
||||
pub async fn bulk_update_tags(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<BulkTagRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkTagResponse>)> {
|
||||
let BulkTagRequest {
|
||||
@@ -1502,10 +1636,9 @@ pub async fn bulk_update_tags(
|
||||
tag_ids.sort();
|
||||
tag_ids.dedup();
|
||||
|
||||
let mut conn = state.db()?;
|
||||
|
||||
let docs: Vec<(Uuid, Option<NaiveDateTime>)> = documents::table
|
||||
.filter(documents::id.eq_any(&document_ids))
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.select((documents::id, documents::deleted_at))
|
||||
.load(&mut conn)?;
|
||||
|
||||
@@ -1523,6 +1656,7 @@ pub async fn bulk_update_tags(
|
||||
|
||||
let existing_tags: Vec<Tag> = tags::table
|
||||
.filter(tags::id.eq_any(&tag_ids))
|
||||
.filter(tags::tenant_id.eq(tenant_id))
|
||||
.load(&mut conn)?;
|
||||
if existing_tags.len() != tag_ids.len() {
|
||||
return Err(AppError::bad_request("one or more tags do not exist"));
|
||||
@@ -1536,7 +1670,8 @@ pub async fn bulk_update_tags(
|
||||
inserts.push(NewDocumentTag {
|
||||
document_id: *doc_id,
|
||||
tag_id: *tag_id,
|
||||
assigned_by: Some(user.user_id),
|
||||
assigned_by: Some(user_id),
|
||||
tenant_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1556,6 +1691,7 @@ pub async fn bulk_update_tags(
|
||||
let removed = diesel::delete(
|
||||
document_tags::table
|
||||
.filter(document_tags::document_id.eq_any(&document_ids))
|
||||
.filter(document_tags::tenant_id.eq(tenant_id))
|
||||
.filter(document_tags::tag_id.eq_any(&tag_ids)),
|
||||
)
|
||||
.execute(&mut conn)?;
|
||||
@@ -1568,13 +1704,17 @@ pub async fn bulk_update_tags(
|
||||
}
|
||||
|
||||
pub async fn remove_tag(
|
||||
State(state): State<AppState>,
|
||||
Path((document_id, tag_id)): Path<(Uuid, Uuid)>,
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<impl IntoResponse> {
|
||||
let mut conn = state.db()?;
|
||||
diesel::delete(
|
||||
document_tags::table
|
||||
.filter(document_tags::document_id.eq(document_id))
|
||||
.filter(document_tags::tenant_id.eq(tenant_id))
|
||||
.filter(document_tags::tag_id.eq(tag_id)),
|
||||
)
|
||||
.execute(&mut conn)?;
|
||||
@@ -1585,6 +1725,7 @@ pub async fn remove_tag(
|
||||
async fn process_upload(
|
||||
state: &AppState,
|
||||
request: UploadRequest,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> AppResult<UploadOutcome> {
|
||||
let UploadRequest {
|
||||
@@ -1596,7 +1737,7 @@ async fn process_upload(
|
||||
} = request;
|
||||
|
||||
if let Some(folder) = folder_id {
|
||||
ensure_folder_exists(state, folder)?;
|
||||
ensure_folder_exists(state, tenant_id, folder)?;
|
||||
}
|
||||
|
||||
let doc_id = Uuid::new_v4();
|
||||
@@ -1610,13 +1751,14 @@ async fn process_upload(
|
||||
let s3_key = document_version_object_key(doc_id, version_number, version_id);
|
||||
|
||||
{
|
||||
let mut conn = state.db()?;
|
||||
let mut conn = state.db_for_tenant(tenant_id)?;
|
||||
|
||||
let existing = documents::table
|
||||
.inner_join(
|
||||
document_versions::table
|
||||
.on(document_versions::id.eq(documents::current_version_id)),
|
||||
)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
.filter(document_versions::checksum.eq(&checksum_hex))
|
||||
.select((documents::all_columns, document_versions::all_columns))
|
||||
.first::<(Document, DocumentVersion)>(&mut conn)
|
||||
@@ -1641,7 +1783,7 @@ async fn process_upload(
|
||||
let tags = tags_map.get(&document.id).cloned();
|
||||
let correspondents = correspondents_map.remove(&document.id).unwrap_or_default();
|
||||
drop(conn);
|
||||
let assets = load_asset_responses(state, version.id).await?;
|
||||
let assets = load_asset_responses(state, tenant_id, version.id).await?;
|
||||
let version_response = to_version_response(version.clone(), true);
|
||||
|
||||
info!(
|
||||
@@ -1689,7 +1831,7 @@ async fn process_upload(
|
||||
};
|
||||
|
||||
let (document, version) = {
|
||||
let mut conn = state.db()?;
|
||||
let mut conn = state.db_for_tenant(tenant_id)?;
|
||||
conn.transaction(|conn| {
|
||||
let new_document = NewDocument {
|
||||
id: doc_id,
|
||||
@@ -1701,6 +1843,7 @@ async fn process_upload(
|
||||
issued_at: None,
|
||||
title: derive_document_title(&original_name),
|
||||
metadata: metadata_value.clone(),
|
||||
tenant_id,
|
||||
};
|
||||
diesel::insert_into(documents::table)
|
||||
.values(&new_document)
|
||||
@@ -1715,6 +1858,7 @@ async fn process_upload(
|
||||
checksum: checksum_hex.clone(),
|
||||
metadata: Value::Object(Default::default()),
|
||||
operations_summary: Value::Object(Default::default()),
|
||||
tenant_id,
|
||||
};
|
||||
|
||||
diesel::insert_into(document_versions::table)
|
||||
@@ -1739,9 +1883,10 @@ async fn process_upload(
|
||||
)?,
|
||||
};
|
||||
|
||||
if let Ok(mut conn) = state.db() {
|
||||
if let Ok(mut conn) = state.db_for_tenant(tenant_id) {
|
||||
if let Err(err) = enqueue_job(
|
||||
&mut conn,
|
||||
tenant_id,
|
||||
JOB_ANALYZE_DOCUMENT,
|
||||
json!({
|
||||
"document_id": doc_id,
|
||||
@@ -1762,10 +1907,14 @@ async fn process_upload(
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_folder_exists(state: &AppState, folder_id: Uuid) -> AppResult<()> {
|
||||
let mut conn = state.db()?;
|
||||
let exists: bool = diesel::select(exists(folders::table.filter(folders::id.eq(folder_id))))
|
||||
.get_result(&mut conn)?;
|
||||
fn ensure_folder_exists(state: &AppState, tenant_id: Uuid, folder_id: Uuid) -> AppResult<()> {
|
||||
let mut conn = state.db_for_tenant(tenant_id)?;
|
||||
let exists: bool = diesel::select(exists(
|
||||
folders::table
|
||||
.filter(folders::id.eq(folder_id))
|
||||
.filter(folders::tenant_id.eq(tenant_id)),
|
||||
))
|
||||
.get_result(&mut conn)?;
|
||||
if !exists {
|
||||
return Err(AppError::bad_request("folder does not exist"));
|
||||
}
|
||||
@@ -1829,6 +1978,7 @@ pub(crate) fn load_correspondents_for_documents(
|
||||
|
||||
pub(crate) async fn load_primary_assets(
|
||||
state: &AppState,
|
||||
tenant_id: Uuid,
|
||||
documents: &[Document],
|
||||
) -> AppResult<HashMap<Uuid, (DocumentVersionResponse, Vec<DocumentAssetResponse>)>> {
|
||||
if documents.is_empty() {
|
||||
@@ -1845,7 +1995,7 @@ pub(crate) async fn load_primary_assets(
|
||||
version_ids.sort();
|
||||
version_ids.dedup();
|
||||
|
||||
let mut conn = state.db()?;
|
||||
let mut conn = state.db_for_tenant(tenant_id)?;
|
||||
let versions: Vec<DocumentVersion> = document_versions::table
|
||||
.filter(document_versions::id.eq_any(&version_ids))
|
||||
.load(&mut conn)?;
|
||||
@@ -1905,7 +2055,7 @@ pub(crate) fn to_document_response(
|
||||
current_version: Option<(DocumentVersionResponse, Vec<DocumentAssetResponse>)>,
|
||||
) -> AppResult<DocumentResponse> {
|
||||
let current_version = if let Some((version, assets)) = current_version {
|
||||
let download_path = build_download_path(state, doc.id, user_id)?;
|
||||
let download_path = build_download_path(state, &doc, user_id)?;
|
||||
Some(DocumentCurrentVersionResponse {
|
||||
version,
|
||||
assets,
|
||||
@@ -1937,10 +2087,10 @@ pub(crate) fn to_document_response(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_download_path(state: &AppState, document_id: Uuid, user_id: Uuid) -> AppResult<String> {
|
||||
fn build_download_path(state: &AppState, document: &Document, user_id: Uuid) -> AppResult<String> {
|
||||
state
|
||||
.jwt
|
||||
.generate_download_token(document_id, user_id)
|
||||
.generate_download_token(document.id, user_id, document.tenant_id)
|
||||
.map(|token| format!("/download/{token}"))
|
||||
.map_err(|err| AppError::internal(format!("failed to generate download token: {err}")))
|
||||
}
|
||||
@@ -2042,9 +2192,10 @@ fn filename_with_retained_extension(title: &str, current_filename: &str) -> Stri
|
||||
|
||||
async fn load_asset_responses(
|
||||
state: &AppState,
|
||||
tenant_id: Uuid,
|
||||
version_id: Uuid,
|
||||
) -> AppResult<Vec<DocumentAssetResponse>> {
|
||||
let mut conn = state.db()?;
|
||||
let mut conn = state.db_for_tenant(tenant_id)?;
|
||||
let assets: Vec<(DocumentAsset, Option<DocumentAssetObject>)> = document_assets::table
|
||||
.left_outer_join(
|
||||
document_asset_objects::table.on(document_asset_objects::asset_id
|
||||
@@ -2052,6 +2203,7 @@ async fn load_asset_responses(
|
||||
.and(document_asset_objects::ordinal.eq(1))),
|
||||
)
|
||||
.filter(document_assets::document_version_id.eq(version_id))
|
||||
.filter(document_assets::tenant_id.eq(tenant_id))
|
||||
.order(document_assets::created_at.asc())
|
||||
.select((
|
||||
document_assets::all_columns,
|
||||
|
||||
Reference in New Issue
Block a user