V3 version with backward comp

This commit is contained in:
m
2026-07-28 18:12:56 +02:00
parent 7713835739
commit bd6e7c38f9
48 changed files with 3685 additions and 1344 deletions
+65
View File
@@ -0,0 +1,65 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/auth"
"github.com/vaultdrop/backend/internal/service"
"github.com/vaultdrop/backend/pkg/api"
)
type DeviceHandler struct {
placement *service.PlacementService
}
func (h *DeviceHandler) List(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
locations, err := h.placement.ListUserLocations(userID)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list devices")
return
}
type deviceResponse struct {
ID string `json:"id"`
DeviceName string `json:"device_name"`
Role string `json:"role"`
}
resp := make([]deviceResponse, len(locations))
for i, l := range locations {
resp[i] = deviceResponse{
ID: l.ID.String(),
DeviceName: l.DeviceName,
Role: l.Role,
}
}
api.Success(c, resp)
}
func (h *DeviceHandler) Register(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
var body struct {
DeviceName string `json:"device_name" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "device_name is required")
return
}
loc, err := h.placement.CreateDeviceLocation(userID, body.DeviceName)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to register device")
return
}
api.Created(c, gin.H{
"id": loc.ID.String(),
"device_name": loc.DeviceName,
"role": loc.Role,
})
}
+27 -7
View File
@@ -6,17 +6,37 @@ import (
)
type Handler struct {
File *FileHandler
OCR *OCRHandler
Health *HealthHandler
Auth *auth.AuthHandler
Resource *ResourceHandler
OCR *OCRHandler
Health *HealthHandler
Auth *auth.AuthHandler
Share *ShareHandler
Device *DeviceHandler
Sync *SyncHandler
}
func New(fileSvc *service.FileService, ocrSvc *service.OCRService, urlSvc *service.URLService, authHandler *auth.AuthHandler, conversionSvc *service.ConversionService) *Handler {
func New(
resourceSvc *service.ResourceService,
ocrSvc *service.OCRService,
urlSvc *service.URLService,
authHandler *auth.AuthHandler,
conversionSvc *service.ConversionService,
rebacSvc *service.RebacService,
placementSvc *service.PlacementService,
syncSvc *service.SyncService,
) *Handler {
return &Handler{
File: &FileHandler{files: fileSvc, urls: urlSvc, ocr: ocrSvc, conversion: conversionSvc},
OCR: &OCRHandler{ocr: ocrSvc, files: fileSvc},
Resource: &ResourceHandler{
resources: resourceSvc,
urls: urlSvc,
ocr: ocrSvc,
conversion: conversionSvc,
},
OCR: &OCRHandler{ocr: ocrSvc, resources: resourceSvc},
Health: &HealthHandler{ocr: ocrSvc},
Auth: authHandler,
Share: &ShareHandler{rebac: rebacSvc},
Device: &DeviceHandler{placement: placementSvc},
Sync: &SyncHandler{sync: syncSvc},
}
}
+2 -2
View File
@@ -9,8 +9,8 @@ import (
)
type OCRHandler struct {
ocr *service.OCRService
files *service.FileService
ocr *service.OCRService
resources *service.ResourceService
}
func (h *OCRHandler) CreateJob(c *gin.Context) {
@@ -8,18 +8,21 @@ import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/auth"
"github.com/vaultdrop/backend/internal/service"
"github.com/vaultdrop/backend/pkg/api"
)
type FileHandler struct {
files *service.FileService
type ResourceHandler struct {
resources *service.ResourceService
urls *service.URLService
ocr *service.OCRService
conversion *service.ConversionService
}
func (h *FileHandler) Upload(c *gin.Context) {
func (h *ResourceHandler) Upload(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
form, err := c.MultipartForm()
if err != nil {
api.Error(c, http.StatusBadRequest, "ERROR_PARSING", "Error while parsing multipart form")
@@ -34,7 +37,7 @@ func (h *FileHandler) Upload(c *gin.Context) {
results := make([]gin.H, 0, len(files))
for _, file := range files {
result, err := h.files.Upload(file)
result, err := h.resources.Upload(file, userID)
if err != nil {
api.Error(c, http.StatusInternalServerError, "UPLOAD_ERROR", err.Error())
return
@@ -55,23 +58,23 @@ func (h *FileHandler) Upload(c *gin.Context) {
api.Success(c, results)
}
func (h *FileHandler) List(c *gin.Context) {
func (h *ResourceHandler) List(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
thumbnailQuality := c.Query("thumbnail")
files, err := h.files.List()
resources, err := h.resources.List(userID)
if err != nil {
log.Printf("ERROR List files: %v", err)
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list files")
log.Printf("ERROR List resources: %v", err)
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list resources")
return
}
type tagResponse struct {
ID string `json:"id"`
TagName string `json:"tag_name"`
TagType string `json:"tag_type"`
}
type fileResponse struct {
type resourceResponse struct {
ID string `json:"id"`
URL string `json:"url"`
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
@@ -81,50 +84,51 @@ func (h *FileHandler) List(c *gin.Context) {
CreatedAt string `json:"createdAt"`
MimeType string `json:"mimeType"`
OcrText string `json:"ocrText,omitempty"`
ParentFileID string `json:"parentFileID,omitempty"`
ParentID string `json:"parentResourceId,omitempty"`
IsFolder bool `json:"isFolder"`
UpdatedAt string `json:"updatedAt"`
OwnerID string `json:"ownerId"`
}
resp := make([]fileResponse, len(files))
for i, f := range files {
resp := make([]resourceResponse, len(resources))
for i, r := range resources {
tags := []tagResponse{}
for _, tag := range f.Tags {
for _, tag := range r.Tags {
tags = append(tags, tagResponse{
ID: tag.ID,
TagName: tag.Name,
TagType: tag.TagType,
})
}
downloadURL := h.urls.GenerateDownloadURL(f.ID)
downloadURL := h.urls.GenerateDownloadURL(r.ID)
thumbURL := downloadURL
if thumbnailQuality != "" {
if best := h.files.GetBestThumbnail(f.ID, thumbnailQuality); best != nil {
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
if best := h.resources.GetBestVariant(r.ID, thumbnailQuality); best != nil {
thumbURL = h.urls.GenerateVariantURL(best.ID)
}
}
resp[i] = fileResponse{
ID: f.ID,
resp[i] = resourceResponse{
ID: r.ID,
URL: downloadURL,
ThumbnailURL: thumbURL,
Name: f.Name,
Size: f.Size,
Name: r.Name,
Size: r.Size,
Tags: tags,
CreatedAt: f.CreatedAt,
ParentFileID: f.ParentFileID,
OcrText: f.OcrText,
IsFolder: f.IsFolder,
UpdatedAt: f.UpdatedAt,
MimeType: f.MimeType,
CreatedAt: r.CreatedAt.String(),
ParentID: r.ParentResourceID,
OcrText: r.OcrText,
IsFolder: r.IsFolder,
UpdatedAt: r.UpdatedAt.String(),
MimeType: r.MimeType,
OwnerID: r.OwnerID,
}
}
api.Paginated(c, resp, 1, len(resp))
}
func (h *FileHandler) Download(c *gin.Context) {
func (h *ResourceHandler) Download(c *gin.Context) {
id := c.Param("id")
exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64)
sig := c.Query("sig")
@@ -134,133 +138,126 @@ func (h *FileHandler) Download(c *gin.Context) {
return
}
storagePath, err := h.files.GetStoragePath(id)
storagePath, err := h.resources.GetStoragePath(id)
if err != nil {
api.Error(c, http.StatusNotFound, "FILE_NOT_FOUND", "File not found")
api.Error(c, http.StatusNotFound, "RESOURCE_NOT_FOUND", "Resource not found")
return
}
c.File(path.Clean(storagePath))
}
func (h *FileHandler) Get(c *gin.Context) {
func (h *ResourceHandler) Get(c *gin.Context) {
id := c.Param("id")
thumbnailQuality := c.Query("thumbnail")
file, err := h.files.Get(id)
resource, err := h.resources.Get(id)
if err != nil {
api.Error(c, http.StatusNotFound, "FILE_NOT_FOUND", "File not found")
api.Error(c, http.StatusNotFound, "RESOURCE_NOT_FOUND", "Resource not found")
return
}
type tagResponse struct {
ID string `json:"id"`
TagName string `json:"tag_name"`
TagType string `json:"tag_type"`
}
type thumbnailResponse struct {
ID string `json:"id"`
PageNumber int `json:"pageNumber"`
ResolutionLabel string `json:"resolutionLabel"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
MimeType string `json:"mimeType"`
type variantResponse struct {
ID string `json:"id"`
PageNumber int `json:"pageNumber"`
VariantType string `json:"variantType"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
MimeType string `json:"mimeType"`
}
tags := []tagResponse{}
for _, tag := range file.Tags {
for _, tag := range resource.Tags {
tags = append(tags, tagResponse{
ID: tag.ID,
TagName: tag.Name,
TagType: tag.TagType,
})
}
downloadURL := h.urls.GenerateDownloadURL(file.ID)
downloadURL := h.urls.GenerateDownloadURL(resource.ID)
thumbURL := downloadURL
if thumbnailQuality != "" {
if best := h.files.GetBestThumbnail(file.ID, thumbnailQuality); best != nil {
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
if best := h.resources.GetBestVariant(resource.ID, thumbnailQuality); best != nil {
thumbURL = h.urls.GenerateVariantURL(best.ID)
}
}
dbThumbnails, _ := h.files.GetThumbnailsByFileID(file.ID)
thumbnails := make([]thumbnailResponse, len(dbThumbnails))
for i, t := range dbThumbnails {
thumbnails[i] = thumbnailResponse{
ID: t.ID,
PageNumber: t.PageNumber,
ResolutionLabel: t.ResolutionLabel,
Width: t.Width,
Height: t.Height,
URL: h.urls.GenerateThumbnailURL(t.ID),
MimeType: t.MimeType,
dbVariants, _ := h.resources.GetVariantsByResourceID(resource.ID)
variants := make([]variantResponse, len(dbVariants))
for i, v := range dbVariants {
variants[i] = variantResponse{
ID: v.ID,
PageNumber: v.PageNumber,
VariantType: v.VariantType,
Width: v.Width,
Height: v.Height,
URL: h.urls.GenerateVariantURL(v.ID),
MimeType: v.MimeType,
}
}
api.Success(c, gin.H{
"id": file.ID,
"name": file.Name,
"id": resource.ID,
"name": resource.Name,
"url": downloadURL,
"thumbnailUrl": thumbURL,
"size": file.Size,
"mimeType": file.MimeType,
"size": resource.Size,
"mimeType": resource.MimeType,
"tags": tags,
"createdAt": file.CreatedAt,
"updatedAt": file.UpdatedAt,
"ocrText": file.OcrText,
"isFolder": file.IsFolder,
"parentFileID": file.ParentFileID,
"thumbnails": thumbnails,
"createdAt": resource.CreatedAt,
"updatedAt": resource.UpdatedAt,
"ocrText": resource.OcrText,
"isFolder": resource.IsFolder,
"parentResourceId": resource.ParentResourceID,
"ownerId": resource.OwnerID,
"variants": variants,
})
}
func (h *FileHandler) Delete(c *gin.Context) {
func (h *ResourceHandler) Delete(c *gin.Context) {
id := c.Param("id")
storagePath, _ := h.files.GetStoragePath(id)
thumbnails, _ := h.files.GetThumbnailsByFileID(id)
storagePath, _ := h.resources.GetStoragePath(id)
variants, _ := h.resources.GetVariantsByResourceID(id)
if err := h.files.Delete(id); err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete file")
if err := h.resources.Delete(id); err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete resource")
return
}
if storagePath != "" {
os.Remove(path.Clean(storagePath))
}
for _, t := range thumbnails {
os.Remove(path.Clean(t.StorageKey))
for _, v := range variants {
os.Remove(path.Clean(v.StorageKey))
}
api.Success(c, gin.H{"deleted": true})
}
func (h *FileHandler) AddTags(c *gin.Context) {
func (h *ResourceHandler) AddTags(c *gin.Context) {
id := c.Param("id")
var body struct {
Tags []string `json:"tags" binding:"required"`
TagType string `json:"tag_type"`
Tags []string `json:"tags" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain a 'tags' array")
return
}
tagType := body.TagType
if tagType == "" {
tagType = "none"
}
if err := h.files.AddTags(id, body.Tags, tagType); err != nil {
if err := h.resources.AddTags(id, body.Tags); err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to add tags")
return
}
tags, err := h.files.GetTagsByFileID(id)
tags, err := h.resources.GetTagsByResourceID(id)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch tags")
return
@@ -269,9 +266,9 @@ func (h *FileHandler) AddTags(c *gin.Context) {
api.Success(c, tags)
}
func (h *FileHandler) GetTags(c *gin.Context) {
func (h *ResourceHandler) GetTags(c *gin.Context) {
id := c.Param("id")
tags, err := h.files.GetTagsByFileID(id)
tags, err := h.resources.GetTagsByResourceID(id)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch tags")
return
@@ -279,26 +276,27 @@ func (h *FileHandler) GetTags(c *gin.Context) {
api.Success(c, tags)
}
func (h *FileHandler) MoveFiles(c *gin.Context) {
func (h *ResourceHandler) MoveResources(c *gin.Context) {
var body struct {
FileIDs []string `json:"file_ids" binding:"required"`
ParentFileID *string `json:"parent_file_id"`
ResourceIDs []string `json:"resource_ids" binding:"required"`
ParentResourceID *string `json:"parent_resource_id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain 'file_ids' array")
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain 'resource_ids' array")
return
}
if err := h.files.MoveFiles(body.FileIDs, body.ParentFileID); err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to move files")
if err := h.resources.MoveResources(body.ResourceIDs, body.ParentResourceID); err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to move resources")
return
}
api.Success(c, gin.H{"moved": len(body.FileIDs)})
api.Success(c, gin.H{"moved": len(body.ResourceIDs)})
}
func (h *FileHandler) ListFolders(c *gin.Context) {
folders, err := h.files.ListFolders()
func (h *ResourceHandler) ListFolders(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
folders, err := h.resources.ListFolders(userID)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list folders")
return
@@ -306,7 +304,9 @@ func (h *FileHandler) ListFolders(c *gin.Context) {
api.Success(c, folders)
}
func (h *FileHandler) CreateFolder(c *gin.Context) {
func (h *ResourceHandler) CreateFolder(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
var body struct {
Name string `json:"name" binding:"required"`
}
@@ -315,7 +315,7 @@ func (h *FileHandler) CreateFolder(c *gin.Context) {
return
}
folder, err := h.files.CreateFolder(body.Name)
folder, err := h.resources.CreateFolder(body.Name, userID)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to create folder")
return
@@ -324,23 +324,23 @@ func (h *FileHandler) CreateFolder(c *gin.Context) {
api.Success(c, folder)
}
func (h *FileHandler) ListFilesByParent(c *gin.Context) {
func (h *ResourceHandler) ListByParent(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
parentID := c.Param("id")
thumbnailQuality := c.Query("thumbnail")
files, err := h.files.ListFilesByParentID(parentID)
resources, err := h.resources.ListResourcesByParentID(parentID, userID)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list files in folder")
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list resources in folder")
return
}
type tagResponse struct {
ID string `json:"id"`
TagName string `json:"tag_name"`
TagType string `json:"tag_type"`
}
type fileResponse struct {
type resourceResponse struct {
ID string `json:"id"`
URL string `json:"url"`
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
@@ -350,84 +350,83 @@ func (h *FileHandler) ListFilesByParent(c *gin.Context) {
CreatedAt string `json:"createdAt"`
MimeType string `json:"mimeType"`
OcrText string `json:"ocrText,omitempty"`
ParentFileID string `json:"parentFileID,omitempty"`
ParentID string `json:"parentResourceId,omitempty"`
IsFolder bool `json:"isFolder"`
UpdatedAt string `json:"updatedAt"`
}
resp := make([]fileResponse, len(files))
for i, f := range files {
resp := make([]resourceResponse, len(resources))
for i, r := range resources {
tags := []tagResponse{}
for _, tag := range f.Tags {
for _, tag := range r.Tags {
tags = append(tags, tagResponse{
ID: tag.ID,
TagName: tag.Name,
TagType: tag.TagType,
})
}
downloadURL := h.urls.GenerateDownloadURL(f.ID)
downloadURL := h.urls.GenerateDownloadURL(r.ID)
thumbURL := downloadURL
if thumbnailQuality != "" {
if best := h.files.GetBestThumbnail(f.ID, thumbnailQuality); best != nil {
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
if best := h.resources.GetBestVariant(r.ID, thumbnailQuality); best != nil {
thumbURL = h.urls.GenerateVariantURL(best.ID)
}
}
resp[i] = fileResponse{
ID: f.ID,
resp[i] = resourceResponse{
ID: r.ID,
URL: downloadURL,
ThumbnailURL: thumbURL,
Name: f.Name,
Size: f.Size,
Name: r.Name,
Size: r.Size,
Tags: tags,
CreatedAt: f.CreatedAt,
ParentFileID: f.ParentFileID,
OcrText: f.OcrText,
IsFolder: f.IsFolder,
UpdatedAt: f.UpdatedAt,
MimeType: f.MimeType,
CreatedAt: r.CreatedAt.String(),
ParentID: r.ParentResourceID,
OcrText: r.OcrText,
IsFolder: r.IsFolder,
UpdatedAt: r.UpdatedAt.String(),
MimeType: r.MimeType,
}
}
api.Success(c, resp)
}
func (h *FileHandler) GetThumbnails(c *gin.Context) {
func (h *ResourceHandler) GetVariants(c *gin.Context) {
id := c.Param("id")
thumbnails, err := h.files.GetThumbnailsByFileID(id)
variants, err := h.resources.GetVariantsByResourceID(id)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch thumbnails")
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch variants")
return
}
type thumbnailResponse struct {
ID string `json:"id"`
PageNumber int `json:"pageNumber"`
ResolutionLabel string `json:"resolutionLabel"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
MimeType string `json:"mimeType"`
type variantResponse struct {
ID string `json:"id"`
PageNumber int `json:"pageNumber"`
VariantType string `json:"variantType"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
MimeType string `json:"mimeType"`
}
resp := make([]thumbnailResponse, len(thumbnails))
for i, t := range thumbnails {
resp[i] = thumbnailResponse{
ID: t.ID,
PageNumber: t.PageNumber,
ResolutionLabel: t.ResolutionLabel,
Width: t.Width,
Height: t.Height,
URL: h.urls.GenerateThumbnailURL(t.ID),
MimeType: t.MimeType,
resp := make([]variantResponse, len(variants))
for i, v := range variants {
resp[i] = variantResponse{
ID: v.ID,
PageNumber: v.PageNumber,
VariantType: v.VariantType,
Width: v.Width,
Height: v.Height,
URL: h.urls.GenerateVariantURL(v.ID),
MimeType: v.MimeType,
}
}
api.Success(c, resp)
}
func (h *FileHandler) ServeThumbnail(c *gin.Context) {
func (h *ResourceHandler) ServeVariant(c *gin.Context) {
id := c.Param("id")
exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64)
sig := c.Query("sig")
@@ -437,16 +436,16 @@ func (h *FileHandler) ServeThumbnail(c *gin.Context) {
return
}
storagePath, err := h.files.GetThumbnailStoragePath(id)
storagePath, err := h.resources.GetVariantStoragePath(id)
if err != nil {
api.Error(c, http.StatusNotFound, "THUMBNAIL_NOT_FOUND", "Thumbnail not found")
api.Error(c, http.StatusNotFound, "VARIANT_NOT_FOUND", "Variant not found")
return
}
c.File(path.Clean(storagePath))
}
func (h *FileHandler) CheckDuplicates(c *gin.Context) {
func (h *ResourceHandler) CheckDuplicates(c *gin.Context) {
var body struct {
Name string `json:"name" binding:"required"`
Size int64 `json:"size" binding:"required"`
@@ -457,7 +456,7 @@ func (h *FileHandler) CheckDuplicates(c *gin.Context) {
return
}
duplicates, err := h.files.FindDuplicatesByNameSize(body.Name, body.Size)
duplicates, err := h.resources.FindDuplicatesByNameSize(body.Name, body.Size)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to check duplicates")
return
@@ -475,7 +474,7 @@ func (h *FileHandler) CheckDuplicates(c *gin.Context) {
resp := make([]dupResponse, len(duplicates))
for i, d := range duplicates {
resp[i] = dupResponse{
ID: d.ID,
ID: d.ID.String(),
Name: d.Name,
MimeType: d.MimeType,
Size: d.Size,
+51 -15
View File
@@ -14,29 +14,65 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) {
api.POST("/auth/login", h.Auth.Login)
api.POST("/auth/refresh", h.Auth.Refresh)
api.POST("/auth/logout", h.Auth.Logout)
api.GET("/files/download/:id", h.File.Download)
api.GET("/thumbnails/:id", h.File.ServeThumbnail)
api.GET("/resources/download/:id", h.Resource.Download)
api.GET("/variants/:id", h.Resource.ServeVariant)
// Protected
protected := api.Group("")
protected.Use(authMiddleware.RequireAuth())
protected.GET("/files", h.File.List)
protected.POST("/files/upload", h.File.Upload)
protected.POST("/files/move", h.File.MoveFiles)
protected.POST("/files/folders", h.File.CreateFolder)
protected.GET("/files/folders", h.File.ListFolders)
protected.GET("/files/folders/:id/files", h.File.ListFilesByParent)
protected.DELETE("/files/:id", h.File.Delete)
protected.GET("/files/:id", h.File.Get)
// Resources (replaces /files)
protected.GET("/resources", h.Resource.List)
protected.POST("/resources/upload", h.Resource.Upload)
protected.POST("/resources/move", h.Resource.MoveResources)
protected.POST("/resources/folders", h.Resource.CreateFolder)
protected.GET("/resources/folders", h.Resource.ListFolders)
protected.GET("/resources/folders/:id/resources", h.Resource.ListByParent)
protected.DELETE("/resources/:id", h.Resource.Delete)
protected.GET("/resources/:id", h.Resource.Get)
protected.POST("/files/:id/tags", h.File.AddTags)
protected.GET("/files/:id/tags", h.File.GetTags)
// Tags on resources
protected.POST("/resources/:id/tags", h.Resource.AddTags)
protected.GET("/resources/:id/tags", h.Resource.GetTags)
// Variants
protected.GET("/resources/:id/variants", h.Resource.GetVariants)
// Dedup check
protected.POST("/resources/dedup-check", h.Resource.CheckDuplicates)
// Sharing (ReBAC)
protected.POST("/resources/:id/share", h.Share.Grant)
protected.DELETE("/resources/:id/share/:userId", h.Share.Revoke)
protected.GET("/resources/:id/share", h.Share.List)
protected.GET("/resources/:id/access", h.Share.Check)
// Device management
protected.GET("/devices", h.Device.List)
protected.POST("/devices", h.Device.Register)
// Placements
protected.GET("/resources/:id/placements", h.Resource.GetVariants)
// Sync
protected.POST("/sync/pull", h.Sync.Pull)
protected.POST("/sync/push", h.Sync.Push)
// OCR
protected.POST("/ocr/jobs", h.OCR.CreateJob)
protected.GET("/ocr/jobs/:id", h.OCR.GetJobStatus)
protected.GET("/files/:id/thumbnails", h.File.GetThumbnails)
protected.POST("/files/dedup-check", h.File.CheckDuplicates)
// Legacy /files/* endpoints (maintain backward compatibility)
protected.GET("/files", h.Resource.List)
protected.POST("/files/upload", h.Resource.Upload)
protected.POST("/files/move", h.Resource.MoveResources)
protected.POST("/files/folders", h.Resource.CreateFolder)
protected.GET("/files/folders", h.Resource.ListFolders)
protected.GET("/files/folders/:id/files", h.Resource.ListByParent)
protected.DELETE("/files/:id", h.Resource.Delete)
protected.GET("/files/:id", h.Resource.Get)
protected.POST("/files/:id/tags", h.Resource.AddTags)
protected.GET("/files/:id/tags", h.Resource.GetTags)
protected.GET("/files/:id/thumbnails", h.Resource.GetVariants)
protected.POST("/files/dedup-check", h.Resource.CheckDuplicates)
}
+89
View File
@@ -0,0 +1,89 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/auth"
"github.com/vaultdrop/backend/internal/service"
"github.com/vaultdrop/backend/pkg/api"
)
type ShareHandler struct {
rebac *service.RebacService
}
func (h *ShareHandler) Grant(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
resourceID := c.Param("id")
var body struct {
SubjectUserID string `json:"subject_user_id" binding:"required"`
Role string `json:"role" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "subject_user_id and role are required")
return
}
if err := h.rebac.GrantRole(userID, resourceID, body.SubjectUserID, body.Role); err != nil {
api.Error(c, http.StatusForbidden, "FORBIDDEN", err.Error())
return
}
api.Success(c, gin.H{"granted": true})
}
func (h *ShareHandler) Revoke(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
resourceID := c.Param("id")
subjectID := c.Param("userId")
if err := h.rebac.RevokeRole(userID, resourceID, subjectID); err != nil {
api.Error(c, http.StatusForbidden, "FORBIDDEN", err.Error())
return
}
api.Success(c, gin.H{"revoked": true})
}
func (h *ShareHandler) List(c *gin.Context) {
resourceID := c.Param("id")
relations, err := h.rebac.ListShares(resourceID)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list shares")
return
}
type shareResponse struct {
UserID string `json:"user_id"`
Role string `json:"role"`
}
resp := make([]shareResponse, len(relations))
for i, r := range relations {
resp[i] = shareResponse{
UserID: r.SubjectUserID.String(),
Role: r.Role,
}
}
api.Success(c, resp)
}
func (h *ShareHandler) Check(c *gin.Context) {
userID := c.GetString(auth.UserIDKey)
resourceID := c.Param("id")
role, err := h.rebac.ResolveEffectiveRole(userID, resourceID)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to resolve role")
return
}
api.Success(c, gin.H{
"role": role,
"access": role != "",
})
}
+56
View File
@@ -0,0 +1,56 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/service"
"github.com/vaultdrop/backend/pkg/api"
)
type SyncHandler struct {
sync *service.SyncService
}
func (h *SyncHandler) Pull(c *gin.Context) {
var body struct {
LocationID string `json:"location_id"`
}
c.ShouldBindJSON(&body)
var err error
var items interface{}
if body.LocationID != "" {
items, err = h.sync.ListPending(body.LocationID)
} else {
items, err = h.sync.ListAllPending()
}
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list pending sync items")
return
}
api.Success(c, items)
}
func (h *SyncHandler) Push(c *gin.Context) {
var body struct {
LocationID string `json:"location_id" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "location_id is required")
return
}
items, err := h.sync.ListPending(body.LocationID)
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list pending sync items")
return
}
api.Created(c, gin.H{
"pending": len(items),
"message": "Push initiated",
})
}