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
+83
View File
@@ -0,0 +1,83 @@
import axios from 'axios'
import { useAuthStore } from '../stores/auth'
import router from '../router'
const apiClient = axios.create({
baseURL: '/api',
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor: attach access token
apiClient.interceptors.request.use((config) => {
const auth = useAuthStore()
if (auth.accessToken) {
config.headers.Authorization = `Bearer ${auth.accessToken}`
}
return config
})
// Response interceptor: handle 401 with token refresh
let isRefreshing = false
let failedQueue = []
const processQueue = (error, token = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error)
} else {
prom.resolve(token)
}
})
failedQueue = []
}
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
if (originalRequest.url === '/auth/refresh' || originalRequest.url === '/auth/login') {
const auth = useAuthStore()
auth.logout()
router.push('/login')
return Promise.reject(error)
}
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`
return apiClient(originalRequest)
})
}
originalRequest._retry = true
isRefreshing = true
try {
const auth = useAuthStore()
const newToken = await auth.refreshToken()
processQueue(null, newToken)
originalRequest.headers.Authorization = `Bearer ${newToken}`
return apiClient(originalRequest)
} catch (refreshError) {
processQueue(refreshError, null)
const auth = useAuthStore()
auth.logout()
router.push('/login')
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
}
)
export default apiClient