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:
parent
e7170b7cc8
commit
35099de2c5
|
|
@ -71,8 +71,27 @@ def admin_required(f):
|
|||
return decorated
|
||||
|
||||
|
||||
@api_bp.route('/auth/registration-status', methods=['GET'])
|
||||
def registration_status():
|
||||
"""Check if public registration is allowed."""
|
||||
from app.models.settings import AppSettings
|
||||
is_first_user = User.query.count() == 0
|
||||
public_registration = AppSettings.get_bool('public_registration', default=False)
|
||||
return jsonify({
|
||||
'allowed': is_first_user or public_registration,
|
||||
'is_first_user': is_first_user,
|
||||
}), 200
|
||||
|
||||
|
||||
@api_bp.route('/auth/register', methods=['POST'])
|
||||
def register():
|
||||
from app.models.settings import AppSettings
|
||||
|
||||
# Check if registration is allowed
|
||||
is_first_user = User.query.count() == 0
|
||||
if not is_first_user and not AppSettings.get_bool('public_registration', default=False):
|
||||
return jsonify({'error': 'Oeffentliche Registrierung ist deaktiviert'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'Keine Daten gesendet'}), 400
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import os
|
||||
|
||||
from flask import request, jsonify
|
||||
|
||||
from app.api import api_bp
|
||||
from app.api.auth import admin_required, token_required
|
||||
from app.extensions import db
|
||||
from app.models.user import User
|
||||
from app.models.settings import AppSettings
|
||||
|
||||
|
||||
@api_bp.route('/users', methods=['GET'])
|
||||
|
|
@ -13,6 +16,51 @@ def list_users():
|
|||
return jsonify([u.to_dict(include_email=True) for u in users]), 200
|
||||
|
||||
|
||||
@api_bp.route('/users', methods=['POST'])
|
||||
@admin_required
|
||||
def create_user():
|
||||
"""Admin creates a new user."""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'Keine Daten gesendet'}), 400
|
||||
|
||||
username = data.get('username', '').strip()
|
||||
password = data.get('password', '')
|
||||
email = data.get('email', '').strip() or None
|
||||
role = data.get('role', 'user')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({'error': 'Benutzername und Passwort erforderlich'}), 400
|
||||
|
||||
if len(username) < 3:
|
||||
return jsonify({'error': 'Benutzername muss mindestens 3 Zeichen lang sein'}), 400
|
||||
|
||||
if len(password) < 8:
|
||||
return jsonify({'error': 'Passwort muss mindestens 8 Zeichen lang sein'}), 400
|
||||
|
||||
if role not in ('admin', 'user'):
|
||||
return jsonify({'error': 'Rolle muss "admin" oder "user" sein'}), 400
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
return jsonify({'error': 'Benutzername bereits vergeben'}), 409
|
||||
|
||||
if email and User.query.filter_by(email=email).first():
|
||||
return jsonify({'error': 'Email-Adresse bereits vergeben'}), 409
|
||||
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
role=role,
|
||||
master_key_salt=os.urandom(32),
|
||||
storage_quota_mb=data.get('storage_quota_mb', 5120),
|
||||
)
|
||||
user.set_password(password)
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict(include_email=True)), 201
|
||||
|
||||
|
||||
@api_bp.route('/users/<int:user_id>', methods=['GET'])
|
||||
@admin_required
|
||||
def get_user(user_id):
|
||||
|
|
@ -41,7 +89,6 @@ def update_user(user_id):
|
|||
user.email = email
|
||||
|
||||
if 'role' in data and data['role'] in ('admin', 'user'):
|
||||
# Prevent removing last admin
|
||||
if user.role == 'admin' and data['role'] == 'user':
|
||||
admin_count = User.query.filter_by(role='admin', is_active=True).count()
|
||||
if admin_count <= 1:
|
||||
|
|
@ -85,6 +132,46 @@ def delete_user(user_id):
|
|||
return jsonify({'message': 'Benutzer geloescht'}), 200
|
||||
|
||||
|
||||
# --- App Settings ---
|
||||
|
||||
@api_bp.route('/settings', methods=['GET'])
|
||||
@admin_required
|
||||
def get_settings():
|
||||
return jsonify({
|
||||
'public_registration': AppSettings.get_bool('public_registration', default=False),
|
||||
}), 200
|
||||
|
||||
|
||||
@api_bp.route('/settings', methods=['PUT'])
|
||||
@admin_required
|
||||
def update_settings():
|
||||
data = request.get_json()
|
||||
if 'public_registration' in data:
|
||||
AppSettings.set('public_registration', str(data['public_registration']).lower())
|
||||
return jsonify({'message': 'Einstellungen gespeichert'}), 200
|
||||
|
||||
|
||||
# --- User search (for sharing dialogs) ---
|
||||
|
||||
@api_bp.route('/users/search', methods=['GET'])
|
||||
@token_required
|
||||
def search_users():
|
||||
"""Search users by username - for sharing dialogs."""
|
||||
query = request.args.get('q', '').strip()
|
||||
if len(query) < 2:
|
||||
return jsonify([]), 200
|
||||
|
||||
users = User.query.filter(
|
||||
User.username.ilike(f'%{query}%'),
|
||||
User.id != request.current_user.id,
|
||||
User.is_active == True,
|
||||
).limit(10).all()
|
||||
|
||||
return jsonify([{'id': u.id, 'username': u.username} for u in users]), 200
|
||||
|
||||
|
||||
# --- Change password (non-admin, own account) ---
|
||||
|
||||
@api_bp.route('/auth/change-password', methods=['POST'])
|
||||
@token_required
|
||||
def change_password():
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from app.models.calendar import Calendar, CalendarEvent, CalendarShare
|
|||
from app.models.contact import AddressBook, Contact, AddressBookShare
|
||||
from app.models.email_account import EmailAccount
|
||||
from app.models.password_vault import PasswordFolder, PasswordEntry, PasswordShare
|
||||
from app.models.settings import AppSettings
|
||||
|
||||
__all__ = [
|
||||
'User',
|
||||
|
|
@ -12,4 +13,5 @@ __all__ = [
|
|||
'AddressBook', 'Contact', 'AddressBookShare',
|
||||
'EmailAccount',
|
||||
'PasswordFolder', 'PasswordEntry', 'PasswordShare',
|
||||
'AppSettings',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
from app.extensions import db
|
||||
|
||||
|
||||
class AppSettings(db.Model):
|
||||
__tablename__ = 'app_settings'
|
||||
|
||||
key = db.Column(db.String(100), primary_key=True)
|
||||
value = db.Column(db.Text, nullable=False)
|
||||
|
||||
@staticmethod
|
||||
def get(key, default=''):
|
||||
setting = db.session.get(AppSettings, key)
|
||||
return setting.value if setting else default
|
||||
|
||||
@staticmethod
|
||||
def set(key, value):
|
||||
setting = db.session.get(AppSettings, key)
|
||||
if setting:
|
||||
setting.value = str(value)
|
||||
else:
|
||||
setting = AppSettings(key=key, value=str(value))
|
||||
db.session.add(setting)
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def get_bool(key, default=False):
|
||||
val = AppSettings.get(key, '')
|
||||
if val == '':
|
||||
return default
|
||||
return val.lower() in ('true', '1', 'yes')
|
||||
|
|
@ -4,12 +4,31 @@
|
|||
<h2>Administration</h2>
|
||||
</div>
|
||||
|
||||
<!-- App Settings -->
|
||||
<div class="admin-section">
|
||||
<h3>Benutzerverwaltung</h3>
|
||||
<h3>Einstellungen</h3>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<strong>Oeffentliche Registrierung</strong>
|
||||
<p>Wenn aktiviert, koennen sich neue Benutzer selbst registrieren. Andernfalls kann nur ein Admin neue Benutzer anlegen.</p>
|
||||
</div>
|
||||
<InputSwitch v-model="publicRegistration" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management -->
|
||||
<div class="admin-section">
|
||||
<div class="section-header">
|
||||
<h3>Benutzerverwaltung</h3>
|
||||
<Button icon="pi pi-user-plus" label="Benutzer hinzufuegen" size="small" @click="openNewUser" />
|
||||
</div>
|
||||
|
||||
<DataTable :value="users" :loading="loading" striped-rows>
|
||||
<Column field="id" header="ID" style="width: 60px" />
|
||||
<Column field="username" header="Benutzername" />
|
||||
<Column field="email" header="E-Mail" />
|
||||
<Column field="email" header="E-Mail">
|
||||
<template #body="{ data }">{{ data.email || '-' }}</template>
|
||||
</Column>
|
||||
<Column field="role" header="Rolle">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.role" :severity="data.role === 'admin' ? 'danger' : 'info'" />
|
||||
|
|
@ -21,32 +40,88 @@
|
|||
</template>
|
||||
</Column>
|
||||
<Column field="storage_quota_mb" header="Quota (MB)" />
|
||||
<Column header="Aktionen" style="width: 100px">
|
||||
<Column header="Aktionen" style="width: 120px">
|
||||
<template #body="{ data }">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
@click="editUser(data)"
|
||||
/>
|
||||
<Button icon="pi pi-pencil" text rounded size="small" @click="openEditUser(data)" />
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger" @click="confirmDeleteUser(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<!-- New/Edit User Dialog -->
|
||||
<Dialog v-model:visible="showUserDialog" :header="editingUser ? 'Benutzer bearbeiten' : 'Neuer Benutzer'" modal :style="{ width: '450px' }">
|
||||
<div class="field">
|
||||
<label>Benutzername</label>
|
||||
<InputText v-model="userForm.username" fluid :disabled="!!editingUser" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>E-Mail (optional)</label>
|
||||
<InputText v-model="userForm.email" type="email" fluid />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ editingUser ? 'Neues Passwort (leer lassen = nicht aendern)' : 'Passwort' }}</label>
|
||||
<Password v-model="userForm.password" :feedback="false" toggle-mask fluid />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Rolle</label>
|
||||
<Select v-model="userForm.role" :options="roleOptions" optionLabel="label" optionValue="value" fluid />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Speicher-Quota (MB)</label>
|
||||
<InputText v-model.number="userForm.storage_quota_mb" type="number" fluid />
|
||||
</div>
|
||||
<div v-if="editingUser" class="field">
|
||||
<label>Aktiv</label>
|
||||
<InputSwitch v-model="userForm.is_active" />
|
||||
</div>
|
||||
<Message v-if="userError" severity="error" :closable="false">{{ userError }}</Message>
|
||||
<template #footer>
|
||||
<Button label="Abbrechen" text @click="showUserDialog = false" />
|
||||
<Button :label="editingUser ? 'Speichern' : 'Erstellen'" @click="saveUser" :loading="userSaving" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Delete Confirm -->
|
||||
<Dialog v-model:visible="showDeleteConfirm" header="Benutzer loeschen" modal :style="{ width: '400px' }">
|
||||
<p>Moechtest du den Benutzer <strong>{{ deleteTarget?.username }}</strong> wirklich loeschen?</p>
|
||||
<template #footer>
|
||||
<Button label="Abbrechen" text @click="showDeleteConfirm = false" />
|
||||
<Button label="Loeschen" severity="danger" @click="doDeleteUser" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import apiClient from '../api/client'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Tag from 'primevue/tag'
|
||||
import Button from 'primevue/button'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import Select from 'primevue/select'
|
||||
import InputSwitch from 'primevue/inputswitch'
|
||||
import Message from 'primevue/message'
|
||||
|
||||
const toast = useToast()
|
||||
const users = ref([])
|
||||
const loading = ref(false)
|
||||
const publicRegistration = ref(false)
|
||||
|
||||
const showUserDialog = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const userForm = ref({ username: '', email: '', password: '', role: 'user', storage_quota_mb: 5120, is_active: true })
|
||||
const userError = ref('')
|
||||
const userSaving = ref(false)
|
||||
const roleOptions = [{ label: 'Benutzer', value: 'user' }, { label: 'Admin', value: 'admin' }]
|
||||
|
||||
const showDeleteConfirm = ref(false)
|
||||
const deleteTarget = ref(null)
|
||||
|
||||
async function loadUsers() {
|
||||
loading.value = true
|
||||
|
|
@ -54,22 +129,115 @@ async function loadUsers() {
|
|||
const response = await apiClient.get('/users')
|
||||
users.value = response.data
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Benutzer:', err)
|
||||
toast.add({ severity: 'error', summary: 'Fehler beim Laden', life: 5000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function editUser(user) {
|
||||
// TODO: Edit dialog in Phase 8
|
||||
console.log('Edit user:', user)
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const res = await apiClient.get('/settings')
|
||||
publicRegistration.value = res.data.public_registration
|
||||
} catch { /* first load, default false */ }
|
||||
}
|
||||
|
||||
onMounted(loadUsers)
|
||||
async function saveSettings() {
|
||||
try {
|
||||
await apiClient.put('/settings', { public_registration: publicRegistration.value })
|
||||
toast.add({ severity: 'success', summary: 'Einstellungen gespeichert', life: 2000 })
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
function openNewUser() {
|
||||
editingUser.value = null
|
||||
userForm.value = { username: '', email: '', password: '', role: 'user', storage_quota_mb: 5120, is_active: true }
|
||||
userError.value = ''
|
||||
showUserDialog.value = true
|
||||
}
|
||||
|
||||
function openEditUser(user) {
|
||||
editingUser.value = user
|
||||
userForm.value = {
|
||||
username: user.username,
|
||||
email: user.email || '',
|
||||
password: '',
|
||||
role: user.role,
|
||||
storage_quota_mb: user.storage_quota_mb,
|
||||
is_active: user.is_active,
|
||||
}
|
||||
userError.value = ''
|
||||
showUserDialog.value = true
|
||||
}
|
||||
|
||||
async function saveUser() {
|
||||
userError.value = ''
|
||||
userSaving.value = true
|
||||
try {
|
||||
if (editingUser.value) {
|
||||
const payload = { ...userForm.value }
|
||||
if (!payload.password) delete payload.password
|
||||
delete payload.username
|
||||
await apiClient.put(`/users/${editingUser.value.id}`, payload)
|
||||
toast.add({ severity: 'success', summary: 'Benutzer aktualisiert', life: 3000 })
|
||||
} else {
|
||||
if (!userForm.value.password) {
|
||||
userError.value = 'Passwort erforderlich'
|
||||
return
|
||||
}
|
||||
await apiClient.post('/users', userForm.value)
|
||||
toast.add({ severity: 'success', summary: 'Benutzer erstellt', life: 3000 })
|
||||
}
|
||||
showUserDialog.value = false
|
||||
await loadUsers()
|
||||
} catch (err) {
|
||||
userError.value = err.response?.data?.error || 'Fehler beim Speichern'
|
||||
} finally {
|
||||
userSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteUser(user) {
|
||||
deleteTarget.value = user
|
||||
showDeleteConfirm.value = true
|
||||
}
|
||||
|
||||
async function doDeleteUser() {
|
||||
if (!deleteTarget.value) return
|
||||
try {
|
||||
await apiClient.delete(`/users/${deleteTarget.value.id}`)
|
||||
showDeleteConfirm.value = false
|
||||
toast.add({ severity: 'success', summary: 'Benutzer geloescht', life: 3000 })
|
||||
await loadUsers()
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
loadSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.view-container { padding: 1.5rem; }
|
||||
.view-header h2 { margin: 0 0 1.5rem; }
|
||||
.admin-section {
|
||||
background: var(--p-surface-0); border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem;
|
||||
}
|
||||
.admin-section h3 { margin: 0 0 1rem; font-size: 1.125rem; }
|
||||
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
|
||||
.section-header h3 { margin: 0; }
|
||||
.setting-row {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 1rem;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
.setting-info { flex: 1; }
|
||||
.setting-info strong { display: block; margin-bottom: 0.25rem; }
|
||||
.setting-info p { margin: 0; font-size: 0.85rem; color: var(--p-text-muted-color); }
|
||||
.field { margin-bottom: 1rem; }
|
||||
.field label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.875rem; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
/>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<div v-if="registrationAllowed" class="auth-footer">
|
||||
<router-link to="/register">Noch kein Konto? Registrieren</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -51,9 +51,10 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import axios from 'axios'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import Button from 'primevue/button'
|
||||
|
|
@ -66,6 +67,14 @@ const username = ref('')
|
|||
const password = ref('')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const registrationAllowed = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/auth/registration-status')
|
||||
registrationAllowed.value = res.data.allowed
|
||||
} catch { registrationAllowed.value = false }
|
||||
})
|
||||
|
||||
async function handleLogin() {
|
||||
error.value = ''
|
||||
|
|
|
|||
|
|
@ -71,9 +71,10 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import axios from 'axios'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import Button from 'primevue/button'
|
||||
|
|
@ -89,6 +90,15 @@ const password2 = ref('')
|
|||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/auth/registration-status')
|
||||
if (!res.data.allowed) {
|
||||
router.push('/login')
|
||||
}
|
||||
} catch { router.push('/login') }
|
||||
})
|
||||
|
||||
async function handleRegister() {
|
||||
error.value = ''
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue