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
+72
View File
@@ -0,0 +1,72 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import apiClient from '../api/client'
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const accessToken = ref(null)
const masterKeySalt = ref(null)
const isAuthenticated = computed(() => !!accessToken.value)
const isAdmin = computed(() => user.value?.role === 'admin')
const hasEmailAccounts = computed(() => (user.value?.email_account_count || 0) > 0)
async function login(username, password) {
const response = await apiClient.post('/auth/login', { username, password })
user.value = response.data.user
accessToken.value = response.data.access_token
masterKeySalt.value = response.data.master_key_salt
return response.data
}
async function register(username, password, email) {
const payload = { username, password }
if (email) payload.email = email
const response = await apiClient.post('/auth/register', payload)
user.value = response.data.user
accessToken.value = response.data.access_token
return response.data
}
async function refreshToken() {
const response = await apiClient.post('/auth/refresh')
accessToken.value = response.data.access_token
return response.data.access_token
}
async function fetchMe() {
const response = await apiClient.get('/auth/me')
user.value = response.data
masterKeySalt.value = response.data.master_key_salt
return response.data
}
async function changePassword(currentPassword, newPassword) {
await apiClient.post('/auth/change-password', {
current_password: currentPassword,
new_password: newPassword,
})
}
function logout() {
apiClient.post('/auth/logout').catch(() => {})
user.value = null
accessToken.value = null
masterKeySalt.value = null
}
return {
user,
accessToken,
masterKeySalt,
isAuthenticated,
isAdmin,
hasEmailAccounts,
login,
register,
refreshToken,
fetchMe,
changePassword,
logout,
}
})
+84
View File
@@ -0,0 +1,84 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import apiClient from '../api/client'
export const useFilesStore = defineStore('files', () => {
const files = ref([])
const breadcrumb = ref([])
const currentParentId = ref(null)
const loading = ref(false)
async function loadFiles(parentId = null) {
loading.value = true
try {
currentParentId.value = parentId
const params = parentId ? { parent_id: parentId } : {}
const response = await apiClient.get('/files', { params })
files.value = response.data.files
breadcrumb.value = response.data.breadcrumb
} finally {
loading.value = false
}
}
async function createFolder(name, parentId = null) {
const response = await apiClient.post('/files/folder', {
name,
parent_id: parentId,
})
await loadFiles(parentId)
return response.data
}
async function uploadFile(file, parentId = null) {
const formData = new FormData()
formData.append('file', file)
if (parentId) formData.append('parent_id', parentId)
const response = await apiClient.post('/files/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
await loadFiles(parentId)
return response.data
}
async function deleteFile(fileId) {
await apiClient.delete(`/files/${fileId}`)
await loadFiles(currentParentId.value)
}
async function renameFile(fileId, newName) {
await apiClient.put(`/files/${fileId}`, { name: newName })
await loadFiles(currentParentId.value)
}
async function moveFile(fileId, newParentId) {
await apiClient.put(`/files/${fileId}`, { parent_id: newParentId })
await loadFiles(currentParentId.value)
}
async function createShareLink(fileId, options = {}) {
const response = await apiClient.post(`/files/${fileId}/share`, options)
return response.data
}
async function getShareLinks(fileId) {
const response = await apiClient.get(`/files/${fileId}/shares`)
return response.data
}
async function deleteShareLink(token) {
await apiClient.delete(`/share/${token}`)
}
function downloadUrl(fileId) {
return `/api/files/${fileId}/download`
}
return {
files, breadcrumb, currentParentId, loading,
loadFiles, createFolder, uploadFile, deleteFile,
renameFile, moveFile, createShareLink, getShareLinks,
deleteShareLink, downloadUrl,
}
})