- StorageClient for Supabase Storage REST API (upload, download, delete)
- DocumentService with CRUD operations + storage integration
- DocumentHandler with multipart form upload support (50MB limit)
- Routes: GET/POST /api/cases/{id}/documents, GET/DELETE /api/documents/{docId}
- file_path format: {tenant_id}/{case_id}/{uuid}_{filename}
- Case events logged on upload/delete
- Added SUPABASE_SERVICE_KEY to config for server-side storage access
- Fixed pre-existing duplicate writeJSON/writeError in appointments.go
184 lines
4.5 KiB
Go
184 lines
4.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
|
"mgit.msbls.de/m/KanzlAI-mGMT/internal/services"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const maxUploadSize = 50 << 20 // 50 MB
|
|
|
|
type DocumentHandler struct {
|
|
svc *services.DocumentService
|
|
}
|
|
|
|
func NewDocumentHandler(svc *services.DocumentService) *DocumentHandler {
|
|
return &DocumentHandler{svc: svc}
|
|
}
|
|
|
|
func (h *DocumentHandler) ListByCase(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
|
|
caseID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid case ID")
|
|
return
|
|
}
|
|
|
|
docs, err := h.svc.ListByCase(r.Context(), tenantID, caseID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"documents": docs,
|
|
"total": len(docs),
|
|
})
|
|
}
|
|
|
|
func (h *DocumentHandler) Upload(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
userID, _ := auth.UserFromContext(r.Context())
|
|
|
|
caseID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid case ID")
|
|
return
|
|
}
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
|
|
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
|
|
writeError(w, http.StatusBadRequest, "file too large or invalid multipart form")
|
|
return
|
|
}
|
|
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "missing file field")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
title := r.FormValue("title")
|
|
if title == "" {
|
|
title = header.Filename
|
|
}
|
|
|
|
contentType := header.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
|
|
input := services.CreateDocumentInput{
|
|
Title: title,
|
|
DocType: r.FormValue("doc_type"),
|
|
Filename: header.Filename,
|
|
ContentType: contentType,
|
|
Size: int(header.Size),
|
|
Data: file,
|
|
}
|
|
|
|
doc, err := h.svc.Create(r.Context(), tenantID, caseID, userID, input)
|
|
if err != nil {
|
|
if err.Error() == "case not found" {
|
|
writeError(w, http.StatusNotFound, "case not found")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, doc)
|
|
}
|
|
|
|
func (h *DocumentHandler) Download(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
|
|
docID, err := uuid.Parse(r.PathValue("docId"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid document ID")
|
|
return
|
|
}
|
|
|
|
body, contentType, title, err := h.svc.Download(r.Context(), tenantID, docID)
|
|
if err != nil {
|
|
if err.Error() == "document not found" || err.Error() == "document has no file" {
|
|
writeError(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
defer body.Close()
|
|
|
|
w.Header().Set("Content-Type", contentType)
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, title))
|
|
io.Copy(w, body)
|
|
}
|
|
|
|
func (h *DocumentHandler) GetMeta(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
|
|
docID, err := uuid.Parse(r.PathValue("docId"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid document ID")
|
|
return
|
|
}
|
|
|
|
doc, err := h.svc.GetByID(r.Context(), tenantID, docID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if doc == nil {
|
|
writeError(w, http.StatusNotFound, "document not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, doc)
|
|
}
|
|
|
|
func (h *DocumentHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
userID, _ := auth.UserFromContext(r.Context())
|
|
|
|
docID, err := uuid.Parse(r.PathValue("docId"))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid document ID")
|
|
return
|
|
}
|
|
|
|
if err := h.svc.Delete(r.Context(), tenantID, docID, userID); err != nil {
|
|
writeError(w, http.StatusNotFound, "document not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
|
}
|