Skip to content

Commit 2346685

Browse files
committed
feat[Go]: implement Box OAuth connector start/callback/result APIs
Ports the full Box OAuth web flow from Python (connector_api.py) to Go, addressing issue #15662 and the review on PR #15664. POST /api/v1/connectors/box/oauth/web/start (StartBoxWebOAuth) GET /api/v1/connectors/box/oauth/web/callback (BoxWebOAuthCallback) POST /api/v1/connectors/box/oauth/web/result (PollBoxWebOAuthResult) Review fix (Hz-186): the /start endpoint was missing, so the frontend could not initiate the flow (no flow_id generated, no initial state written to Redis, 404 on the Go backend). Implemented StartBoxWebOAuth for full parity with Python start_box_web_oauth: - validates client_id / client_secret (ARGUMENT_ERROR when missing) - resolves redirect_uri (request value or BOX_WEB_OAUTH_REDIRECT_URI) - generates a flow_id, builds the Box authorize URL (https://account.box.com/api/oauth2/authorize?...&state=flow_id) - writes the initial boxWebOAuthState to Redis under the box flow-state key with the standard web-flow TTL - returns {flow_id, authorization_url, expires_in} Wired the authenticated POST route in the /api/v1/connectors group and added the service-interface method on the handler. Also addressed CodeRabbit's docstring-coverage warning: documented the Box request/response/state types, the new start + URL-builder helpers, and the previously-undocumented boxOAuthTokenResponse and defaultBoxWebOAuthRedirectURI.
1 parent 35527f6 commit 2346685

3 files changed

Lines changed: 379 additions & 1 deletion

File tree

internal/handler/connector.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ type connectorServiceIface interface {
4242
StartGoogleWebOAuth(userID, source string, req *service.StartGoogleWebOAuthRequest) (*service.StartGoogleWebOAuthResponse, common.ErrorCode, error)
4343
GoogleWebOAuthCallback(source, stateID, oauthError, errorDescription, code string) string
4444
PollGoogleWebOAuthResult(userID, source string, req *service.PollGoogleWebOAuthResultRequest) (*service.PollGoogleWebOAuthResultResponse, common.ErrorCode, error)
45+
StartBoxWebOAuth(userID string, req *service.StartBoxWebOAuthRequest) (*service.StartBoxWebOAuthResponse, common.ErrorCode, error)
46+
BoxWebOAuthCallback(stateID, oauthError, errorDescription, code string) string
47+
PollBoxWebOAuthResult(userID string, req *service.PollBoxWebOAuthResultRequest) (*service.PollBoxWebOAuthResultResponse, common.ErrorCode, error)
4548
}
4649

4750
// ConnectorHandler connector handler
@@ -504,3 +507,107 @@ func (h *ConnectorHandler) googleWebOAuthCallback(c *gin.Context, source string)
504507
)
505508
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
506509
}
510+
511+
// StartBoxWebOAuth initiates the Box OAuth web flow for the authenticated user.
512+
// It returns the authorization URL the frontend should open along with the flow
513+
// ID and expiry, and writes the initial flow state to Redis.
514+
// @Summary Start Box OAuth Web Flow
515+
// @Description Begin the Box OAuth web flow: generate a flow ID, build the Box
516+
// authorization URL, and persist the initial state in Redis.
517+
// @Tags connector
518+
// @Accept json
519+
// @Produce json
520+
// @Param request body service.StartBoxWebOAuthRequest true "Box client credentials"
521+
// @Success 200 {object} map[string]interface{}
522+
// @Router /api/v1/connectors/box/oauth/web/start [post]
523+
func (h *ConnectorHandler) StartBoxWebOAuth(c *gin.Context) {
524+
user, errorCode, errorMessage := GetUser(c)
525+
if errorCode != common.CodeSuccess {
526+
jsonError(c, errorCode, errorMessage)
527+
return
528+
}
529+
530+
var req service.StartBoxWebOAuthRequest
531+
if err := c.ShouldBindJSON(&req); err != nil {
532+
c.JSON(http.StatusBadRequest, gin.H{
533+
"code": common.CodeBadRequest,
534+
"data": nil,
535+
"message": err.Error(),
536+
})
537+
return
538+
}
539+
540+
data, code, err := h.connectorService.StartBoxWebOAuth(user.ID, &req)
541+
if err != nil {
542+
jsonError(c, code, err.Error())
543+
return
544+
}
545+
546+
c.JSON(http.StatusOK, gin.H{
547+
"code": common.CodeSuccess,
548+
"data": data,
549+
"message": "success",
550+
})
551+
}
552+
553+
// BoxWebOAuthCallback handles the redirect from Box after the user grants access.
554+
// This endpoint is public (no auth middleware) — Box redirects the user's browser here.
555+
// @Summary Box OAuth Web Callback
556+
// @Description Receives the authorization code from Box, exchanges it for tokens,
557+
// stores the result in Redis, and renders a self-closing popup page.
558+
// @Tags connector
559+
// @Produce text/html
560+
// @Param state query string true "OAuth state (flow ID)"
561+
// @Param code query string false "Authorization code"
562+
// @Param error query string false "Error code from Box"
563+
// @Param error_description query string false "Human-readable error from Box"
564+
// @Router /api/v1/connectors/box/oauth/web/callback [get]
565+
func (h *ConnectorHandler) BoxWebOAuthCallback(c *gin.Context) {
566+
htmlPage := h.connectorService.BoxWebOAuthCallback(
567+
c.Query("state"),
568+
c.Query("error"),
569+
c.Query("error_description"),
570+
c.Query("code"),
571+
)
572+
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlPage))
573+
}
574+
575+
// PollBoxWebOAuthResult polls for the result of a Box OAuth web flow.
576+
// @Summary Poll Box OAuth Result
577+
// @Description Check whether the Box OAuth callback has completed and retrieve the credentials.
578+
// Returns code 106 (RUNNING) while authorization is still pending.
579+
// @Tags connector
580+
// @Accept json
581+
// @Produce json
582+
// @Param body body service.PollBoxWebOAuthResultRequest true "Flow ID"
583+
// @Success 200 {object} map[string]interface{}
584+
// @Router /api/v1/connectors/box/oauth/web/result [post]
585+
func (h *ConnectorHandler) PollBoxWebOAuthResult(c *gin.Context) {
586+
user, errorCode, errorMessage := GetUser(c)
587+
if errorCode != common.CodeSuccess {
588+
jsonError(c, errorCode, errorMessage)
589+
return
590+
}
591+
592+
var req service.PollBoxWebOAuthResultRequest
593+
if err := c.ShouldBindJSON(&req); err != nil {
594+
c.JSON(http.StatusBadRequest, gin.H{
595+
"code": common.CodeBadRequest,
596+
"data": nil,
597+
"message": err.Error(),
598+
})
599+
return
600+
}
601+
602+
data, code, err := h.connectorService.PollBoxWebOAuthResult(user.ID, &req)
603+
if err != nil {
604+
jsonError(c, code, err.Error())
605+
return
606+
}
607+
608+
c.JSON(http.StatusOK, gin.H{
609+
"code": common.CodeSuccess,
610+
"data": data,
611+
"message": "success",
612+
})
613+
}

internal/router/router.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func (r *Router) Setup(engine *gin.Engine) {
123123
// the RAGFlow auth middleware.
124124
engine.GET("/connectors/gmail/oauth/web/callback", r.connectorHandler.GmailWebOAuthCallback)
125125
engine.GET("/connectors/google-drive/oauth/web/callback", r.connectorHandler.GoogleDriveWebOAuthCallback)
126+
engine.GET("/connectors/box/oauth/web/callback", r.connectorHandler.BoxWebOAuthCallback)
126127

127128
apiNoAuth := engine.Group("/api/v1")
128129
{
@@ -150,9 +151,11 @@ func (r *Router) Setup(engine *gin.Engine) {
150151
// Document images are embedded directly in pages and match Python's public route.
151152
apiNoAuth.GET("/documents/images/:image_id", r.documentHandler.GetDocumentImage)
152153

153-
// Google redirects here after Gmail / Google Drive web OAuth completes.
154+
// OAuth callbacks — Gmail, Google Drive, and Box redirect the user's browser here;
155+
// no auth middleware is applied on this group.
154156
apiNoAuth.GET("/connectors/gmail/oauth/web/callback", r.connectorHandler.GmailWebOAuthCallback)
155157
apiNoAuth.GET("/connectors/google-drive/oauth/web/callback", r.connectorHandler.GoogleDriveWebOAuthCallback)
158+
apiNoAuth.GET("/connectors/box/oauth/web/callback", r.connectorHandler.BoxWebOAuthCallback)
156159
}
157160

158161
// Protected routes
@@ -398,6 +401,8 @@ func (r *Router) Setup(engine *gin.Engine) {
398401
connector.POST("/", r.connectorHandler.CreateConnector)
399402
connector.POST("/google/oauth/web/start", r.connectorHandler.StartGoogleWebOAuth)
400403
connector.POST("/google/oauth/web/result", r.connectorHandler.PollGoogleWebOAuthResult)
404+
connector.POST("/box/oauth/web/start", r.connectorHandler.StartBoxWebOAuth)
405+
connector.POST("/box/oauth/web/result", r.connectorHandler.PollBoxWebOAuthResult)
401406
connector.GET("/:connector_id", r.connectorHandler.GetConnector)
402407
connector.GET("/:connector_id/logs", r.connectorHandler.ListLogs)
403408
connector.DELETE("/:connector_id", r.connectorHandler.DeleteConnector)

0 commit comments

Comments
 (0)