2025-03-28 08:09:18 +01:00
|
|
|
/**
|
|
|
|
|
* OxiCloud - File Sharing Module
|
2026-02-15 23:45:11 +01:00
|
|
|
* All operations go through the backend API at /api/shares.
|
|
|
|
|
* No localStorage is used for share data.
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
|
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
import { switchToSharedSection } from '../../app/navigation.js';
|
|
|
|
|
import { ui } from '../../app/ui.js';
|
|
|
|
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
|
|
|
|
import { formatDateTime } from '../../core/formatters.js';
|
2026-06-15 00:17:17 +02:00
|
|
|
import { i18n } from '../../core/i18n.js';
|
2026-04-13 15:09:10 +02:00
|
|
|
|
2026-05-07 23:40:02 +02:00
|
|
|
/**
|
|
|
|
|
* @import {CreateShare, ShareItem, UpdateShare} from '../../core/types.js'
|
|
|
|
|
*/
|
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
const fileSharing = {
|
2026-03-03 01:10:50 +01:00
|
|
|
/** Auth header helper — tokens are in HttpOnly cookies now */
|
2026-02-15 23:45:11 +01:00
|
|
|
_headers(json = true) {
|
2026-03-03 01:10:50 +01:00
|
|
|
const h = { ...getCsrfHeaders() };
|
2026-02-15 23:45:11 +01:00
|
|
|
if (json) h['Content-Type'] = 'application/json';
|
|
|
|
|
return h;
|
|
|
|
|
},
|
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
2026-02-15 23:45:11 +01:00
|
|
|
* Create a shared link via backend API
|
2025-03-28 08:09:18 +01:00
|
|
|
* @param {string} itemId - ID of the file or folder
|
2026-02-15 23:45:11 +01:00
|
|
|
* @param {string} itemType - 'file' or 'folder'
|
2026-05-07 23:40:02 +02:00
|
|
|
* @param {CreateShare} [options] -
|
2026-02-15 23:45:11 +01:00
|
|
|
* @returns {Promise<Object>} ShareDto from backend
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-05-07 23:40:02 +02:00
|
|
|
// FIXME unused ?? duplicate with createSharedLink() from contextMenu
|
|
|
|
|
async createSharedLink(itemId, itemType, options) {
|
2026-02-15 23:45:11 +01:00
|
|
|
const body = {
|
|
|
|
|
item_id: itemId,
|
2026-05-07 23:40:02 +02:00
|
|
|
item_name: options.item_name || null,
|
2026-02-15 23:45:11 +01:00
|
|
|
item_type: itemType,
|
|
|
|
|
password: options.password || null,
|
2026-05-29 12:54:52 +02:00
|
|
|
expires_at: options.expires_at ? Math.floor(new Date(options.expires_at).getTime() / 1000) : null
|
2026-02-15 23:45:11 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const res = await fetch('/api/shares', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: this._headers(),
|
|
|
|
|
body: JSON.stringify(body)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.json().catch(() => ({}));
|
|
|
|
|
throw new Error(err.error || `Server error ${res.status}`);
|
2025-03-28 08:09:18 +01:00
|
|
|
}
|
2026-02-15 23:45:11 +01:00
|
|
|
|
|
|
|
|
return await res.json();
|
2025-03-28 08:09:18 +01:00
|
|
|
},
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
2026-02-15 23:45:11 +01:00
|
|
|
* Get all shared links for the current user
|
2026-05-07 23:40:02 +02:00
|
|
|
* @returns {Promise<ShareItem[]>} Array of ShareDto
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-02-15 23:45:11 +01:00
|
|
|
async getSharedLinks() {
|
2025-03-28 08:09:18 +01:00
|
|
|
try {
|
2026-02-15 23:45:11 +01:00
|
|
|
const res = await fetch('/api/shares?page=1&per_page=1000', {
|
|
|
|
|
headers: this._headers(false)
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) return [];
|
2026-05-07 23:40:02 +02:00
|
|
|
const data = /** @type {ShareItem[]} */ await res.json();
|
2026-02-15 23:45:11 +01:00
|
|
|
return data.items || [];
|
2025-03-28 08:09:18 +01:00
|
|
|
} catch (error) {
|
2026-02-15 23:45:11 +01:00
|
|
|
console.error('Error fetching shared links:', error);
|
|
|
|
|
return [];
|
2025-03-28 08:09:18 +01:00
|
|
|
}
|
|
|
|
|
},
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
2026-02-16 21:51:53 +01:00
|
|
|
* Get shared links for a specific item (server-side filtered)
|
2026-02-15 23:45:11 +01:00
|
|
|
* @param {string} itemId
|
|
|
|
|
* @param {string} itemType - 'file' or 'folder'
|
2026-05-07 23:40:02 +02:00
|
|
|
* @returns {Promise<ShareItem[]>} Shares for this item
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-02-15 23:45:11 +01:00
|
|
|
async getSharedLinksForItem(itemId, itemType) {
|
2025-03-28 08:09:18 +01:00
|
|
|
try {
|
2026-04-07 22:48:59 +02:00
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
item_id: itemId,
|
|
|
|
|
item_type: itemType
|
|
|
|
|
});
|
2026-02-16 21:51:53 +01:00
|
|
|
const res = await fetch(`/api/shares?${params}`, {
|
|
|
|
|
headers: this._headers(false)
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) return [];
|
|
|
|
|
const data = await res.json();
|
2026-04-07 22:48:59 +02:00
|
|
|
return Array.isArray(data) ? data : data.items || [];
|
2025-03-28 08:09:18 +01:00
|
|
|
} catch (error) {
|
2026-02-15 23:45:11 +01:00
|
|
|
console.error('Error getting shared links for item:', error);
|
|
|
|
|
return [];
|
2025-03-28 08:09:18 +01:00
|
|
|
}
|
|
|
|
|
},
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
2026-02-15 23:45:11 +01:00
|
|
|
* Check if an item has any shared links
|
2026-05-07 23:40:02 +02:00
|
|
|
* @param {string} itemId
|
|
|
|
|
* @param {string} itemType
|
2026-02-15 23:45:11 +01:00
|
|
|
* @returns {Promise<boolean>}
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-02-15 23:45:11 +01:00
|
|
|
async hasSharedLinks(itemId, itemType) {
|
|
|
|
|
const links = await this.getSharedLinksForItem(itemId, itemType);
|
|
|
|
|
return links.length > 0;
|
2025-03-28 08:09:18 +01:00
|
|
|
},
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
2026-02-15 23:45:11 +01:00
|
|
|
* Update a shared link
|
|
|
|
|
* @param {string} shareId
|
2026-05-29 12:54:52 +02:00
|
|
|
* @param {UpdateShare} updateData - { password, expires_at }
|
2026-02-15 23:45:11 +01:00
|
|
|
* @returns {Promise<Object>} Updated ShareDto
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-02-15 23:45:11 +01:00
|
|
|
async updateSharedLink(shareId, updateData) {
|
|
|
|
|
const body = {};
|
|
|
|
|
if (updateData.password !== undefined) body.password = updateData.password;
|
|
|
|
|
if (updateData.expires_at !== undefined) body.expires_at = updateData.expires_at;
|
|
|
|
|
|
|
|
|
|
const res = await fetch(`/api/shares/${shareId}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: this._headers(),
|
|
|
|
|
body: JSON.stringify(body)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.json().catch(() => ({}));
|
|
|
|
|
throw new Error(err.error || `Server error ${res.status}`);
|
2025-03-28 08:09:18 +01:00
|
|
|
}
|
2026-02-15 23:45:11 +01:00
|
|
|
|
|
|
|
|
return await res.json();
|
2025-03-28 08:09:18 +01:00
|
|
|
},
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
2026-02-15 23:45:11 +01:00
|
|
|
* Delete a shared link
|
|
|
|
|
* @param {string} shareId
|
|
|
|
|
* @returns {Promise<boolean>}
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-02-15 23:45:11 +01:00
|
|
|
async removeSharedLink(shareId) {
|
2025-03-28 08:09:18 +01:00
|
|
|
try {
|
2026-02-15 23:45:11 +01:00
|
|
|
const res = await fetch(`/api/shares/${shareId}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: this._headers(false)
|
|
|
|
|
});
|
|
|
|
|
return res.ok || res.status === 204;
|
2025-03-28 08:09:18 +01:00
|
|
|
} catch (error) {
|
2026-02-15 23:45:11 +01:00
|
|
|
console.error('Error removing shared link:', error);
|
|
|
|
|
return false;
|
2025-03-28 08:09:18 +01:00
|
|
|
}
|
|
|
|
|
},
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2026-05-29 02:16:15 +02:00
|
|
|
/**
|
|
|
|
|
* Fetch a single share by its UUID and return the full ShareItem.
|
|
|
|
|
* Used to resolve a token's URL on demand (lazy fetch on copy-link click).
|
|
|
|
|
* @param {string} shareId
|
|
|
|
|
* @returns {Promise<import('../../core/types.js').ShareItem>}
|
|
|
|
|
*/
|
|
|
|
|
async getShareById(shareId) {
|
|
|
|
|
const res = await fetch(`/api/shares/${shareId}`, {
|
|
|
|
|
headers: this._headers(false)
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error(`getShareById ${shareId}: HTTP ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
},
|
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
|
|
|
|
* Copy a shared link to clipboard
|
2026-02-15 23:45:11 +01:00
|
|
|
* @param {string} url
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-02-15 23:45:11 +01:00
|
|
|
async copyLinkToClipboard(url) {
|
2025-03-28 08:09:18 +01:00
|
|
|
try {
|
2026-02-15 23:45:11 +01:00
|
|
|
await navigator.clipboard.writeText(url);
|
2026-06-15 00:17:17 +02:00
|
|
|
ui.showNotification(i18n.t('notif.linkCopied'), i18n.t('notif.linkCopiedBody'));
|
2025-03-28 08:09:18 +01:00
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error copying to clipboard:', error);
|
2026-06-15 00:17:17 +02:00
|
|
|
ui.showNotification(i18n.t('notif.errorTitle'), i18n.t('notif.linkCopyError'));
|
2025-03-28 08:09:18 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
2026-02-15 23:45:11 +01:00
|
|
|
* Format expiration date for display (Unix timestamp in seconds or ISO string)
|
2026-04-30 02:02:27 +02:00
|
|
|
* @param {number|Date} value
|
2026-02-15 23:45:11 +01:00
|
|
|
* @returns {string}
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-02-15 23:45:11 +01:00
|
|
|
formatExpirationDate(value) {
|
|
|
|
|
if (!value) return 'No expiration';
|
2026-04-13 15:09:10 +02:00
|
|
|
return formatDateTime(value);
|
2025-03-28 08:09:18 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-15 23:45:11 +01:00
|
|
|
* Send a notification about a shared resource (stub — no backend endpoint yet)
|
|
|
|
|
* @param {string} shareUrl
|
|
|
|
|
* @param {string} recipientEmail
|
2026-04-30 02:02:27 +02:00
|
|
|
* @param {string} _message
|
2026-02-15 23:45:11 +01:00
|
|
|
* @returns {Promise<boolean>}
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
2026-04-12 02:13:08 +02:00
|
|
|
async sendShareNotification(shareUrl, recipientEmail, _message = '') {
|
2026-02-15 23:45:11 +01:00
|
|
|
// TODO: implement backend endpoint for email notifications
|
|
|
|
|
console.log(`Share notification for ${shareUrl} sent to ${recipientEmail}`);
|
2026-06-15 00:17:17 +02:00
|
|
|
ui.showNotification(i18n.t('notif.notificationSent'), i18n.t('notif.notificationSentBody', { email: recipientEmail }));
|
2026-02-15 23:45:11 +01:00
|
|
|
return true;
|
2025-03-28 08:09:18 +01:00
|
|
|
},
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
/**
|
2026-02-15 23:45:11 +01:00
|
|
|
* Initialize file sharing event listeners
|
2025-03-28 08:09:18 +01:00
|
|
|
*/
|
|
|
|
|
init() {
|
2026-02-15 23:45:11 +01:00
|
|
|
console.log('File sharing module initialized (API-backed)');
|
2026-04-07 22:48:59 +02:00
|
|
|
document.querySelectorAll('.nav-item').forEach((item) => {
|
2026-02-15 23:45:11 +01:00
|
|
|
const span = item.querySelector('span');
|
|
|
|
|
if (span && span.getAttribute('data-i18n') === 'nav.shared') {
|
2025-03-28 08:09:18 +01:00
|
|
|
item.addEventListener('click', () => {
|
2026-04-13 15:09:10 +02:00
|
|
|
switchToSharedSection();
|
2025-03-28 08:09:18 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
export { fileSharing };
|