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) {