diff --git a/apps/backend/docs/swagger.json b/apps/backend/docs/swagger.json index 448306e..b4e1871 100644 --- a/apps/backend/docs/swagger.json +++ b/apps/backend/docs/swagger.json @@ -17,8 +17,40 @@ "basePath": "/api", "paths": { "/accounts": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve all accounts for the authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "accounts" + ], + "summary": "List accounts", + "responses": { + "200": { + "description": "List of accounts for the authenticated user", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_account.Account" + } + } + }, + "401": { + "description": "Not authenticated", + "schema": { + "type": "string" + } + } + } + }, "post": { - "description": "Create a new user account with email and password. Returns the account, user, and authentication tokens.", + "description": "Create a new user account with email and password. Returns the account, user, and authentication tokens. Tokens can be delivered via HTTP-only cookies or in the response body based on the tokenDelivery field.", "consumes": [ "application/json" ], @@ -48,7 +80,7 @@ } }, "400": { - "description": "Invalid request body", + "description": "Invalid request body or token delivery method", "schema": { "type": "string" } @@ -1325,6 +1357,15 @@ "description": "Password for the new account (min 8 characters)", "type": "string", "example": "securepassword123" + }, + "tokenDelivery": { + "description": "How to deliver tokens: \"cookie\" (set HTTP-only cookies) or \"body\" (include in response)", + "type": "string", + "enum": [ + "cookie", + "body" + ], + "example": "body" } } }, @@ -1333,7 +1374,7 @@ "type": "object", "properties": { "accessToken": { - "description": "JWT access token for immediate authentication", + "description": "JWT access token for immediate authentication (only included when tokenDelivery is \"body\")", "type": "string", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature" }, @@ -1346,7 +1387,7 @@ ] }, "refreshToken": { - "description": "Base64 URL encoded refresh token", + "description": "Base64 URL encoded refresh token (only included when tokenDelivery is \"body\")", "type": "string", "example": "dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi" }, diff --git a/apps/backend/internal/account/http.go b/apps/backend/internal/account/http.go index 4bdc35d..d4d62d4 100644 --- a/apps/backend/internal/account/http.go +++ b/apps/backend/internal/account/http.go @@ -17,6 +17,7 @@ type HTTPHandler struct { authService *auth.Service db *bun.DB authMiddleware fiber.Handler + cookieConfig auth.CookieConfig } // registerAccountRequest represents a new account registration @@ -28,6 +29,8 @@ type registerAccountRequest struct { Password string `json:"password" example:"securepassword123"` // Display name for the user DisplayName string `json:"displayName" example:"Jane Doe"` + // How to deliver tokens: "cookie" (set HTTP-only cookies) or "body" (include in response) + TokenDelivery string `json:"tokenDelivery" example:"body" enums:"cookie,body"` } // registerAccountResponse represents a successful registration @@ -37,10 +40,10 @@ type registerAccountResponse struct { 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"` + // JWT access token for immediate authentication (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"` } const currentAccountKey = "currentAccount" @@ -49,8 +52,8 @@ func CurrentAccount(c *fiber.Ctx) *Account { return c.Locals(currentAccountKey).(*Account) } -func NewHTTPHandler(accountService *Service, authService *auth.Service, db *bun.DB, authMiddleware fiber.Handler) *HTTPHandler { - return &HTTPHandler{accountService: accountService, authService: authService, db: db, authMiddleware: authMiddleware} +func NewHTTPHandler(accountService *Service, authService *auth.Service, db *bun.DB, authMiddleware fiber.Handler, cookieConfig auth.CookieConfig) *HTTPHandler { + return &HTTPHandler{accountService: accountService, authService: authService, db: db, authMiddleware: authMiddleware, cookieConfig: cookieConfig} } func (h *HTTPHandler) RegisterRoutes(api fiber.Router) fiber.Router { @@ -126,13 +129,13 @@ func (h *HTTPHandler) getAccount(c *fiber.Ctx) error { // 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. +// @Description Create a new user account with email and password. Returns the account, user, and authentication tokens. Tokens can be delivered via HTTP-only cookies or in the response body based on the tokenDelivery field. // @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 400 {string} string "Invalid request body or token delivery method" // @Failure 409 {string} string "Email already registered" // @Router /accounts [post] func (h *HTTPHandler) registerAccount(c *fiber.Ctx) error { @@ -173,10 +176,23 @@ func (h *HTTPHandler) registerAccount(c *fiber.Ctx) error { return httperr.Internal(err) } - return c.JSON(registerAccountResponse{ - Account: acc, - User: u, - AccessToken: result.AccessToken, - RefreshToken: result.RefreshToken, - }) + switch req.TokenDelivery { + default: + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid token delivery method"}) + + case auth.TokenDeliveryCookie: + auth.SetAuthCookies(c, result.AccessToken, result.RefreshToken, h.cookieConfig) + return c.JSON(registerAccountResponse{ + Account: acc, + User: u, + }) + + case auth.TokenDeliveryBody: + return c.JSON(registerAccountResponse{ + Account: acc, + User: u, + AccessToken: result.AccessToken, + RefreshToken: result.RefreshToken, + }) + } } diff --git a/apps/backend/internal/auth/cookies.go b/apps/backend/internal/auth/cookies.go index 6948708..e5f2eee 100644 --- a/apps/backend/internal/auth/cookies.go +++ b/apps/backend/internal/auth/cookies.go @@ -32,10 +32,10 @@ func authCookies(c *fiber.Ctx) map[string]string { return m } -// setAuthCookies sets HTTP-only auth cookies with security settings derived from the request. +// SetAuthCookies sets HTTP-only auth cookies with security settings derived from the request. // Secure flag is based on actual protocol (works automatically with proxies/tunnels), // unless explicitly set in cfg.Secure. -func setAuthCookies(c *fiber.Ctx, accessToken, refreshToken string, cfg CookieConfig) { +func SetAuthCookies(c *fiber.Ctx, accessToken, refreshToken string, cfg CookieConfig) { secure := c.Protocol() == "https" accessTokenCookie := &fiber.Cookie{ diff --git a/apps/backend/internal/auth/http.go b/apps/backend/internal/auth/http.go index 4544053..0109ff8 100644 --- a/apps/backend/internal/auth/http.go +++ b/apps/backend/internal/auth/http.go @@ -11,8 +11,8 @@ import ( ) const ( - tokenDeliveryCookie = "cookie" - tokenDeliveryBody = "body" + TokenDeliveryCookie = "cookie" + TokenDeliveryBody = "body" ) const ( @@ -113,13 +113,13 @@ func (h *HTTPHandler) Login(c *fiber.Ctx) error { default: return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid token delivery method"}) - case tokenDeliveryCookie: - setAuthCookies(c, result.AccessToken, result.RefreshToken, h.cookieConfig) + case TokenDeliveryCookie: + SetAuthCookies(c, result.AccessToken, result.RefreshToken, h.cookieConfig) return c.JSON(loginResponse{ User: *result.User, }) - case tokenDeliveryBody: + case TokenDeliveryBody: return c.JSON(loginResponse{ User: *result.User, AccessToken: result.AccessToken, diff --git a/apps/backend/internal/auth/middleware.go b/apps/backend/internal/auth/middleware.go index b9088a7..8404751 100644 --- a/apps/backend/internal/auth/middleware.go +++ b/apps/backend/internal/auth/middleware.go @@ -58,7 +58,7 @@ func NewAuthMiddleware(s *Service, db *bun.DB, cookieConfig CookieConfig) fiber. return c.SendStatus(fiber.StatusUnauthorized) } - setAuthCookies(c, newTokens.AccessToken, newTokens.RefreshToken, cookieConfig) + SetAuthCookies(c, newTokens.AccessToken, newTokens.RefreshToken, cookieConfig) at = newTokens.AccessToken rt = newTokens.RefreshToken } @@ -90,7 +90,7 @@ func NewAuthMiddleware(s *Service, db *bun.DB, cookieConfig CookieConfig) fiber. newTokens, err := s.RefreshAccessToken(c.Context(), tx, rt) if err == nil { if commitErr := tx.Commit(); commitErr == nil { - setAuthCookies(c, newTokens.AccessToken, newTokens.RefreshToken, cookieConfig) + SetAuthCookies(c, newTokens.AccessToken, newTokens.RefreshToken, cookieConfig) } } else { _ = tx.Rollback() diff --git a/apps/backend/internal/drexa/server.go b/apps/backend/internal/drexa/server.go index 2c4f29a..af5b8bb 100644 --- a/apps/backend/internal/drexa/server.go +++ b/apps/backend/internal/drexa/server.go @@ -113,7 +113,7 @@ func NewServer(c Config) (*Server, error) { auth.NewHTTPHandler(authService, db, cookieConfig).RegisterRoutes(api) user.NewHTTPHandler(userService, db, authMiddleware).RegisterRoutes(api) - accountRouter := account.NewHTTPHandler(accountService, authService, db, authMiddleware).RegisterRoutes(api) + accountRouter := account.NewHTTPHandler(accountService, authService, db, authMiddleware, cookieConfig).RegisterRoutes(api) upload.NewHTTPHandler(uploadService, db).RegisterRoutes(accountRouter) catalog.NewHTTPHandler(vfs, db).RegisterRoutes(accountRouter)