feat: partage dirigé entre utilisateurs #196
- create_share/shared_with + validation des destinataires
- gate auth sur /s/{token} (+pdf, raw) : 404 opaque hors créateur/admin/destinataires
- GET /api/shares scopé (non-admin = créés + reçus), révocation créateur/admin
- UI : dialogue dirigé/public + destinataires, dashboard « partagé par X »
- i18n FR/EN, tests test_directed_shares.py (9), fiche feature
This commit is contained in:
+24
-2
@@ -1656,10 +1656,14 @@ export async function openShareDialog(vault, path) {
|
||||
const expiresInfo = existingShare.expires_at
|
||||
? `<p style="font-size:0.75rem;color:var(--text-muted)">Expire le ${new Date(existingShare.expires_at).toLocaleDateString("fr-FR")}</p>`
|
||||
: '<p style="font-size:0.75rem;color:var(--text-muted)">Sans expiration</p>';
|
||||
const recipientsInfo = (existingShare.shared_with && existingShare.shared_with.length)
|
||||
? `<p style="font-size:0.75rem;color:var(--text-muted);margin-bottom:4px">${t('config.share_shared_with')} : ${escapeHtml(existingShare.shared_with.join(", "))}</p>`
|
||||
: '';
|
||||
div.innerHTML = `
|
||||
<div class="share-dialog">
|
||||
<h3>📤 Document partagé</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:4px">${escapeHtml(vault)}/${escapeHtml(path)}</p>
|
||||
${recipientsInfo}
|
||||
${expiresInfo}
|
||||
<p style="font-size:0.75rem;color:var(--text-muted);margin-bottom:8px">${existingShare.access_count} vue(s)</p>
|
||||
<input type="text" class="share-url-input" value="${url}" readonly>
|
||||
@@ -1697,7 +1701,16 @@ export async function openShareDialog(vault, path) {
|
||||
<div class="share-dialog">
|
||||
<h3>📤 Partager ce document</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:12px">${escapeHtml(vault)}/${escapeHtml(path)}</p>
|
||||
<p style="font-size:0.8rem;margin-bottom:8px">Ce lien sera accessible <strong>publiquement, sans authentification</strong>.</p>
|
||||
<label style="font-size:0.8rem;display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
||||
<input type="radio" name="share-visibility" value="directed" checked>
|
||||
${t('config.share_directed')}
|
||||
</label>
|
||||
<input type="text" id="share-recipients" placeholder="${t('config.share_recipients_ph')}"
|
||||
style="width:100%;padding:6px 8px;margin-bottom:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem">
|
||||
<label style="font-size:0.8rem;display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
||||
<input type="radio" name="share-visibility" value="public">
|
||||
${t('config.share_public')}
|
||||
</label>
|
||||
<label style="font-size:0.8rem;display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
Expiration :
|
||||
<select id="share-expiry" style="padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem">
|
||||
@@ -1716,10 +1729,19 @@ export async function openShareDialog(vault, path) {
|
||||
div.querySelector(".share-create-btn").addEventListener("click", async () => {
|
||||
try {
|
||||
const expiry = document.getElementById("share-expiry")?.value;
|
||||
const visibility = div.querySelector('input[name="share-visibility"]:checked')?.value || "directed";
|
||||
const recipientsRaw = document.getElementById("share-recipients")?.value || "";
|
||||
const recipients = visibility === "directed"
|
||||
? recipientsRaw.split(",").map(s => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
if (visibility === "directed" && !recipients.length) {
|
||||
showToast(t('config.share_need_recipient'), "error");
|
||||
return;
|
||||
}
|
||||
const share = await api(`/api/share/${encodeURIComponent(vault)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path, expires_in_hours: expiry ? parseInt(expiry) : null }),
|
||||
body: JSON.stringify({ path, expires_in_hours: expiry ? parseInt(expiry) : null, shared_with: recipients }),
|
||||
});
|
||||
existingShare = share;
|
||||
renderContent();
|
||||
|
||||
+24
-11
@@ -1,5 +1,5 @@
|
||||
// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870)
|
||||
import { api } from './auth.js';
|
||||
import { api, AuthManager } from './auth.js';
|
||||
import { state } from './state.js';
|
||||
import { escapeHtml, safeCreateIcons, getFileIcon } from './utils.js';
|
||||
import { icon, openFile } from './viewer.js';
|
||||
@@ -60,24 +60,29 @@ const DashboardSharedWidget = {
|
||||
const shares = await api("/api/shares");
|
||||
if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; }
|
||||
if (empty) empty.style.display = "none";
|
||||
grid.innerHTML = shares.map(s => `
|
||||
<div class="shared-card" data-vault="${escapeHtml(s.vault)}" data-path="${escapeHtml(s.path)}">
|
||||
const me = (AuthManager.getUser() || {}).username;
|
||||
grid.innerHTML = shares.map(s => {
|
||||
const received = me && s.created_by && s.created_by !== me;
|
||||
const actions = received
|
||||
? `<button class="shared-open-btn">${t('dashboard.open_btn')}</button>`
|
||||
: `<button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">${t('dashboard.copy_btn')}</button>
|
||||
<button class="shared-open-btn">${t('dashboard.open_btn')}</button>
|
||||
<button class="shared-revoke-btn" data-id="${s.id}">🗑</button>`;
|
||||
return `
|
||||
<div class="shared-card" data-vault="${escapeHtml(s.vault)}" data-path="${escapeHtml(s.path)}" data-token="${s.token}" data-received="${received ? "1" : ""}">
|
||||
<div class="shared-card-header">
|
||||
<i data-lucide="file-text" style="width:14px;height:14px"></i>
|
||||
<span class="shared-card-title">${escapeHtml(s.path.split("/").pop().replace(/\.md$/i, ""))}</span>
|
||||
<span class="shared-card-title">${escapeHtml(s.path.split("/").pop().replace(/\\.md$/i, ""))}</span>
|
||||
<span class="shared-card-vault">${escapeHtml(s.vault)}</span>
|
||||
</div>
|
||||
<div class="shared-card-meta">
|
||||
${received ? `<span>${t('dashboard.shared_by', { user: s.created_by })}</span>` : ""}
|
||||
<span>${s.access_count || 0} ${t('dashboard.views', {count: s.access_count || 0})}</span>
|
||||
${s.expires_at ? `<span>${t('dashboard.expires', {date: new Date(s.expires_at).toLocaleDateString(getLocale() === 'fr' ? 'fr-FR' : 'en-US')})}</span>` : ""}
|
||||
</div>
|
||||
<div class="shared-card-actions">
|
||||
<button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">${t('dashboard.copy_btn')}</button>
|
||||
<button class="shared-open-btn">${t('dashboard.open_btn')}</button>
|
||||
<button class="shared-revoke-btn" data-id="${s.id}">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
<div class="shared-card-actions">${actions}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
lucide.createIcons();
|
||||
grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -88,6 +93,10 @@ const DashboardSharedWidget = {
|
||||
grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const card = b.closest(".shared-card");
|
||||
if (card && card.dataset.received === "1" && card.dataset.token) {
|
||||
window.open(`/s/${card.dataset.token}`, "_blank");
|
||||
return;
|
||||
}
|
||||
if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path);
|
||||
}));
|
||||
grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => {
|
||||
@@ -97,6 +106,10 @@ const DashboardSharedWidget = {
|
||||
this.load();
|
||||
}));
|
||||
grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => {
|
||||
if (card.dataset.received === "1" && card.dataset.token) {
|
||||
window.open(`/s/${card.dataset.token}`, "_blank");
|
||||
return;
|
||||
}
|
||||
TabManager.openPreview(card.dataset.vault, card.dataset.path);
|
||||
}));
|
||||
} catch (err) { if (empty) empty.style.display = ""; }
|
||||
|
||||
@@ -604,6 +604,11 @@
|
||||
"config.server_config": "Server config",
|
||||
"config.settings_saved": "✓ Settings saved",
|
||||
"config.share_revoked": "Share revoked",
|
||||
"config.share_directed": "Share with users (authentication required)",
|
||||
"config.share_public": "Public link (no authentication)",
|
||||
"config.share_recipients_ph": "Recipients (usernames, comma-separated)",
|
||||
"config.share_need_recipient": "Provide at least one recipient",
|
||||
"config.share_shared_with": "Shared with",
|
||||
"config.shares": "Public shares",
|
||||
"config.shares_desc": "Manage public share links for your documents.",
|
||||
"config.shares_empty": "No active shares",
|
||||
@@ -701,6 +706,7 @@
|
||||
"dashboard.recent": "Recent",
|
||||
"dashboard.recent_hint": "Open a file to see it appear here",
|
||||
"dashboard.shared_hint": "Share a document to see it appear here",
|
||||
"dashboard.shared_by": "Shared by {user}",
|
||||
"dashboard.shares": "Shares",
|
||||
"dashboard.show": "Show",
|
||||
"dashboard.size_bytes": " B",
|
||||
|
||||
@@ -604,6 +604,11 @@
|
||||
"config.server_config": "Configuration serveur",
|
||||
"config.settings_saved": "✓ Paramètres sauvegardés",
|
||||
"config.share_revoked": "Partage révoqué",
|
||||
"config.share_directed": "Partager avec des utilisateurs (authentification requise)",
|
||||
"config.share_public": "Lien public (sans authentification)",
|
||||
"config.share_recipients_ph": "Destinataires (noms d'utilisateur, séparés par des virgules)",
|
||||
"config.share_need_recipient": "Indiquez au moins un destinataire",
|
||||
"config.share_shared_with": "Partagé avec",
|
||||
"config.shares": "Partages publics",
|
||||
"config.shares_desc": "Gérez les liens de partage public de vos documents.",
|
||||
"config.shares_empty": "Aucun partage actif",
|
||||
@@ -701,6 +706,7 @@
|
||||
"dashboard.recent": "Récents",
|
||||
"dashboard.recent_hint": "Ouvrez un fichier pour le voir apparaître ici",
|
||||
"dashboard.shared_hint": "Partagez un document pour le voir apparaître ici",
|
||||
"dashboard.shared_by": "Partagé par {user}",
|
||||
"dashboard.shares": "Partages",
|
||||
"dashboard.show": "Afficher",
|
||||
"dashboard.size_bytes": " o",
|
||||
|
||||
Reference in New Issue
Block a user