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

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

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

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"`
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 {

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 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"`
}

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