diff --git a/apps/backend/Makefile b/apps/backend/Makefile new file mode 100644 index 0000000..b6b4139 --- /dev/null +++ b/apps/backend/Makefile @@ -0,0 +1,41 @@ +.PHONY: build build-docs build-all run run-docs docs install-tools fmt clean + +# Build the API server +build: + go build -o bin/drexa ./cmd/drexa + +# Build the documentation server +build-docs: + go build -o bin/drexa-docs ./cmd/docs + +# Build all binaries +build-all: build build-docs + +# Run the API server +run: + go run ./cmd/drexa --config config.yaml + +# Run the documentation server +run-docs: + go run ./cmd/docs --port 8081 --api-url http://localhost:8080 + +# Generate API documentation +docs: + @echo "Generating OpenAPI documentation..." + swag init -g cmd/drexa/main.go -o docs --parseDependency --parseInternal --outputTypes go,json,yaml + @echo "Documentation generated in docs/" + @echo "Run 'make run-docs' to start the documentation server" + +# Install development tools +install-tools: + go install github.com/swaggo/swag/cmd/swag@latest + +# Format and lint +fmt: + go fmt ./... + swag fmt + +# Clean build artifacts +clean: + rm -rf bin/ + rm -f docs/swagger.json docs/swagger.yaml diff --git a/apps/backend/cmd/docs/main.go b/apps/backend/cmd/docs/main.go new file mode 100644 index 0000000..781baed --- /dev/null +++ b/apps/backend/cmd/docs/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + _ "github.com/get-drexa/drexa/docs" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/swaggo/swag" +) + +func main() { + port := flag.Int("port", 8081, "port to listen on") + apiURL := flag.String("api-url", "http://localhost:8080", "base URL of the API server") + flag.Parse() + + app := fiber.New(fiber.Config{ + AppName: "Drexa API Documentation", + }) + + app.Use(logger.New()) + app.Use(cors.New()) + + // Serve Scalar UI + app.Get("/", func(c *fiber.Ctx) error { + html := fmt.Sprintf(` + + + Drexa API Documentation + + + + + + + + +`, *apiURL) + c.Set("Content-Type", "text/html; charset=utf-8") + return c.SendString(html) + }) + + // Serve OpenAPI spec + app.Get("/openapi.json", func(c *fiber.Ctx) error { + c.Set("Content-Type", "application/json") + c.Set("Access-Control-Allow-Origin", "*") + doc, err := swag.ReadDoc() + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + return c.SendString(doc) + }) + + // Health check + app.Get("/health", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{"status": "ok"}) + }) + + fmt.Fprintf(os.Stderr, "📚 Drexa API Documentation server starting on http://localhost:%d\n", *port) + fmt.Fprintf(os.Stderr, " API server configured at: %s\n", *apiURL) + + log.Fatal(app.Listen(fmt.Sprintf(":%d", *port))) +} + diff --git a/apps/backend/cmd/drexa/main.go b/apps/backend/cmd/drexa/main.go index 88b528a..c22a5cc 100644 --- a/apps/backend/cmd/drexa/main.go +++ b/apps/backend/cmd/drexa/main.go @@ -6,9 +6,28 @@ import ( "log" "os" + _ "github.com/get-drexa/drexa/docs" "github.com/get-drexa/drexa/internal/drexa" ) +// @title Drexa API +// @version 1.0 +// @description Drexa is a file storage and management API. It provides endpoints for authentication, user management, file uploads, and virtual filesystem operations. + +// @contact.name Drexa Support +// @contact.url https://github.com/get-drexa/drexa + +// @license.name MIT +// @license.url https://opensource.org/licenses/MIT + +// @host localhost:8080 +// @BasePath /api + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description JWT access token. Format: "Bearer {token}" + func main() { configPath := flag.String("config", "", "path to config file (required)") flag.Parse() diff --git a/apps/backend/docs/docs.go b/apps/backend/docs/docs.go new file mode 100644 index 0000000..af4f05f --- /dev/null +++ b/apps/backend/docs/docs.go @@ -0,0 +1,1601 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Drexa Support", + "url": "https://github.com/get-drexa/drexa" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/accounts": { + "post": { + "description": "Create a new user account with email and password. Returns the account, user, and authentication tokens.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Register new account", + "parameters": [ + { + "description": "Registration details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_account.registerAccountRequest" + } + } + ], + "responses": { + "200": { + "description": "Account created successfully", + "schema": { + "$ref": "#/definitions/internal_account.registerAccountResponse" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "type": "string" + } + }, + "409": { + "description": "Email already registered", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve account details including storage usage and quota", + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Get account", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Account details", + "schema": { + "$ref": "#/definitions/internal_account.Account" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Account not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/directories": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new directory within a parent directory", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "Create directory", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "description": "Directory details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_catalog.createDirectoryRequest" + } + }, + { + "enum": [ + "path" + ], + "type": "string", + "description": "Include additional fields", + "name": "include", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Created directory", + "schema": { + "$ref": "#/definitions/internal_catalog.DirectoryInfo" + } + }, + "400": { + "description": "Parent not found or not a directory", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "409": { + "description": "Directory already exists", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/accounts/{accountID}/directories/{directoryID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve metadata for a specific directory", + "produces": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "Get directory info", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Directory ID", + "name": "directoryID", + "in": "path", + "required": true + }, + { + "enum": [ + "path" + ], + "type": "string", + "description": "Include additional fields", + "name": "include", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Directory metadata", + "schema": { + "$ref": "#/definitions/internal_catalog.DirectoryInfo" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Directory not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a directory permanently or move it to trash. Deleting a directory also affects all its contents.", + "tags": [ + "directories" + ], + "summary": "Delete directory", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Directory ID", + "name": "directoryID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Move to trash instead of permanent delete", + "name": "trash", + "in": "query" + } + ], + "responses": { + "204": { + "description": "Directory deleted", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Directory not found", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update directory properties such as name (rename)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "Update directory", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Directory ID", + "name": "directoryID", + "in": "path", + "required": true + }, + { + "description": "Directory update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_catalog.patchDirectoryRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated directory metadata", + "schema": { + "$ref": "#/definitions/internal_catalog.DirectoryInfo" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Directory not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/directories/{directoryID}/content": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all files and subdirectories within a directory", + "produces": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "List directory contents", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Directory ID", + "name": "directoryID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Array of FileInfo and DirectoryInfo objects", + "schema": { + "type": "array", + "items": {} + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Directory not found", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Move one or more files or directories into this directory. All items must currently be in the same source directory.", + "consumes": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "Move items to directory", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Target directory ID", + "name": "directoryID", + "in": "path", + "required": true + }, + { + "description": "Items to move", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_catalog.postDirectoryContentRequest" + } + } + ], + "responses": { + "204": { + "description": "Items moved successfully", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid request or items not in same directory", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "One or more items not found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Name conflict in target directory", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/accounts/{accountID}/files/{fileID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve metadata for a specific file", + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Get file info", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File ID", + "name": "fileID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "File metadata", + "schema": { + "$ref": "#/definitions/internal_catalog.FileInfo" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a file permanently or move it to trash", + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Delete file", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File ID", + "name": "fileID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Move to trash instead of permanent delete", + "name": "trash", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Trashed file info (when trash=true)", + "schema": { + "$ref": "#/definitions/internal_catalog.FileInfo" + } + }, + "204": { + "description": "Permanently deleted (when trash=false)", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File not found", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update file properties such as name (rename)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Update file", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File ID", + "name": "fileID", + "in": "path", + "required": true + }, + { + "description": "File update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_catalog.patchFileRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated file metadata", + "schema": { + "$ref": "#/definitions/internal_catalog.FileInfo" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/files/{fileID}/content": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Download the file content. May redirect to a signed URL for external storage.", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "files" + ], + "summary": "Download file", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File ID", + "name": "fileID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "File content stream", + "schema": { + "type": "file" + } + }, + "307": { + "description": "Redirect to download URL", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/uploads": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Start a new file upload session. Returns an upload URL to PUT file content to.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "uploads" + ], + "summary": "Create upload session", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "description": "Upload details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_upload.createUploadRequest" + } + } + ], + "responses": { + "200": { + "description": "Upload session created", + "schema": { + "$ref": "#/definitions/internal_upload.Upload" + } + }, + "400": { + "description": "Parent is not a directory", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Parent directory not found", + "schema": { + "type": "string" + } + }, + "409": { + "description": "File with this name already exists", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/accounts/{accountID}/uploads/{uploadID}": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark an upload as completed after content has been uploaded. This finalizes the file in the filesystem.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "uploads" + ], + "summary": "Complete upload", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Upload session ID", + "name": "uploadID", + "in": "path", + "required": true + }, + { + "description": "Status update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_upload.updateUploadRequest" + } + } + ], + "responses": { + "200": { + "description": "Upload completed", + "schema": { + "$ref": "#/definitions/internal_upload.Upload" + } + }, + "400": { + "description": "Content not uploaded yet or invalid status", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Upload session not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/uploads/{uploadID}/content": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Stream file content to complete an upload. Send raw binary data in the request body.", + "consumes": [ + "application/octet-stream" + ], + "tags": [ + "uploads" + ], + "summary": "Upload file content", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Upload session ID", + "name": "uploadID", + "in": "path", + "required": true + }, + { + "description": "File content (binary)", + "name": "file", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "204": { + "description": "Content received successfully", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Upload session not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/login": { + "post": { + "description": "Authenticate with email and password to receive JWT tokens. Tokens can be delivered via HTTP-only cookies or in the response body based on the tokenDelivery field.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "User login", + "parameters": [ + { + "description": "Login credentials", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_auth.loginRequest" + } + } + ], + "responses": { + "200": { + "description": "Successful authentication", + "schema": { + "$ref": "#/definitions/internal_auth.loginResponse" + } + }, + "400": { + "description": "Invalid request body or token delivery method", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Invalid email or password", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/tokens": { + "post": { + "description": "Exchange a valid refresh token for a new pair of access and refresh tokens. The old refresh token is invalidated (rotation).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Refresh access token", + "parameters": [ + { + "description": "Refresh token", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_auth.refreshAccessTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "New tokens", + "schema": { + "$ref": "#/definitions/internal_auth.tokenResponse" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Invalid, expired, or reused refresh token", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/users/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve the authenticated user's profile information", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get current user", + "responses": { + "200": { + "description": "User profile", + "schema": { + "$ref": "#/definitions/internal_user.User" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "github_com_get-drexa_drexa_internal_user.User": { + "description": "User account information", + "type": "object", + "properties": { + "displayName": { + "description": "User's display name", + "type": "string", + "example": "John Doe" + }, + "email": { + "description": "User's email address", + "type": "string", + "example": "john@example.com" + }, + "id": { + "description": "Unique user identifier", + "type": "string", + "example": "550e8400-e29b-41d4-a716-446655440000" + } + } + }, + "github_com_get-drexa_drexa_internal_virtualfs.PathSegment": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "internal_account.Account": { + "description": "Storage account with usage and quota details", + "type": "object", + "properties": { + "createdAt": { + "description": "When the account was created (ISO 8601)", + "type": "string", + "example": "2024-12-13T15:04:05Z" + }, + "id": { + "description": "Unique account identifier", + "type": "string", + "example": "550e8400-e29b-41d4-a716-446655440000" + }, + "storageQuotaBytes": { + "description": "Maximum storage quota in bytes", + "type": "integer", + "example": 10737418240 + }, + "storageUsageBytes": { + "description": "Current storage usage in bytes", + "type": "integer", + "example": 1073741824 + }, + "updatedAt": { + "description": "When the account was last updated (ISO 8601)", + "type": "string", + "example": "2024-12-13T16:30:00Z" + }, + "userId": { + "description": "ID of the user who owns this account", + "type": "string", + "example": "550e8400-e29b-41d4-a716-446655440001" + } + } + }, + "internal_account.registerAccountRequest": { + "description": "Request to create a new account and user", + "type": "object", + "properties": { + "displayName": { + "description": "Display name for the user", + "type": "string", + "example": "Jane Doe" + }, + "email": { + "description": "Email address for the new account", + "type": "string", + "example": "newuser@example.com" + }, + "password": { + "description": "Password for the new account (min 8 characters)", + "type": "string", + "example": "securepassword123" + } + } + }, + "internal_account.registerAccountResponse": { + "description": "Response after successful account registration", + "type": "object", + "properties": { + "accessToken": { + "description": "JWT access token for immediate authentication", + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature" + }, + "account": { + "description": "The created account", + "allOf": [ + { + "$ref": "#/definitions/internal_account.Account" + } + ] + }, + "refreshToken": { + "description": "Base64 URL encoded refresh token", + "type": "string", + "example": "dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi" + }, + "user": { + "description": "The created user", + "allOf": [ + { + "$ref": "#/definitions/github_com_get-drexa_drexa_internal_user.User" + } + ] + } + } + }, + "internal_auth.loginRequest": { + "description": "Login request with email, password, and token delivery preference", + "type": "object", + "properties": { + "email": { + "description": "User's email address", + "type": "string", + "example": "user@example.com" + }, + "password": { + "description": "User's password", + "type": "string", + "example": "secretpassword123" + }, + "tokenDelivery": { + "description": "How to deliver tokens: \"cookie\" (set HTTP-only cookies) or \"body\" (include in response)", + "type": "string", + "enum": [ + "cookie", + "body" + ], + "example": "body" + } + } + }, + "internal_auth.loginResponse": { + "description": "Login response containing user info and optionally tokens", + "type": "object", + "properties": { + "accessToken": { + "description": "JWT access token (only included when tokenDelivery is \"body\")", + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature" + }, + "refreshToken": { + "description": "Base64 URL encoded refresh token (only included when tokenDelivery is \"body\")", + "type": "string", + "example": "dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi" + }, + "user": { + "description": "Authenticated user information", + "allOf": [ + { + "$ref": "#/definitions/github_com_get-drexa_drexa_internal_user.User" + } + ] + } + } + }, + "internal_auth.refreshAccessTokenRequest": { + "description": "Request to exchange a refresh token for new tokens", + "type": "object", + "properties": { + "refreshToken": { + "description": "Base64 URL encoded refresh token", + "type": "string", + "example": "dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi" + } + } + }, + "internal_auth.tokenResponse": { + "description": "Response containing new access token and refresh token", + "type": "object", + "properties": { + "accessToken": { + "description": "New JWT access token", + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature" + }, + "refreshToken": { + "description": "New base64 URL encoded refresh token", + "type": "string", + "example": "xK9mPqRsTuVwXyZ0AbCdEfGhIjKlMnOpQrStUvWxYz1234567890abcdefgh" + } + } + }, + "internal_catalog.DirectoryInfo": { + "description": "Directory information including path and timestamps", + "type": "object", + "properties": { + "createdAt": { + "description": "When the directory was created (ISO 8601)", + "type": "string", + "example": "2024-12-13T15:04:05Z" + }, + "deletedAt": { + "description": "When the directory was trashed, null if not trashed (ISO 8601)", + "type": "string", + "example": "2024-12-14T10:00:00Z" + }, + "id": { + "description": "Unique directory identifier", + "type": "string", + "example": "kRp2XYTq9A55" + }, + "kind": { + "description": "Item type, always \"directory\"", + "type": "string", + "example": "directory" + }, + "name": { + "description": "Directory name", + "type": "string", + "example": "My Documents" + }, + "path": { + "description": "Full path from root (included when ?include=path)", + "type": "array", + "items": { + "$ref": "#/definitions/github_com_get-drexa_drexa_internal_virtualfs.PathSegment" + } + }, + "updatedAt": { + "description": "When the directory was last updated (ISO 8601)", + "type": "string", + "example": "2024-12-13T16:30:00Z" + } + } + }, + "internal_catalog.FileInfo": { + "description": "File information including name, size, and timestamps", + "type": "object", + "properties": { + "createdAt": { + "description": "When the file was created (ISO 8601)", + "type": "string", + "example": "2024-12-13T15:04:05Z" + }, + "deletedAt": { + "description": "When the file was trashed, null if not trashed (ISO 8601)", + "type": "string", + "example": "2024-12-14T10:00:00Z" + }, + "id": { + "description": "Unique file identifier", + "type": "string", + "example": "mElnUNCm8F22" + }, + "kind": { + "description": "Item type, always \"file\"", + "type": "string", + "example": "file" + }, + "mimeType": { + "description": "MIME type of the file", + "type": "string", + "example": "application/pdf" + }, + "name": { + "description": "File name", + "type": "string", + "example": "document.pdf" + }, + "size": { + "description": "File size in bytes", + "type": "integer", + "example": 1048576 + }, + "updatedAt": { + "description": "When the file was last updated (ISO 8601)", + "type": "string", + "example": "2024-12-13T16:30:00Z" + } + } + }, + "internal_catalog.createDirectoryRequest": { + "description": "Request to create a new directory", + "type": "object", + "properties": { + "name": { + "description": "Name for the new directory", + "type": "string", + "example": "New Folder" + }, + "parentID": { + "description": "ID of the parent directory", + "type": "string", + "example": "kRp2XYTq9A55" + } + } + }, + "internal_catalog.patchDirectoryRequest": { + "description": "Request to update directory properties", + "type": "object", + "properties": { + "name": { + "description": "New name for the directory", + "type": "string", + "example": "My Documents" + } + } + }, + "internal_catalog.patchFileRequest": { + "description": "Request to update file properties", + "type": "object", + "properties": { + "name": { + "description": "New name for the file", + "type": "string", + "example": "renamed-document.pdf" + } + } + }, + "internal_catalog.postDirectoryContentRequest": { + "description": "Request to move items into this directory", + "type": "object", + "properties": { + "items": { + "description": "Array of file/directory IDs to move", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "mElnUNCm8F22", + "kRp2XYTq9A55" + ] + } + } + }, + "internal_upload.Status": { + "description": "Upload status enumeration", + "type": "string", + "enum": [ + "pending", + "completed", + "failed" + ], + "x-enum-varnames": [ + "StatusPending", + "StatusCompleted", + "StatusFailed" + ] + }, + "internal_upload.Upload": { + "description": "File upload session with status and upload URL", + "type": "object", + "properties": { + "id": { + "description": "Unique upload session identifier", + "type": "string", + "example": "xNq5RVBt3K88" + }, + "status": { + "description": "Current upload status", + "enum": [ + "pending", + "completed", + "failed" + ], + "allOf": [ + { + "$ref": "#/definitions/internal_upload.Status" + } + ], + "example": "pending" + }, + "uploadUrl": { + "description": "URL to upload file content to", + "type": "string", + "example": "https://api.example.com/api/accounts/550e8400-e29b-41d4-a716-446655440000/uploads/xNq5RVBt3K88/content" + } + } + }, + "internal_upload.createUploadRequest": { + "description": "Request to initiate a file upload", + "type": "object", + "properties": { + "name": { + "description": "Name of the file being uploaded", + "type": "string", + "example": "document.pdf" + }, + "parentId": { + "description": "ID of the parent directory to upload into", + "type": "string", + "example": "kRp2XYTq9A55" + } + } + }, + "internal_upload.updateUploadRequest": { + "description": "Request to update upload status (e.g., mark as completed)", + "type": "object", + "properties": { + "status": { + "description": "New status for the upload", + "enum": [ + "completed" + ], + "allOf": [ + { + "$ref": "#/definitions/internal_upload.Status" + } + ], + "example": "completed" + } + } + }, + "internal_user.User": { + "description": "User account information", + "type": "object", + "properties": { + "displayName": { + "description": "User's display name", + "type": "string", + "example": "John Doe" + }, + "email": { + "description": "User's email address", + "type": "string", + "example": "john@example.com" + }, + "id": { + "description": "Unique user identifier", + "type": "string", + "example": "550e8400-e29b-41d4-a716-446655440000" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "JWT access token. Format: \"Bearer {token}\"", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/api", + Schemes: []string{}, + Title: "Drexa API", + Description: "Drexa is a file storage and management API. It provides endpoints for authentication, user management, file uploads, and virtual filesystem operations.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/apps/backend/docs/swagger.json b/apps/backend/docs/swagger.json new file mode 100644 index 0000000..a136b3c --- /dev/null +++ b/apps/backend/docs/swagger.json @@ -0,0 +1,1577 @@ +{ + "swagger": "2.0", + "info": { + "description": "Drexa is a file storage and management API. It provides endpoints for authentication, user management, file uploads, and virtual filesystem operations.", + "title": "Drexa API", + "contact": { + "name": "Drexa Support", + "url": "https://github.com/get-drexa/drexa" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/api", + "paths": { + "/accounts": { + "post": { + "description": "Create a new user account with email and password. Returns the account, user, and authentication tokens.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Register new account", + "parameters": [ + { + "description": "Registration details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_account.registerAccountRequest" + } + } + ], + "responses": { + "200": { + "description": "Account created successfully", + "schema": { + "$ref": "#/definitions/internal_account.registerAccountResponse" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "type": "string" + } + }, + "409": { + "description": "Email already registered", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve account details including storage usage and quota", + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "Get account", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Account details", + "schema": { + "$ref": "#/definitions/internal_account.Account" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Account not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/directories": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new directory within a parent directory", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "Create directory", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "description": "Directory details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_catalog.createDirectoryRequest" + } + }, + { + "enum": [ + "path" + ], + "type": "string", + "description": "Include additional fields", + "name": "include", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Created directory", + "schema": { + "$ref": "#/definitions/internal_catalog.DirectoryInfo" + } + }, + "400": { + "description": "Parent not found or not a directory", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "409": { + "description": "Directory already exists", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/accounts/{accountID}/directories/{directoryID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve metadata for a specific directory", + "produces": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "Get directory info", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Directory ID", + "name": "directoryID", + "in": "path", + "required": true + }, + { + "enum": [ + "path" + ], + "type": "string", + "description": "Include additional fields", + "name": "include", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Directory metadata", + "schema": { + "$ref": "#/definitions/internal_catalog.DirectoryInfo" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Directory not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a directory permanently or move it to trash. Deleting a directory also affects all its contents.", + "tags": [ + "directories" + ], + "summary": "Delete directory", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Directory ID", + "name": "directoryID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Move to trash instead of permanent delete", + "name": "trash", + "in": "query" + } + ], + "responses": { + "204": { + "description": "Directory deleted", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Directory not found", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update directory properties such as name (rename)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "Update directory", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Directory ID", + "name": "directoryID", + "in": "path", + "required": true + }, + { + "description": "Directory update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_catalog.patchDirectoryRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated directory metadata", + "schema": { + "$ref": "#/definitions/internal_catalog.DirectoryInfo" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Directory not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/directories/{directoryID}/content": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all files and subdirectories within a directory", + "produces": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "List directory contents", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Directory ID", + "name": "directoryID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Array of FileInfo and DirectoryInfo objects", + "schema": { + "type": "array", + "items": {} + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Directory not found", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Move one or more files or directories into this directory. All items must currently be in the same source directory.", + "consumes": [ + "application/json" + ], + "tags": [ + "directories" + ], + "summary": "Move items to directory", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Target directory ID", + "name": "directoryID", + "in": "path", + "required": true + }, + { + "description": "Items to move", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_catalog.postDirectoryContentRequest" + } + } + ], + "responses": { + "204": { + "description": "Items moved successfully", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid request or items not in same directory", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "One or more items not found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Name conflict in target directory", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/accounts/{accountID}/files/{fileID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve metadata for a specific file", + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Get file info", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File ID", + "name": "fileID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "File metadata", + "schema": { + "$ref": "#/definitions/internal_catalog.FileInfo" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a file permanently or move it to trash", + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Delete file", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File ID", + "name": "fileID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Move to trash instead of permanent delete", + "name": "trash", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Trashed file info (when trash=true)", + "schema": { + "$ref": "#/definitions/internal_catalog.FileInfo" + } + }, + "204": { + "description": "Permanently deleted (when trash=false)", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File not found", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update file properties such as name (rename)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Update file", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File ID", + "name": "fileID", + "in": "path", + "required": true + }, + { + "description": "File update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_catalog.patchFileRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated file metadata", + "schema": { + "$ref": "#/definitions/internal_catalog.FileInfo" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/files/{fileID}/content": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Download the file content. May redirect to a signed URL for external storage.", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "files" + ], + "summary": "Download file", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "File ID", + "name": "fileID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "File content stream", + "schema": { + "type": "file" + } + }, + "307": { + "description": "Redirect to download URL", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/uploads": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Start a new file upload session. Returns an upload URL to PUT file content to.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "uploads" + ], + "summary": "Create upload session", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "description": "Upload details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_upload.createUploadRequest" + } + } + ], + "responses": { + "200": { + "description": "Upload session created", + "schema": { + "$ref": "#/definitions/internal_upload.Upload" + } + }, + "400": { + "description": "Parent is not a directory", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Parent directory not found", + "schema": { + "type": "string" + } + }, + "409": { + "description": "File with this name already exists", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/accounts/{accountID}/uploads/{uploadID}": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark an upload as completed after content has been uploaded. This finalizes the file in the filesystem.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "uploads" + ], + "summary": "Complete upload", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Upload session ID", + "name": "uploadID", + "in": "path", + "required": true + }, + { + "description": "Status update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_upload.updateUploadRequest" + } + } + ], + "responses": { + "200": { + "description": "Upload completed", + "schema": { + "$ref": "#/definitions/internal_upload.Upload" + } + }, + "400": { + "description": "Content not uploaded yet or invalid status", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Upload session not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/accounts/{accountID}/uploads/{uploadID}/content": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Stream file content to complete an upload. Send raw binary data in the request body.", + "consumes": [ + "application/octet-stream" + ], + "tags": [ + "uploads" + ], + "summary": "Upload file content", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Account ID", + "name": "accountID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Upload session ID", + "name": "uploadID", + "in": "path", + "required": true + }, + { + "description": "File content (binary)", + "name": "file", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "204": { + "description": "Content received successfully", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Upload session not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/login": { + "post": { + "description": "Authenticate with email and password to receive JWT tokens. Tokens can be delivered via HTTP-only cookies or in the response body based on the tokenDelivery field.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "User login", + "parameters": [ + { + "description": "Login credentials", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_auth.loginRequest" + } + } + ], + "responses": { + "200": { + "description": "Successful authentication", + "schema": { + "$ref": "#/definitions/internal_auth.loginResponse" + } + }, + "400": { + "description": "Invalid request body or token delivery method", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Invalid email or password", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/tokens": { + "post": { + "description": "Exchange a valid refresh token for a new pair of access and refresh tokens. The old refresh token is invalidated (rotation).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Refresh access token", + "parameters": [ + { + "description": "Refresh token", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_auth.refreshAccessTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "New tokens", + "schema": { + "$ref": "#/definitions/internal_auth.tokenResponse" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Invalid, expired, or reused refresh token", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/users/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve the authenticated user's profile information", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get current user", + "responses": { + "200": { + "description": "User profile", + "schema": { + "$ref": "#/definitions/internal_user.User" + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "github_com_get-drexa_drexa_internal_user.User": { + "description": "User account information", + "type": "object", + "properties": { + "displayName": { + "description": "User's display name", + "type": "string", + "example": "John Doe" + }, + "email": { + "description": "User's email address", + "type": "string", + "example": "john@example.com" + }, + "id": { + "description": "Unique user identifier", + "type": "string", + "example": "550e8400-e29b-41d4-a716-446655440000" + } + } + }, + "github_com_get-drexa_drexa_internal_virtualfs.PathSegment": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "internal_account.Account": { + "description": "Storage account with usage and quota details", + "type": "object", + "properties": { + "createdAt": { + "description": "When the account was created (ISO 8601)", + "type": "string", + "example": "2024-12-13T15:04:05Z" + }, + "id": { + "description": "Unique account identifier", + "type": "string", + "example": "550e8400-e29b-41d4-a716-446655440000" + }, + "storageQuotaBytes": { + "description": "Maximum storage quota in bytes", + "type": "integer", + "example": 10737418240 + }, + "storageUsageBytes": { + "description": "Current storage usage in bytes", + "type": "integer", + "example": 1073741824 + }, + "updatedAt": { + "description": "When the account was last updated (ISO 8601)", + "type": "string", + "example": "2024-12-13T16:30:00Z" + }, + "userId": { + "description": "ID of the user who owns this account", + "type": "string", + "example": "550e8400-e29b-41d4-a716-446655440001" + } + } + }, + "internal_account.registerAccountRequest": { + "description": "Request to create a new account and user", + "type": "object", + "properties": { + "displayName": { + "description": "Display name for the user", + "type": "string", + "example": "Jane Doe" + }, + "email": { + "description": "Email address for the new account", + "type": "string", + "example": "newuser@example.com" + }, + "password": { + "description": "Password for the new account (min 8 characters)", + "type": "string", + "example": "securepassword123" + } + } + }, + "internal_account.registerAccountResponse": { + "description": "Response after successful account registration", + "type": "object", + "properties": { + "accessToken": { + "description": "JWT access token for immediate authentication", + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature" + }, + "account": { + "description": "The created account", + "allOf": [ + { + "$ref": "#/definitions/internal_account.Account" + } + ] + }, + "refreshToken": { + "description": "Base64 URL encoded refresh token", + "type": "string", + "example": "dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi" + }, + "user": { + "description": "The created user", + "allOf": [ + { + "$ref": "#/definitions/github_com_get-drexa_drexa_internal_user.User" + } + ] + } + } + }, + "internal_auth.loginRequest": { + "description": "Login request with email, password, and token delivery preference", + "type": "object", + "properties": { + "email": { + "description": "User's email address", + "type": "string", + "example": "user@example.com" + }, + "password": { + "description": "User's password", + "type": "string", + "example": "secretpassword123" + }, + "tokenDelivery": { + "description": "How to deliver tokens: \"cookie\" (set HTTP-only cookies) or \"body\" (include in response)", + "type": "string", + "enum": [ + "cookie", + "body" + ], + "example": "body" + } + } + }, + "internal_auth.loginResponse": { + "description": "Login response containing user info and optionally tokens", + "type": "object", + "properties": { + "accessToken": { + "description": "JWT access token (only included when tokenDelivery is \"body\")", + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature" + }, + "refreshToken": { + "description": "Base64 URL encoded refresh token (only included when tokenDelivery is \"body\")", + "type": "string", + "example": "dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi" + }, + "user": { + "description": "Authenticated user information", + "allOf": [ + { + "$ref": "#/definitions/github_com_get-drexa_drexa_internal_user.User" + } + ] + } + } + }, + "internal_auth.refreshAccessTokenRequest": { + "description": "Request to exchange a refresh token for new tokens", + "type": "object", + "properties": { + "refreshToken": { + "description": "Base64 URL encoded refresh token", + "type": "string", + "example": "dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi" + } + } + }, + "internal_auth.tokenResponse": { + "description": "Response containing new access token and refresh token", + "type": "object", + "properties": { + "accessToken": { + "description": "New JWT access token", + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature" + }, + "refreshToken": { + "description": "New base64 URL encoded refresh token", + "type": "string", + "example": "xK9mPqRsTuVwXyZ0AbCdEfGhIjKlMnOpQrStUvWxYz1234567890abcdefgh" + } + } + }, + "internal_catalog.DirectoryInfo": { + "description": "Directory information including path and timestamps", + "type": "object", + "properties": { + "createdAt": { + "description": "When the directory was created (ISO 8601)", + "type": "string", + "example": "2024-12-13T15:04:05Z" + }, + "deletedAt": { + "description": "When the directory was trashed, null if not trashed (ISO 8601)", + "type": "string", + "example": "2024-12-14T10:00:00Z" + }, + "id": { + "description": "Unique directory identifier", + "type": "string", + "example": "kRp2XYTq9A55" + }, + "kind": { + "description": "Item type, always \"directory\"", + "type": "string", + "example": "directory" + }, + "name": { + "description": "Directory name", + "type": "string", + "example": "My Documents" + }, + "path": { + "description": "Full path from root (included when ?include=path)", + "type": "array", + "items": { + "$ref": "#/definitions/github_com_get-drexa_drexa_internal_virtualfs.PathSegment" + } + }, + "updatedAt": { + "description": "When the directory was last updated (ISO 8601)", + "type": "string", + "example": "2024-12-13T16:30:00Z" + } + } + }, + "internal_catalog.FileInfo": { + "description": "File information including name, size, and timestamps", + "type": "object", + "properties": { + "createdAt": { + "description": "When the file was created (ISO 8601)", + "type": "string", + "example": "2024-12-13T15:04:05Z" + }, + "deletedAt": { + "description": "When the file was trashed, null if not trashed (ISO 8601)", + "type": "string", + "example": "2024-12-14T10:00:00Z" + }, + "id": { + "description": "Unique file identifier", + "type": "string", + "example": "mElnUNCm8F22" + }, + "kind": { + "description": "Item type, always \"file\"", + "type": "string", + "example": "file" + }, + "mimeType": { + "description": "MIME type of the file", + "type": "string", + "example": "application/pdf" + }, + "name": { + "description": "File name", + "type": "string", + "example": "document.pdf" + }, + "size": { + "description": "File size in bytes", + "type": "integer", + "example": 1048576 + }, + "updatedAt": { + "description": "When the file was last updated (ISO 8601)", + "type": "string", + "example": "2024-12-13T16:30:00Z" + } + } + }, + "internal_catalog.createDirectoryRequest": { + "description": "Request to create a new directory", + "type": "object", + "properties": { + "name": { + "description": "Name for the new directory", + "type": "string", + "example": "New Folder" + }, + "parentID": { + "description": "ID of the parent directory", + "type": "string", + "example": "kRp2XYTq9A55" + } + } + }, + "internal_catalog.patchDirectoryRequest": { + "description": "Request to update directory properties", + "type": "object", + "properties": { + "name": { + "description": "New name for the directory", + "type": "string", + "example": "My Documents" + } + } + }, + "internal_catalog.patchFileRequest": { + "description": "Request to update file properties", + "type": "object", + "properties": { + "name": { + "description": "New name for the file", + "type": "string", + "example": "renamed-document.pdf" + } + } + }, + "internal_catalog.postDirectoryContentRequest": { + "description": "Request to move items into this directory", + "type": "object", + "properties": { + "items": { + "description": "Array of file/directory IDs to move", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "mElnUNCm8F22", + "kRp2XYTq9A55" + ] + } + } + }, + "internal_upload.Status": { + "description": "Upload status enumeration", + "type": "string", + "enum": [ + "pending", + "completed", + "failed" + ], + "x-enum-varnames": [ + "StatusPending", + "StatusCompleted", + "StatusFailed" + ] + }, + "internal_upload.Upload": { + "description": "File upload session with status and upload URL", + "type": "object", + "properties": { + "id": { + "description": "Unique upload session identifier", + "type": "string", + "example": "xNq5RVBt3K88" + }, + "status": { + "description": "Current upload status", + "enum": [ + "pending", + "completed", + "failed" + ], + "allOf": [ + { + "$ref": "#/definitions/internal_upload.Status" + } + ], + "example": "pending" + }, + "uploadUrl": { + "description": "URL to upload file content to", + "type": "string", + "example": "https://api.example.com/api/accounts/550e8400-e29b-41d4-a716-446655440000/uploads/xNq5RVBt3K88/content" + } + } + }, + "internal_upload.createUploadRequest": { + "description": "Request to initiate a file upload", + "type": "object", + "properties": { + "name": { + "description": "Name of the file being uploaded", + "type": "string", + "example": "document.pdf" + }, + "parentId": { + "description": "ID of the parent directory to upload into", + "type": "string", + "example": "kRp2XYTq9A55" + } + } + }, + "internal_upload.updateUploadRequest": { + "description": "Request to update upload status (e.g., mark as completed)", + "type": "object", + "properties": { + "status": { + "description": "New status for the upload", + "enum": [ + "completed" + ], + "allOf": [ + { + "$ref": "#/definitions/internal_upload.Status" + } + ], + "example": "completed" + } + } + }, + "internal_user.User": { + "description": "User account information", + "type": "object", + "properties": { + "displayName": { + "description": "User's display name", + "type": "string", + "example": "John Doe" + }, + "email": { + "description": "User's email address", + "type": "string", + "example": "john@example.com" + }, + "id": { + "description": "Unique user identifier", + "type": "string", + "example": "550e8400-e29b-41d4-a716-446655440000" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "JWT access token. Format: \"Bearer {token}\"", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/apps/backend/docs/swagger.yaml b/apps/backend/docs/swagger.yaml new file mode 100644 index 0000000..31db283 --- /dev/null +++ b/apps/backend/docs/swagger.yaml @@ -0,0 +1,1078 @@ +basePath: /api +definitions: + github_com_get-drexa_drexa_internal_user.User: + description: User account information + properties: + displayName: + description: User's display name + example: John Doe + type: string + email: + description: User's email address + example: john@example.com + type: string + id: + description: Unique user identifier + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + type: object + github_com_get-drexa_drexa_internal_virtualfs.PathSegment: + properties: + id: + type: string + name: + type: string + type: object + internal_account.Account: + description: Storage account with usage and quota details + properties: + createdAt: + description: When the account was created (ISO 8601) + example: "2024-12-13T15:04:05Z" + type: string + id: + description: Unique account identifier + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + storageQuotaBytes: + description: Maximum storage quota in bytes + example: 10737418240 + type: integer + storageUsageBytes: + description: Current storage usage in bytes + example: 1073741824 + type: integer + updatedAt: + description: When the account was last updated (ISO 8601) + example: "2024-12-13T16:30:00Z" + type: string + userId: + description: ID of the user who owns this account + example: 550e8400-e29b-41d4-a716-446655440001 + type: string + type: object + internal_account.registerAccountRequest: + description: Request to create a new account and user + properties: + displayName: + description: Display name for the user + example: Jane Doe + type: string + email: + description: Email address for the new account + example: newuser@example.com + type: string + password: + description: Password for the new account (min 8 characters) + example: securepassword123 + type: string + type: object + internal_account.registerAccountResponse: + description: Response after successful account registration + properties: + accessToken: + description: JWT access token for immediate authentication + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature + type: string + account: + allOf: + - $ref: '#/definitions/internal_account.Account' + description: The created account + refreshToken: + description: Base64 URL encoded refresh token + example: dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi + type: string + user: + allOf: + - $ref: '#/definitions/github_com_get-drexa_drexa_internal_user.User' + description: The created user + type: object + internal_auth.loginRequest: + description: Login request with email, password, and token delivery preference + properties: + email: + description: User's email address + example: user@example.com + type: string + password: + description: User's password + example: secretpassword123 + type: string + tokenDelivery: + description: 'How to deliver tokens: "cookie" (set HTTP-only cookies) or "body" + (include in response)' + enum: + - cookie + - body + example: body + type: string + type: object + internal_auth.loginResponse: + description: Login response containing user info and optionally tokens + properties: + accessToken: + description: JWT access token (only included when tokenDelivery is "body") + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature + type: string + refreshToken: + description: Base64 URL encoded refresh token (only included when tokenDelivery + is "body") + example: dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi + type: string + user: + allOf: + - $ref: '#/definitions/github_com_get-drexa_drexa_internal_user.User' + description: Authenticated user information + type: object + internal_auth.refreshAccessTokenRequest: + description: Request to exchange a refresh token for new tokens + properties: + refreshToken: + description: Base64 URL encoded refresh token + example: dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi + type: string + type: object + internal_auth.tokenResponse: + description: Response containing new access token and refresh token + properties: + accessToken: + description: New JWT access token + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature + type: string + refreshToken: + description: New base64 URL encoded refresh token + example: xK9mPqRsTuVwXyZ0AbCdEfGhIjKlMnOpQrStUvWxYz1234567890abcdefgh + type: string + type: object + internal_catalog.DirectoryInfo: + description: Directory information including path and timestamps + properties: + createdAt: + description: When the directory was created (ISO 8601) + example: "2024-12-13T15:04:05Z" + type: string + deletedAt: + description: When the directory was trashed, null if not trashed (ISO 8601) + example: "2024-12-14T10:00:00Z" + type: string + id: + description: Unique directory identifier + example: kRp2XYTq9A55 + type: string + kind: + description: Item type, always "directory" + example: directory + type: string + name: + description: Directory name + example: My Documents + type: string + path: + description: Full path from root (included when ?include=path) + items: + $ref: '#/definitions/github_com_get-drexa_drexa_internal_virtualfs.PathSegment' + type: array + updatedAt: + description: When the directory was last updated (ISO 8601) + example: "2024-12-13T16:30:00Z" + type: string + type: object + internal_catalog.FileInfo: + description: File information including name, size, and timestamps + properties: + createdAt: + description: When the file was created (ISO 8601) + example: "2024-12-13T15:04:05Z" + type: string + deletedAt: + description: When the file was trashed, null if not trashed (ISO 8601) + example: "2024-12-14T10:00:00Z" + type: string + id: + description: Unique file identifier + example: mElnUNCm8F22 + type: string + kind: + description: Item type, always "file" + example: file + type: string + mimeType: + description: MIME type of the file + example: application/pdf + type: string + name: + description: File name + example: document.pdf + type: string + size: + description: File size in bytes + example: 1048576 + type: integer + updatedAt: + description: When the file was last updated (ISO 8601) + example: "2024-12-13T16:30:00Z" + type: string + type: object + internal_catalog.createDirectoryRequest: + description: Request to create a new directory + properties: + name: + description: Name for the new directory + example: New Folder + type: string + parentID: + description: ID of the parent directory + example: kRp2XYTq9A55 + type: string + type: object + internal_catalog.patchDirectoryRequest: + description: Request to update directory properties + properties: + name: + description: New name for the directory + example: My Documents + type: string + type: object + internal_catalog.patchFileRequest: + description: Request to update file properties + properties: + name: + description: New name for the file + example: renamed-document.pdf + type: string + type: object + internal_catalog.postDirectoryContentRequest: + description: Request to move items into this directory + properties: + items: + description: Array of file/directory IDs to move + example: + - mElnUNCm8F22 + - kRp2XYTq9A55 + items: + type: string + type: array + type: object + internal_upload.Status: + description: Upload status enumeration + enum: + - pending + - completed + - failed + type: string + x-enum-varnames: + - StatusPending + - StatusCompleted + - StatusFailed + internal_upload.Upload: + description: File upload session with status and upload URL + properties: + id: + description: Unique upload session identifier + example: xNq5RVBt3K88 + type: string + status: + allOf: + - $ref: '#/definitions/internal_upload.Status' + description: Current upload status + enum: + - pending + - completed + - failed + example: pending + uploadUrl: + description: URL to upload file content to + example: https://api.example.com/api/accounts/550e8400-e29b-41d4-a716-446655440000/uploads/xNq5RVBt3K88/content + type: string + type: object + internal_upload.createUploadRequest: + description: Request to initiate a file upload + properties: + name: + description: Name of the file being uploaded + example: document.pdf + type: string + parentId: + description: ID of the parent directory to upload into + example: kRp2XYTq9A55 + type: string + type: object + internal_upload.updateUploadRequest: + description: Request to update upload status (e.g., mark as completed) + properties: + status: + allOf: + - $ref: '#/definitions/internal_upload.Status' + description: New status for the upload + enum: + - completed + example: completed + type: object + internal_user.User: + description: User account information + properties: + displayName: + description: User's display name + example: John Doe + type: string + email: + description: User's email address + example: john@example.com + type: string + id: + description: Unique user identifier + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + type: object +host: localhost:8080 +info: + contact: + name: Drexa Support + url: https://github.com/get-drexa/drexa + description: Drexa is a file storage and management API. It provides endpoints for + authentication, user management, file uploads, and virtual filesystem operations. + license: + name: MIT + url: https://opensource.org/licenses/MIT + title: Drexa API + version: "1.0" +paths: + /accounts: + post: + consumes: + - application/json + description: Create a new user account with email and password. Returns the + account, user, and authentication tokens. + parameters: + - description: Registration details + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_account.registerAccountRequest' + produces: + - application/json + responses: + "200": + description: Account created successfully + schema: + $ref: '#/definitions/internal_account.registerAccountResponse' + "400": + description: Invalid request body + schema: + type: string + "409": + description: Email already registered + schema: + type: string + summary: Register new account + tags: + - accounts + /accounts/{accountID}: + get: + description: Retrieve account details including storage usage and quota + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Account details + schema: + $ref: '#/definitions/internal_account.Account' + "401": + description: Not authenticated + schema: + type: string + "404": + description: Account not found + schema: + type: string + security: + - BearerAuth: [] + summary: Get account + tags: + - accounts + /accounts/{accountID}/directories: + post: + consumes: + - application/json + description: Create a new directory within a parent directory + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Directory details + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_catalog.createDirectoryRequest' + - description: Include additional fields + enum: + - path + in: query + name: include + type: string + produces: + - application/json + responses: + "200": + description: Created directory + schema: + $ref: '#/definitions/internal_catalog.DirectoryInfo' + "400": + description: Parent not found or not a directory + schema: + additionalProperties: + type: string + type: object + "401": + description: Not authenticated + schema: + type: string + "409": + description: Directory already exists + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Create directory + tags: + - directories + /accounts/{accountID}/directories/{directoryID}: + delete: + description: Delete a directory permanently or move it to trash. Deleting a + directory also affects all its contents. + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Directory ID + in: path + name: directoryID + required: true + type: string + - default: false + description: Move to trash instead of permanent delete + in: query + name: trash + type: boolean + responses: + "204": + description: Directory deleted + schema: + type: string + "401": + description: Not authenticated + schema: + type: string + "404": + description: Directory not found + schema: + type: string + security: + - BearerAuth: [] + summary: Delete directory + tags: + - directories + get: + description: Retrieve metadata for a specific directory + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Directory ID + in: path + name: directoryID + required: true + type: string + - description: Include additional fields + enum: + - path + in: query + name: include + type: string + produces: + - application/json + responses: + "200": + description: Directory metadata + schema: + $ref: '#/definitions/internal_catalog.DirectoryInfo' + "401": + description: Not authenticated + schema: + type: string + "404": + description: Directory not found + schema: + type: string + security: + - BearerAuth: [] + summary: Get directory info + tags: + - directories + patch: + consumes: + - application/json + description: Update directory properties such as name (rename) + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Directory ID + in: path + name: directoryID + required: true + type: string + - description: Directory update + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_catalog.patchDirectoryRequest' + produces: + - application/json + responses: + "200": + description: Updated directory metadata + schema: + $ref: '#/definitions/internal_catalog.DirectoryInfo' + "400": + description: Invalid request + schema: + additionalProperties: + type: string + type: object + "401": + description: Not authenticated + schema: + type: string + "404": + description: Directory not found + schema: + type: string + security: + - BearerAuth: [] + summary: Update directory + tags: + - directories + /accounts/{accountID}/directories/{directoryID}/content: + get: + description: Get all files and subdirectories within a directory + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Directory ID + in: path + name: directoryID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Array of FileInfo and DirectoryInfo objects + schema: + items: {} + type: array + "401": + description: Not authenticated + schema: + type: string + "404": + description: Directory not found + schema: + type: string + security: + - BearerAuth: [] + summary: List directory contents + tags: + - directories + post: + consumes: + - application/json + description: Move one or more files or directories into this directory. All + items must currently be in the same source directory. + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Target directory ID + in: path + name: directoryID + required: true + type: string + - description: Items to move + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_catalog.postDirectoryContentRequest' + responses: + "204": + description: Items moved successfully + schema: + type: string + "400": + description: Invalid request or items not in same directory + schema: + additionalProperties: + type: string + type: object + "401": + description: Not authenticated + schema: + type: string + "404": + description: One or more items not found + schema: + additionalProperties: + type: string + type: object + "409": + description: Name conflict in target directory + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Move items to directory + tags: + - directories + /accounts/{accountID}/files/{fileID}: + delete: + description: Delete a file permanently or move it to trash + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: File ID + in: path + name: fileID + required: true + type: string + - default: false + description: Move to trash instead of permanent delete + in: query + name: trash + type: boolean + produces: + - application/json + responses: + "200": + description: Trashed file info (when trash=true) + schema: + $ref: '#/definitions/internal_catalog.FileInfo' + "204": + description: Permanently deleted (when trash=false) + schema: + type: string + "401": + description: Not authenticated + schema: + type: string + "404": + description: File not found + schema: + type: string + security: + - BearerAuth: [] + summary: Delete file + tags: + - files + get: + description: Retrieve metadata for a specific file + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: File ID + in: path + name: fileID + required: true + type: string + produces: + - application/json + responses: + "200": + description: File metadata + schema: + $ref: '#/definitions/internal_catalog.FileInfo' + "401": + description: Not authenticated + schema: + type: string + "404": + description: File not found + schema: + type: string + security: + - BearerAuth: [] + summary: Get file info + tags: + - files + patch: + consumes: + - application/json + description: Update file properties such as name (rename) + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: File ID + in: path + name: fileID + required: true + type: string + - description: File update + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_catalog.patchFileRequest' + produces: + - application/json + responses: + "200": + description: Updated file metadata + schema: + $ref: '#/definitions/internal_catalog.FileInfo' + "400": + description: Invalid request + schema: + additionalProperties: + type: string + type: object + "401": + description: Not authenticated + schema: + type: string + "404": + description: File not found + schema: + type: string + security: + - BearerAuth: [] + summary: Update file + tags: + - files + /accounts/{accountID}/files/{fileID}/content: + get: + description: Download the file content. May redirect to a signed URL for external + storage. + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: File ID + in: path + name: fileID + required: true + type: string + produces: + - application/octet-stream + responses: + "200": + description: File content stream + schema: + type: file + "307": + description: Redirect to download URL + schema: + type: string + "401": + description: Not authenticated + schema: + type: string + "404": + description: File not found + schema: + type: string + security: + - BearerAuth: [] + summary: Download file + tags: + - files + /accounts/{accountID}/uploads: + post: + consumes: + - application/json + description: Start a new file upload session. Returns an upload URL to PUT file + content to. + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Upload details + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_upload.createUploadRequest' + produces: + - application/json + responses: + "200": + description: Upload session created + schema: + $ref: '#/definitions/internal_upload.Upload' + "400": + description: Parent is not a directory + schema: + additionalProperties: + type: string + type: object + "401": + description: Not authenticated + schema: + type: string + "404": + description: Parent directory not found + schema: + type: string + "409": + description: File with this name already exists + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Create upload session + tags: + - uploads + /accounts/{accountID}/uploads/{uploadID}: + patch: + consumes: + - application/json + description: Mark an upload as completed after content has been uploaded. This + finalizes the file in the filesystem. + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Upload session ID + in: path + name: uploadID + required: true + type: string + - description: Status update + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_upload.updateUploadRequest' + produces: + - application/json + responses: + "200": + description: Upload completed + schema: + $ref: '#/definitions/internal_upload.Upload' + "400": + description: Content not uploaded yet or invalid status + schema: + additionalProperties: + type: string + type: object + "401": + description: Not authenticated + schema: + type: string + "404": + description: Upload session not found + schema: + type: string + security: + - BearerAuth: [] + summary: Complete upload + tags: + - uploads + /accounts/{accountID}/uploads/{uploadID}/content: + put: + consumes: + - application/octet-stream + description: Stream file content to complete an upload. Send raw binary data + in the request body. + parameters: + - description: Account ID + format: uuid + in: path + name: accountID + required: true + type: string + - description: Upload session ID + in: path + name: uploadID + required: true + type: string + - description: File content (binary) + in: body + name: file + required: true + schema: + items: + type: integer + type: array + responses: + "204": + description: Content received successfully + schema: + type: string + "401": + description: Not authenticated + schema: + type: string + "404": + description: Upload session not found + schema: + type: string + security: + - BearerAuth: [] + summary: Upload file content + tags: + - uploads + /auth/login: + post: + consumes: + - application/json + description: Authenticate with email and password to receive JWT tokens. Tokens + can be delivered via HTTP-only cookies or in the response body based on the + tokenDelivery field. + parameters: + - description: Login credentials + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_auth.loginRequest' + produces: + - application/json + responses: + "200": + description: Successful authentication + schema: + $ref: '#/definitions/internal_auth.loginResponse' + "400": + description: Invalid request body or token delivery method + schema: + additionalProperties: + type: string + type: object + "401": + description: Invalid email or password + schema: + additionalProperties: + type: string + type: object + summary: User login + tags: + - auth + /auth/tokens: + post: + consumes: + - application/json + description: Exchange a valid refresh token for a new pair of access and refresh + tokens. The old refresh token is invalidated (rotation). + parameters: + - description: Refresh token + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_auth.refreshAccessTokenRequest' + produces: + - application/json + responses: + "200": + description: New tokens + schema: + $ref: '#/definitions/internal_auth.tokenResponse' + "400": + description: Invalid request body + schema: + additionalProperties: + type: string + type: object + "401": + description: Invalid, expired, or reused refresh token + schema: + additionalProperties: + type: string + type: object + summary: Refresh access token + tags: + - auth + /users/me: + get: + description: Retrieve the authenticated user's profile information + produces: + - application/json + responses: + "200": + description: User profile + schema: + $ref: '#/definitions/internal_user.User' + "401": + description: Not authenticated + schema: + type: string + security: + - BearerAuth: [] + summary: Get current user + tags: + - users +securityDefinitions: + BearerAuth: + description: 'JWT access token. Format: "Bearer {token}"' + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/apps/backend/go.mod b/apps/backend/go.mod index 0d76d2f..3ecf8d3 100644 --- a/apps/backend/go.mod +++ b/apps/backend/go.mod @@ -7,6 +7,7 @@ require ( github.com/gofiber/fiber/v2 v2.52.9 github.com/google/uuid v1.6.0 github.com/sqids/sqids-go v0.4.1 + github.com/swaggo/swag v1.16.6 github.com/uptrace/bun v1.2.16 github.com/uptrace/bun/extra/bundebug v1.2.16 golang.org/x/crypto v0.45.0 @@ -14,9 +15,24 @@ require ( ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect mellium.im/sasl v0.3.2 // indirect ) diff --git a/apps/backend/go.sum b/apps/backend/go.sum index 2981614..5f10b2e 100644 --- a/apps/backend/go.sum +++ b/apps/backend/go.sum @@ -1,11 +1,29 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= @@ -16,10 +34,19 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -36,8 +63,13 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw= github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/uptrace/bun v1.2.16 h1:QlObi6ZIK5Ao7kAALnh91HWYNZUBbVwye52fmlQM9kc= @@ -64,12 +96,35 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= diff --git a/apps/backend/internal/account/account.go b/apps/backend/internal/account/account.go index d925ea0..c580e1d 100644 --- a/apps/backend/internal/account/account.go +++ b/apps/backend/internal/account/account.go @@ -7,15 +7,23 @@ import ( "github.com/uptrace/bun" ) +// Account represents a storage account with quota information +// @Description Storage account with usage and quota details type Account struct { - bun.BaseModel `bun:"accounts"` + bun.BaseModel `bun:"accounts" swaggerignore:"true"` - ID uuid.UUID `bun:",pk,type:uuid" json:"id"` - UserID uuid.UUID `bun:"user_id,notnull,type:uuid" json:"userId"` - StorageUsageBytes int64 `bun:"storage_usage_bytes,notnull" json:"storageUsageBytes"` - StorageQuotaBytes int64 `bun:"storage_quota_bytes,notnull" json:"storageQuotaBytes"` - CreatedAt time.Time `bun:"created_at,notnull,nullzero" json:"createdAt"` - UpdatedAt time.Time `bun:"updated_at,notnull,nullzero" json:"updatedAt"` + // Unique account identifier + ID uuid.UUID `bun:",pk,type:uuid" json:"id" example:"550e8400-e29b-41d4-a716-446655440000"` + // ID of the user who owns this account + UserID uuid.UUID `bun:"user_id,notnull,type:uuid" json:"userId" example:"550e8400-e29b-41d4-a716-446655440001"` + // Current storage usage in bytes + StorageUsageBytes int64 `bun:"storage_usage_bytes,notnull" json:"storageUsageBytes" example:"1073741824"` + // Maximum storage quota in bytes + StorageQuotaBytes int64 `bun:"storage_quota_bytes,notnull" json:"storageQuotaBytes" example:"10737418240"` + // When the account was created (ISO 8601) + CreatedAt time.Time `bun:"created_at,notnull,nullzero" json:"createdAt" example:"2024-12-13T15:04:05Z"` + // When the account was last updated (ISO 8601) + UpdatedAt time.Time `bun:"updated_at,notnull,nullzero" json:"updatedAt" example:"2024-12-13T16:30:00Z"` } func newAccountID() (uuid.UUID, error) { diff --git a/apps/backend/internal/account/http.go b/apps/backend/internal/account/http.go index f737d2e..71c24fa 100644 --- a/apps/backend/internal/account/http.go +++ b/apps/backend/internal/account/http.go @@ -19,17 +19,28 @@ type HTTPHandler struct { authMiddleware fiber.Handler } +// registerAccountRequest represents a new account registration +// @Description Request to create a new account and user type registerAccountRequest struct { - Email string `json:"email"` - Password string `json:"password"` - DisplayName string `json:"displayName"` + // Email address for the new account + Email string `json:"email" example:"newuser@example.com"` + // Password for the new account (min 8 characters) + Password string `json:"password" example:"securepassword123"` + // Display name for the user + DisplayName string `json:"displayName" example:"Jane Doe"` } +// registerAccountResponse represents a successful registration +// @Description Response after successful account registration type registerAccountResponse struct { - Account *Account `json:"account"` - User *user.User `json:"user"` - AccessToken string `json:"accessToken"` - RefreshToken string `json:"refreshToken"` + // The created account + Account *Account `json:"account"` + // The created user + User *user.User `json:"user"` + // JWT access token for immediate authentication + AccessToken string `json:"accessToken" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature"` + // Base64 URL encoded refresh token + RefreshToken string `json:"refreshToken" example:"dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi"` } const currentAccountKey = "currentAccount" @@ -75,6 +86,17 @@ func (h *HTTPHandler) accountMiddleware(c *fiber.Ctx) error { return c.Next() } +// getAccount retrieves account information +// @Summary Get account +// @Description Retrieve account details including storage usage and quota +// @Tags accounts +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Success 200 {object} Account "Account details" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "Account not found" +// @Router /accounts/{accountID} [get] func (h *HTTPHandler) getAccount(c *fiber.Ctx) error { account := CurrentAccount(c) if account == nil { @@ -83,6 +105,17 @@ func (h *HTTPHandler) getAccount(c *fiber.Ctx) error { return c.JSON(account) } +// registerAccount creates a new account and user +// @Summary Register new account +// @Description Create a new user account with email and password. Returns the account, user, and authentication tokens. +// @Tags accounts +// @Accept json +// @Produce json +// @Param request body registerAccountRequest true "Registration details" +// @Success 200 {object} registerAccountResponse "Account created successfully" +// @Failure 400 {string} string "Invalid request body" +// @Failure 409 {string} string "Email already registered" +// @Router /accounts [post] func (h *HTTPHandler) registerAccount(c *fiber.Ctx) error { req := new(registerAccountRequest) if err := c.BodyParser(req); err != nil { diff --git a/apps/backend/internal/auth/http.go b/apps/backend/internal/auth/http.go index 6d3d1e8..4544053 100644 --- a/apps/backend/internal/auth/http.go +++ b/apps/backend/internal/auth/http.go @@ -20,25 +20,42 @@ const ( cookieKeyRefreshToken = "refresh_token" ) +// loginRequest represents the login credentials +// @Description Login request with email, password, and token delivery preference type loginRequest struct { - Email string `json:"email"` - Password string `json:"password"` - TokenDelivery string `json:"tokenDelivery"` + // User's email address + Email string `json:"email" example:"user@example.com"` + // User's password + Password string `json:"password" example:"secretpassword123"` + // How to deliver tokens: "cookie" (set HTTP-only cookies) or "body" (include in response) + TokenDelivery string `json:"tokenDelivery" example:"body" enums:"cookie,body"` } +// loginResponse represents a successful login response +// @Description Login response containing user info and optionally tokens type loginResponse struct { - User user.User `json:"user"` - AccessToken string `json:"accessToken,omitempty"` - RefreshToken string `json:"refreshToken,omitempty"` + // Authenticated user information + User user.User `json:"user"` + // JWT access token (only included when tokenDelivery is "body") + AccessToken string `json:"accessToken,omitempty" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature"` + // Base64 URL encoded refresh token (only included when tokenDelivery is "body") + RefreshToken string `json:"refreshToken,omitempty" example:"dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi"` } +// refreshAccessTokenRequest represents a token refresh request +// @Description Request to exchange a refresh token for new tokens type refreshAccessTokenRequest struct { - RefreshToken string `json:"refreshToken"` + // Base64 URL encoded refresh token + RefreshToken string `json:"refreshToken" example:"dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi"` } +// tokenResponse represents new access and refresh tokens +// @Description Response containing new access token and refresh token type tokenResponse struct { - AccessToken string `json:"accessToken"` - RefreshToken string `json:"refreshToken"` + // New JWT access token + AccessToken string `json:"accessToken" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature"` + // New base64 URL encoded refresh token + RefreshToken string `json:"refreshToken" example:"xK9mPqRsTuVwXyZ0AbCdEfGhIjKlMnOpQrStUvWxYz1234567890abcdefgh"` } type HTTPHandler struct { @@ -57,6 +74,17 @@ func (h *HTTPHandler) RegisterRoutes(api fiber.Router) { auth.Post("/tokens", h.refreshAccessToken) } +// Login authenticates a user with email and password +// @Summary User login +// @Description Authenticate with email and password to receive JWT tokens. Tokens can be delivered via HTTP-only cookies or in the response body based on the tokenDelivery field. +// @Tags auth +// @Accept json +// @Produce json +// @Param request body loginRequest true "Login credentials" +// @Success 200 {object} loginResponse "Successful authentication" +// @Failure 400 {object} map[string]string "Invalid request body or token delivery method" +// @Failure 401 {object} map[string]string "Invalid email or password" +// @Router /auth/login [post] func (h *HTTPHandler) Login(c *fiber.Ctx) error { req := new(loginRequest) if err := c.BodyParser(req); err != nil { @@ -100,6 +128,17 @@ func (h *HTTPHandler) Login(c *fiber.Ctx) error { } } +// refreshAccessToken exchanges a refresh token for new access and refresh tokens +// @Summary Refresh access token +// @Description Exchange a valid refresh token for a new pair of access and refresh tokens. The old refresh token is invalidated (rotation). +// @Tags auth +// @Accept json +// @Produce json +// @Param request body refreshAccessTokenRequest true "Refresh token" +// @Success 200 {object} tokenResponse "New tokens" +// @Failure 400 {object} map[string]string "Invalid request body" +// @Failure 401 {object} map[string]string "Invalid, expired, or reused refresh token" +// @Router /auth/tokens [post] func (h *HTTPHandler) refreshAccessToken(c *fiber.Ctx) error { req := new(refreshAccessTokenRequest) if err := c.BodyParser(req); err != nil { diff --git a/apps/backend/internal/catalog/directory.go b/apps/backend/internal/catalog/directory.go index 4915b0e..463ca3d 100644 --- a/apps/backend/internal/catalog/directory.go +++ b/apps/backend/internal/catalog/directory.go @@ -17,23 +17,39 @@ const ( DirItemKindFile = "file" ) +// DirectoryInfo represents directory metadata +// @Description Directory information including path and timestamps type DirectoryInfo struct { - Kind string `json:"kind"` - ID string `json:"id"` - Path virtualfs.Path `json:"path,omitempty"` - Name string `json:"name"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt *time.Time `json:"deletedAt,omitempty"` + // Item type, always "directory" + Kind string `json:"kind" example:"directory"` + // Unique directory identifier + ID string `json:"id" example:"kRp2XYTq9A55"` + // Full path from root (included when ?include=path) + Path virtualfs.Path `json:"path,omitempty"` + // Directory name + Name string `json:"name" example:"My Documents"` + // When the directory was created (ISO 8601) + CreatedAt time.Time `json:"createdAt" example:"2024-12-13T15:04:05Z"` + // When the directory was last updated (ISO 8601) + UpdatedAt time.Time `json:"updatedAt" example:"2024-12-13T16:30:00Z"` + // When the directory was trashed, null if not trashed (ISO 8601) + DeletedAt *time.Time `json:"deletedAt,omitempty" example:"2024-12-14T10:00:00Z"` } +// createDirectoryRequest represents a new directory creation request +// @Description Request to create a new directory type createDirectoryRequest struct { - ParentID string `json:"parentID"` - Name string `json:"name"` + // ID of the parent directory + ParentID string `json:"parentID" example:"kRp2XYTq9A55"` + // Name for the new directory + Name string `json:"name" example:"New Folder"` } +// postDirectoryContentRequest represents a move items request +// @Description Request to move items into this directory type postDirectoryContentRequest struct { - Items []string `json:"items"` + // Array of file/directory IDs to move + Items []string `json:"items" example:"mElnUNCm8F22,kRp2XYTq9A55"` } func (h *HTTPHandler) currentDirectoryMiddleware(c *fiber.Ctx) error { @@ -64,6 +80,21 @@ func includeParam(c *fiber.Ctx) []string { return strings.Split(c.Query("include"), ",") } +// createDirectory creates a new directory +// @Summary Create directory +// @Description Create a new directory within a parent directory +// @Tags directories +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param request body createDirectoryRequest true "Directory details" +// @Param include query string false "Include additional fields" Enums(path) +// @Success 200 {object} DirectoryInfo "Created directory" +// @Failure 400 {object} map[string]string "Parent not found or not a directory" +// @Failure 401 {string} string "Not authenticated" +// @Failure 409 {object} map[string]string "Directory already exists" +// @Router /accounts/{accountID}/directories [post] func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error { account := account.CurrentAccount(c) if account == nil { @@ -127,6 +158,19 @@ func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error { return c.JSON(i) } +// fetchDirectory returns directory metadata +// @Summary Get directory info +// @Description Retrieve metadata for a specific directory +// @Tags directories +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param directoryID path string true "Directory ID" +// @Param include query string false "Include additional fields" Enums(path) +// @Success 200 {object} DirectoryInfo "Directory metadata" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "Directory not found" +// @Router /accounts/{accountID}/directories/{directoryID} [get] func (h *HTTPHandler) fetchDirectory(c *fiber.Ctx) error { node := mustCurrentDirectoryNode(c) @@ -151,6 +195,18 @@ func (h *HTTPHandler) fetchDirectory(c *fiber.Ctx) error { return c.JSON(i) } +// listDirectory returns directory contents +// @Summary List directory contents +// @Description Get all files and subdirectories within a directory +// @Tags directories +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param directoryID path string true "Directory ID" +// @Success 200 {array} interface{} "Array of FileInfo and DirectoryInfo objects" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "Directory not found" +// @Router /accounts/{accountID}/directories/{directoryID}/content [get] func (h *HTTPHandler) listDirectory(c *fiber.Ctx) error { node := mustCurrentDirectoryNode(c) children, err := h.vfs.ListChildren(c.Context(), h.db, node) @@ -190,6 +246,21 @@ func (h *HTTPHandler) listDirectory(c *fiber.Ctx) error { return c.JSON(items) } +// patchDirectory updates directory properties +// @Summary Update directory +// @Description Update directory properties such as name (rename) +// @Tags directories +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param directoryID path string true "Directory ID" +// @Param request body patchDirectoryRequest true "Directory update" +// @Success 200 {object} DirectoryInfo "Updated directory metadata" +// @Failure 400 {object} map[string]string "Invalid request" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "Directory not found" +// @Router /accounts/{accountID}/directories/{directoryID} [patch] func (h *HTTPHandler) patchDirectory(c *fiber.Ctx) error { node := mustCurrentDirectoryNode(c) @@ -229,6 +300,18 @@ func (h *HTTPHandler) patchDirectory(c *fiber.Ctx) error { }) } +// deleteDirectory removes a directory +// @Summary Delete directory +// @Description Delete a directory permanently or move it to trash. Deleting a directory also affects all its contents. +// @Tags directories +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param directoryID path string true "Directory ID" +// @Param trash query bool false "Move to trash instead of permanent delete" default(false) +// @Success 204 {string} string "Directory deleted" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "Directory not found" +// @Router /accounts/{accountID}/directories/{directoryID} [delete] func (h *HTTPHandler) deleteDirectory(c *fiber.Ctx) error { node := mustCurrentDirectoryNode(c) @@ -259,6 +342,21 @@ func (h *HTTPHandler) deleteDirectory(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } +// moveItemsToDirectory moves files and directories into this directory +// @Summary Move items to directory +// @Description Move one or more files or directories into this directory. All items must currently be in the same source directory. +// @Tags directories +// @Accept json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param directoryID path string true "Target directory ID" +// @Param request body postDirectoryContentRequest true "Items to move" +// @Success 204 {string} string "Items moved successfully" +// @Failure 400 {object} map[string]string "Invalid request or items not in same directory" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {object} map[string]string "One or more items not found" +// @Failure 409 {object} map[string]string "Name conflict in target directory" +// @Router /accounts/{accountID}/directories/{directoryID}/content [post] func (h *HTTPHandler) moveItemsToDirectory(c *fiber.Ctx) error { acc := account.CurrentAccount(c) if acc == nil { diff --git a/apps/backend/internal/catalog/file.go b/apps/backend/internal/catalog/file.go index ba9b564..58553ae 100644 --- a/apps/backend/internal/catalog/file.go +++ b/apps/backend/internal/catalog/file.go @@ -11,15 +11,25 @@ import ( "github.com/gofiber/fiber/v2" ) +// FileInfo represents file metadata +// @Description File information including name, size, and timestamps type FileInfo struct { - Kind string `json:"kind"` - ID string `json:"id"` - Name string `json:"name"` - Size int64 `json:"size"` - MimeType string `json:"mimeType"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt *time.Time `json:"deletedAt,omitempty"` + // Item type, always "file" + Kind string `json:"kind" example:"file"` + // Unique file identifier + ID string `json:"id" example:"mElnUNCm8F22"` + // File name + Name string `json:"name" example:"document.pdf"` + // File size in bytes + Size int64 `json:"size" example:"1048576"` + // MIME type of the file + MimeType string `json:"mimeType" example:"application/pdf"` + // When the file was created (ISO 8601) + CreatedAt time.Time `json:"createdAt" example:"2024-12-13T15:04:05Z"` + // When the file was last updated (ISO 8601) + UpdatedAt time.Time `json:"updatedAt" example:"2024-12-13T16:30:00Z"` + // When the file was trashed, null if not trashed (ISO 8601) + DeletedAt *time.Time `json:"deletedAt,omitempty" example:"2024-12-14T10:00:00Z"` } func mustCurrentFileNode(c *fiber.Ctx) *virtualfs.Node { @@ -46,6 +56,18 @@ func (h *HTTPHandler) currentFileMiddleware(c *fiber.Ctx) error { return c.Next() } +// fetchFile returns file metadata +// @Summary Get file info +// @Description Retrieve metadata for a specific file +// @Tags files +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param fileID path string true "File ID" +// @Success 200 {object} FileInfo "File metadata" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "File not found" +// @Router /accounts/{accountID}/files/{fileID} [get] func (h *HTTPHandler) fetchFile(c *fiber.Ctx) error { node := mustCurrentFileNode(c) i := FileInfo{ @@ -61,6 +83,19 @@ func (h *HTTPHandler) fetchFile(c *fiber.Ctx) error { return c.JSON(i) } +// downloadFile streams file content +// @Summary Download file +// @Description Download the file content. May redirect to a signed URL for external storage. +// @Tags files +// @Produce application/octet-stream +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param fileID path string true "File ID" +// @Success 200 {file} binary "File content stream" +// @Success 307 {string} string "Redirect to download URL" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "File not found" +// @Router /accounts/{accountID}/files/{fileID}/content [get] func (h *HTTPHandler) downloadFile(c *fiber.Ctx) error { node := mustCurrentFileNode(c) @@ -89,6 +124,21 @@ func (h *HTTPHandler) downloadFile(c *fiber.Ctx) error { return httperr.Internal(errors.New("vfs returned neither a reader nor a URL")) } +// patchFile updates file properties +// @Summary Update file +// @Description Update file properties such as name (rename) +// @Tags files +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param fileID path string true "File ID" +// @Param request body patchFileRequest true "File update" +// @Success 200 {object} FileInfo "Updated file metadata" +// @Failure 400 {object} map[string]string "Invalid request" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "File not found" +// @Router /accounts/{accountID}/files/{fileID} [patch] func (h *HTTPHandler) patchFile(c *fiber.Ctx) error { node := mustCurrentFileNode(c) @@ -131,6 +181,20 @@ func (h *HTTPHandler) patchFile(c *fiber.Ctx) error { }) } +// deleteFile removes a file +// @Summary Delete file +// @Description Delete a file permanently or move it to trash +// @Tags files +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @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)" +// @Success 204 {string} string "Permanently deleted (when trash=false)" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "File not found" +// @Router /accounts/{accountID}/files/{fileID} [delete] func (h *HTTPHandler) deleteFile(c *fiber.Ctx) error { node := mustCurrentFileNode(c) diff --git a/apps/backend/internal/catalog/http.go b/apps/backend/internal/catalog/http.go index 2944afe..37ad8cb 100644 --- a/apps/backend/internal/catalog/http.go +++ b/apps/backend/internal/catalog/http.go @@ -11,12 +11,18 @@ type HTTPHandler struct { db *bun.DB } +// patchFileRequest represents a file update request +// @Description Request to update file properties type patchFileRequest struct { - Name string `json:"name"` + // New name for the file + Name string `json:"name" example:"renamed-document.pdf"` } +// patchDirectoryRequest represents a directory update request +// @Description Request to update directory properties type patchDirectoryRequest struct { - Name string `json:"name"` + // New name for the directory + Name string `json:"name" example:"My Documents"` } func NewHTTPHandler(vfs *virtualfs.VirtualFS, db *bun.DB) *HTTPHandler { diff --git a/apps/backend/internal/upload/http.go b/apps/backend/internal/upload/http.go index 3ce4b8a..1618083 100644 --- a/apps/backend/internal/upload/http.go +++ b/apps/backend/internal/upload/http.go @@ -10,13 +10,20 @@ import ( "github.com/uptrace/bun" ) +// createUploadRequest represents a new upload session request +// @Description Request to initiate a file upload type createUploadRequest struct { - ParentID string `json:"parentId"` - Name string `json:"name"` + // ID of the parent directory to upload into + ParentID string `json:"parentId" example:"kRp2XYTq9A55"` + // Name of the file being uploaded + Name string `json:"name" example:"document.pdf"` } +// updateUploadRequest represents an upload status update +// @Description Request to update upload status (e.g., mark as completed) type updateUploadRequest struct { - Status Status `json:"status"` + // New status for the upload + Status Status `json:"status" example:"completed" enums:"completed"` } type HTTPHandler struct { @@ -36,6 +43,21 @@ func (h *HTTPHandler) RegisterRoutes(api fiber.Router) { upload.Patch("/:uploadID", h.Update) } +// Create initiates a new file upload session +// @Summary Create upload session +// @Description Start a new file upload session. Returns an upload URL to PUT file content to. +// @Tags uploads +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @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" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "Parent directory not found" +// @Failure 409 {object} map[string]string "File with this name already exists" +// @Router /accounts/{accountID}/uploads [post] func (h *HTTPHandler) Create(c *fiber.Ctx) error { account := account.CurrentAccount(c) if account == nil { @@ -71,6 +93,19 @@ func (h *HTTPHandler) Create(c *fiber.Ctx) error { return c.JSON(upload) } +// ReceiveContent receives the file content for an upload +// @Summary Upload file content +// @Description Stream file content to complete an upload. Send raw binary data in the request body. +// @Tags uploads +// @Accept application/octet-stream +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param uploadID path string true "Upload session ID" +// @Param file body []byte true "File content (binary)" +// @Success 204 {string} string "Content received successfully" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "Upload session not found" +// @Router /accounts/{accountID}/uploads/{uploadID}/content [put] func (h *HTTPHandler) ReceiveContent(c *fiber.Ctx) error { account := account.CurrentAccount(c) if account == nil { @@ -91,6 +126,21 @@ func (h *HTTPHandler) ReceiveContent(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } +// Update updates the upload status +// @Summary Complete upload +// @Description Mark an upload as completed after content has been uploaded. This finalizes the file in the filesystem. +// @Tags uploads +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param accountID path string true "Account ID" format(uuid) +// @Param uploadID path string true "Upload session ID" +// @Param request body updateUploadRequest true "Status update" +// @Success 200 {object} Upload "Upload completed" +// @Failure 400 {object} map[string]string "Content not uploaded yet or invalid status" +// @Failure 401 {string} string "Not authenticated" +// @Failure 404 {string} string "Upload session not found" +// @Router /accounts/{accountID}/uploads/{uploadID} [patch] func (h *HTTPHandler) Update(c *fiber.Ctx) error { account := account.CurrentAccount(c) if account == nil { diff --git a/apps/backend/internal/upload/upload.go b/apps/backend/internal/upload/upload.go index 6bc9e00..f14ce1d 100644 --- a/apps/backend/internal/upload/upload.go +++ b/apps/backend/internal/upload/upload.go @@ -2,17 +2,28 @@ package upload import "github.com/get-drexa/drexa/internal/virtualfs" +// Status represents the upload state +// @Description Upload status enumeration type Status string const ( - StatusPending Status = "pending" + // StatusPending indicates upload is awaiting content + StatusPending Status = "pending" + // StatusCompleted indicates upload finished successfully StatusCompleted Status = "completed" - StatusFailed Status = "failed" + // StatusFailed indicates upload failed + StatusFailed Status = "failed" ) +// Upload represents a file upload session +// @Description File upload session with status and upload URL type Upload struct { - ID string `json:"id"` - Status Status `json:"status"` - TargetNode *virtualfs.Node `json:"-"` - UploadURL string `json:"uploadUrl"` + // Unique upload session identifier + ID string `json:"id" example:"xNq5RVBt3K88"` + // Current upload status + Status Status `json:"status" example:"pending" enums:"pending,completed,failed"` + // 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/accounts/550e8400-e29b-41d4-a716-446655440000/uploads/xNq5RVBt3K88/content"` } diff --git a/apps/backend/internal/user/http.go b/apps/backend/internal/user/http.go index bebfd79..40cec46 100644 --- a/apps/backend/internal/user/http.go +++ b/apps/backend/internal/user/http.go @@ -22,6 +22,15 @@ func (h *HTTPHandler) RegisterRoutes(api fiber.Router) { user.Get("/me", h.getAuthenticatedUser) } +// getAuthenticatedUser returns the currently authenticated user +// @Summary Get current user +// @Description Retrieve the authenticated user's profile information +// @Tags users +// @Produce json +// @Security BearerAuth +// @Success 200 {object} User "User profile" +// @Failure 401 {string} string "Not authenticated" +// @Router /users/me [get] func (h *HTTPHandler) getAuthenticatedUser(c *fiber.Ctx) error { u := reqctx.AuthenticatedUser(c).(*User) if u == nil { diff --git a/apps/backend/internal/user/user.go b/apps/backend/internal/user/user.go index d6e3f71..56ad448 100644 --- a/apps/backend/internal/user/user.go +++ b/apps/backend/internal/user/user.go @@ -8,15 +8,20 @@ import ( "github.com/uptrace/bun" ) +// User represents a user account in the system +// @Description User account information type User struct { - bun.BaseModel `bun:"users"` + bun.BaseModel `bun:"users" swaggerignore:"true"` - ID uuid.UUID `bun:",pk,type:uuid" json:"id"` - DisplayName string `bun:"display_name" json:"displayName"` - Email string `bun:"email,unique,notnull" json:"email"` - Password password.Hashed `bun:"password,notnull" json:"-"` - CreatedAt time.Time `bun:"created_at,notnull,nullzero" json:"-"` - UpdatedAt time.Time `bun:"updated_at,notnull,nullzero" json:"-"` + // Unique user identifier + ID uuid.UUID `bun:",pk,type:uuid" json:"id" example:"550e8400-e29b-41d4-a716-446655440000"` + // User's display name + DisplayName string `bun:"display_name" json:"displayName" example:"John Doe"` + // User's email address + Email string `bun:"email,unique,notnull" json:"email" example:"john@example.com"` + Password password.Hashed `bun:"password,notnull" json:"-" swaggerignore:"true"` + CreatedAt time.Time `bun:"created_at,notnull,nullzero" json:"-" swaggerignore:"true"` + UpdatedAt time.Time `bun:"updated_at,notnull,nullzero" json:"-" swaggerignore:"true"` } func newUserID() (uuid.UUID, error) {