docs: add OpenAPI documentation with Scalar UI

- Add swaggo annotations to all HTTP handlers
- Add Swagger/OpenAPI spec generation with swag
- Create separate docs server binary (drexa-docs)
- Add Makefile with build, run, and docs targets
- Configure Scalar as the API documentation UI

Run 'make docs' to regenerate, 'make run-docs' to serve.
This commit is contained in:
2025-12-13 22:44:37 +00:00
parent 918b85dfd5
commit 7b13326e22
18 changed files with 4853 additions and 59 deletions

41
apps/backend/Makefile Normal file
View File

@@ -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

View File

@@ -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(`<!DOCTYPE html>
<html>
<head>
<title>Drexa API Documentation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%%3E%%3Ctext y='.9em' font-size='90'%%3E📚%%3C/text%%3E%%3C/svg%%3E" />
</head>
<body>
<script
id="api-reference"
data-url="/openapi.json"
data-configuration='{
"theme": "kepler",
"darkMode": true,
"authentication": {
"preferredSecurityScheme": "BearerAuth"
},
"servers": [
{
"url": "%s",
"description": "API Server"
}
]
}'
></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>`, *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)))
}

View File

@@ -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()

1601
apps/backend/docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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=

View File

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

View File

@@ -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 {
// The created account
Account *Account `json:"account"`
// The created user
User *user.User `json:"user"`
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
// 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 {

View File

@@ -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 {
// Authenticated user information
User user.User `json:"user"`
AccessToken string `json:"accessToken,omitempty"`
RefreshToken string `json:"refreshToken,omitempty"`
// 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 {

View File

@@ -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"`
// 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"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt,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 {

View File

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

View File

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

View File

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

View File

@@ -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 indicates upload is awaiting content
StatusPending Status = "pending"
// StatusCompleted indicates upload finished successfully
StatusCompleted Status = "completed"
// 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"`
}

View File

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

View File

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