Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions internal/dao/langfuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed 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 dao

import (
"gorm.io/gorm/clause"

"ragflow/internal/entity"
)

// LangfuseDAO data access for tenant_langfuse table.
type LangfuseDAO struct{}

// NewLangfuseDAO creates a LangfuseDAO.
func NewLangfuseDAO() *LangfuseDAO {
return &LangfuseDAO{}
}

// GetByTenantID returns the Langfuse credential record for a tenant, or nil
// when none exists.
func (d *LangfuseDAO) GetByTenantID(tenantID string) (*entity.TenantLangfuse, error) {
var entry entity.TenantLangfuse
err := DB.Where("tenant_id = ?", tenantID).First(&entry).Error
if err != nil {
return nil, err
}
return &entry, nil
}

// Create inserts a new TenantLangfuse record.
func (d *LangfuseDAO) Create(entry *entity.TenantLangfuse) error {
return DB.Create(entry).Error
}

// UpdateByTenantID applies updates to the record for a tenant.
func (d *LangfuseDAO) UpdateByTenantID(tenantID string, updates map[string]interface{}) error {
return DB.Model(&entity.TenantLangfuse{}).
Where("tenant_id = ?", tenantID).
Updates(updates).Error
}

// UpsertByTenantID atomically inserts the record or, when a row already exists
// for the same tenant_id (unique index), updates its credentials in a single
// statement. This removes the read-then-write race between concurrent callers.
func (d *LangfuseDAO) UpsertByTenantID(entry *entity.TenantLangfuse) error {
return DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "tenant_id"}},
DoUpdates: clause.AssignmentColumns([]string{
"secret_key", "public_key", "host", "update_time", "update_date",
}),
}).Create(entry).Error
}

// DeleteByTenantID hard-deletes the record for a tenant.
func (d *LangfuseDAO) DeleteByTenantID(tenantID string) error {
return DB.Unscoped().
Where("tenant_id = ?", tenantID).
Delete(&entity.TenantLangfuse{}).Error
}
34 changes: 34 additions & 0 deletions internal/entity/langfuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed 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 entity

// TenantLangfuse stores per-tenant Langfuse credentials.
type TenantLangfuse struct {
ID string `gorm:"column:id;primaryKey;size:32" json:"id"`
TenantID string `gorm:"column:tenant_id;size:32;not null;uniqueIndex" json:"tenant_id"`
// SecretKey is never serialised to JSON (json:"-") so it cannot leak through
// any response that marshals the entity directly.
SecretKey string `gorm:"column:secret_key;size:255;not null" json:"-"`
PublicKey string `gorm:"column:public_key;size:255;not null" json:"public_key"`
Host string `gorm:"column:host;size:255;not null" json:"host"`
BaseModel
}

// TableName maps to the tenant_langfuse table (matches Python model).
func (TenantLangfuse) TableName() string {
return "tenant_langfuse"
}
125 changes: 125 additions & 0 deletions internal/handler/langfuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed 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 handler

import (
"net/http"

"github.com/gin-gonic/gin"

"ragflow/internal/common"
"ragflow/internal/service"
)

// LangfuseHandler manages Langfuse credential endpoints.
type LangfuseHandler struct {
langfuseService *service.LangfuseService
}

// NewLangfuseHandler creates a LangfuseHandler.
func NewLangfuseHandler() *LangfuseHandler {
return &LangfuseHandler{langfuseService: service.NewLangfuseService()}
}

// SetAPIKey handles POST/PUT /api/v1/langfuse/api-key.
// Validates the supplied keys against Langfuse, then upserts the record.
// Secret key is stored but not echoed back to the caller.
// @Summary Set Langfuse API key
// @Description Create or update the Langfuse credentials for the current tenant.
// @Tags langfuse
// @Accept json
// @Produce json
// @Param request body service.SetAPIKeyRequest true "Langfuse credentials"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/langfuse/api-key [post]
func (h *LangfuseHandler) SetAPIKey(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
jsonError(c, code, msg)
return
}

var req service.SetAPIKeyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
return
}

data, err := h.langfuseService.SetAPIKey(user.ID, &req)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": data, "message": "success"})
}

// GetAPIKey handles GET /api/v1/langfuse/api-key.
// Returns stored metadata and Langfuse project info; secret_key is never returned.
// @Summary Get Langfuse API key info
// @Description Retrieve the stored Langfuse credentials (without secret_key) and project info.
// @Tags langfuse
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/langfuse/api-key [get]
func (h *LangfuseHandler) GetAPIKey(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
jsonError(c, code, msg)
return
}

data, err := h.langfuseService.GetAPIKey(user.ID)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
return
}
if data == nil {
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": nil, "message": "Have not record any Langfuse keys."})
return
}

c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": data, "message": "success"})
}

// DeleteAPIKey handles DELETE /api/v1/langfuse/api-key.
// Removes the stored Langfuse credentials for the current tenant.
// @Summary Delete Langfuse API key
// @Description Remove the stored Langfuse credentials for the current tenant.
// @Tags langfuse
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/langfuse/api-key [delete]
func (h *LangfuseHandler) DeleteAPIKey(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
jsonError(c, code, msg)
return
}

deleted, err := h.langfuseService.DeleteAPIKey(user.ID)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
return
}
if !deleted {
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": nil, "message": "Have not record any Langfuse keys."})
return
}

c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": true, "message": "success"})
}
11 changes: 11 additions & 0 deletions internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Router struct {
providerHandler *handler.ProviderHandler
agentHandler *handler.AgentHandler
relatedQuestionsHandler *handler.SearchbotHandler
langfuseHandler *handler.LangfuseHandler
}

// NewRouter create router
Expand Down Expand Up @@ -89,6 +90,7 @@ func NewRouter(
providerHandler: providerHandler,
agentHandler: agentHandler,
relatedQuestionsHandler: relatedQuestionsHandler,
langfuseHandler: handler.NewLangfuseHandler(),
}
}

Expand Down Expand Up @@ -381,6 +383,15 @@ func (r *Router) Setup(engine *gin.Engine) {

}

// Langfuse credential management routes.
langfuse := v1.Group("/langfuse")
{
langfuse.POST("/api-key", r.langfuseHandler.SetAPIKey)
langfuse.PUT("/api-key", r.langfuseHandler.SetAPIKey)
langfuse.GET("/api-key", r.langfuseHandler.GetAPIKey)
langfuse.DELETE("/api-key", r.langfuseHandler.DeleteAPIKey)
}

connector := v1.Group("/connectors")
{
connector.GET("/", r.connectorHandler.ListConnectors)
Expand Down
Loading