feat: Mini-Cloud Plattform - komplette Implementierung Phase 0-8

Selbstgehostete Web-Cloud mit Dateiverwaltung, Kalender, Kontakte,
Email-Webclient, Office-Viewer und Passwort-Manager.

Backend (Flask/Python):
- JWT-Auth mit Access/Refresh Tokens, Benutzerverwaltung
- Dateien: Upload/Download, Ordner, Berechtigungen, Share-Links
- Kalender: CRUD, Teilen, iCal-Export, CalDAV well-known URLs
- Kontakte: Adressbuecher, vCard-Export, Teilen
- Email: IMAP/SMTP-Proxy, Multi-Account
- Office-Viewer: DOCX/XLSX/PPTX/PDF Vorschau
- Passwort-Manager: AES-256-GCM clientseitig, KeePass-Import
- Sync-API fuer Desktop/Mobile-Clients
- SQLite mit WAL-Modus

Frontend (Vue 3 + PrimeVue):
- Datei-Explorer mit Breadcrumbs und Share-Dialogen
- Monatskalender mit Event-Verwaltung
- Kontaktliste mit Adressbuch-Sidebar
- Email-Client mit 3-Spalten-Layout
- Passwort-Manager mit TOTP und Passwort-Generator
- Admin-Panel, Settings, oeffentliche Share-Seite

Docker: Multi-Stage Build, Bind Mounts (keine Volumes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-11 14:53:28 +02:00
parent d4f7e90d0c
commit 62f550c373
56 changed files with 8047 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
<template>
<div class="share-container">
<div class="share-card">
<i class="pi pi-cloud" style="font-size: 2rem; color: var(--p-primary-color)"></i>
<div v-if="loading" class="share-loading">
<i class="pi pi-spin pi-spinner" style="font-size: 1.5rem"></i>
<p>Laden...</p>
</div>
<div v-else-if="error" class="share-error">
<i class="pi pi-times-circle" style="font-size: 2rem; color: var(--p-red-500)"></i>
<p>{{ error }}</p>
</div>
<div v-else-if="fileInfo" class="share-info">
<h2>{{ fileInfo.name }}</h2>
<p class="file-size" v-if="fileInfo.size">{{ formatSize(fileInfo.size) }}</p>
<div v-if="fileInfo.has_password && !authenticated" class="password-form">
<p>Diese Datei ist passwortgeschuetzt.</p>
<div class="field">
<Password v-model="password" placeholder="Passwort eingeben" :feedback="false" toggle-mask fluid />
</div>
<Message v-if="authError" severity="error" :closable="false">{{ authError }}</Message>
<Button label="Entsperren" @click="verifyPassword" :loading="verifying" fluid />
</div>
<div v-else class="download-section">
<Button
label="Herunterladen"
icon="pi pi-download"
size="large"
@click="downloadFile"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
import Button from 'primevue/button'
import Password from 'primevue/password'
import Message from 'primevue/message'
const route = useRoute()
const token = route.params.token
const loading = ref(true)
const error = ref('')
const fileInfo = ref(null)
const password = ref('')
const authenticated = ref(false)
const authError = ref('')
const verifying = ref(false)
function formatSize(bytes) {
if (!bytes) return ''
const units = ['B', 'KB', 'MB', 'GB']
let i = 0
let size = bytes
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++ }
return `${size.toFixed(i > 0 ? 1 : 0)} ${units[i]}`
}
async function loadInfo() {
try {
const res = await axios.get(`/api/share/${token}/info`)
fileInfo.value = res.data
if (!res.data.has_password) authenticated.value = true
} catch (err) {
error.value = err.response?.data?.error || 'Link nicht gefunden oder abgelaufen'
} finally {
loading.value = false
}
}
async function verifyPassword() {
authError.value = ''
verifying.value = true
try {
await axios.post(`/api/share/${token}/verify`, { password: password.value })
authenticated.value = true
} catch (err) {
authError.value = err.response?.data?.error || 'Falsches Passwort'
} finally {
verifying.value = false
}
}
function downloadFile() {
let url = `/api/share/${token}/download`
if (fileInfo.value?.has_password && password.value) {
url += `?password=${encodeURIComponent(password.value)}`
}
window.location.href = url
}
onMounted(loadInfo)
</script>
<style scoped>
.share-container {
min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: var(--p-surface-50);
}
.share-card {
background: var(--p-surface-0); border-radius: 12px; padding: 3rem;
text-align: center; max-width: 450px; width: 100%;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.share-card h2 { margin: 1rem 0 0.25rem; font-size: 1.25rem; }
.file-size { color: var(--p-text-muted-color); margin-bottom: 1.5rem; }
.password-form { text-align: left; margin-top: 1.5rem; }
.password-form p { margin-bottom: 1rem; color: var(--p-text-muted-color); }
.field { margin-bottom: 1rem; }
.download-section { margin-top: 1.5rem; }
.share-loading, .share-error { margin-top: 1.5rem; }
</style>