- {/* Header */}
-
-
-
-
Nuage
-
Gestionnaire de fichiers
-
-
-
-
- {canCreateSubfolder && (
-
- )}
-
-
-
-
-
-
- {/* Breadcrumb + Explorer */}
-
-
navigateTo(null),
- active: breadcrumb.length === 0,
- },
- ...(breadcrumb.length > 3
- ? [
- {
- key: 'ellipsis',
- label: '···',
- onClick: () => navigateTo(breadcrumb[0].id),
- active: false,
- },
- ...breadcrumb.slice(-2).map((folder, i) => ({
- key: folder.id,
- label: folder.name,
- onClick: () => navigateTo(folder.id),
- active: i === 1,
- })),
- ]
- : breadcrumb.map((folder, i) => ({
- key: folder.id,
- label: folder.name,
- onClick: () => navigateTo(folder.id),
- active: i === breadcrumb.length - 1,
- }))
- ),
- ]}
- />
-
- {/* Upload queue indicator */}
- {uploading && uploadQueue.length > 0 && (
-
-
Téléversement en cours…
-
- {uploadQueue.map((f, i) => (
-
- {f.done ? : }
- {f.name}
-
- ))}
-
-
- )}
-
- {/* Explorer area */}
-
- { e.preventDefault(); if (!dragOverFolder) setDragOver(true); }}
- onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) { setDragOver(false); setDragOverFolder(null); } }}
- onDrop={handleDrop}
- >
- {viewMode === 'list' ? (
- /* List view */
-
{
- if (item._type === 'folder') navigateTo(item.id);
- else if (isViewable(item.mime_type)) handleOpenViewer({ ...item, type: 'file', name: item.display_name });
- }}
- getRowProps={getRowProps}
- renderActions={(item) => (
-
- )}
- showShareBadge
- emptyMessage="Ce dossier est vide"
- emptyDescription='Glissez des fichiers ici ou cliquez sur « Téléverser »'
- size="sm"
- />
- ) : loading ? (
-
-
-
- ) : allFoldersSorted.length === 0 && allFilesSorted.length === 0 ? (
-
-
-
Ce dossier est vide
-
Glissez des fichiers ici ou cliquez sur « Téléverser »
-
- ) : (
- /* Grid view */
-
- {allFoldersSorted.map(folder => (
-
navigateTo(folder.id)}
- onDragOver={e => { e.preventDefault(); e.stopPropagation(); setDragOver(false); setDragOverFolder(folder.id); }}
- onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverFolder(null); }}
- onDrop={e => { e.preventDefault(); e.stopPropagation(); setDragOverFolder(null); if (e.dataTransfer.files?.length) uploadFiles(e.dataTransfer.files, folder.id); }}
- >
-
-
{folder.name}
- {dragOverFolder === folder.id && (
-
Déposer ici
- )}
- {folder.has_active_share && dragOverFolder !== folder.id && (
-
- )}
-
-
- ))}
- {allFilesSorted.map(file => {
- const FileIcon = getFileIcon(file.mime_type);
- const fileColor = getFileColor(file.mime_type);
- const canPreview = isViewable(file.mime_type);
- return (
-
canPreview && handleOpenViewer({ ...file, type: 'file', name: file.display_name })}
- >
-
-
{file.display_name}
- {file.has_active_share && (
-
- )}
-
-
- );
- })}
-
- )}
-
-
-
-
- {/* Context menu */}
- {contextMenu && (
-
e.stopPropagation()}
- >
- {contextMenuActions(contextMenu.item).map((action, i) => (
-
- ))}
-
- )}
-
- {/* Share panel (side panel) */}
- {shareTarget && (
-
-
setShareTarget(null)} />
-
- setShareTarget(null)} />
-
-
- )}
-
- {/* New folder modal */}
- {showNewFolder && (
-
{ setShowNewFolder(false); setNewFolderName(''); }}
- title="Nouveau dossier"
- size="sm"
- footer={
-
-
-
-
- }
- >
- setNewFolderName(v)}
- placeholder="Nom du dossier"
- onKeyDown={e => e.key === 'Enter' && handleCreateFolder()}
- />
-
- )}
-
- {/* Rename modal */}
- {renameTarget && (
-
{ setRenameTarget(null); setRenameName(''); }}
- title="Renommer"
- size="sm"
- footer={
-
-
-
-
- }
- >
- setRenameName(v)}
- onKeyDown={e => e.key === 'Enter' && handleRenameConfirm()}
- />
-
- )}
-
- {/* Delete confirmation modal */}
- {deleteTarget && (
-
setDeleteTarget(null)}
- title="Confirmer la suppression"
- size="sm"
- footer={
-
-
-
-
- }
- >
-
-
-
- Voulez-vous vraiment supprimer{' '}
- « {deleteTarget.name} »
- {deleteTarget.type === 'folder' && ' et tout son contenu'} ? Cette action est irréversible.
-
-
-
- )}
-
- {/* File viewer modal */}
- {viewerFile && (
-
- )}
-
- {/* Move modal */}
- {moveTarget && (
-
setMoveTarget(null)}
- title="Déplacer vers…"
- size="sm"
- footer={
-
- }
- >
-
-
- {allFolders
- .filter(f => f.id !== moveTarget.id)
- .map(f => (
-
- ))}
-
-
- )}
-
- );
-};
-
-export default ExplorerPage;
diff --git a/src/modules/nuage/admin/SharesPage.js b/src/modules/nuage/admin/SharesPage.js
deleted file mode 100644
index 38ea724..0000000
--- a/src/modules/nuage/admin/SharesPage.js
+++ /dev/null
@@ -1,254 +0,0 @@
-'use client';
-
-import React, { useState, useEffect } from 'react';
-import { useRouter } from 'next/navigation';
-import {
- Link02Icon,
- Delete02Icon,
- UserCircle02Icon,
- Folder01Icon,
- FileSecurityIcon,
- Copy01Icon,
-} from '../../../shared/Icons.js';
-import { Card, Badge, Button, Table, FilterTabs } from '../../../shared/components';
-import { useToast } from '@hykocx/zen/toast';
-
-function formatDate(dateStr) {
- if (!dateStr) return '—';
- return new Date(dateStr).toLocaleDateString('fr-CA', {
- year: 'numeric', month: 'short', day: 'numeric',
- });
-}
-
-function isExpired(share) {
- if (!share.expires_at) return false;
- return new Date(share.expires_at) < new Date();
-}
-
-const SharesPage = ({ user }) => {
- const router = useRouter();
- const toast = useToast();
- const [shares, setShares] = useState([]);
- const [loading, setLoading] = useState(true);
- const [filter, setFilter] = useState('all'); // 'all' | 'user' | 'anonymous'
- const [revoking, setRevoking] = useState(null);
-
- useEffect(() => {
- loadShares();
- }, []);
-
- const loadShares = async () => {
- setLoading(true);
- try {
- const res = await fetch('/zen/api/admin/nuage/shares', { credentials: 'include' });
- const data = await res.json();
- if (data.success) {
- setShares(data.shares);
- } else {
- toast.error(data.error || 'Erreur lors du chargement');
- }
- } catch (e) {
- console.error(e);
- toast.error('Erreur réseau');
- } finally {
- setLoading(false);
- }
- };
-
- const handleRevoke = async (id) => {
- setRevoking(id);
- try {
- const res = await fetch(`/zen/api/admin/nuage/shares?id=${id}`, {
- method: 'DELETE',
- credentials: 'include',
- });
- const data = await res.json();
- if (data.success) {
- toast.success('Partage révoqué');
- loadShares();
- } else {
- toast.error(data.error || 'Erreur');
- }
- } catch (e) {
- toast.error('Erreur réseau');
- } finally {
- setRevoking(null);
- }
- };
-
- const copyLink = (token) => {
- const url = `${window.location.origin}/zen/nuage/partage/${token}`;
- navigator.clipboard.writeText(url).then(() => toast.success('Lien copié !'));
- };
-
- const filtered = shares.filter(s => {
- if (filter === 'all') return true;
- return s.share_type === filter;
- });
-
- const getShareStatus = (share) => {
- if (!share.is_active) return { label: 'Révoqué', variant: 'default' };
- if (isExpired(share)) return { label: 'Expiré', variant: 'danger' };
- return { label: 'Actif', variant: 'success' };
- };
-
- const columns = [
- {
- key: 'type',
- label: 'Type',
- render: (share) => (
-
- {share.share_type === 'user' ? (
-
- ) : (
-
- )}
-
- {share.share_type === 'user' ? 'Utilisateur' : 'Lien anonyme'}
-
-
- ),
- skeleton: { height: 'h-4', width: '60%' },
- },
- {
- key: 'recipient',
- label: 'Destinataire',
- render: (share) => (
-
- {share.share_type === 'user' && share.name ? (
- <>
-
- {share.name}
-
-
{share.email}
- >
- ) : (
-
—
- )}
-
- ),
- skeleton: { height: 'h-4', width: '70%' },
- },
- {
- key: 'target',
- label: 'Élément partagé',
- render: (share) => (
-
- {share.target_type === 'folder' ? (
-
- ) : (
-
- )}
-
- {share.target_name || share.target_id}
-
-
- ),
- skeleton: { height: 'h-4', width: '50%' },
- },
- {
- key: 'permission',
- label: 'Permission',
- render: (share) => (
-
- {share.permission === 'reader' ? 'Lecteur' : 'Collaborateur'}
-
- ),
- skeleton: { height: 'h-4', width: '40%' },
- },
- {
- key: 'expires_at',
- label: 'Expiration',
- render: (share) => (
-
- {share.expires_at ? formatDate(share.expires_at) : 'Aucune'}
-
- ),
- skeleton: { height: 'h-4', width: '40%' },
- },
- {
- key: 'status',
- label: 'Statut',
- render: (share) => {
- const status = getShareStatus(share);
- return
{status.label};
- },
- skeleton: { height: 'h-4', width: '30%' },
- },
- {
- key: 'actions',
- label: '',
- render: (share) => (
-
- {share.share_type === 'anonymous' && share.is_active && !isExpired(share) && (
-
- ),
- skeleton: { height: 'h-4', width: '20%' },
- },
- ];
-
- const activeCount = shares.filter(s => s.is_active && !isExpired(s)).length;
-
- return (
-
- {/* Header */}
-
-
-
Partages
-
Gérez les liens et accès partagés
-
-
router.push('/admin/nuage/explorateur')}
- icon={}
- >
- Explorateur
-
-
-
- {/* Filter tabs + Table */}
-
-
-
- {!loading && filtered.length === 0 ? (
-
-
-
Aucun partage trouvé
-
- ) : (
-
- )}
-
-
-
- );
-};
-
-export default SharesPage;
diff --git a/src/modules/nuage/admin/index.js b/src/modules/nuage/admin/index.js
deleted file mode 100644
index a7e0199..0000000
--- a/src/modules/nuage/admin/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * Nuage Admin Components
- */
-export { default as ExplorerPage } from './ExplorerPage.js';
-export { default as SharesPage } from './SharesPage.js';
diff --git a/src/modules/nuage/api.js b/src/modules/nuage/api.js
deleted file mode 100644
index 29b68f8..0000000
--- a/src/modules/nuage/api.js
+++ /dev/null
@@ -1,679 +0,0 @@
-/**
- * Nuage Module API Routes
- * All API endpoints for the Nuage file manager
- */
-
-import { validateSession } from '../../features/auth/lib/session.js';
-import { cookies } from 'next/headers';
-import { getSessionCookieName } from '../../shared/lib/appConfig.js';
-import {
- getFolderContents,
- getFolderBreadcrumb,
- getFolderById,
- createFolder,
- renameFolder,
- moveFolder,
- deleteFolder,
- getFolderItemCount,
- getFileById,
- uploadNuageFile,
- renameFile,
- moveFile,
- deleteNuageFile,
- proxyNuageFile,
- getFolderUploadedSize,
- getShareByToken,
- getSharesForTarget,
- getSharesForUser,
- getAllActiveShares,
- createShare,
- updateShare,
- revokeShare,
- isShareValid,
- searchUsers,
- getSharedFolderContents,
- getSharedBreadcrumb,
- isFileInShare,
- isFolderInShare,
- getPasswordCookieName,
- signPasswordToken,
-} from './crud.js';
-import { sendEmail } from '@hykocx/zen/email';
-import { render } from '@react-email/components';
-import { NuageShareEmail } from './email/index.js';
-
-const COOKIE_NAME = getSessionCookieName();
-
-const MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024; // 500 MB hard cap
-
-const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-
-// ─── Auth helpers ────────────────────────────────────────────────────────────
-
-async function getAdminSession() {
- const cookieStore = await cookies();
- const sessionToken = cookieStore.get(COOKIE_NAME)?.value;
- if (!sessionToken) return null;
- const session = await validateSession(sessionToken);
- if (!session?.user?.id || session.user.role !== 'admin') return null;
- return session;
-}
-
-async function getUserSession() {
- const cookieStore = await cookies();
- const sessionToken = cookieStore.get(COOKIE_NAME)?.value;
- if (!sessionToken) return null;
- const session = await validateSession(sessionToken);
- if (!session?.user?.id) return null;
- return session;
-}
-
-// ─── Explorer: Folder Contents ───────────────────────────────────────────────
-
-async function handleGetContents(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const folderId = url.searchParams.get('folder') || null;
-
- const contents = await getFolderContents(folderId);
- const breadcrumb = await getFolderBreadcrumb(folderId);
- const currentFolder = folderId ? await getFolderById(folderId) : null;
-
- return { success: true, ...contents, breadcrumb, currentFolder };
- } catch (error) {
- console.error('handleGetContents error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── Folders ─────────────────────────────────────────────────────────────────
-
-async function handleCreateFolder(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const body = await request.json();
- const { name, parentId } = body;
- if (!name?.trim()) return { success: false, error: 'Le nom du dossier est requis' };
-
- const folder = await createFolder(name, parentId || null);
- return { success: true, folder };
- } catch (error) {
- console.error('handleCreateFolder error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleUpdateFolder(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const body = await request.json();
- const { id, action, name, newParentId } = body;
- if (!id) return { success: false, error: 'ID du dossier requis' };
-
- let folder;
- if (action === 'rename') {
- if (!name?.trim()) return { success: false, error: 'Le nom est requis' };
- folder = await renameFolder(id, name);
- } else if (action === 'move') {
- folder = await moveFolder(id, newParentId || null);
- } else {
- return { success: false, error: 'Action inconnue' };
- }
-
- return { success: true, folder };
- } catch (error) {
- console.error('handleUpdateFolder error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleDeleteFolder(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const id = url.searchParams.get('id');
- if (!id) return { success: false, error: 'ID du dossier requis' };
-
- const count = await getFolderItemCount(id);
- const forceDelete = url.searchParams.get('force') === 'true';
- if (count > 0 && !forceDelete) {
- return { success: false, error: 'FOLDER_NOT_EMPTY', itemCount: count };
- }
-
- await deleteFolder(id);
- return { success: true };
- } catch (error) {
- console.error('handleDeleteFolder error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── Files ───────────────────────────────────────────────────────────────────
-
-async function handleUploadFile(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const formData = await request.formData();
- const file = formData.get('file');
- const folderId = formData.get('folderId') || null;
-
- if (!file || typeof file === 'string') {
- return { success: false, error: 'Aucun fichier fourni' };
- }
-
- if (file.size > MAX_FILE_SIZE_BYTES) {
- return { success: false, error: 'Fichier trop volumineux (max 500 Mo)' };
- }
-
- const buffer = Buffer.from(await file.arrayBuffer());
- const nuageFile = await uploadNuageFile(
- folderId,
- buffer,
- file.name,
- file.type || 'application/octet-stream',
- buffer.length
- );
-
- return { success: true, file: nuageFile };
- } catch (error) {
- console.error('handleUploadFile error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleUpdateFile(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const body = await request.json();
- const { id, action, displayName, newFolderId } = body;
- if (!id) return { success: false, error: 'ID du fichier requis' };
-
- let file;
- if (action === 'rename') {
- if (!displayName?.trim()) return { success: false, error: 'Le nom est requis' };
- file = await renameFile(id, displayName);
- } else if (action === 'move') {
- file = await moveFile(id, newFolderId || null);
- } else {
- return { success: false, error: 'Action inconnue' };
- }
-
- return { success: true, file };
- } catch (error) {
- console.error('handleUpdateFile error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleDeleteFile(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const id = url.searchParams.get('id');
- if (!id) return { success: false, error: 'ID du fichier requis' };
-
- await deleteNuageFile(id);
- return { success: true };
- } catch (error) {
- console.error('handleDeleteFile error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleGetDownloadUrl(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const id = url.searchParams.get('id');
- if (!id) return { success: false, error: 'ID du fichier requis' };
-
- return await proxyNuageFile(id);
- } catch (error) {
- console.error('handleGetDownloadUrl error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── Shares ──────────────────────────────────────────────────────────────────
-
-async function handleGetShares(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const targetId = url.searchParams.get('targetId');
- const targetType = url.searchParams.get('targetType');
-
- if (targetId && targetType) {
- const shares = await getSharesForTarget(targetId, targetType);
- return { success: true, shares };
- }
-
- const shares = await getAllActiveShares();
- return { success: true, shares };
- } catch (error) {
- console.error('handleGetShares error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleCreateShare(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const body = await request.json();
- const {
- targetId, targetType, shareType, userId, permission,
- password, expiresAt, uploadLimitBytes,
- } = body;
-
- if (!targetId || !targetType || !shareType || !permission) {
- return { success: false, error: 'Paramètres manquants' };
- }
-
- const share = await createShare({
- targetId, targetType, shareType,
- userId: userId || null,
- permission,
- password: password || null,
- expiresAt: expiresAt || null,
- uploadLimitBytes: uploadLimitBytes || null,
- });
-
- return { success: true, share };
- } catch (error) {
- console.error('handleCreateShare error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleUpdateShare(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const body = await request.json();
- const {
- id, permission, expiresAt, uploadLimitBytes,
- password, clearPassword, userId, shareType, isActive,
- } = body;
-
- if (!id) return { success: false, error: 'ID du partage requis' };
-
- const share = await updateShare(id, {
- permission,
- expiresAt,
- uploadLimitBytes,
- password: password || null,
- clearPassword: clearPassword || false,
- userId,
- shareType,
- isActive,
- });
-
- return { success: true, share };
- } catch (error) {
- console.error('handleUpdateShare error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleRevokeShare(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const id = url.searchParams.get('id');
- if (!id) return { success: false, error: 'ID du partage requis' };
-
- const share = await revokeShare(id);
- return { success: true, share };
- } catch (error) {
- console.error('handleRevokeShare error:', error);
- return { success: false, error: error.message };
- }
-}
-
-async function handleSendShareEmail(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const body = await request.json();
- const { shareId, toEmail, toName, customMessage, shareUrl, targetName, targetType, shareType, expiresAt } = body;
-
- if (!toEmail) return { success: false, error: 'Adresse courriel requise' };
- if (!EMAIL_REGEX.test(toEmail)) return { success: false, error: 'Adresse courriel invalide' };
-
- const appName = process.env.ZEN_NAME || 'Nuage';
- const html = await render(
- NuageShareEmail({
- recipientName: toName || toEmail,
- shareUrl,
- targetName,
- targetType: targetType || 'folder',
- shareType,
- expiresAt: expiresAt || null,
- customMessage: customMessage || null,
- companyName: appName,
- })
- );
-
- await sendEmail({
- to: toEmail,
- subject: `Vous avez reçu un nouveau partage !`,
- html,
- });
-
- return { success: true };
- } catch (error) {
- console.error('handleSendShareEmail error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── User search ─────────────────────────────────────────────────────────────
-
-async function handleSearchUsers(request) {
- try {
- const session = await getAdminSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const q = url.searchParams.get('q') || '';
- if (q.length < 2) return { success: true, users: [] };
-
- const users = await searchUsers(q, 10);
- return { success: true, users };
- } catch (error) {
- console.error('handleSearchUsers error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── Client-facing: My Shares ────────────────────────────────────────────────
-
-async function handleGetMyShares(request) {
- try {
- const session = await getUserSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const shares = await getSharesForUser(session.user.id);
-
- // Enrich with target names
- const enriched = await Promise.all(
- shares.map(async (share) => {
- let targetName = '';
- if (share.target_type === 'file') {
- const file = await getFileById(share.target_id);
- targetName = file?.display_name || 'Fichier inconnu';
- } else {
- const folder = await getFolderById(share.target_id);
- targetName = folder?.name || 'Dossier inconnu';
- }
- return { ...share, target_name: targetName };
- })
- );
-
- return { success: true, shares: enriched };
- } catch (error) {
- console.error('handleGetMyShares error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── Client-facing: Shared folder contents ───────────────────────────────────
-
-async function handleGetSharedContents(request) {
- try {
- const session = await getUserSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const shareId = url.searchParams.get('shareId');
- const folderId = url.searchParams.get('folder') || null;
-
- if (!shareId) return { success: false, error: 'shareId requis' };
-
- const share = await getShareByToken(shareId);
- if (!share || !isShareValid(share) || share.user_id !== session.user.id) {
- return { success: false, error: 'Accès refusé' };
- }
-
- const contents = await getSharedFolderContents(share, folderId);
- const breadcrumb = await getSharedBreadcrumb(share, folderId);
-
- return { success: true, ...contents, breadcrumb, permission: share.permission };
- } catch (error) {
- console.error('handleGetSharedContents error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── Public share: File proxy download ───────────────────────────────────────
-
-async function handleShareFileProxy(request) {
- try {
- const url = new URL(request.url);
- const token = url.searchParams.get('token');
- const fileId = url.searchParams.get('fileId');
-
- if (!token || !fileId) return { error: 'Bad Request', message: 'Paramètres manquants' };
-
- const share = await getShareByToken(token);
- if (!share || !isShareValid(share)) {
- return { error: 'Unauthorized', message: 'Lien invalide ou expiré' };
- }
-
- // Enforce password protection server-side
- if (share.password_hash) {
- const cookieStore = await cookies();
- const cookieValue = cookieStore.get(getPasswordCookieName(token))?.value;
- if (!cookieValue || cookieValue !== signPasswordToken(token)) {
- return { error: 'Unauthorized', message: 'Mot de passe requis' };
- }
- }
-
- const file = await getFileById(fileId);
- if (!file) return { error: 'Not Found', message: 'Fichier introuvable' };
-
- // Ensure the requested file belongs to this share (prevent IDOR)
- const inScope = await isFileInShare(fileId, share);
- if (!inScope) return { error: 'Forbidden', message: 'Accès refusé' };
-
- const { getFile } = await import('@hykocx/zen/storage');
- const result = await getFile(file.r2_key);
- if (!result.success) return { error: 'Not Found', message: 'Fichier introuvable' };
-
- return {
- success: true,
- file: {
- body: result.data.body,
- contentType: file.mime_type || result.data.contentType,
- contentLength: result.data.contentLength,
- filename: file.display_name,
- }
- };
- } catch (error) {
- console.error('handleShareFileProxy error:', error);
- return { error: 'Internal Server Error', message: error.message };
- }
-}
-
-// ─── Client-facing: Download ─────────────────────────────────────────────────
-
-async function handleClientDownload(request) {
- try {
- const session = await getUserSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const url = new URL(request.url);
- const fileId = url.searchParams.get('fileId');
- const shareId = url.searchParams.get('shareId');
-
- if (!fileId || !shareId) return { success: false, error: 'Paramètres manquants' };
-
- // Validate the share belongs to the user
- const share = await getShareByToken(shareId);
- if (!share || !isShareValid(share) || share.user_id !== session.user.id) {
- return { success: false, error: 'Accès refusé' };
- }
-
- // Ensure the requested file belongs to this share (prevent IDOR)
- const inScope = await isFileInShare(fileId, share);
- if (!inScope) return { success: false, error: 'Accès refusé' };
-
- const inline = url.searchParams.get('inline') === 'true';
- return await proxyNuageFile(fileId, { inline });
- } catch (error) {
- console.error('handleClientDownload error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── Client-facing: Upload (collaborator) ────────────────────────────────────
-
-async function handleClientUpload(request) {
- try {
- const session = await getUserSession();
- if (!session) return { success: false, error: 'Unauthorized' };
-
- const formData = await request.formData();
- const file = formData.get('file');
- const shareId = formData.get('shareId');
- const folderId = formData.get('folderId') || null;
-
- if (!shareId) return { success: false, error: 'shareId requis' };
-
- const share = await getShareByToken(shareId);
- if (!share || !isShareValid(share) || share.user_id !== session.user.id) {
- return { success: false, error: 'Accès refusé' };
- }
- if (share.permission !== 'collaborator') {
- return { success: false, error: 'Permission insuffisante' };
- }
- if (share.target_type !== 'folder') {
- return { success: false, error: 'L\'upload n\'est possible que sur un dossier partagé' };
- }
- if (!file || typeof file === 'string') {
- return { success: false, error: 'Aucun fichier fourni' };
- }
-
- // Ensure the target folder is within the share scope (prevent IDOR)
- if (folderId && folderId !== share.target_id) {
- const inScope = await isFolderInShare(folderId, share);
- if (!inScope) return { success: false, error: 'Accès refusé' };
- }
-
- if (file.size > MAX_FILE_SIZE_BYTES) {
- return { success: false, error: 'Fichier trop volumineux (max 500 Mo)' };
- }
-
- // Check upload limit before buffering to avoid loading oversized files into memory
- if (share.upload_limit_bytes) {
- const fileSize = file.size;
- const targetFolder = folderId || share.target_id;
- const currentSize = await getFolderUploadedSize(targetFolder);
- if (currentSize + fileSize > share.upload_limit_bytes) {
- return { success: false, error: 'Limite de stockage du dossier partagé atteinte' };
- }
- }
-
- const buffer = Buffer.from(await file.arrayBuffer());
-
- const targetFolderId = folderId || share.target_id;
- const nuageFile = await uploadNuageFile(
- targetFolderId,
- buffer,
- file.name,
- file.type || 'application/octet-stream',
- buffer.length
- );
-
- return { success: true, file: nuageFile };
- } catch (error) {
- console.error('handleClientUpload error:', error);
- return { success: false, error: error.message };
- }
-}
-
-// ─── Route Definitions ───────────────────────────────────────────────────────
-
-export default {
- routes: [
- // Admin: Explorer
- { path: '/admin/nuage', method: 'GET', handler: handleGetContents, auth: 'admin' },
-
- // Admin: Folders
- { path: '/admin/nuage/folders', method: 'POST', handler: handleCreateFolder, auth: 'admin' },
- { path: '/admin/nuage/folders', method: 'PUT', handler: handleUpdateFolder, auth: 'admin' },
- { path: '/admin/nuage/folders', method: 'DELETE', handler: handleDeleteFolder, auth: 'admin' },
-
- // Admin: Files
- { path: '/admin/nuage/files', method: 'POST', handler: handleUploadFile, auth: 'admin' },
- { path: '/admin/nuage/files', method: 'PUT', handler: handleUpdateFile, auth: 'admin' },
- { path: '/admin/nuage/files', method: 'DELETE', handler: handleDeleteFile, auth: 'admin' },
- { path: '/admin/nuage/files/download', method: 'GET', handler: handleGetDownloadUrl, auth: 'admin' },
-
- // Admin: Shares
- { path: '/admin/nuage/shares', method: 'GET', handler: handleGetShares, auth: 'admin' },
- { path: '/admin/nuage/shares', method: 'POST', handler: handleCreateShare, auth: 'admin' },
- { path: '/admin/nuage/shares', method: 'PATCH', handler: handleUpdateShare, auth: 'admin' },
- { path: '/admin/nuage/shares', method: 'DELETE', handler: handleRevokeShare, auth: 'admin' },
- { path: '/admin/nuage/shares/email', method: 'POST', handler: handleSendShareEmail, auth: 'admin' },
-
- // Admin: User search
- { path: '/admin/nuage/users', method: 'GET', handler: handleSearchUsers, auth: 'admin' },
-
- // Public share proxy download (no auth required)
- { path: '/nuage/share/download', method: 'GET', handler: handleShareFileProxy, auth: 'none' },
-
- // Client-facing (authenticated user)
- { path: '/nuage/me', method: 'GET', handler: handleGetMyShares, auth: 'user' },
- { path: '/nuage/shared', method: 'GET', handler: handleGetSharedContents, auth: 'user' },
- { path: '/nuage/download', method: 'GET', handler: handleClientDownload, auth: 'user' },
- { path: '/nuage/upload', method: 'POST', handler: handleClientUpload, auth: 'user' },
- ],
-};
-
-export {
- handleGetContents,
- handleCreateFolder,
- handleUpdateFolder,
- handleDeleteFolder,
- handleUploadFile,
- handleUpdateFile,
- handleDeleteFile,
- handleGetDownloadUrl,
- handleGetShares,
- handleCreateShare,
- handleUpdateShare,
- handleRevokeShare,
- handleSendShareEmail,
- handleSearchUsers,
- handleGetMyShares,
- handleGetSharedContents,
- handleClientDownload,
- handleClientUpload,
- handleShareFileProxy,
-};
diff --git a/src/modules/nuage/components/FileViewerModal.js b/src/modules/nuage/components/FileViewerModal.js
deleted file mode 100644
index 046c507..0000000
--- a/src/modules/nuage/components/FileViewerModal.js
+++ /dev/null
@@ -1,85 +0,0 @@
-'use client';
-
-import React, { useEffect } from 'react';
-import { Cancel01Icon } from '../../../shared/Icons.js';
-
-// ─── Viewable file helpers ────────────────────────────────────────────────────
-
-const VIEWABLE_TYPES = [
- 'image/',
- 'application/pdf',
- 'video/',
- 'audio/',
- 'text/plain',
-];
-
-export function isViewable(mimeType) {
- if (!mimeType) return false;
- return VIEWABLE_TYPES.some(t => mimeType.startsWith(t));
-}
-
-function getViewerType(mimeType) {
- if (!mimeType) return null;
- if (mimeType.startsWith('image/')) return 'image';
- if (mimeType === 'application/pdf') return 'pdf';
- if (mimeType.startsWith('video/')) return 'video';
- if (mimeType.startsWith('audio/')) return 'audio';
- if (mimeType === 'text/plain') return 'text';
- return null;
-}
-
-// ─── File viewer modal ────────────────────────────────────────────────────────
-
-export default function FileViewerModal({ file, url, onClose }) {
- const viewerType = getViewerType(file.mime_type);
- const fileName = file.display_name || file.name;
-
- useEffect(() => {
- const handler = (e) => { if (e.key === 'Escape') onClose(); };
- window.addEventListener('keydown', handler);
- return () => window.removeEventListener('keydown', handler);
- }, [onClose]);
-
- return (
-
- {/* Header */}
-
-
- {/* Content */}
-
- {viewerType === 'image' && (
-

- )}
- {(viewerType === 'pdf' || viewerType === 'text') && (
-
- )}
- {viewerType === 'video' && (
-
- )}
- {viewerType === 'audio' && (
-
- )}
-
-
- );
-}
diff --git a/src/modules/nuage/components/NuageFileTable.js b/src/modules/nuage/components/NuageFileTable.js
deleted file mode 100644
index 6e64fcc..0000000
--- a/src/modules/nuage/components/NuageFileTable.js
+++ /dev/null
@@ -1,219 +0,0 @@
-'use client';
-
-import React from 'react';
-import {
- Folder01Icon,
- File02Icon,
- Image01Icon,
- PlaySquareIcon,
- Pdf02Icon,
- Mp302Icon,
- Ppt02Icon,
- Gif02Icon,
- Rar02Icon,
- Raw02Icon,
- Svg02Icon,
- Tiff02Icon,
- Txt02Icon,
- Typescript03Icon,
- Wav02Icon,
- Xls02Icon,
- Xml02Icon,
- Zip02Icon,
- Csv02Icon,
- Doc02Icon,
-} from '../../../shared/Icons.js';
-import { Badge, Table } from '../../../shared/components';
-
-// ─── File type helpers ────────────────────────────────────────────────────────
-
-export function getFileIcon(mimeType) {
- if (!mimeType) return File02Icon;
- if (mimeType === 'image/gif') return Gif02Icon;
- if (mimeType === 'image/svg+xml') return Svg02Icon;
- if (mimeType === 'image/tiff') return Tiff02Icon;
- if (mimeType.startsWith('image/')) return Image01Icon;
- if (mimeType.startsWith('video/')) return PlaySquareIcon;
- if (mimeType === 'application/pdf') return Pdf02Icon;
- if (mimeType === 'audio/mpeg' || mimeType === 'audio/mp3') return Mp302Icon;
- if (mimeType === 'audio/wav' || mimeType === 'audio/wave') return Wav02Icon;
- if (mimeType === 'application/vnd.ms-powerpoint' || mimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') return Ppt02Icon;
- if (mimeType === 'application/vnd.rar' || mimeType === 'application/x-rar-compressed') return Rar02Icon;
- if (mimeType === 'image/x-raw' || mimeType === 'image/x-canon-cr2' || mimeType === 'image/x-nikon-nef') return Raw02Icon;
- if (mimeType === 'text/plain') return Txt02Icon;
- if (mimeType === 'application/typescript' || mimeType === 'text/typescript') return Typescript03Icon;
- if (mimeType === 'application/vnd.ms-excel' || mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') return Xls02Icon;
- if (mimeType === 'application/xml' || mimeType === 'text/xml') return Xml02Icon;
- if (mimeType === 'application/zip' || mimeType === 'application/x-zip-compressed') return Zip02Icon;
- if (mimeType === 'text/csv') return Csv02Icon;
- if (mimeType === 'application/msword' || mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') return Doc02Icon;
- return File02Icon;
-}
-
-export function getFileColor(mimeType) {
- if (!mimeType) return 'text-neutral-500';
- if (mimeType.startsWith('image/')) return 'text-teal-700';
- if (mimeType.startsWith('video/')) return 'text-red-800';
- if (mimeType === 'application/pdf') return 'text-red-800';
- if (mimeType === 'audio/mpeg' || mimeType === 'audio/mp3' || mimeType === 'audio/wav' || mimeType === 'audio/wave') return 'text-indigo-700';
- if (mimeType === 'application/vnd.ms-powerpoint' || mimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') return 'text-orange-700';
- if (mimeType === 'application/vnd.rar' || mimeType === 'application/x-rar-compressed') return 'text-amber-800';
- if (mimeType === 'text/plain') return 'text-neutral-500';
- if (mimeType === 'application/typescript' || mimeType === 'text/typescript') return 'text-blue-700';
- if (mimeType === 'application/vnd.ms-excel' || mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') return 'text-green-700';
- if (mimeType === 'application/xml' || mimeType === 'text/xml') return 'text-orange-800';
- if (mimeType === 'application/zip' || mimeType === 'application/x-zip-compressed') return 'text-amber-700';
- if (mimeType === 'text/csv') return 'text-emerald-700';
- if (mimeType === 'application/msword' || mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') return 'text-blue-800';
- return 'text-neutral-500';
-}
-
-export function formatBytes(bytes) {
- if (!bytes || bytes === 0) return '0 o';
- const units = ['o', 'Ko', 'Mo', 'Go'];
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
- return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
-}
-
-export function formatDate(dateStr) {
- if (!dateStr) return '';
- return new Date(dateStr).toLocaleDateString('fr-CA', {
- year: 'numeric', month: 'short', day: 'numeric',
- });
-}
-
-// ─── NuageFileTable ───────────────────────────────────────────────────────────
-
-/**
- * Unified file listing table for the Nuage module.
- *
- * Each item must have a `_type` field: 'folder' | 'file' | 'share'.
- *
- * Props:
- * items — array of rows (folders, files, or shares with `_type`)
- * loading — show skeleton rows
- * sortBy — active sort key
- * sortOrder — 'asc' | 'desc'
- * onSort — (key) => void — called when a sortable header is clicked
- * onRowClick — (item) => void
- * renderActions — (item) => ReactNode — content for the actions column
- * showShareBadge — show a "Partagé" badge on items with has_active_share
- * emptyMessage — primary empty state text
- * emptyDescription — secondary empty state text
- * size — Table size prop (default 'sm')
- * className — extra class on the Table
- * getRowProps — (item) => object — extra props per row
- */
-export default function NuageFileTable({
- items = [],
- loading = false,
- sortBy,
- sortOrder,
- onSort,
- onRowClick,
- renderActions,
- showShareBadge = false,
- emptyMessage = 'Ce dossier est vide',
- emptyDescription,
- size = 'sm',
- className,
- getRowProps,
-}) {
- const sortable = !!onSort;
-
- const columns = [
- {
- key: 'name',
- label: 'Nom',
- sortable,
- render: (item) => {
- if (item._type === 'share') {
- const isFolder = item.target_type === 'folder';
- const isExpired = item.expires_at && new Date(item.expires_at) < new Date();
- const Icon = isFolder ? Folder01Icon : File02Icon;
- return (
-
-
-
- {item.target_name || (isFolder ? 'Dossier' : 'Fichier')}
-
-
- );
- }
-
- if (item._type === 'folder') {
- return (
-
-
- {item.name}
- {showShareBadge && item.has_active_share && (
- Partagé
- )}
-
- );
- }
-
- // file
- const FileIcon = getFileIcon(item.mime_type);
- const fileColor = getFileColor(item.mime_type);
- return (
-
-
- {item.display_name}
- {showShareBadge && item.has_active_share && (
- Partagé
- )}
-
- );
- },
- skeleton: { height: 'h-4', width: '60%' },
- },
- {
- key: 'date',
- label: 'Date',
- sortable,
- render: (item) => {
- if (item._type === 'share') {
- return item.expires_at ? (
-
Expire {formatDate(item.expires_at)}
- ) : null;
- }
- return
{formatDate(item.created_at)};
- },
- skeleton: { height: 'h-4', width: '40%' },
- },
- {
- key: 'size',
- label: 'Taille',
- sortable,
- render: (item) => {
- if (item._type !== 'file') return
—;
- return
{formatBytes(item.size)};
- },
- skeleton: { height: 'h-4', width: '30%' },
- },
- ...(renderActions ? [{
- key: 'actions',
- label: '',
- render: (item) => renderActions(item),
- skeleton: { height: 'h-8', width: '40px' },
- }] : []),
- ];
-
- return (
-
- );
-}
diff --git a/src/modules/nuage/components/SharePanel.js b/src/modules/nuage/components/SharePanel.js
deleted file mode 100644
index 074eaa8..0000000
--- a/src/modules/nuage/components/SharePanel.js
+++ /dev/null
@@ -1,776 +0,0 @@
-'use client';
-
-import React, { useState, useEffect, useRef } from 'react';
-import {
- Cancel01Icon,
- Link02Icon,
- Copy01Icon,
- Delete02Icon,
- UserCircle02Icon,
-} from '../../../shared/Icons.js';
-import { Button, Badge, Loading, Input, Modal, Textarea } from '../../../shared/components';
-import { formatBytes, formatDate } from './NuageFileTable.js';
-import { useToast } from '@hykocx/zen/toast';
-
-// ─── User search input ────────────────────────────────────────────────────────
-
-function UserSearchInput({ selectedUser, onSelect, onClear, query, onQueryChange, results, searching }) {
- return (
-
-
- {selectedUser ? (
-
-
-
{selectedUser.name}
-
{selectedUser.email}
-
-
-
-
-
- ) : (
- <>
-
- {results.length > 0 && (
-
- {results.map(u => (
-
onSelect(u)}
- className="w-full text-left px-3 py-2 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors first:rounded-t-xl last:rounded-b-xl"
- >
- {u.name}
- {u.email}
-
- ))}
-
- )}
- {searching &&
Recherche…
}
- >
- )}
-
- );
-}
-
-// ─── Share panel ──────────────────────────────────────────────────────────────
-
-export default function SharePanel({ target, onClose }) {
- const toast = useToast();
- const [shares, setShares] = useState([]);
- const [loading, setLoading] = useState(true);
- const [tab, setTab] = useState('list'); // 'list' | 'create'
-
- // Create share form
- const [shareType, setShareType] = useState('anonymous'); // 'user' | 'anonymous'
- const [permission, setPermission] = useState('reader');
- const [password, setPassword] = useState('');
- const [expiresPreset, setExpiresPreset] = useState('');
- const [uploadLimit, setUploadLimit] = useState('');
- const [creating, setCreating] = useState(false);
- const [createdShare, setCreatedShare] = useState(null);
-
- // User search (create)
- const [userQuery, setUserQuery] = useState('');
- const [userResults, setUserResults] = useState([]);
- const [selectedUser, setSelectedUser] = useState(null);
- const [searchingUsers, setSearchingUsers] = useState(false);
- const searchTimeout = useRef(null);
-
- // Email modal
- const [showEmailModal, setShowEmailModal] = useState(false);
- const [emailAddress, setEmailAddress] = useState('');
- const [emailMessage, setEmailMessage] = useState('');
- const [sendingEmail, setSendingEmail] = useState(false);
- const [emailShareTarget, setEmailShareTarget] = useState(null);
-
- // Edit modal
- const [editingShare, setEditingShare] = useState(null);
- const [editShareType, setEditShareType] = useState('anonymous');
- const [editPermission, setEditPermission] = useState('reader');
- const [editExpiresPreset, setEditExpiresPreset] = useState('keep');
- const [editUploadLimit, setEditUploadLimit] = useState('');
- const [editPassword, setEditPassword] = useState('');
- const [editClearPassword, setEditClearPassword] = useState(false);
- const [editSelectedUser, setEditSelectedUser] = useState(null);
- const [editUserQuery, setEditUserQuery] = useState('');
- const [editUserResults, setEditUserResults] = useState([]);
- const [editSearchingUsers, setEditSearchingUsers] = useState(false);
- const [updating, setUpdating] = useState(false);
- const editSearchTimeout = useRef(null);
-
- useEffect(() => {
- loadShares();
- }, [target]);
-
- const loadShares = async () => {
- setLoading(true);
- try {
- const res = await fetch(
- `/zen/api/admin/nuage/shares?targetId=${target.id}&targetType=${target.type}`,
- { credentials: 'include' }
- );
- const data = await res.json();
- if (data.success) setShares(data.shares);
- } catch (e) {
- console.error(e);
- } finally {
- setLoading(false);
- }
- };
-
- const makeUserSearchHandler = (setQuery, setSelected, setResults, setSearching, timeoutRef) => (q) => {
- setQuery(q);
- setSelected(null);
- clearTimeout(timeoutRef.current);
- if (q.length < 2) { setResults([]); return; }
- timeoutRef.current = setTimeout(async () => {
- setSearching(true);
- try {
- const res = await fetch(`/zen/api/admin/nuage/users?q=${encodeURIComponent(q)}`, { credentials: 'include' });
- const data = await res.json();
- if (data.success) setResults(data.users);
- } catch (e) {
- console.error(e);
- } finally {
- setSearching(false);
- }
- }, 300);
- };
-
- const handleUserSearch = makeUserSearchHandler(setUserQuery, setSelectedUser, setUserResults, setSearchingUsers, searchTimeout);
- const handleEditUserSearch = makeUserSearchHandler(setEditUserQuery, setEditSelectedUser, setEditUserResults, setEditSearchingUsers, editSearchTimeout);
-
- const getExpiresAt = (preset) => {
- if (!preset || preset === 'keep') return preset === 'keep' ? undefined : null;
- const days = { '24h': 1, '7d': 7, '30d': 30, '90d': 90 }[preset];
- if (!days) return null;
- const d = new Date();
- d.setDate(d.getDate() + days);
- return d.toISOString();
- };
-
- const handleCreate = async () => {
- if (shareType === 'user' && !selectedUser) {
- toast.error('Veuillez sélectionner un utilisateur');
- return;
- }
- setCreating(true);
- try {
- const res = await fetch('/zen/api/admin/nuage/shares', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({
- targetId: target.id,
- targetType: target.type,
- shareType,
- userId: shareType === 'user' ? selectedUser?.id : null,
- permission,
- password: shareType === 'anonymous' && password ? password : null,
- expiresAt: getExpiresAt(expiresPreset),
- uploadLimitBytes: permission === 'collaborator' && target.type === 'folder' && uploadLimit
- ? parseInt(uploadLimit) * 1024 * 1024
- : null,
- }),
- });
- const data = await res.json();
- if (data.success) {
- toast.success('Partage créé');
- setCreatedShare(data.share);
- loadShares();
- setTab('list');
- setPassword('');
- setExpiresPreset('');
- setUploadLimit('');
- setSelectedUser(null);
- setUserQuery('');
- } else {
- toast.error(data.error || 'Erreur lors de la création du partage');
- }
- } catch (e) {
- toast.error('Erreur réseau');
- } finally {
- setCreating(false);
- }
- };
-
- const openEditModal = (share) => {
- setEditingShare(share);
- setEditShareType(share.share_type);
- setEditPermission(share.permission);
- setEditExpiresPreset('keep');
- setEditUploadLimit(share.upload_limit_bytes ? String(Math.round(share.upload_limit_bytes / (1024 * 1024))) : '');
- setEditPassword('');
- setEditClearPassword(false);
- setEditSelectedUser(share.share_type === 'user' ? { id: share.user_id, name: share.name, email: share.email } : null);
- setEditUserQuery('');
- setEditUserResults([]);
- };
-
- const handleUpdate = async () => {
- if (editShareType === 'user' && !editSelectedUser) {
- toast.error('Veuillez sélectionner un utilisateur');
- return;
- }
- setUpdating(true);
- try {
- const body = { id: editingShare.id };
- body.permission = editPermission;
- body.shareType = editShareType;
- body.userId = editShareType === 'user' ? (editSelectedUser?.id || null) : null;
-
- const expiresAt = getExpiresAt(editExpiresPreset);
- if (expiresAt !== undefined) body.expiresAt = expiresAt;
-
- if (editPermission === 'collaborator' && target.type === 'folder') {
- body.uploadLimitBytes = editUploadLimit ? parseInt(editUploadLimit) * 1024 * 1024 : null;
- } else {
- body.uploadLimitBytes = null;
- }
-
- if (editShareType === 'anonymous') {
- if (editClearPassword) {
- body.clearPassword = true;
- } else if (editPassword) {
- body.password = editPassword;
- }
- } else {
- body.clearPassword = true;
- }
-
- const res = await fetch('/zen/api/admin/nuage/shares', {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify(body),
- });
- const data = await res.json();
- if (data.success) {
- toast.success('Partage modifié');
- setEditingShare(null);
- loadShares();
- } else {
- toast.error(data.error || 'Erreur lors de la modification');
- }
- } catch (e) {
- toast.error('Erreur réseau');
- } finally {
- setUpdating(false);
- }
- };
-
- const handleRevoke = async (shareId) => {
- try {
- const res = await fetch(`/zen/api/admin/nuage/shares?id=${shareId}`, {
- method: 'DELETE',
- credentials: 'include',
- });
- const data = await res.json();
- if (data.success) {
- toast.success('Partage révoqué');
- loadShares();
- } else {
- toast.error(data.error || 'Erreur');
- }
- } catch (e) {
- toast.error('Erreur réseau');
- }
- };
-
- const getSharePublicUrl = (token) => {
- const base = typeof window !== 'undefined' ? window.location.origin : '';
- return `${base}/zen/nuage/partage/${token}`;
- };
-
- const copyToClipboard = (text) => {
- navigator.clipboard.writeText(text).then(() => toast.success('Lien copié !'));
- };
-
- const openEmailModal = (share) => {
- setEmailShareTarget(share);
- setEmailAddress(share.share_type === 'user' ? (share.email || '') : '');
- setEmailMessage('');
- setShowEmailModal(true);
- };
-
- const handleSendEmail = async () => {
- if (!emailAddress) { toast.error('Adresse courriel requise'); return; }
- setSendingEmail(true);
- try {
- const shareUrl = getSharePublicUrl(emailShareTarget.token);
- const res = await fetch('/zen/api/admin/nuage/shares/email', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({
- shareId: emailShareTarget.id,
- toEmail: emailAddress,
- toName: emailShareTarget.share_type === 'user' ? (emailShareTarget.name || '') : '',
- customMessage: emailMessage || null,
- shareUrl,
- targetName: target.name,
- targetType: target.type,
- shareType: emailShareTarget.share_type,
- expiresAt: emailShareTarget.expires_at || null,
- }),
- });
- const data = await res.json();
- if (data.success) {
- toast.success('Courriel envoyé');
- setShowEmailModal(false);
- } else {
- toast.error(data.error || 'Erreur lors de l\'envoi');
- }
- } catch (e) {
- toast.error('Erreur réseau');
- } finally {
- setSendingEmail(false);
- }
- };
-
- const isShareExpired = (share) => {
- if (!share.expires_at) return false;
- return new Date(share.expires_at) < new Date();
- };
-
- const activeTabClass = 'border-b-2 border-neutral-900 dark:border-white text-neutral-900 dark:text-white font-medium';
- const inactiveTabClass = 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-600 dark:hover:text-neutral-300';
- const activeChipClass = 'bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 border-neutral-900 dark:border-white';
- const inactiveChipClass = 'bg-transparent border-neutral-200 dark:border-neutral-700 text-neutral-500 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-500';
-
- return (
-
- {/* Header */}
-
-
-
Partager
-
{target.name}
-
-
-
-
-
-
- {/* Tabs */}
-
- setTab('list')}
- className={`py-2.5 text-xs mr-4 transition-colors ${tab === 'list' ? activeTabClass : inactiveTabClass}`}
- >
- Actifs {shares.length > 0 && ({shares.length})}
-
- setTab('create')}
- className={`py-2.5 text-xs transition-colors ${tab === 'create' ? activeTabClass : inactiveTabClass}`}
- >
- + Nouveau
-
-
-
-
- {/* ── List tab ── */}
- {tab === 'list' && (
-
- {loading ? (
-
- ) : shares.length === 0 ? (
-
-
-
Aucun partage actif
-
- ) : (
- shares.map(share => {
- const expired = isShareExpired(share);
- const url = getSharePublicUrl(share.token);
- return (
-
-
-
-
- {share.share_type === 'user' ? (
-
- ) : (
-
- )}
-
-
-
- {share.share_type === 'user'
- ? share.name || share.email || 'Utilisateur'
- : 'Lien anonyme'}
-
-
- {share.permission === 'reader' ? 'Lecteur' : 'Collaborateur'}
- {share.expires_at && ` · ${formatDate(share.expires_at)}`}
- {share.upload_limit_bytes && ` · ${formatBytes(share.upload_limit_bytes)}`}
-
-
-
-
- {!share.is_active ? 'Révoqué' : expired ? 'Expiré' : 'Actif'}
-
-
-
- {share.is_active && !expired && (
- <>
- copyToClipboard(url)}
- className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
- >
- Copier
-
- openEmailModal(share)}
- className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
- >
- Envoyer
-
- >
- )}
- openEditModal(share)}
- className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
- >
- Modifier
-
- {share.is_active && !expired && (
- handleRevoke(share.id)}
- className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-lg transition-colors ml-auto"
- >
- Révoquer
-
- )}
-
-
- );
- })
- )}
-
- )}
-
- {/* ── Create tab ── */}
- {tab === 'create' && (
-
- {/* Share type */}
-
-
-
- setShareType('anonymous')}
- className={`flex-1 py-1.5 text-xs rounded-xl border font-medium transition-all ${shareType === 'anonymous' ? activeChipClass : inactiveChipClass}`}
- >
- Lien
-
- setShareType('user')}
- className={`flex-1 py-1.5 text-xs rounded-xl border font-medium transition-all ${shareType === 'user' ? activeChipClass : inactiveChipClass}`}
- >
- Utilisateur
-
-
-
-
- {/* User search */}
- {shareType === 'user' && (
-
{ setSelectedUser(u); setUserQuery(''); setUserResults([]); }}
- onClear={() => { setSelectedUser(null); setUserQuery(''); }}
- query={userQuery}
- onQueryChange={handleUserSearch}
- results={userResults}
- searching={searchingUsers}
- />
- )}
-
- {/* Permission */}
-
-
-
- setPermission('reader')}
- className={`flex-1 py-1.5 text-xs rounded-xl border font-medium transition-all ${permission === 'reader' ? activeChipClass : inactiveChipClass}`}
- >
- Lecteur
-
- setPermission('collaborator')}
- className={`flex-1 py-1.5 text-xs rounded-xl border font-medium transition-all ${permission === 'collaborator' ? activeChipClass : inactiveChipClass}`}
- >
- Collaborateur
-
-
-
-
- {/* Upload limit (collaborator + folder only) */}
- {permission === 'collaborator' && target.type === 'folder' && (
-
- setUploadLimit(v)}
- placeholder="Illimité"
- description="Quota de stockage total pour ce partage"
- />
-
- )}
-
- {/* Password (anonymous only) */}
- {shareType === 'anonymous' && (
-
- setPassword(v)}
- placeholder="Aucun"
- />
-
- )}
-
- {/* Expiration */}
-
-
-
- {['', '24h', '7d', '30d', '90d'].map(preset => (
- setExpiresPreset(preset)}
- className={`px-2.5 py-1 text-xs rounded-xl border font-medium transition-all ${expiresPreset === preset ? activeChipClass : inactiveChipClass}`}
- >
- {preset === '' ? 'Aucune' : preset === '24h' ? '24 h' : preset === '7d' ? '7 j' : preset === '30d' ? '30 j' : '90 j'}
-
- ))}
-
-
-
-
- {shareType === 'anonymous' ? 'Générer le lien' : 'Créer le partage'}
-
-
- )}
-
-
- {/* Email modal */}
- {showEmailModal && (
-
setShowEmailModal(false)}
- title="Envoyer par courriel"
- size="sm"
- footer={
-
- setShowEmailModal(false)}>Annuler
- Envoyer
-
- }
- >
-
- setEmailAddress(v)}
- placeholder="courriel@exemple.com"
- />
-
-
- )}
-
- {/* Edit share modal */}
- {editingShare && (
-
setEditingShare(null)}
- title="Modifier le partage"
- size="sm"
- footer={
-
- setEditingShare(null)}>Annuler
- Enregistrer
-
- }
- >
-
- {/* Type */}
-
-
-
- setEditShareType('anonymous')}
- className={`flex-1 py-1.5 text-xs rounded-xl border font-medium transition-all ${editShareType === 'anonymous' ? activeChipClass : inactiveChipClass}`}
- >
- Lien
-
- setEditShareType('user')}
- className={`flex-1 py-1.5 text-xs rounded-xl border font-medium transition-all ${editShareType === 'user' ? activeChipClass : inactiveChipClass}`}
- >
- Utilisateur
-
-
-
-
- {/* User search */}
- {editShareType === 'user' && (
-
{ setEditSelectedUser(u); setEditUserQuery(''); setEditUserResults([]); }}
- onClear={() => { setEditSelectedUser(null); setEditUserQuery(''); }}
- query={editUserQuery}
- onQueryChange={handleEditUserSearch}
- results={editUserResults}
- searching={editSearchingUsers}
- />
- )}
-
- {/* Permission */}
-
-
-
- setEditPermission('reader')}
- className={`flex-1 py-1.5 text-xs rounded-xl border font-medium transition-all ${editPermission === 'reader' ? activeChipClass : inactiveChipClass}`}
- >
- Lecteur
-
- setEditPermission('collaborator')}
- className={`flex-1 py-1.5 text-xs rounded-xl border font-medium transition-all ${editPermission === 'collaborator' ? activeChipClass : inactiveChipClass}`}
- >
- Collaborateur
-
-
-
-
- {/* Upload limit (collaborator + folder only) */}
- {editPermission === 'collaborator' && target.type === 'folder' && (
- setEditUploadLimit(v)}
- placeholder="Illimité"
- description="Quota de stockage total pour ce partage"
- />
- )}
-
- {/* Password (anonymous only) */}
- {editShareType === 'anonymous' && (
-
-
- {editingShare.password_hash && !editClearPassword && (
-
-
Mot de passe défini
-
setEditClearPassword(true)}
- className="text-[11px] font-medium text-red-500 hover:text-red-600 transition-colors"
- >
- Supprimer
-
-
- )}
- {editClearPassword && (
-
-
Mot de passe supprimé
-
setEditClearPassword(false)}
- className="text-[11px] font-medium text-neutral-500 hover:text-neutral-700 transition-colors"
- >
- Annuler
-
-
- )}
- {!editClearPassword && (
-
setEditPassword(v)}
- placeholder={editingShare.password_hash ? 'Nouveau mot de passe…' : 'Ajouter un mot de passe…'}
- />
- )}
-
- )}
-
- {/* Expiration */}
-
-
-
- {['keep', '', '24h', '7d', '30d', '90d'].map(preset => (
- setEditExpiresPreset(preset)}
- className={`px-2.5 py-1 text-xs rounded-xl border font-medium transition-all ${editExpiresPreset === preset ? activeChipClass : inactiveChipClass}`}
- >
- {preset === 'keep' ? 'Conserver' : preset === '' ? 'Aucune' : preset === '24h' ? '24 h' : preset === '7d' ? '7 j' : preset === '30d' ? '30 j' : '90 j'}
-
- ))}
-
-
-
- {/* Reactivate revoked share */}
- {!editingShare.is_active && (
-
-
-
Réactiver le partage
-
Ce partage est actuellement révoqué
-
-
{
- setUpdating(true);
- try {
- const res = await fetch('/zen/api/admin/nuage/shares', {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ id: editingShare.id, isActive: true }),
- });
- const data = await res.json();
- if (data.success) {
- toast.success('Partage réactivé');
- setEditingShare(null);
- loadShares();
- } else {
- toast.error(data.error || 'Erreur');
- }
- } catch (e) {
- toast.error('Erreur réseau');
- } finally {
- setUpdating(false);
- }
- }}>
- Réactiver
-
-
- )}
-
-
- )}
-
- );
-}
diff --git a/src/modules/nuage/crud.js b/src/modules/nuage/crud.js
deleted file mode 100644
index ca418b2..0000000
--- a/src/modules/nuage/crud.js
+++ /dev/null
@@ -1,461 +0,0 @@
-/**
- * Nuage Module — CRUD
- * Database operations for folders, files, and shares + R2 integration
- */
-
-import { query } from '@hykocx/zen/database';
-import { uploadFile, deleteFile, proxyFile } from '@hykocx/zen/storage';
-import crypto from 'crypto';
-
-// ─── Helpers ────────────────────────────────────────────────────────────────
-
-const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
-
-function sanitizeFilename(name) {
- const sanitized = name
- .normalize('NFD')
- .replace(/[\u0300-\u036f]/g, '')
- .replace(/[^a-zA-Z0-9._-]/g, '_')
- .replace(/_+/g, '_')
- .substring(0, 200);
- return sanitized || 'fichier';
-}
-
-function generateNuageFilePath(fileId, filename) {
- return `nuage/files/${fileId}/${sanitizeFilename(filename)}`;
-}
-
-// ─── Folders ────────────────────────────────────────────────────────────────
-
-export async function getFolderById(id) {
- if (!id || !UUID_REGEX.test(id)) return null;
- const result = await query('SELECT * FROM zen_nuage_folders WHERE id = $1', [id]);
- return result.rows[0] || null;
-}
-
-export async function getFolderContents(folderId = null) {
- const param = folderId || null;
-
- const folderResult = await query(
- `SELECT f.*,
- (SELECT COUNT(*) > 0 FROM zen_nuage_shares s WHERE s.target_id = f.id AND s.target_type = 'folder' AND s.is_active = true AND (s.expires_at IS NULL OR s.expires_at > NOW())) AS has_active_share
- FROM zen_nuage_folders f
- WHERE f.parent_id ${param ? '= $1' : 'IS NULL'}
- ORDER BY f.name ASC`,
- param ? [param] : []
- );
-
- const fileResult = await query(
- `SELECT f.*,
- (SELECT COUNT(*) > 0 FROM zen_nuage_shares s WHERE s.target_id = f.id AND s.target_type = 'file' AND s.is_active = true AND (s.expires_at IS NULL OR s.expires_at > NOW())) AS has_active_share
- FROM zen_nuage_files f
- WHERE f.folder_id ${param ? '= $1' : 'IS NULL'}
- ORDER BY f.display_name ASC`,
- param ? [param] : []
- );
-
- return {
- folders: folderResult.rows,
- files: fileResult.rows,
- };
-}
-
-export async function getFolderBreadcrumb(folderId) {
- if (!folderId) return [];
- const breadcrumb = [];
- let currentId = folderId;
-
- while (currentId) {
- const result = await query('SELECT * FROM zen_nuage_folders WHERE id = $1', [currentId]);
- const folder = result.rows[0];
- if (!folder) break;
- breadcrumb.unshift(folder);
- currentId = folder.parent_id;
- }
- return breadcrumb;
-}
-
-export async function createFolder(name, parentId = null) {
- let depth = 0;
- if (parentId) {
- const parent = await getFolderById(parentId);
- if (!parent) throw new Error('Dossier parent introuvable');
- depth = parent.depth + 1;
- if (depth > 10) throw new Error('Profondeur maximale de 10 niveaux atteinte');
- }
- const result = await query(
- `INSERT INTO zen_nuage_folders (name, parent_id, depth) VALUES ($1, $2, $3) RETURNING *`,
- [name.trim(), parentId, depth]
- );
- return result.rows[0];
-}
-
-export async function renameFolder(id, name) {
- const result = await query(
- `UPDATE zen_nuage_folders SET name = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *`,
- [name.trim(), id]
- );
- return result.rows[0] || null;
-}
-
-export async function moveFolder(id, newParentId) {
- let depth = 0;
- if (newParentId) {
- const parent = await getFolderById(newParentId);
- if (!parent) throw new Error('Dossier parent introuvable');
- depth = parent.depth + 1;
- if (depth > 10) throw new Error('Profondeur maximale de 10 niveaux atteinte');
- }
- const result = await query(
- `UPDATE zen_nuage_folders SET parent_id = $1, depth = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3 RETURNING *`,
- [newParentId, depth, id]
- );
- return result.rows[0] || null;
-}
-
-export async function deleteFolder(id) {
- // Collect all r2_keys recursively before deleting
- const r2Keys = await collectFolderR2Keys(id);
- for (const key of r2Keys) {
- await deleteFile(key).catch(() => {});
- }
- await query('DELETE FROM zen_nuage_folders WHERE id = $1', [id]);
- return { success: true };
-}
-
-async function collectFolderR2Keys(folderId) {
- const keys = [];
- const filesResult = await query('SELECT r2_key FROM zen_nuage_files WHERE folder_id = $1', [folderId]);
- for (const row of filesResult.rows) keys.push(row.r2_key);
-
- const subFolders = await query('SELECT id FROM zen_nuage_folders WHERE parent_id = $1', [folderId]);
- for (const sub of subFolders.rows) {
- const subKeys = await collectFolderR2Keys(sub.id);
- keys.push(...subKeys);
- }
- return keys;
-}
-
-export async function getFolderItemCount(folderId) {
- const folders = await query('SELECT COUNT(*) FROM zen_nuage_folders WHERE parent_id = $1', [folderId]);
- const files = await query('SELECT COUNT(*) FROM zen_nuage_files WHERE folder_id = $1', [folderId]);
- return parseInt(folders.rows[0].count) + parseInt(files.rows[0].count);
-}
-
-// ─── Files ──────────────────────────────────────────────────────────────────
-
-export async function getFileById(id) {
- if (!id || !UUID_REGEX.test(id)) return null;
- const result = await query('SELECT * FROM zen_nuage_files WHERE id = $1', [id]);
- return result.rows[0] || null;
-}
-
-export async function uploadNuageFile(folderId, buffer, originalName, mimeType, size) {
- const fileId = crypto.randomUUID();
- const r2Key = generateNuageFilePath(fileId, originalName);
-
- const uploadResult = await uploadFile({ key: r2Key, body: buffer, contentType: mimeType });
- if (!uploadResult.success) throw new Error(uploadResult.error || 'Échec du téléversement vers R2');
-
- const result = await query(
- `INSERT INTO zen_nuage_files (id, folder_id, original_name, display_name, mime_type, size, r2_key)
- VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
- [fileId, folderId || null, originalName, originalName, mimeType, size, r2Key]
- );
- return result.rows[0];
-}
-
-export async function renameFile(id, displayName) {
- const result = await query(
- `UPDATE zen_nuage_files SET display_name = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *`,
- [displayName.trim(), id]
- );
- return result.rows[0] || null;
-}
-
-export async function moveFile(id, newFolderId) {
- const result = await query(
- `UPDATE zen_nuage_files SET folder_id = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *`,
- [newFolderId || null, id]
- );
- return result.rows[0] || null;
-}
-
-export async function deleteNuageFile(id) {
- const file = await getFileById(id);
- if (!file) throw new Error('Fichier introuvable');
- await deleteFile(file.r2_key).catch(() => {});
- await query('DELETE FROM zen_nuage_files WHERE id = $1', [id]);
- return { success: true };
-}
-
-export async function proxyNuageFile(id, { inline = false } = {}) {
- const file = await getFileById(id);
- if (!file) throw new Error('Fichier introuvable');
- return proxyFile(file.r2_key, { filename: inline ? undefined : file.display_name });
-}
-
-export async function getFolderUploadedSize(folderId) {
- const result = await query(
- `SELECT COALESCE(SUM(size), 0) AS total FROM zen_nuage_files WHERE folder_id = $1`,
- [folderId]
- );
- return parseInt(result.rows[0].total);
-}
-
-// ─── Shares ─────────────────────────────────────────────────────────────────
-
-export async function getShareByToken(token) {
- const result = await query('SELECT * FROM zen_nuage_shares WHERE token = $1', [token]);
- return result.rows[0] || null;
-}
-
-export async function getShareInfoForMetadata(token) {
- const result = await query(
- `SELECT s.*,
- CASE
- WHEN s.target_type = 'file' THEN (SELECT display_name FROM zen_nuage_files WHERE id = s.target_id)
- WHEN s.target_type = 'folder' THEN (SELECT name FROM zen_nuage_folders WHERE id = s.target_id)
- END AS target_name
- FROM zen_nuage_shares s
- WHERE s.token = $1`,
- [token]
- );
- return result.rows[0] || null;
-}
-
-export async function getSharesForTarget(targetId, targetType) {
- const result = await query(
- `SELECT s.*, u.name, u.email
- FROM zen_nuage_shares s
- LEFT JOIN zen_auth_users u ON u.id = s.user_id
- WHERE s.target_id = $1 AND s.target_type = $2
- ORDER BY s.created_at DESC`,
- [targetId, targetType]
- );
- return result.rows;
-}
-
-export async function getSharesForUser(userId) {
- const result = await query(
- `SELECT s.*
- FROM zen_nuage_shares s
- WHERE s.user_id = $1
- AND s.is_active = true
- AND (s.expires_at IS NULL OR s.expires_at > CURRENT_TIMESTAMP)
- ORDER BY s.created_at DESC`,
- [userId]
- );
- return result.rows;
-}
-
-export async function getAllActiveShares() {
- const result = await query(
- `SELECT s.*, u.name, u.email,
- CASE
- WHEN s.target_type = 'file' THEN (SELECT display_name FROM zen_nuage_files WHERE id = s.target_id)
- WHEN s.target_type = 'folder' THEN (SELECT name FROM zen_nuage_folders WHERE id = s.target_id)
- END AS target_name
- FROM zen_nuage_shares s
- LEFT JOIN zen_auth_users u ON u.id = s.user_id
- ORDER BY s.created_at DESC`
- );
- return result.rows;
-}
-
-export async function createShare({
- targetId,
- targetType,
- shareType,
- userId = null,
- permission,
- password = null,
- expiresAt = null,
- uploadLimitBytes = null,
-}) {
- const token = crypto.randomBytes(32).toString('hex');
- let passwordHash = null;
-
- if (password) {
- const salt = crypto.randomBytes(16).toString('hex');
- passwordHash = await new Promise((resolve, reject) => {
- crypto.scrypt(password, salt, 64, (err, key) => {
- if (err) reject(err);
- else resolve(`${salt}:${key.toString('hex')}`);
- });
- });
- }
-
- const result = await query(
- `INSERT INTO zen_nuage_shares
- (token, target_type, target_id, share_type, user_id, permission, password_hash, expires_at, upload_limit_bytes)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
- RETURNING *`,
- [token, targetType, targetId, shareType, userId, permission, passwordHash, expiresAt, uploadLimitBytes]
- );
- return result.rows[0];
-}
-
-export async function revokeShare(id) {
- const result = await query(
- `UPDATE zen_nuage_shares SET is_active = false WHERE id = $1 RETURNING *`,
- [id]
- );
- return result.rows[0] || null;
-}
-
-export async function updateShare(id, {
- permission,
- expiresAt,
- uploadLimitBytes,
- password,
- clearPassword,
- userId,
- shareType,
- isActive,
-} = {}) {
- let passwordHash = undefined;
- if (clearPassword) {
- passwordHash = null;
- } else if (password) {
- const salt = crypto.randomBytes(16).toString('hex');
- passwordHash = await new Promise((resolve, reject) => {
- crypto.scrypt(password, salt, 64, (err, key) => {
- if (err) reject(err);
- else resolve(`${salt}:${key.toString('hex')}`);
- });
- });
- }
-
- const sets = [];
- const values = [];
- let idx = 1;
-
- if (permission !== undefined) { sets.push(`permission = $${idx++}`); values.push(permission); }
- if (expiresAt !== undefined) { sets.push(`expires_at = $${idx++}`); values.push(expiresAt); }
- if (uploadLimitBytes !== undefined){ sets.push(`upload_limit_bytes = $${idx++}`); values.push(uploadLimitBytes); }
- if (passwordHash !== undefined) { sets.push(`password_hash = $${idx++}`); values.push(passwordHash); }
- if (userId !== undefined) { sets.push(`user_id = $${idx++}`); values.push(userId); }
- if (shareType !== undefined) { sets.push(`share_type = $${idx++}`); values.push(shareType); }
- if (isActive !== undefined) { sets.push(`is_active = $${idx++}`); values.push(isActive); }
-
- if (sets.length === 0) throw new Error('Aucun champ à modifier');
-
- values.push(id);
- const result = await query(
- `UPDATE zen_nuage_shares SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`,
- values
- );
- return result.rows[0] || null;
-}
-
-export function isShareValid(share) {
- if (!share || !share.is_active) return false;
- if (share.expires_at && new Date(share.expires_at) < new Date()) return false;
- return true;
-}
-
-export async function verifySharePassword(share, password) {
- if (!share.password_hash) return true;
- const [salt, storedKey] = share.password_hash.split(':');
- return new Promise((resolve, reject) => {
- crypto.scrypt(password, salt, 64, (err, derivedKey) => {
- if (err) reject(err);
- else {
- const storedBuf = Buffer.from(storedKey, 'hex');
- resolve(
- storedBuf.length === derivedKey.length &&
- crypto.timingSafeEqual(storedBuf, derivedKey)
- );
- }
- });
- });
-}
-
-// ─── User search (for share panel) ──────────────────────────────────────────
-
-export async function searchUsers(q, limit = 10) {
- const like = `%${q}%`;
- const result = await query(
- `SELECT id, name, email
- FROM zen_auth_users
- WHERE (name ILIKE $1 OR email ILIKE $1)
- ORDER BY name
- LIMIT $2`,
- [like, limit]
- );
- return result.rows;
-}
-
-// ─── Shared content access (for public pages and client section) ─────────────
-
-export async function getSharedFolderContents(share, folderId = null) {
- if (share.target_type === 'file') {
- const file = await getFileById(share.target_id);
- return { folders: [], files: file ? [file] : [] };
- }
-
- // For folder shares: start at the shared root or a sub-folder within it
- const targetId = folderId || share.target_id;
-
- // Verify the requested folder is within the share scope
- if (folderId && folderId !== share.target_id) {
- const breadcrumb = await getFolderBreadcrumb(folderId);
- const inScope = breadcrumb.some(f => f.id === share.target_id);
- if (!inScope) throw new Error('Dossier hors de la portée du partage');
- }
-
- return getFolderContents(targetId);
-}
-
-export async function getSharedBreadcrumb(share, folderId = null) {
- if (!folderId || folderId === share.target_id) return [];
- const full = await getFolderBreadcrumb(folderId);
- const rootIndex = full.findIndex(f => f.id === share.target_id);
- return rootIndex >= 0 ? full.slice(rootIndex + 1) : [];
-}
-
-// ─── Share scope validators ──────────────────────────────────────────────────
-
-/**
- * Checks whether a file belongs to the scope of a share.
- * For file shares: the file must be the shared file itself.
- * For folder shares: the file must live inside the shared folder tree.
- */
-export async function isFileInShare(fileId, share) {
- const file = await getFileById(fileId);
- if (!file) return false;
-
- if (share.target_type === 'file') {
- return file.id === share.target_id;
- }
-
- // Folder share: file must be in the shared folder or a descendant
- if (!file.folder_id) return false;
- if (file.folder_id === share.target_id) return true;
- const breadcrumb = await getFolderBreadcrumb(file.folder_id);
- return breadcrumb.some(f => f.id === share.target_id);
-}
-
-/**
- * Checks whether a folder belongs to the scope of a share.
- * Only meaningful for folder shares; the folder must be the root or a descendant.
- */
-export async function isFolderInShare(folderId, share) {
- if (share.target_type !== 'folder') return false;
- if (folderId === share.target_id) return true;
- const breadcrumb = await getFolderBreadcrumb(folderId);
- return breadcrumb.some(f => f.id === share.target_id);
-}
-
-// ─── Password cookie helpers (server-side enforcement) ───────────────────────
-
-export function getPasswordCookieName(token) {
- return `zen_nuage_pw_${token}`;
-}
-
-export function signPasswordToken(token) {
- const secret = process.env.ZEN_SESSION_SECRET;
- if (!secret) throw new Error('ZEN_SESSION_SECRET environment variable is not set');
- return crypto.createHmac('sha256', secret).update(token).digest('hex');
-}
diff --git a/src/modules/nuage/dashboard/ClientNuageSection.js b/src/modules/nuage/dashboard/ClientNuageSection.js
deleted file mode 100644
index 0cfd8ca..0000000
--- a/src/modules/nuage/dashboard/ClientNuageSection.js
+++ /dev/null
@@ -1,424 +0,0 @@
-'use client';
-
-/**
- * Client Nuage Section
- * Unified file explorer for the client portal.
- * Shares are displayed as folders/files at the root level.
- * Clicking a folder share navigates into it like a real file explorer.
- *
- * Props:
- * apiBasePath — base URL for the Zen API (default: '/zen/api')
- * emptyMessage — message shown when there are no active shares
- * newTab — open file download links in a new tab
- */
-
-import React, { useState, useEffect, useRef } from 'react';
-import {
- CloudUploadIcon,
- Tick02Icon,
-} from '../../../shared/Icons.js';
-import { Loading, Button, Card, Breadcrumb } from '../../../shared/components';
-import NuageFileTable from '../components/NuageFileTable.js';
-
-// ─── Main explorer ─────────────────────────────────────────────────────────────
-
-export default function ClientNuageSection({
- apiBasePath = '/zen/api',
- emptyMessage = 'Aucun document partagé pour le moment.',
- newTab = false,
-}) {
- const fileInputRef = useRef(null);
- const isInitialMount = useRef(true);
- const skipHistoryPush = useRef(false);
-
- // Navigation
- const [activeShare, setActiveShare] = useState(null);
- const [folderId, setFolderId] = useState(null);
-
- // Data
- const [shares, setShares] = useState([]);
- const [contents, setContents] = useState({ folders: [], files: [] });
- const [breadcrumb, setBreadcrumb] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- // Upload
- const [uploading, setUploading] = useState(false);
- const [uploadQueue, setUploadQueue] = useState([]);
- const [uploadError, setUploadError] = useState(null);
-
- // Sort (only used inside a share)
- const [sortBy, setSortBy] = useState('name');
- const [sortOrder, setSortOrder] = useState('asc');
-
- // ─── Data fetching ──────────────────────────────────────────────────────────
-
- useEffect(() => {
- if (!activeShare) {
- fetchShares();
- } else {
- fetchContents();
- }
- }, [activeShare, folderId]);
-
- // Sync navigation state to URL history
- useEffect(() => {
- if (typeof window === 'undefined') return;
- if (isInitialMount.current) {
- isInitialMount.current = false;
- return;
- }
- if (skipHistoryPush.current) {
- skipHistoryPush.current = false;
- return;
- }
- const url = new URL(window.location.href);
- if (activeShare) {
- url.searchParams.set('share', activeShare.token);
- if (folderId) url.searchParams.set('folder', folderId);
- else url.searchParams.delete('folder');
- } else {
- url.searchParams.delete('share');
- url.searchParams.delete('folder');
- }
- window.history.pushState(
- { shareToken: activeShare?.token ?? null, folderId: folderId ?? null },
- '',
- url.toString()
- );
- }, [activeShare, folderId]);
-
- // Handle browser back/forward
- useEffect(() => {
- if (typeof window === 'undefined') return;
- const onPop = (event) => {
- skipHistoryPush.current = true;
- const state = event.state;
- if (!state?.shareToken) {
- setActiveShare(null);
- setFolderId(null);
- setBreadcrumb([]);
- setContents({ folders: [], files: [] });
- } else {
- const share = shares.find(s => s.token === state.shareToken);
- setActiveShare(share || null);
- setFolderId(state.folderId || null);
- }
- };
- window.addEventListener('popstate', onPop);
- return () => window.removeEventListener('popstate', onPop);
- }, [shares]);
-
- const fetchShares = async () => {
- setLoading(true);
- setError(null);
- try {
- const base = typeof window !== 'undefined' ? window.location.origin : '';
- const res = await fetch(`${base}${apiBasePath}/nuage/me`, { credentials: 'include' });
- const data = await res.json();
- if (!res.ok || !data.success) throw new Error(data.error || 'Erreur lors du chargement');
- const allShares = data.shares || [];
- setShares(allShares);
- // Restore navigation from URL on initial load
- const urlParams = new URLSearchParams(window.location.search);
- const shareTokenFromUrl = urlParams.get('share');
- if (shareTokenFromUrl) {
- const share = allShares.find(s => s.token === shareTokenFromUrl);
- if (share) {
- skipHistoryPush.current = true;
- setActiveShare(share);
- setFolderId(urlParams.get('folder') || null);
- }
- }
- } catch (e) {
- setError(e.message);
- } finally {
- setLoading(false);
- }
- };
-
- const fetchContents = async () => {
- setLoading(true);
- setError(null);
- try {
- const base = typeof window !== 'undefined' ? window.location.origin : '';
- const params = new URLSearchParams({ shareId: activeShare.token });
- if (folderId) params.set('folder', folderId);
- const res = await fetch(`${base}${apiBasePath}/nuage/shared?${params}`, { credentials: 'include' });
- const data = await res.json();
- if (!res.ok || !data.success) throw new Error(data.error || 'Erreur');
- setContents({ folders: data.folders || [], files: data.files || [] });
- setBreadcrumb(data.breadcrumb || []);
- } catch (e) {
- setError(e.message);
- } finally {
- setLoading(false);
- }
- };
-
- // ─── Navigation ─────────────────────────────────────────────────────────────
-
- const goToRoot = () => {
- setActiveShare(null);
- setFolderId(null);
- setBreadcrumb([]);
- setContents({ folders: [], files: [] });
- setSortBy('name');
- setSortOrder('asc');
- };
-
- const goToShareRoot = () => {
- setFolderId(null);
- };
-
- const openShare = (share) => {
- const isExpired = share.expires_at && new Date(share.expires_at) < new Date();
- if (isExpired) return;
- setActiveShare(share);
- setFolderId(null);
- setBreadcrumb([]);
- };
-
- // ─── Sort helpers ────────────────────────────────────────────────────────────
-
- const sorted = (items, isFolder) => {
- return [...items].sort((a, b) => {
- let valA, valB;
- if (sortBy === 'name') {
- valA = (isFolder ? a.name : a.display_name)?.toLowerCase();
- valB = (isFolder ? b.name : b.display_name)?.toLowerCase();
- } else if (sortBy === 'date') {
- valA = a.created_at;
- valB = b.created_at;
- } else if (sortBy === 'size' && !isFolder) {
- valA = a.size;
- valB = b.size;
- } else {
- return 0;
- }
- if (valA < valB) return sortOrder === 'asc' ? -1 : 1;
- if (valA > valB) return sortOrder === 'asc' ? 1 : -1;
- return 0;
- });
- };
-
- const toggleSort = (field) => {
- if (sortBy === field) setSortOrder(o => o === 'asc' ? 'desc' : 'asc');
- else { setSortBy(field); setSortOrder('asc'); }
- };
-
- // ─── Download ────────────────────────────────────────────────────────────────
-
- const getProxyUrl = (fileId) => {
- const params = new URLSearchParams({ fileId, shareId: activeShare.token });
- const base = typeof window !== 'undefined' ? window.location.origin : '';
- return `${base}${apiBasePath}/nuage/download?${params}`;
- };
-
- const handleDownload = (file) => {
- const a = document.createElement('a');
- a.href = getProxyUrl(file.id);
- a.download = file.display_name;
- if (newTab) a.target = '_blank';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- };
-
- // ─── Upload ──────────────────────────────────────────────────────────────────
-
- const uploadFiles = async (files) => {
- const fileList = Array.from(files);
- setUploading(true);
- setUploadError(null);
- setUploadQueue(fileList.map(f => ({ name: f.name, done: false })));
- const base = typeof window !== 'undefined' ? window.location.origin : '';
-
- for (let i = 0; i < fileList.length; i++) {
- const file = fileList[i];
- const formData = new FormData();
- formData.append('file', file);
- formData.append('shareId', activeShare.token);
- if (folderId) formData.append('folderId', folderId);
- try {
- const res = await fetch(`${base}${apiBasePath}/nuage/upload`, {
- method: 'POST',
- credentials: 'include',
- body: formData,
- });
- const data = await res.json();
- if (data.success) {
- setUploadQueue(q => q.map((f, idx) => idx === i ? { ...f, done: true } : f));
- } else {
- setUploadError(data.error || 'Erreur lors du téléversement');
- }
- } catch (e) {
- setUploadError('Erreur réseau');
- }
- }
-
- setUploading(false);
- setUploadQueue([]);
- fetchContents();
- };
-
- // ─── Table data ──────────────────────────────────────────────────────────────
-
- let tableData = [];
- let tableSortBy;
- let tableSortOrder;
- let tableOnSort;
-
- if (!activeShare) {
- // Root: shares as rows (expired shares hidden)
- tableData = shares
- .filter(share => !share.expires_at || new Date(share.expires_at) >= new Date())
- .map(share => ({ ...share, _type: 'share' }));
- } else {
- // Inside a share: folders + files
- tableData = [
- ...sorted(contents.folders, true).map(f => ({ ...f, _type: 'folder' })),
- ...sorted(contents.files, false).map(f => ({ ...f, _type: 'file' })),
- ];
- tableSortBy = sortBy;
- tableSortOrder = sortOrder;
- tableOnSort = toggleSort;
- }
-
-
- // ─── Breadcrumb ──────────────────────────────────────────────────────────────
-
- const breadcrumbItems = [
- {
- key: 'root',
- label: 'Mes fichiers',
- onClick: goToRoot,
- active: !activeShare,
- },
- ...(activeShare
- ? [
- {
- key: 'share',
- label: activeShare.target_name || 'Partage',
- onClick: goToShareRoot,
- active: breadcrumb.length === 0,
- },
- ...(breadcrumb.length > 3
- ? [
- { key: 'ellipsis', label: '···', onClick: () => setFolderId(breadcrumb[0].id), active: false },
- ...breadcrumb.slice(-2).map((f, i) => ({
- key: f.id,
- label: f.name,
- onClick: () => setFolderId(f.id),
- active: i === 1,
- })),
- ]
- : breadcrumb.map((f, i) => ({
- key: f.id,
- label: f.name,
- onClick: () => setFolderId(f.id),
- active: i === breadcrumb.length - 1,
- }))
- ),
- ]
- : []),
- ];
-
- // ─── Render ───────────────────────────────────────────────────────────────────
-
- const showUploadZone = activeShare &&
- activeShare.permission === 'collaborator' &&
- activeShare.target_type === 'folder';
-
- return (
-
-
{ if (e.target.files?.length) uploadFiles(e.target.files); e.target.value = ''; }}
- />
-
- {/* Breadcrumb */}
-
-
- {/* Upload zone (collaborator folder shares only) */}
- {showUploadZone && (
-
e.preventDefault()}
- onDrop={e => { e.preventDefault(); uploadFiles(e.dataTransfer.files); }}
- >
-
-
- {uploading ? 'Téléversement…' : 'Glissez des fichiers ici'}
-
-
fileInputRef.current?.click()}
- loading={uploading}
- icon={}
- >
- Choisir des fichiers
-
- {uploadError &&
{uploadError}
}
-
- )}
-
- {/* Upload queue */}
- {uploading && uploadQueue.length > 0 && (
-
-
Téléversement en cours…
-
- {uploadQueue.map((f, i) => (
-
- {f.done ? : }
- {f.name}
-
- ))}
-
-
- )}
-
- {/* Explorer table */}
-
- {error ? (
- {error}
- ) : (
- {
- if (item._type === 'share') {
- openShare(item);
- } else if (item._type === 'folder') {
- setFolderId(item.id);
- }
- }}
- renderActions={(item) => {
- if (item._type !== 'file') return null;
- return (
- { e.stopPropagation(); handleDownload(item); }}
- className='-m-2'
- >
- Télécharger
-
- );
- }}
- emptyMessage={!activeShare ? emptyMessage : 'Ce dossier est vide'}
- size="sm"
- className='min-h-64'
- />
- )}
-
-
-
- );
-}
diff --git a/src/modules/nuage/dashboard/index.js b/src/modules/nuage/dashboard/index.js
deleted file mode 100644
index a20ff45..0000000
--- a/src/modules/nuage/dashboard/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Nuage Dashboard Module
- * Exports client-facing section for the user portal
- *
- * Stats actions are kept separate to avoid 'use server' conflicts:
- * import via modules.actions.js or directly from statsActions.js
- */
-
-export { default as ClientNuageSection } from './ClientNuageSection.js';
-export { default } from './ClientNuageSection.js';
diff --git a/src/modules/nuage/db.js b/src/modules/nuage/db.js
deleted file mode 100644
index 1f53dd2..0000000
--- a/src/modules/nuage/db.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * Nuage Module — Database
- * Creates tables for folders, files, and shares
- */
-
-import { query } from '@hykocx/zen/database';
-
-async function tableExists(tableName) {
- const result = await query(
- `SELECT EXISTS (
- SELECT FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = $1
- )`,
- [tableName]
- );
- return result.rows[0].exists;
-}
-
-/**
- * Create all Nuage tables
- * @returns {Promise<{ created: string[], skipped: string[] }>}
- */
-export async function createTables() {
- const created = [];
- const skipped = [];
-
- // zen_nuage_folders
- if (!(await tableExists('zen_nuage_folders'))) {
- await query(`
- CREATE TABLE zen_nuage_folders (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- name VARCHAR(255) NOT NULL,
- parent_id UUID REFERENCES zen_nuage_folders(id) ON DELETE CASCADE,
- depth INTEGER NOT NULL DEFAULT 0,
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
- )
- `);
- await query(`CREATE INDEX ON zen_nuage_folders(parent_id)`);
- console.log(' ✓ Created zen_nuage_folders');
- created.push('zen_nuage_folders');
- } else {
- console.log(' - Skipped zen_nuage_folders (already exists)');
- skipped.push('zen_nuage_folders');
- }
-
- // zen_nuage_files
- if (!(await tableExists('zen_nuage_files'))) {
- await query(`
- CREATE TABLE zen_nuage_files (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- folder_id UUID REFERENCES zen_nuage_folders(id) ON DELETE CASCADE,
- original_name VARCHAR(255) NOT NULL,
- display_name VARCHAR(255) NOT NULL,
- mime_type VARCHAR(255) NOT NULL,
- size BIGINT NOT NULL DEFAULT 0,
- r2_key TEXT NOT NULL,
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
- )
- `);
- await query(`CREATE INDEX ON zen_nuage_files(folder_id)`);
- console.log(' ✓ Created zen_nuage_files');
- created.push('zen_nuage_files');
- } else {
- console.log(' - Skipped zen_nuage_files (already exists)');
- skipped.push('zen_nuage_files');
- }
-
- // zen_nuage_shares
- // References zen_auth_users (CMS auth users table)
- if (!(await tableExists('zen_nuage_shares'))) {
- await query(`
- CREATE TABLE zen_nuage_shares (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- token VARCHAR(255) UNIQUE NOT NULL,
- target_type VARCHAR(10) NOT NULL CHECK (target_type IN ('file', 'folder')),
- target_id UUID NOT NULL,
- share_type VARCHAR(10) NOT NULL CHECK (share_type IN ('user', 'anonymous')),
- user_id TEXT REFERENCES zen_auth_users(id) ON DELETE CASCADE,
- permission VARCHAR(15) NOT NULL CHECK (permission IN ('reader', 'collaborator')),
- password_hash TEXT,
- expires_at TIMESTAMPTZ,
- upload_limit_bytes BIGINT,
- is_active BOOLEAN NOT NULL DEFAULT true,
- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
- )
- `);
- await query(`CREATE INDEX ON zen_nuage_shares(token)`);
- await query(`CREATE INDEX ON zen_nuage_shares(target_id, target_type, is_active)`);
- await query(`CREATE INDEX ON zen_nuage_shares(user_id, is_active)`);
- console.log(' ✓ Created zen_nuage_shares');
- created.push('zen_nuage_shares');
- } else {
- console.log(' - Skipped zen_nuage_shares (already exists)');
- skipped.push('zen_nuage_shares');
- }
-
- return { created, skipped };
-}
-
-/**
- * Drop all Nuage tables (in dependency order)
- */
-export async function dropTables() {
- await query('DROP TABLE IF EXISTS zen_nuage_shares CASCADE');
- await query('DROP TABLE IF EXISTS zen_nuage_files CASCADE');
- await query('DROP TABLE IF EXISTS zen_nuage_folders CASCADE');
-}
diff --git a/src/modules/nuage/email/NuageShareEmail.jsx b/src/modules/nuage/email/NuageShareEmail.jsx
deleted file mode 100644
index 9f91b32..0000000
--- a/src/modules/nuage/email/NuageShareEmail.jsx
+++ /dev/null
@@ -1,105 +0,0 @@
-/**
- * Nuage Share Notification Email
- * Sent when sharing a file or folder with a user or via anonymous link
- */
-
-import { Button, Section, Text, Link } from '@react-email/components';
-import { BaseLayout } from '@hykocx/zen/email/templates';
-
-/**
- * @param {object} props
- * @param {string} props.recipientName — Name or email of the recipient
- * @param {string} props.shareUrl — Full URL to access the share
- * @param {string} props.targetName — Name of the shared file or folder
- * @param {'file'|'folder'} props.targetType
- * @param {'user'|'anonymous'} props.shareType
- * @param {string|null} props.expiresAt — ISO date string of expiration, or null
- * @param {string|null} props.customMessage — Optional message from the sender
- * @param {string} props.companyName
- */
-export const NuageShareEmail = ({
- recipientName,
- shareUrl,
- targetName,
- targetType = 'folder',
- shareType = 'anonymous',
- expiresAt = null,
- customMessage = null,
- companyName,
-}) => {
- const appName = companyName || process.env.ZEN_NAME || 'Nuage';
- const supportEmail = process.env.ZEN_SUPPORT_EMAIL || 'support@exemple.com';
-
- const isFile = targetType === 'file';
- const formattedExpiry = expiresAt
- ? new Date(expiresAt).toLocaleDateString('fr-CA', { year: 'numeric', month: 'long', day: 'numeric' })
- : null;
- const preview = `Le ${isFile ? 'fichier' : 'dossier'} « ${targetName} » a été partagé avec vous.${formattedExpiry ? ` Ce partage reste accessible jusqu'au ${formattedExpiry}.` : ''}`;
-
- return (
-
-
- Bonjour {recipientName},{' '}
- {isFile ? 'un fichier a été partagé avec vous.' : 'un dossier a été partagé avec vous.'}
- {formattedExpiry && <>{' '}Ce partage expire le {formattedExpiry}.>}
-
-
- {customMessage && (
-
- )}
-
-
-
- Détails du partage
-
-
- Type :
- {isFile ? 'Fichier' : 'Dossier'}
-
-
- Nom :
- {targetName}
-
- {formattedExpiry && (
-
- Expire le :
- {formattedExpiry}
-
- )}
-
-
-
-
- {isFile ? 'Accéder au fichier' : 'Accéder au dossier'}
-
-
-
-
- Lien :{' '}
-
- {shareUrl}
-
-
-
-
- {shareType === 'anonymous'
- ? 'Ce lien est accessible à toute personne qui le possède.'
- : 'Ce partage est destiné à votre compte uniquement.'}
- {' '}Si vous n'attendiez pas ce message, ignorez-le.
-
-
- );
-};
diff --git a/src/modules/nuage/email/index.js b/src/modules/nuage/email/index.js
deleted file mode 100644
index b22a15c..0000000
--- a/src/modules/nuage/email/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * Nuage Email Templates
- */
-export { NuageShareEmail } from './NuageShareEmail.jsx';
diff --git a/src/modules/nuage/metadata.js b/src/modules/nuage/metadata.js
deleted file mode 100644
index 258a88e..0000000
--- a/src/modules/nuage/metadata.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * Nuage Metadata Utilities
- * Functions to generate dynamic metadata for nuage share pages
- */
-
-import { generateMetadata, generateRobots } from '../../shared/lib/metadata/index.js';
-import { getShareInfoForMetadata } from './crud.js';
-import { getAppName } from '../../shared/lib/appConfig.js';
-
-/**
- * Generate metadata for a public share page.
- *
- * @param {string} token - Share token
- * @param {Object} options - Generation options
- * @returns {Promise