feat: Share-Dialog Fix, User-Sharing, Admin-Benutzerverwaltung, Registrierungs-Toggle
- Fix: Share-Dialog oeffnet sich jetzt auch bei bereits geteilten Dateien - Neu: Dateien/Ordner direkt mit anderen Benutzern teilen (Lesen/Schreiben/Admin) - Neu: Benutzersuche im Share-Dialog, bestehende Freigaben anzeigen/entfernen - Neu: Admin kann Benutzer ueber die Weboberflaeche anlegen - Neu: Admin kann Benutzer bearbeiten (Rolle, Quota, aktiv/inaktiv) und loeschen - Neu: Schieberegler fuer oeffentliche Registrierung in den Admin-Einstellungen - Neu: Register-Link auf Login-Seite nur sichtbar wenn Registrierung erlaubt - Neu: Register-Seite leitet um wenn Registrierung deaktiviert - Neu: AppSettings-Model fuer persistente App-Konfiguration - Neu: /api/users/search Endpunkt fuer Benutzersuche in Share-Dialogen Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,27 +107,56 @@
|
||||
</Dialog>
|
||||
|
||||
<!-- Share Dialog -->
|
||||
<Dialog v-model:visible="showShare" header="Teilen" modal :style="{ width: '500px' }">
|
||||
<Dialog v-model:visible="showShare" header="Teilen" modal :style="{ width: '550px' }">
|
||||
<div v-if="shareFile" class="share-content">
|
||||
<h4>{{ shareFile.name }}</h4>
|
||||
|
||||
<div class="share-form">
|
||||
<div class="field">
|
||||
<label>Passwort (optional)</label>
|
||||
<Password v-model="sharePassword" :feedback="false" toggle-mask fluid />
|
||||
<!-- User Sharing -->
|
||||
<div class="share-section">
|
||||
<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" />
|
||||
<Button label="Teilen" size="small" @click="shareWithUser" :disabled="!selectedShareUser" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Ablaufdatum (optional)</label>
|
||||
<InputText v-model="shareExpiry" type="date" fluid />
|
||||
<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 }"
|
||||
@click="selectedShareUser = u; shareUserQuery = u.username; userSearchResults = []">
|
||||
<i class="pi pi-user"></i> {{ u.username }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filePermissions.length" class="existing-shares">
|
||||
<div v-for="perm in filePermissions" :key="perm.id" class="share-perm-item">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>{{ perm.username }}</span>
|
||||
<Tag :value="permLabel(perm.permission)" size="small" />
|
||||
<Button icon="pi pi-trash" text size="small" severity="danger" @click="removeUserShare(perm.id)" />
|
||||
</div>
|
||||
</div>
|
||||
<Button label="Link erstellen" icon="pi pi-link" @click="createShare" :loading="shareLoading" />
|
||||
</div>
|
||||
|
||||
<div v-if="shareLinks.length" class="existing-links">
|
||||
<h4>Bestehende Links</h4>
|
||||
<!-- Link Sharing -->
|
||||
<div class="share-section">
|
||||
<h5>Freigabe-Link erstellen</h5>
|
||||
<div class="share-form">
|
||||
<div class="field">
|
||||
<label>Passwort (optional)</label>
|
||||
<Password v-model="sharePassword" :feedback="false" toggle-mask fluid />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Ablaufdatum (optional)</label>
|
||||
<InputText v-model="shareExpiry" type="date" fluid />
|
||||
</div>
|
||||
<Button label="Link erstellen" icon="pi pi-link" @click="createShare" :loading="shareLoading" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="shareLinks.length" class="share-section">
|
||||
<h5>Bestehende Links</h5>
|
||||
<div v-for="link in shareLinks" :key="link.id" class="share-link-item">
|
||||
<div class="link-info">
|
||||
<code>{{ window.location.origin }}/share/{{ link.token }}</code>
|
||||
<code>{{ currentOrigin }}/share/{{ link.token }}</code>
|
||||
<small>
|
||||
{{ link.download_count }} Downloads
|
||||
<template v-if="link.expires_at"> | Bis {{ formatDate(link.expires_at) }}</template>
|
||||
@@ -160,6 +189,7 @@ import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useFilesStore } from '../stores/files'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import apiClient from '../api/client'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Button from 'primevue/button'
|
||||
@@ -167,6 +197,7 @@ import Dialog from 'primevue/dialog'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import Tag from 'primevue/tag'
|
||||
import Select from 'primevue/select'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -184,6 +215,13 @@ const shareFile = ref(null)
|
||||
const sharePassword = ref('')
|
||||
const shareExpiry = ref('')
|
||||
const shareLinks = ref([])
|
||||
const filePermissions = ref([])
|
||||
const shareUserQuery = ref('')
|
||||
const selectedShareUser = ref(null)
|
||||
const shareUserPermission = ref('read')
|
||||
const userSearchResults = ref([])
|
||||
const userPermOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Schreiben', value: 'write' }, { label: 'Admin', value: 'admin' }]
|
||||
const currentOrigin = window.location.origin
|
||||
const shareLoading = ref(false)
|
||||
const showDeleteConfirm = ref(false)
|
||||
const deleteTarget = ref(null)
|
||||
@@ -297,15 +335,69 @@ async function doRename() {
|
||||
}
|
||||
}
|
||||
|
||||
function permLabel(perm) {
|
||||
return { read: 'Lesen', write: 'Schreiben', admin: 'Admin' }[perm] || perm
|
||||
}
|
||||
|
||||
async function openShare(data) {
|
||||
shareFile.value = data
|
||||
sharePassword.value = ''
|
||||
shareExpiry.value = ''
|
||||
shareUserQuery.value = ''
|
||||
selectedShareUser.value = null
|
||||
userSearchResults.value = []
|
||||
showShare.value = true
|
||||
// Load existing links and permissions in parallel
|
||||
try {
|
||||
shareLinks.value = await filesStore.getShareLinks(data.id)
|
||||
const [links, perms] = await Promise.all([
|
||||
filesStore.getShareLinks(data.id),
|
||||
apiClient.get(`/files/${data.id}/permissions`).then(r => r.data).catch(() => []),
|
||||
])
|
||||
shareLinks.value = links
|
||||
filePermissions.value = perms
|
||||
} catch {
|
||||
shareLinks.value = []
|
||||
filePermissions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
let searchTimeout = null
|
||||
function searchUsers() {
|
||||
selectedShareUser.value = null
|
||||
clearTimeout(searchTimeout)
|
||||
if (shareUserQuery.value.length < 2) { userSearchResults.value = []; return }
|
||||
searchTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await apiClient.get('/users/search', { params: { q: shareUserQuery.value } })
|
||||
userSearchResults.value = res.data
|
||||
} catch { userSearchResults.value = [] }
|
||||
}, 300)
|
||||
}
|
||||
|
||||
async function shareWithUser() {
|
||||
if (!selectedShareUser.value || !shareFile.value) return
|
||||
try {
|
||||
await apiClient.post(`/files/${shareFile.value.id}/permissions`, {
|
||||
user_id: selectedShareUser.value.id,
|
||||
permission: shareUserPermission.value,
|
||||
})
|
||||
toast.add({ severity: 'success', summary: `Mit ${selectedShareUser.value.username} geteilt`, life: 3000 })
|
||||
shareUserQuery.value = ''
|
||||
selectedShareUser.value = null
|
||||
const res = await apiClient.get(`/files/${shareFile.value.id}/permissions`)
|
||||
filePermissions.value = res.data
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUserShare(permId) {
|
||||
if (!shareFile.value) return
|
||||
try {
|
||||
await apiClient.delete(`/files/${shareFile.value.id}/permissions/${permId}`)
|
||||
filePermissions.value = filePermissions.value.filter(p => p.id !== permId)
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,8 +487,16 @@ onMounted(() => {
|
||||
.field { margin-bottom: 1rem; }
|
||||
.field label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.875rem; }
|
||||
.share-content h4 { margin: 0 0 1rem; }
|
||||
.share-form { margin-bottom: 1.5rem; }
|
||||
.existing-links { border-top: 1px solid var(--p-surface-200); padding-top: 1rem; }
|
||||
.share-section { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--p-surface-200); }
|
||||
.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-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); }
|
||||
.existing-shares { margin-top: 0.5rem; }
|
||||
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; }
|
||||
.share-link-item {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 0.5rem 0; border-bottom: 1px solid var(--p-surface-100);
|
||||
|
||||
Reference in New Issue
Block a user