diff --git a/apps/backend/internal/catalog/directory.go b/apps/backend/internal/catalog/directory.go index 1578940..b2bab23 100644 --- a/apps/backend/internal/catalog/directory.go +++ b/apps/backend/internal/catalog/directory.go @@ -147,7 +147,7 @@ func includeParam(c *fiber.Ctx) []string { // @Accept json // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param request body createDirectoryRequest true "Directory details" // @Param include query string false "Include additional fields" Enums(path) // @Success 200 {object} DirectoryInfo "Created directory" @@ -230,7 +230,7 @@ func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error { // @Tags directories // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param directoryID path string true "Directory ID" // @Param include query string false "Include additional fields" Enums(path) // @Success 200 {object} DirectoryInfo "Directory metadata" @@ -274,7 +274,7 @@ func (h *HTTPHandler) fetchDirectory(c *fiber.Ctx) error { // @Tags directories // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param directoryID path string true "Directory ID (use 'root' for the root directory)" // @Param orderBy query string false "Sort field: name, createdAt, or updatedAt" Enums(name,createdAt,updatedAt) // @Param dir query string false "Sort direction: asc or desc" Enums(asc,desc) @@ -405,7 +405,7 @@ func (h *HTTPHandler) listDirectory(c *fiber.Ctx) error { // @Accept json // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param directoryID path string true "Directory ID" // @Param request body patchDirectoryRequest true "Directory update" // @Success 200 {object} DirectoryInfo "Updated directory metadata" @@ -464,7 +464,7 @@ func (h *HTTPHandler) patchDirectory(c *fiber.Ctx) error { // @Description Delete a directory permanently or move it to trash. Deleting a directory also affects all its contents. // @Tags directories // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param directoryID path string true "Directory ID" // @Param trash query bool false "Move to trash instead of permanent delete" default(false) // @Success 200 {object} DirectoryInfo "Trashed directory info (when trash=true)" @@ -524,7 +524,7 @@ func (h *HTTPHandler) deleteDirectory(c *fiber.Ctx) error { // @Description Delete multiple directories permanently or move them to trash. Deleting directories also affects all their contents. All items must be directories. // @Tags directories // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param id query string true "Comma-separated list of directory IDs to delete" example:"kRp2XYTq9A55,xYz123AbC456" // @Param trash query bool false "Move to trash instead of permanent delete" default(false) // @Success 200 {array} DirectoryInfo "Trashed directories (when trash=true)" @@ -619,7 +619,7 @@ func (h *HTTPHandler) deleteDirectories(c *fiber.Ctx) error { // @Accept json // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param directoryID path string true "Target directory ID" // @Param request body postDirectoryContentRequest true "Items to move" // @Success 200 {object} moveItemsToDirectoryResponse "Move operation results with moved, conflict, and error states" @@ -769,7 +769,7 @@ func decodeListChildrenCursor(s string) (*decodedListChildrenCursor, error) { // @Description Get all share links that include this directory // @Tags directories // @Produce json -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param directoryID path string true "Directory ID" // @Success 200 {array} sharing.Share "Array of shares" // @Failure 401 {string} string "Not authenticated" diff --git a/apps/backend/internal/catalog/file.go b/apps/backend/internal/catalog/file.go index cd9787f..38bdb2c 100644 --- a/apps/backend/internal/catalog/file.go +++ b/apps/backend/internal/catalog/file.go @@ -64,7 +64,7 @@ func (h *HTTPHandler) currentFileMiddleware(c *fiber.Ctx) error { // @Tags files // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param fileID path string true "File ID" // @Success 200 {object} FileInfo "File metadata" // @Failure 401 {string} string "Not authenticated" @@ -91,7 +91,7 @@ func (h *HTTPHandler) fetchFile(c *fiber.Ctx) error { // @Tags files // @Produce application/octet-stream // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param fileID path string true "File ID" // @Success 200 {file} binary "File content stream" // @Success 307 {string} string "Redirect to download URL" @@ -140,7 +140,7 @@ func (h *HTTPHandler) downloadFile(c *fiber.Ctx) error { // @Accept json // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param fileID path string true "File ID" // @Param request body patchFileRequest true "File update" // @Success 200 {object} FileInfo "Updated file metadata" @@ -201,7 +201,7 @@ func (h *HTTPHandler) patchFile(c *fiber.Ctx) error { // @Tags files // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param fileID path string true "File ID" // @Param trash query bool false "Move to trash instead of permanent delete" default(false) // @Success 200 {object} FileInfo "Trashed file info (when trash=true)" @@ -264,7 +264,7 @@ func (h *HTTPHandler) deleteFile(c *fiber.Ctx) error { // @Description Delete multiple files permanently or move them to trash. All items must be files. // @Tags files // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param id query string true "Comma-separated list of file IDs to delete" example:"mElnUNCm8F22,kRp2XYTq9A55" // @Param trash query bool false "Move to trash instead of permanent delete" default(false) // @Success 200 {array} FileInfo "Trashed files (when trash=true)" @@ -352,7 +352,7 @@ func (h *HTTPHandler) deleteFiles(c *fiber.Ctx) error { // @Description Get all share links that include this file // @Tags files // @Produce json -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param fileID path string true "File ID" // @Success 200 {array} sharing.Share "Array of shares" // @Failure 401 {string} string "Not authenticated" diff --git a/apps/backend/internal/drexa/api_integration_test.go b/apps/backend/internal/drexa/api_integration_test.go index cd8d3c1..3203c19 100644 --- a/apps/backend/internal/drexa/api_integration_test.go +++ b/apps/backend/internal/drexa/api_integration_test.go @@ -164,6 +164,20 @@ func TestRegistrationFlow(t *testing.T) { } }) + t.Run("drive info", func(t *testing.T) { + var info struct { + ID string `json:"id"` + RootDirID string `json:"rootDirId"` + } + doJSON(t, s.App(), http.MethodGet, fmt.Sprintf("/api/my/drives/%s", reg.Drive.ID), reg.AccessToken, nil, http.StatusOK, &info) + if info.ID != reg.Drive.ID { + t.Fatalf("unexpected drive id: got %q want %q", info.ID, reg.Drive.ID) + } + if info.RootDirID == "" { + t.Fatalf("expected rootDirId to be set") + } + }) + t.Run("users/me", func(t *testing.T) { var me struct { ID string `json:"id"` diff --git a/apps/backend/internal/drive/drive.go b/apps/backend/internal/drive/drive.go index 1467673..54766f2 100644 --- a/apps/backend/internal/drive/drive.go +++ b/apps/backend/internal/drive/drive.go @@ -10,12 +10,12 @@ import ( type Drive struct { bun.BaseModel `bun:"drives" swaggerignore:"true"` - ID uuid.UUID `bun:",pk,type:uuid" json:"id"` - PublicID string `bun:"public_id,notnull" json:"publicId"` - OrgID uuid.UUID `bun:"org_id,notnull,type:uuid" json:"orgId"` - Name string `bun:"name,notnull" json:"name"` + ID uuid.UUID `bun:",pk,type:uuid" json:"-"` + PublicID string `bun:"public_id,notnull" json:"id"` + OrgID uuid.UUID `bun:"org_id,notnull,type:uuid" json:"-"` + Name string `bun:"name,notnull" json:"name"` - OwnerAccountID *uuid.UUID `bun:"owner_account_id,type:uuid" json:"ownerAccountId,omitempty"` + OwnerAccountID *uuid.UUID `bun:"owner_account_id,type:uuid" json:"-"` StorageUsageBytes int64 `bun:"storage_usage_bytes,notnull" json:"storageUsageBytes"` StorageQuotaBytes int64 `bun:"storage_quota_bytes,notnull" json:"storageQuotaBytes"` diff --git a/apps/backend/internal/drive/http.go b/apps/backend/internal/drive/http.go index 2be0a82..1296645 100644 --- a/apps/backend/internal/drive/http.go +++ b/apps/backend/internal/drive/http.go @@ -10,10 +10,14 @@ import ( "github.com/get-drexa/drexa/internal/user" "github.com/get-drexa/drexa/internal/virtualfs" "github.com/gofiber/fiber/v2" - "github.com/google/uuid" "github.com/uptrace/bun" ) +type driveInfoResponse struct { + *Drive + RootDirID string `json:"rootDirId"` +} + type HTTPHandler struct { driveService *Service accountService *account.Service @@ -39,7 +43,7 @@ func (h *HTTPHandler) RegisterRoutes(api fiber.Router) *virtualfs.ScopedRouter { drive.Use(h.authMiddleware) drive.Use(h.driveMiddleware) - drive.Get("/", h.getDrive) + drive.Get("/", h.getDriveInfo) return &virtualfs.ScopedRouter{Router: drive} } @@ -69,12 +73,34 @@ func (h *HTTPHandler) listDrives(c *fiber.Ctx) error { return c.JSON(drives) } -func (h *HTTPHandler) getDrive(c *fiber.Ctx) error { +func (h *HTTPHandler) getDriveInfo(c *fiber.Ctx) error { drive, ok := reqctx.CurrentDrive(c).(*Drive) if !ok || drive == nil { return c.SendStatus(fiber.StatusNotFound) } - return c.JSON(drive) + + rootDirID, _ := c.Locals("rootDirId").(string) + if rootDirID == "" { + scopeAny := reqctx.VFSAccessScope(c) + scope, ok := scopeAny.(*virtualfs.Scope) + if !ok || scope == nil { + return c.SendStatus(fiber.StatusUnauthorized) + } + + root, err := h.vfs.FindNode(c.Context(), h.db, scope.RootNodeID.String(), scope) + if err != nil { + if errors.Is(err, virtualfs.ErrNodeNotFound) || errors.Is(err, virtualfs.ErrAccessDenied) { + return c.SendStatus(fiber.StatusNotFound) + } + return httperr.Internal(err) + } + rootDirID = root.PublicID + } + + return c.JSON(driveInfoResponse{ + Drive: drive, + RootDirID: rootDirID, + }) } func (h *HTTPHandler) driveMiddleware(c *fiber.Ctx) error { @@ -83,12 +109,8 @@ func (h *HTTPHandler) driveMiddleware(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNotFound) } - driveID, err := uuid.Parse(c.Params("driveID")) - if err != nil { - return c.SendStatus(fiber.StatusNotFound) - } - - drive, err := h.driveService.DriveByID(c.Context(), h.db, driveID) + driveID := c.Params("driveID") + drive, err := h.driveService.DriveByPublicID(c.Context(), h.db, driveID) if err != nil { if errors.Is(err, ErrDriveNotFound) { return c.SendStatus(fiber.StatusNotFound) @@ -113,6 +135,8 @@ func (h *HTTPHandler) driveMiddleware(c *fiber.Ctx) error { return httperr.Internal(err) } + c.Locals("rootDirId", root.PublicID) + scope := &virtualfs.Scope{ DriveID: drive.ID, RootNodeID: root.ID, diff --git a/apps/backend/internal/drive/service.go b/apps/backend/internal/drive/service.go index d256738..bd75fe3 100644 --- a/apps/backend/internal/drive/service.go +++ b/apps/backend/internal/drive/service.go @@ -65,6 +65,18 @@ func (s *Service) DriveByID(ctx context.Context, db bun.IDB, id uuid.UUID) (*Dri return &drive, nil } +func (s *Service) DriveByPublicID(ctx context.Context, db bun.IDB, publicID string) (*Drive, error) { + var drive Drive + err := db.NewSelect().Model(&drive).Where("public_id = ?", publicID).Scan(ctx) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrDriveNotFound + } + return nil, err + } + return &drive, nil +} + // ListAccessibleDrives returns drives a principal account can access: // - personal drives: owner_account_id = account.ID // - shared drives: owner_account_id IS NULL (future) diff --git a/apps/backend/internal/sharing/http.go b/apps/backend/internal/sharing/http.go index f51b524..21c4436 100644 --- a/apps/backend/internal/sharing/http.go +++ b/apps/backend/internal/sharing/http.go @@ -130,7 +130,7 @@ func (h *HTTPHandler) shareMiddleware(c *fiber.Ctx) error { // @Tags shares // @Accept json // @Produce json -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param shareID path string true "Share ID" // @Success 200 {object} Share "Share details" // @Failure 401 {string} string "Not authenticated" @@ -161,7 +161,7 @@ func (h *HTTPHandler) getShare(c *fiber.Ctx) error { // @Tags shares // @Accept json // @Produce json -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param request body createShareRequest true "Share details" // @Success 200 {object} Share "Created share" // @Failure 400 {object} map[string]string "Invalid request, items not in same directory, or root directory cannot be shared" @@ -244,7 +244,7 @@ func (h *HTTPHandler) createShare(c *fiber.Ctx) error { // @Tags shares // @Accept json // @Produce json -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param shareID path string true "Share ID" // @Param request body patchShareRequest true "Share details" // @Success 200 {object} Share "Updated share" @@ -313,7 +313,7 @@ func (h *HTTPHandler) updateShare(c *fiber.Ctx) error { // @Summary Delete share // @Description Delete a share link, revoking access for all users // @Tags shares -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param shareID path string true "Share ID" // @Success 204 {string} string "Share deleted" // @Failure 401 {string} string "Not authenticated" diff --git a/apps/backend/internal/upload/http.go b/apps/backend/internal/upload/http.go index c794d5b..3eee886 100644 --- a/apps/backend/internal/upload/http.go +++ b/apps/backend/internal/upload/http.go @@ -51,7 +51,7 @@ func (h *HTTPHandler) RegisterRoutes(api *virtualfs.ScopedRouter) { // @Accept json // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param request body createUploadRequest true "Upload details" // @Success 200 {object} Upload "Upload session created" // @Failure 400 {object} map[string]string "Parent is not a directory" @@ -107,7 +107,7 @@ func (h *HTTPHandler) Create(c *fiber.Ctx) error { // @Tags uploads // @Accept application/octet-stream // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param uploadID path string true "Upload session ID" // @Param file body []byte true "File content (binary)" // @Success 204 {string} string "Content received successfully" @@ -148,7 +148,7 @@ func (h *HTTPHandler) ReceiveContent(c *fiber.Ctx) error { // @Accept json // @Produce json // @Security BearerAuth -// @Param driveID path string true "Drive ID" format(uuid) +// @Param driveID path string true "Drive ID" example:"kRp2XYTq9A55" // @Param uploadID path string true "Upload session ID" // @Param request body updateUploadRequest true "Status update" // @Success 200 {object} Upload "Upload completed" diff --git a/apps/backend/internal/upload/upload.go b/apps/backend/internal/upload/upload.go index c7af176..4ef74f5 100644 --- a/apps/backend/internal/upload/upload.go +++ b/apps/backend/internal/upload/upload.go @@ -25,5 +25,5 @@ type Upload struct { // Internal target node reference TargetNode *virtualfs.Node `json:"-" swaggerignore:"true"` // URL to upload file content to - UploadURL string `json:"uploadUrl" example:"https://api.example.com/api/drives/550e8400-e29b-41d4-a716-446655440000/uploads/xNq5RVBt3K88/content"` + UploadURL string `json:"uploadUrl" example:"https://api.example.com/api/drives/kRp2XYTq9A55/uploads/xNq5RVBt3K88/content"` }