This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/answer.git

commit 90022ce039b2973a12481117f92a3180ee92c7f3
Author: LinkinStars <[email protected]>
AuthorDate: Thu Jan 29 16:48:35 2026 +0800

    fix: integrate API key authentication into existing services and routes
---
 cmd/wire_gen.go                          |  6 ++--
 internal/base/middleware/api_key_auth.go | 51 ++++++++++++++++++++++++++++++++
 internal/base/server/http.go             |  5 ++++
 internal/cli/reset_password.go           |  4 ++-
 internal/migrations/init.go              | 28 ++++++++++++++++++
 internal/migrations/init_data.go         |  3 ++
 internal/router/answer_api_router.go     |  3 ++
 internal/router/mcp_router.go            | 44 +++++++++++++++++++++++++++
 internal/service/auth/auth.go            | 26 ++++++++++++++--
 9 files changed, 163 insertions(+), 7 deletions(-)

diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index 24d32f7e..9fe134ed 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -149,7 +149,8 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        siteInfoCommonService := 
siteinfo_common.NewSiteInfoCommonService(siteInfoRepo)
        langController := controller.NewLangController(i18nTranslator, 
siteInfoCommonService)
        authRepo := auth.NewAuthRepo(dataData)
-       authService := auth2.NewAuthService(authRepo)
+       apiKeyRepo := api_key.NewAPIKeyRepo(dataData)
+       authService := auth2.NewAuthService(authRepo, apiKeyRepo)
        userRepo := user.NewUserRepo(dataData)
        uniqueIDRepo := unique.NewUniqueIDRepo(dataData)
        configRepo := config.NewConfigRepo(dataData)
@@ -279,7 +280,6 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, 
badgeAwardRepo, badgeEventService, siteInfoCommonService)
        badgeController := controller.NewBadgeController(badgeService, 
badgeAwardService)
        controller_adminBadgeController := 
controller_admin.NewBadgeController(badgeService)
-       apiKeyRepo := api_key.NewAPIKeyRepo(dataData)
        apiKeyService := apikey.NewAPIKeyService(apiKeyRepo)
        adminAPIKeyController := 
controller_admin.NewAdminAPIKeyController(apiKeyService)
        featureToggleService := 
feature_toggle.NewFeatureToggleService(siteInfoRepo)
@@ -289,7 +289,7 @@ func initApplication(debug bool, serverConf *conf.Server, 
dbConf *data.Database,
        aiController := controller.NewAIController(searchService, 
siteInfoCommonService, tagCommonService, questionCommon, commentRepo, 
userCommon, answerRepo, mcpController, aiConversationService, 
featureToggleService)
        aiConversationController := 
controller.NewAIConversationController(aiConversationService, 
featureToggleService)
        aiConversationAdminController := 
controller_admin.NewAIConversationAdminController(aiConversationService, 
featureToggleService)
-       answerAPIRouter := router.NewAnswerAPIRouter(langController, 
userController, commentController, reportController, voteController, 
tagController, followController, collectionController, questionController, 
answerController, searchController, revisionController, rankController, 
userAdminController, reasonController, themeController, siteInfoController, 
controllerSiteInfoController, notificationController, dashboardController, 
uploadController, activityController, roleController, pluginCon [...]
+       answerAPIRouter := router.NewAnswerAPIRouter(langController, 
userController, commentController, reportController, voteController, 
tagController, followController, collectionController, questionController, 
answerController, searchController, revisionController, rankController, 
userAdminController, reasonController, themeController, siteInfoController, 
controllerSiteInfoController, notificationController, dashboardController, 
uploadController, activityController, roleController, pluginCon [...]
        swaggerRouter := router.NewSwaggerRouter(swaggerConf)
        uiRouter := router.NewUIRouter(controllerSiteInfoController, 
siteInfoCommonService)
        authUserMiddleware := middleware.NewAuthUserMiddleware(authService, 
siteInfoCommonService)
diff --git a/internal/base/middleware/api_key_auth.go 
b/internal/base/middleware/api_key_auth.go
new file mode 100644
index 00000000..cc8182d4
--- /dev/null
+++ b/internal/base/middleware/api_key_auth.go
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package middleware
+
+import (
+       "github.com/apache/answer/internal/base/handler"
+       "github.com/apache/answer/internal/base/reason"
+       "github.com/gin-gonic/gin"
+       "github.com/segmentfault/pacman/errors"
+)
+
+// AuthAPIKey middleware to authenticate API key
+func (am *AuthUserMiddleware) AuthAPIKey() gin.HandlerFunc {
+       return func(ctx *gin.Context) {
+               token := ExtractToken(ctx)
+               if len(token) == 0 {
+                       handler.HandleResponse(ctx, 
errors.Unauthorized(reason.UnauthorizedError), nil)
+                       ctx.Abort()
+                       return
+               }
+               pass, err := am.authService.AuthAPIKey(ctx, ctx.Request.Method 
== "GET", token)
+               if err != nil {
+                       handler.HandleResponse(ctx, 
errors.Unauthorized(reason.UnauthorizedError), nil)
+                       ctx.Abort()
+                       return
+               }
+               if !pass {
+                       handler.HandleResponse(ctx, 
errors.Unauthorized(reason.UnauthorizedError), nil)
+                       ctx.Abort()
+                       return
+               }
+               ctx.Next()
+       }
+}
diff --git a/internal/base/server/http.go b/internal/base/server/http.go
index 050e3192..765cbf6b 100644
--- a/internal/base/server/http.go
+++ b/internal/base/server/http.go
@@ -113,5 +113,10 @@ func NewHTTPServer(debug bool,
                agent.RegisterAuthAdminRouter(adminauthV1)
                return nil
        })
+
+       // mcp
+       mcpAPIGroup := r.Group(uiConf.APIBaseURL + "/answer/api/v1")
+       mcpAPIGroup.Use(authUserMiddleware.AuthMcpEnable(), 
authUserMiddleware.AuthAPIKey())
+       answerRouter.RegisterMCPRouter(mcpAPIGroup)
        return r
 }
diff --git a/internal/cli/reset_password.go b/internal/cli/reset_password.go
index dbb3422a..00ffe911 100644
--- a/internal/cli/reset_password.go
+++ b/internal/cli/reset_password.go
@@ -32,6 +32,7 @@ import (
        "github.com/apache/answer/internal/base/conf"
        "github.com/apache/answer/internal/base/data"
        "github.com/apache/answer/internal/base/path"
+       "github.com/apache/answer/internal/repo/api_key"
        "github.com/apache/answer/internal/repo/auth"
        "github.com/apache/answer/internal/repo/user"
        authService "github.com/apache/answer/internal/service/auth"
@@ -95,7 +96,8 @@ func ResetPassword(ctx context.Context, dataDirPath string, 
opts *ResetPasswordO
 
        userRepo := user.NewUserRepo(dataData)
        authRepo := auth.NewAuthRepo(dataData)
-       authSvc := authService.NewAuthService(authRepo)
+       apiKeyRepo := api_key.NewAPIKeyRepo(dataData)
+       authSvc := authService.NewAuthService(authRepo, apiKeyRepo)
 
        email := strings.TrimSpace(opts.Email)
        if email == "" {
diff --git a/internal/migrations/init.go b/internal/migrations/init.go
index ae2eeb9a..9dbe6eb8 100644
--- a/internal/migrations/init.go
+++ b/internal/migrations/init.go
@@ -87,6 +87,8 @@ func (m *Mentor) InitDB() error {
        m.do("init site info security", m.initSiteInfoSecurityConfig)
        m.do("init default content", m.initDefaultContent)
        m.do("init default badges", m.initDefaultBadges)
+       m.do("init default ai config", m.initSiteInfoAI)
+       m.do("init default MCP config", m.initSiteInfoMCP)
        return m.err
 }
 
@@ -606,3 +608,29 @@ func (m *Mentor) initDefaultBadges() {
                }
        }
 }
+
+func (m *Mentor) initSiteInfoAI() {
+       content := &schema.SiteAIReq{
+               PromptConfig: &schema.AIPromptConfig{
+                       ZhCN: constant.DefaultAIPromptConfigZhCN,
+                       EnUS: constant.DefaultAIPromptConfigEnUS,
+               },
+       }
+       writeDataBytes, _ := json.Marshal(content)
+       _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
+               Type:    constant.SiteTypeAI,
+               Content: string(writeDataBytes),
+               Status:  1,
+       })
+}
+func (m *Mentor) initSiteInfoMCP() {
+       content := &schema.SiteMCPReq{
+               Enabled: true,
+       }
+       writeDataBytes, _ := json.Marshal(content)
+       _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
+               Type:    constant.SiteTypeMCP,
+               Content: string(writeDataBytes),
+               Status:  1,
+       })
+}
diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go
index 356a915a..daef5bb0 100644
--- a/internal/migrations/init_data.go
+++ b/internal/migrations/init_data.go
@@ -76,6 +76,9 @@ var (
                &entity.BadgeAward{},
                &entity.FileRecord{},
                &entity.PluginKVStorage{},
+               &entity.APIKey{},
+               &entity.AIConversation{},
+               &entity.AIConversationRecord{},
        }
 
        roles = []*entity.Role{
diff --git a/internal/router/answer_api_router.go 
b/internal/router/answer_api_router.go
index c642b2a1..84b8b4e1 100644
--- a/internal/router/answer_api_router.go
+++ b/internal/router/answer_api_router.go
@@ -61,6 +61,7 @@ type AnswerAPIRouter struct {
        aiController                  *controller.AIController
        aiConversationController      *controller.AIConversationController
        aiConversationAdminController 
*controller_admin.AIConversationAdminController
+       mcpController                 *controller.MCPController
 }
 
 func NewAnswerAPIRouter(
@@ -98,6 +99,7 @@ func NewAnswerAPIRouter(
        aiController *controller.AIController,
        aiConversationController *controller.AIConversationController,
        aiConversationAdminController 
*controller_admin.AIConversationAdminController,
+       mcpController *controller.MCPController,
 ) *AnswerAPIRouter {
        return &AnswerAPIRouter{
                langController:                langController,
@@ -134,6 +136,7 @@ func NewAnswerAPIRouter(
                aiController:                  aiController,
                aiConversationController:      aiConversationController,
                aiConversationAdminController: aiConversationAdminController,
+               mcpController:                 mcpController,
        }
 }
 
diff --git a/internal/router/mcp_router.go b/internal/router/mcp_router.go
new file mode 100644
index 00000000..54f41971
--- /dev/null
+++ b/internal/router/mcp_router.go
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package router
+
+import (
+       "github.com/apache/answer/internal/schema/mcp_tools"
+       "github.com/gin-gonic/gin"
+       "github.com/mark3labs/mcp-go/server"
+)
+
+func (a *AnswerAPIRouter) RegisterMCPRouter(r *gin.RouterGroup) {
+       s := server.NewMCPServer("Answer Enterprise MCP Server", "1.0.0")
+
+       s.AddTool(mcp_tools.NewQuestionsTool(), 
a.mcpController.MCPQuestionsHandler())
+       s.AddTool(mcp_tools.NewAnswersTool(), 
a.mcpController.MCPAnswersHandler())
+       s.AddTool(mcp_tools.NewCommentsTool(), 
a.mcpController.MCPCommentsHandler())
+       s.AddTool(mcp_tools.NewTagsTool(), a.mcpController.MCPTagsHandler())
+       s.AddTool(mcp_tools.NewTagDetailTool(), 
a.mcpController.MCPTagDetailsHandler())
+       s.AddTool(mcp_tools.NewUserTool(), 
a.mcpController.MCPUserDetailsHandler())
+
+       sseServer := server.NewSSEServer(s,
+               server.WithSSEEndpoint("/answer/api/v1/mcp/see"),
+               server.WithMessageEndpoint("/answer/api/v1/mcp/message"),
+       )
+       r.GET("/mcp/sse", gin.WrapH(sseServer.SSEHandler()))
+       r.POST("/mcp/message", gin.WrapH(sseServer.MessageHandler()))
+}
diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go
index b94f7912..8f539bf1 100644
--- a/internal/service/auth/auth.go
+++ b/internal/service/auth/auth.go
@@ -23,8 +23,10 @@ import (
        "context"
 
        "github.com/apache/answer/internal/entity"
+       "github.com/apache/answer/internal/service/apikey"
        "github.com/apache/answer/pkg/token"
        "github.com/apache/answer/plugin"
+       "github.com/segmentfault/pacman/log"
 )
 
 // AuthRepo auth repository
@@ -46,13 +48,15 @@ type AuthRepo interface {
 
 // AuthService kit service
 type AuthService struct {
-       authRepo AuthRepo
+       authRepo   AuthRepo
+       apiKeyRepo apikey.APIKeyRepo
 }
 
 // NewAuthService email service
-func NewAuthService(authRepo AuthRepo) *AuthService {
+func NewAuthService(authRepo AuthRepo, apiKeyRepo apikey.APIKeyRepo) 
*AuthService {
        return &AuthService{
-               authRepo: authRepo,
+               authRepo:   authRepo,
+               apiKeyRepo: apiKeyRepo,
        }
 }
 
@@ -152,3 +156,19 @@ func (as *AuthService) SetAdminUserCacheInfo(ctx 
context.Context, accessToken st
 func (as *AuthService) RemoveAdminUserCacheInfo(ctx context.Context, 
accessToken string) (err error) {
        return as.authRepo.RemoveAdminUserCacheInfo(ctx, accessToken)
 }
+func (as *AuthService) AuthAPIKey(ctx context.Context, read bool, apiKey 
string) (pass bool, err error) {
+       apiKeyInfo, exist, err := as.apiKeyRepo.GetAPIKey(ctx, apiKey)
+       if err != nil {
+               return false, err
+       }
+       if !exist {
+               return false, nil
+       }
+       // If the request is not read-only, check if the API key has write 
permissions
+       if !read && apiKeyInfo.Scope == "read-only" {
+               log.Warnf("API key %s does not have write permissions", 
apiKeyInfo.AccessKey)
+               return false, nil
+       }
+       log.Infof("API key %s is valid, scope: %s", apiKeyInfo.AccessKey, 
apiKeyInfo.Scope)
+       return true, nil
+}

Reply via email to