feat: Benutzerfreigabe - Weiterteilen-Recht + Lesezugriff wird erzwungen
Neues Berechtigungs-Modell fuer Benutzerfreigaben: * FilePermission bekommt zwei neue Spalten: - can_reshare (bool): darf dieser Nutzer die Freigabe weiterverteilen? - granted_by (user_id): wer hat diese Freigabe erstellt? * set_permission / create_share_link erlauben jetzt auch Nicht-Owner, sofern sie can_reshare haben. Dabei gilt: - Lesend + reshare -> kann nur lesend weiterteilen - Schreibend + reshare -> kann lesend ODER schreibend weiterteilen - Admin kann nur der Eigentuemer vergeben - Jeder Re-Sharer kann wiederum can_reshare weitergeben * remove_permission: Owner kann alle Freigaben entfernen; Re-Sharer nur die von ihnen selbst erstellten. * get_permissions: Owner sieht alle; Re-Sharer nur selbst-erstellte. * list_files liefert my_permission + my_can_reshare pro Eintrag - Frontend kann Rename/Delete/Share-Buttons gezielt ein- und ausblenden statt blind alle anzuzeigen. Frontend: * Rename/Delete-Buttons nur fuer Write-Zugriff * Share-Button nur fuer Owner oder Re-Sharer * "darf weiterteilen" Checkbox neben Permission-Dropdown im Dialog * Dropdown-Optionen nach eigenem Level gefiltert (Re-Sharer sieht keine hoeheren Stufen als seine eigene) * Hinweis-Text "Du hast X - du kannst maximal X weiterteilen" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,7 @@
|
||||
@click.stop="downloadFile(data)"
|
||||
/>
|
||||
<Button
|
||||
v-if="canShare(data)"
|
||||
:icon="(data.has_shares || data.has_permissions) ? 'pi pi-users' : 'pi pi-share-alt'"
|
||||
text rounded size="small"
|
||||
:severity="(data.has_shares || data.has_permissions) ? 'success' : undefined"
|
||||
@@ -116,12 +117,14 @@
|
||||
@click.stop="unlockFile(data)"
|
||||
/>
|
||||
<Button
|
||||
v-if="canWrite(data)"
|
||||
icon="pi pi-pencil"
|
||||
text rounded size="small"
|
||||
:disabled="data.locked && data.locked_by !== auth.user?.username"
|
||||
@click.stop="openRename(data)"
|
||||
/>
|
||||
<Button
|
||||
v-if="canWrite(data)"
|
||||
icon="pi pi-trash"
|
||||
text rounded size="small"
|
||||
severity="danger"
|
||||
@@ -167,9 +170,15 @@
|
||||
<h5>Mit Benutzer teilen</h5>
|
||||
<div class="user-share-row">
|
||||
<InputText v-model="shareUserQuery" placeholder="Benutzername suchen..." fluid @input="searchUsers" />
|
||||
<Select v-model="shareUserPermission" :options="userPermOptions" optionLabel="label" optionValue="value" />
|
||||
<Select v-model="shareUserPermission" :options="availableUserPermOptions" optionLabel="label" optionValue="value" />
|
||||
<label class="reshare-check">
|
||||
<input type="checkbox" v-model="shareUserReshare" /> darf weiterteilen
|
||||
</label>
|
||||
<Button label="Teilen" size="small" @click="shareWithUser" :disabled="!selectedShareUser" />
|
||||
</div>
|
||||
<div v-if="!isOwner(shareFile) && shareFile" class="share-hint">
|
||||
Du hast {{ myPermLabel(shareFile) }} - du kannst maximal {{ myPermLabel(shareFile) }} weiterteilen.
|
||||
</div>
|
||||
<div v-if="userSearchResults.length" class="user-search-results">
|
||||
<div v-for="u in userSearchResults" :key="u.id"
|
||||
class="user-result" :class="{ selected: selectedShareUser?.id === u.id }"
|
||||
@@ -182,6 +191,7 @@
|
||||
<i class="pi pi-user"></i>
|
||||
<span>{{ perm.username }}</span>
|
||||
<Tag :value="permLabel(perm.permission)" size="small" />
|
||||
<Tag v-if="perm.can_reshare" value="darf weiterteilen" severity="info" size="small" />
|
||||
<Button icon="pi pi-trash" text size="small" severity="danger" @click="removeUserShare(perm.id)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +203,7 @@
|
||||
<div class="share-form">
|
||||
<div class="field">
|
||||
<label>Berechtigung</label>
|
||||
<Select v-model="shareLinkPermission" :options="linkPermOptions" optionLabel="label" optionValue="value" fluid />
|
||||
<Select v-model="shareLinkPermission" :options="availableLinkPermOptions" optionLabel="label" optionValue="value" fluid />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Passwort (optional)</label>
|
||||
@@ -241,7 +251,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useFilesStore } from '../stores/files'
|
||||
@@ -284,6 +294,7 @@ const filePermissions = ref([])
|
||||
const shareUserQuery = ref('')
|
||||
const selectedShareUser = ref(null)
|
||||
const shareUserPermission = ref('read')
|
||||
const shareUserReshare = ref(false)
|
||||
const userSearchResults = ref([])
|
||||
const userPermOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Schreiben', value: 'write' }, { label: 'Admin', value: 'admin' }]
|
||||
const linkPermOptions = [
|
||||
@@ -291,6 +302,12 @@ const linkPermOptions = [
|
||||
{ label: 'Lesen + Hochladen (nur Ordner)', value: 'write' },
|
||||
{ label: 'Nur Upload (Ordner, kein Einblick)', value: 'upload_only' },
|
||||
]
|
||||
const availableLinkPermOptions = computed(() => {
|
||||
const f = shareFile.value
|
||||
if (!f || isOwner(f)) return linkPermOptions
|
||||
if (f.my_permission === 'read') return linkPermOptions.filter(o => o.value === 'read')
|
||||
return linkPermOptions
|
||||
})
|
||||
const shareLinkPermission = ref('read')
|
||||
const currentOrigin = window.location.origin
|
||||
const shareLoading = ref(false)
|
||||
@@ -570,6 +587,37 @@ function permLabel(perm) {
|
||||
return { read: 'Lesen', write: 'Schreiben', admin: 'Admin' }[perm] || perm
|
||||
}
|
||||
|
||||
function isOwner(data) {
|
||||
return data && data.owner_id === auth.user?.id
|
||||
}
|
||||
|
||||
function canWrite(data) {
|
||||
if (!data) return false
|
||||
if (isOwner(data)) return true
|
||||
return data.my_permission === 'write' || data.my_permission === 'admin'
|
||||
}
|
||||
|
||||
function canShare(data) {
|
||||
if (!data) return false
|
||||
if (isOwner(data)) return true
|
||||
return !!data.my_can_reshare
|
||||
}
|
||||
|
||||
function myPermLabel(data) {
|
||||
if (!data || !data.my_permission) return ''
|
||||
return permLabel(data.my_permission)
|
||||
}
|
||||
|
||||
// Option list for the "Mit Benutzer teilen" dropdown - re-sharers can only
|
||||
// hand out permissions up to their own level. Admin is owner-only.
|
||||
const availableUserPermOptions = computed(() => {
|
||||
const f = shareFile.value
|
||||
const levels = { read: 0, write: 1, admin: 2 }
|
||||
if (!f || isOwner(f)) return userPermOptions
|
||||
const myLevel = levels[f.my_permission] ?? -1
|
||||
return userPermOptions.filter(o => levels[o.value] <= myLevel && o.value !== 'admin')
|
||||
})
|
||||
|
||||
async function openShare(data) {
|
||||
shareFile.value = data
|
||||
sharePassword.value = ''
|
||||
@@ -611,10 +659,12 @@ async function shareWithUser() {
|
||||
await apiClient.post(`/files/${shareFile.value.id}/permissions`, {
|
||||
user_id: selectedShareUser.value.id,
|
||||
permission: shareUserPermission.value,
|
||||
can_reshare: shareUserReshare.value,
|
||||
})
|
||||
toast.add({ severity: 'success', summary: `Mit ${selectedShareUser.value.username} geteilt`, life: 3000 })
|
||||
shareUserQuery.value = ''
|
||||
selectedShareUser.value = null
|
||||
shareUserReshare.value = false
|
||||
const res = await apiClient.get(`/files/${shareFile.value.id}/permissions`)
|
||||
filePermissions.value = res.data
|
||||
await filesStore.loadFiles(currentParentId())
|
||||
@@ -811,7 +861,9 @@ onUnmounted(() => {
|
||||
.share-section:last-child { border-bottom: none; }
|
||||
.share-section h5 { margin: 0 0 0.75rem; font-size: 0.9rem; }
|
||||
.share-form { }
|
||||
.user-share-row { display: flex; gap: 0.5rem; align-items: flex-start; }
|
||||
.user-share-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
.reshare-check { display: flex; align-items: center; gap: 0.25rem; font-size: 0.8rem; white-space: nowrap; }
|
||||
.share-hint { font-size: 0.75rem; color: var(--p-surface-500); margin-top: 0.35rem; font-style: italic; }
|
||||
.user-search-results { border: 1px solid var(--p-surface-200); border-radius: 6px; margin-top: 0.25rem; max-height: 150px; overflow-y: auto; }
|
||||
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; }
|
||||
.user-result:hover, .user-result.selected { background: var(--p-primary-50); }
|
||||
|
||||
Reference in New Issue
Block a user