joplock/app/items/shareAccess.js
igor fcc8f8c56d feat: notebook/folder sharing
Backend: shareAccess helpers, shares CRUD API with can_write, proxy
write guard, fragment/API ownership gates, widened item reads via
user_items, share_id propagation on move-in/out, leave endpoint.

UI: share dialog with owner-only controls, can_write toggle checkbox,
recipient leave button, read-only editor mode, shared-folder emoji.

Tests: 754 unit (+46 share), 8 Playwright browser specs.
2026-07-24 11:15:11 +12:00

76 lines
2.5 KiB
JavaScript

'use strict';
const deriveShareFieldsForMove = (targetFolder) => {
const shareId = (targetFolder && targetFolder.shareId) ? targetFolder.shareId : '';
return { shareId, isShared: !!shareId };
};
const resolveItemShareAccess = async (itemService, userId, itemId, options = {}) => {
const item = await itemService.noteByUserIdAndJopId(userId, itemId, options).catch(() => null);
if (!item) return null;
const ownerId = item.ownerId || '';
const shareId = item.shareId || '';
const isOwner = !ownerId || ownerId === userId;
const isShared = !!(item.isShared || shareId);
let canWrite = isOwner;
if (!isOwner && shareId && itemService.database) {
const su = await itemService.database.query(
`SELECT can_write FROM share_users WHERE share_id = $1 AND user_id = $2 AND status = 1 LIMIT 1`,
[shareId, userId],
).catch(() => ({ rows: [] }));
if (su.rows && su.rows[0]) canWrite = !!(Number(su.rows[0].can_write));
}
return { item, ownerId, shareId, isShared, isOwner, canRead: true, canWrite };
};
const resolveFolderShareState = async (itemService, userId, folderId) => {
const folder = await itemService.folderByUserIdAndJopId(userId, folderId).catch(() => null);
if (!folder) return null;
const ownerId = folder.ownerId || '';
const shareId = folder.shareId || '';
const isOwner = !ownerId || ownerId === userId;
const isShared = !!(folder.isShared || shareId);
let canWrite = isOwner;
if (!isOwner && shareId && itemService.database) {
const su = await itemService.database.query(
`SELECT can_write FROM share_users WHERE share_id = $1 AND user_id = $2 AND status = 1 LIMIT 1`,
[shareId, userId],
).catch(() => ({ rows: [] }));
if (su.rows && su.rows[0]) canWrite = !!(Number(su.rows[0].can_write));
}
return { folder, ownerId, shareId, isShared, isOwner, canWrite };
};
const assertCanWrite = (access) => {
if (!access) {
const err = new Error('Not found');
err.statusCode = 404;
throw err;
}
if (!access.canWrite) {
const err = new Error('Shared items are read-only');
err.statusCode = 403;
throw err;
}
};
const assertOwnerForDestructive = (access) => {
if (!access) {
const err = new Error('Not found');
err.statusCode = 404;
throw err;
}
if (!access.isOwner) {
const err = new Error('Only the owner can move or delete this item');
err.statusCode = 403;
throw err;
}
};
module.exports = {
deriveShareFieldsForMove,
resolveItemShareAccess,
resolveFolderShareState,
assertCanWrite,
assertOwnerForDestructive,
};