mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-03-19 05:37:51 +08:00
### What problem does this PR solve? 1. Fix go server date precision 2. Use API_SCHEME_PROXY to control the web API route ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
1674 lines
46 KiB
Go
1674 lines
46 KiB
Go
//
|
|
// 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 admin
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"ragflow/internal/cache"
|
|
"ragflow/internal/common"
|
|
"ragflow/internal/dao"
|
|
"ragflow/internal/engine/elasticsearch"
|
|
"ragflow/internal/logger"
|
|
"ragflow/internal/model"
|
|
"ragflow/internal/server"
|
|
"ragflow/internal/utility"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Service errors
|
|
var (
|
|
ErrInvalidToken = errors.New("invalid token")
|
|
ErrNotAdmin = errors.New("user is not admin")
|
|
ErrUserInactive = errors.New("user is inactive")
|
|
)
|
|
|
|
// Service admin service layer
|
|
type Service struct {
|
|
userDAO *dao.UserDAO
|
|
licenseDAO *dao.LicenseDAO
|
|
timeRecordDAO *dao.TimeRecordDAO
|
|
systemSettingsDAO *dao.SystemSettingsDAO
|
|
tenantDAO *dao.TenantDAO
|
|
userTenantDAO *dao.UserTenantDAO
|
|
tenantLLMDAO *dao.TenantLLMDAO
|
|
fileDAO *dao.FileDAO
|
|
documentDAO *dao.DocumentDAO
|
|
taskDAO *dao.TaskDAO
|
|
kbDAO *dao.KnowledgebaseDAO
|
|
canvasDAO *dao.UserCanvasDAO
|
|
chatDAO *dao.ChatDAO
|
|
chatSessionDAO *dao.ChatSessionDAO
|
|
apiTokenDAO *dao.APITokenDAO
|
|
api4ConvDAO *dao.API4ConversationDAO
|
|
llmDAO *dao.LLMDAO
|
|
}
|
|
|
|
// NewService create admin service
|
|
func NewService() *Service {
|
|
return &Service{
|
|
userDAO: dao.NewUserDAO(),
|
|
licenseDAO: dao.NewLicenseDAO(),
|
|
timeRecordDAO: dao.NewTimeRecordDAO(),
|
|
systemSettingsDAO: dao.NewSystemSettingsDAO(),
|
|
tenantDAO: dao.NewTenantDAO(),
|
|
userTenantDAO: dao.NewUserTenantDAO(),
|
|
tenantLLMDAO: dao.NewTenantLLMDAO(),
|
|
fileDAO: dao.NewFileDAO(),
|
|
documentDAO: dao.NewDocumentDAO(),
|
|
taskDAO: dao.NewTaskDAO(),
|
|
kbDAO: dao.NewKnowledgebaseDAO(),
|
|
canvasDAO: dao.NewUserCanvasDAO(),
|
|
chatDAO: dao.NewChatDAO(),
|
|
chatSessionDAO: dao.NewChatSessionDAO(),
|
|
apiTokenDAO: dao.NewAPITokenDAO(),
|
|
api4ConvDAO: dao.NewAPI4ConversationDAO(),
|
|
llmDAO: dao.NewLLMDAO(),
|
|
}
|
|
}
|
|
|
|
// Logout user logout
|
|
func (s *Service) Logout(user interface{}) error {
|
|
// Invalidate token by setting it to INVALID_ prefix
|
|
if u, ok := user.(*model.User); ok {
|
|
invalidToken := "INVALID_" + generateRandomHex(16)
|
|
return s.userDAO.UpdateAccessToken(u, invalidToken)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetUserByToken get user by access token
|
|
func (s *Service) GetUserByToken(token string) (*model.User, error) {
|
|
user, err := s.userDAO.GetByAccessToken(token)
|
|
if err != nil {
|
|
return nil, ErrInvalidToken
|
|
}
|
|
|
|
if user.IsSuperuser == nil || !*user.IsSuperuser {
|
|
return nil, ErrNotAdmin
|
|
}
|
|
|
|
if user.IsActive != "1" {
|
|
return nil, fmt.Errorf("user inactive")
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// generateRandomHex generate random hex string
|
|
func generateRandomHex(n int) string {
|
|
bytes := make([]byte, n)
|
|
rand.Read(bytes)
|
|
return hex.EncodeToString(bytes)
|
|
}
|
|
|
|
// ListUsers list all users
|
|
func (s *Service) ListUsers() ([]map[string]interface{}, error) {
|
|
users, _, err := s.userDAO.List(0, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]map[string]interface{}, 0, len(users))
|
|
for _, user := range users {
|
|
result = append(result, map[string]interface{}{
|
|
"email": user.Email,
|
|
"nickname": user.Nickname,
|
|
"create_date": user.CreateTime,
|
|
"is_active": user.IsActive,
|
|
"is_superuser": user.IsSuperuser,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// CreateUser create a new user
|
|
// Parameters:
|
|
// - username: email address of the user
|
|
// - password: encrypted password (base64 encoded RSA encrypted)
|
|
// - role: user role ("user" or "admin")
|
|
//
|
|
// Returns:
|
|
// - map[string]interface{}: user information without password
|
|
// - error: error message
|
|
func (s *Service) CreateUser(username, password, role string) (map[string]interface{}, error) {
|
|
emailRegex := regexp.MustCompile(`^[\w\._-]+@([\w_-]+\.)+[\w-]{2,}$`)
|
|
if !emailRegex.MatchString(username) {
|
|
return nil, fmt.Errorf("Invalid email address: %s!", username)
|
|
}
|
|
|
|
existUser, _ := s.userDAO.GetByEmail(username)
|
|
if existUser != nil {
|
|
return nil, fmt.Errorf("User '%s' already exists", username)
|
|
}
|
|
|
|
decryptedPassword, err := DecryptPassword(password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt password: %w", err)
|
|
}
|
|
|
|
hashedPassword, err := GenerateWerkzeugPasswordHash(decryptedPassword, 150000)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
|
|
userID := utility.GenerateToken()
|
|
accessToken := utility.GenerateToken()
|
|
status := "1"
|
|
loginChannel := "password"
|
|
isSuperuser := role == "admin"
|
|
|
|
now := time.Now().Unix()
|
|
nowDate := time.Now().Truncate(time.Second)
|
|
|
|
user := &model.User{
|
|
ID: userID,
|
|
AccessToken: &accessToken,
|
|
Email: username,
|
|
Nickname: "",
|
|
Password: &hashedPassword,
|
|
Status: &status,
|
|
IsActive: "1",
|
|
IsAuthenticated: "1",
|
|
IsAnonymous: "0",
|
|
LoginChannel: &loginChannel,
|
|
IsSuperuser: &isSuperuser,
|
|
BaseModel: model.BaseModel{
|
|
CreateTime: &now,
|
|
CreateDate: &nowDate,
|
|
UpdateTime: &now,
|
|
UpdateDate: &nowDate,
|
|
},
|
|
}
|
|
|
|
// Start transaction for creating user and related data
|
|
tx := dao.DB.Begin()
|
|
if tx.Error != nil {
|
|
return nil, fmt.Errorf("failed to begin transaction: %w", tx.Error)
|
|
}
|
|
|
|
// Rollback helper function
|
|
rollbackTx := func() {
|
|
if rbErr := tx.Rollback(); rbErr.Error != nil {
|
|
logger.Error("failed to rollback transaction", rbErr.Error)
|
|
}
|
|
}
|
|
|
|
// 1. Create user
|
|
if err := tx.Create(user).Error; err != nil {
|
|
rollbackTx()
|
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
|
}
|
|
|
|
// 2. Create tenant (tenant_id = user_id)
|
|
// tenant name = nickname + "'s Kingdom" (same as Python)
|
|
tenantName := user.Nickname + "'s Kingdom"
|
|
|
|
// Get default model IDs from config
|
|
cfg := server.GetConfig()
|
|
chatMdl := ""
|
|
embdMdl := ""
|
|
asrMdl := ""
|
|
img2txtMdl := ""
|
|
rerankMdl := ""
|
|
parserIDs := "naive:General,qa:Q&A,resume:Resume,manual:Manual,table:Table,paper:Paper,book:Book,laws:Laws,presentation:Presentation,picture:Picture,one:One,audio:Audio,email:Email,tag:Tag"
|
|
|
|
if cfg != nil {
|
|
chatMdl = cfg.UserDefaultLLM.DefaultModels.ChatModel.Name
|
|
embdMdl = cfg.UserDefaultLLM.DefaultModels.EmbeddingModel.Name
|
|
asrMdl = cfg.UserDefaultLLM.DefaultModels.ASRModel.Name
|
|
img2txtMdl = cfg.UserDefaultLLM.DefaultModels.Image2TextModel.Name
|
|
rerankMdl = cfg.UserDefaultLLM.DefaultModels.RerankModel.Name
|
|
}
|
|
|
|
tenantStatus := "1"
|
|
tenant := &model.Tenant{
|
|
ID: userID,
|
|
Name: &tenantName,
|
|
LLMID: chatMdl,
|
|
EmbdID: embdMdl,
|
|
ASRID: asrMdl,
|
|
Img2TxtID: img2txtMdl,
|
|
RerankID: rerankMdl,
|
|
ParserIDs: parserIDs,
|
|
Credit: 512,
|
|
Status: &tenantStatus,
|
|
BaseModel: model.BaseModel{
|
|
CreateTime: &now,
|
|
CreateDate: &nowDate,
|
|
UpdateTime: &now,
|
|
UpdateDate: &nowDate,
|
|
},
|
|
}
|
|
if err := tx.Create(tenant).Error; err != nil {
|
|
rollbackTx()
|
|
return nil, fmt.Errorf("failed to create tenant: %w", err)
|
|
}
|
|
|
|
// 3. Create user-tenant relation
|
|
userTenantStatus := "1"
|
|
userTenant := &model.UserTenant{
|
|
ID: utility.GenerateToken(),
|
|
UserID: userID,
|
|
TenantID: userID,
|
|
Role: "owner",
|
|
InvitedBy: userID,
|
|
Status: &userTenantStatus,
|
|
BaseModel: model.BaseModel{
|
|
CreateTime: &now,
|
|
CreateDate: &nowDate,
|
|
UpdateTime: &now,
|
|
UpdateDate: &nowDate,
|
|
},
|
|
}
|
|
if err := tx.Create(userTenant).Error; err != nil {
|
|
rollbackTx()
|
|
return nil, fmt.Errorf("failed to create user-tenant relation: %w", err)
|
|
}
|
|
|
|
// 4. Create tenant LLM configurations
|
|
tenantLLMs, err := s.getInitTenantLLM(userID)
|
|
if err != nil {
|
|
logger.Warn("failed to get init tenant LLM configs", zap.Error(err))
|
|
// Continue without LLM configs - not a critical error
|
|
} else if len(tenantLLMs) > 0 {
|
|
if err := tx.Create(&tenantLLMs).Error; err != nil {
|
|
logger.Warn("failed to create tenant LLM configs", zap.Error(err))
|
|
// Continue without LLM configs - not a critical error
|
|
}
|
|
}
|
|
|
|
// 5. Create root file folder
|
|
fileID := utility.GenerateToken()
|
|
fileLocation := ""
|
|
file := &model.File{
|
|
ID: fileID,
|
|
ParentID: fileID,
|
|
TenantID: userID,
|
|
CreatedBy: userID,
|
|
Name: "/",
|
|
Type: "folder",
|
|
Size: 0,
|
|
Location: &fileLocation,
|
|
BaseModel: model.BaseModel{
|
|
CreateTime: &now,
|
|
CreateDate: &nowDate,
|
|
UpdateTime: &now,
|
|
UpdateDate: &nowDate,
|
|
},
|
|
}
|
|
if err := tx.Create(file).Error; err != nil {
|
|
rollbackTx()
|
|
return nil, fmt.Errorf("failed to create root file folder: %w", err)
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
return nil, fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
logger.Info("Create user success with tenant and related data", zap.String("username", username))
|
|
|
|
return map[string]interface{}{
|
|
"id": user.ID,
|
|
"email": user.Email,
|
|
"nickname": user.Nickname,
|
|
"is_active": user.IsActive,
|
|
"is_superuser": isSuperuser,
|
|
"create_date": user.CreateDate,
|
|
}, nil
|
|
}
|
|
|
|
// getInitTenantLLM gets initial tenant LLM configurations
|
|
// This matches Python's get_init_tenant_llm function
|
|
func (s *Service) getInitTenantLLM(userID string) ([]*model.TenantLLM, error) {
|
|
cfg := server.GetConfig()
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("config not initialized")
|
|
}
|
|
|
|
var tenantLLMs []*model.TenantLLM
|
|
|
|
// Get model configs from configuration
|
|
modelConfigs := []server.ModelConfig{
|
|
cfg.UserDefaultLLM.DefaultModels.ChatModel,
|
|
cfg.UserDefaultLLM.DefaultModels.EmbeddingModel,
|
|
cfg.UserDefaultLLM.DefaultModels.RerankModel,
|
|
cfg.UserDefaultLLM.DefaultModels.ASRModel,
|
|
cfg.UserDefaultLLM.DefaultModels.Image2TextModel,
|
|
}
|
|
|
|
// Track seen factories to avoid duplicates
|
|
seenFactories := make(map[string]bool)
|
|
var uniqueFactories []server.ModelConfig
|
|
|
|
for _, mc := range modelConfigs {
|
|
if mc.Factory == "" {
|
|
continue
|
|
}
|
|
if !seenFactories[mc.Factory] {
|
|
seenFactories[mc.Factory] = true
|
|
uniqueFactories = append(uniqueFactories, mc)
|
|
}
|
|
}
|
|
|
|
// Get LLMs for each unique factory
|
|
for _, factoryConfig := range uniqueFactories {
|
|
llms, err := s.llmDAO.GetByFactory(factoryConfig.Factory)
|
|
if err != nil {
|
|
logger.Warn("failed to get LLMs for factory", zap.String("factory", factoryConfig.Factory), zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
for _, llm := range llms {
|
|
// Determine API key and base URL based on model type
|
|
var apiKey, apiBase string
|
|
switch llm.ModelType {
|
|
case string(model.ModelTypeChat):
|
|
apiKey = factoryConfig.APIKey
|
|
apiBase = factoryConfig.BaseURL
|
|
case string(model.ModelTypeEmbedding):
|
|
apiKey = cfg.UserDefaultLLM.DefaultModels.EmbeddingModel.APIKey
|
|
apiBase = cfg.UserDefaultLLM.DefaultModels.EmbeddingModel.BaseURL
|
|
if apiKey == "" {
|
|
apiKey = factoryConfig.APIKey
|
|
}
|
|
if apiBase == "" {
|
|
apiBase = factoryConfig.BaseURL
|
|
}
|
|
case string(model.ModelTypeRerank):
|
|
apiKey = cfg.UserDefaultLLM.DefaultModels.RerankModel.APIKey
|
|
apiBase = cfg.UserDefaultLLM.DefaultModels.RerankModel.BaseURL
|
|
if apiKey == "" {
|
|
apiKey = factoryConfig.APIKey
|
|
}
|
|
if apiBase == "" {
|
|
apiBase = factoryConfig.BaseURL
|
|
}
|
|
case string(model.ModelTypeSpeech2Text):
|
|
apiKey = cfg.UserDefaultLLM.DefaultModels.ASRModel.APIKey
|
|
apiBase = cfg.UserDefaultLLM.DefaultModels.ASRModel.BaseURL
|
|
if apiKey == "" {
|
|
apiKey = factoryConfig.APIKey
|
|
}
|
|
if apiBase == "" {
|
|
apiBase = factoryConfig.BaseURL
|
|
}
|
|
case string(model.ModelTypeImage2Text):
|
|
apiKey = cfg.UserDefaultLLM.DefaultModels.Image2TextModel.APIKey
|
|
apiBase = cfg.UserDefaultLLM.DefaultModels.Image2TextModel.BaseURL
|
|
if apiKey == "" {
|
|
apiKey = factoryConfig.APIKey
|
|
}
|
|
if apiBase == "" {
|
|
apiBase = factoryConfig.BaseURL
|
|
}
|
|
default:
|
|
apiKey = factoryConfig.APIKey
|
|
apiBase = factoryConfig.BaseURL
|
|
}
|
|
|
|
maxTokens := int64(8192)
|
|
if llm.MaxTokens > 0 {
|
|
maxTokens = llm.MaxTokens
|
|
}
|
|
|
|
llmName := llm.LLMName
|
|
modelType := llm.ModelType
|
|
now := time.Now().Unix()
|
|
nowDate := time.Now().Truncate(time.Second)
|
|
|
|
tenantLLM := &model.TenantLLM{
|
|
TenantID: userID,
|
|
LLMFactory: factoryConfig.Factory,
|
|
LLMName: &llmName,
|
|
ModelType: &modelType,
|
|
APIKey: &apiKey,
|
|
APIBase: &apiBase,
|
|
MaxTokens: maxTokens,
|
|
Status: "1",
|
|
BaseModel: model.BaseModel{
|
|
CreateTime: &now,
|
|
CreateDate: &nowDate,
|
|
UpdateTime: &now,
|
|
UpdateDate: &nowDate,
|
|
},
|
|
}
|
|
tenantLLMs = append(tenantLLMs, tenantLLM)
|
|
}
|
|
}
|
|
|
|
// Remove duplicates based on (tenant_id, llm_factory, llm_name)
|
|
seen := make(map[string]bool)
|
|
var uniqueLLMs []*model.TenantLLM
|
|
for _, tllm := range tenantLLMs {
|
|
key := fmt.Sprintf("%s|%s|%s", tllm.TenantID, tllm.LLMFactory, *tllm.LLMName)
|
|
if !seen[key] {
|
|
seen[key] = true
|
|
uniqueLLMs = append(uniqueLLMs, tllm)
|
|
}
|
|
}
|
|
|
|
return uniqueLLMs, nil
|
|
}
|
|
|
|
// GetUserDetails get user details
|
|
func (s *Service) GetUserDetails(username string) (map[string]interface{}, error) {
|
|
// Query user by email/username
|
|
var user model.User
|
|
err := dao.DB.Where("email = ?", username).First(&user).Error
|
|
if err != nil {
|
|
return nil, ErrUserNotFound
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"id": user.ID,
|
|
"email": user.Email,
|
|
"nickname": user.Nickname,
|
|
"is_active": user.IsActive,
|
|
"create_time": user.CreateTime,
|
|
"update_time": user.UpdateTime,
|
|
}, nil
|
|
}
|
|
|
|
// DeleteUser delete user with cascade delete of all related data
|
|
// Parameters:
|
|
// - username: email address of the user to delete
|
|
//
|
|
// Returns:
|
|
// - error: error message
|
|
func (s *Service) DeleteUser(username string) error {
|
|
userList, err := s.userDAO.ListByEmail(username)
|
|
if err != nil || len(userList) == 0 {
|
|
return fmt.Errorf("User '%s' not found", username)
|
|
}
|
|
|
|
if len(userList) > 1 {
|
|
return fmt.Errorf("Exist more than 1 user: %s!", username)
|
|
}
|
|
|
|
user := userList[0]
|
|
|
|
// Check if user is active - cannot delete active users
|
|
if user.IsActive == "1" {
|
|
return fmt.Errorf("User '%s' is active and can't be deleted. Please deactivate the user first", username)
|
|
}
|
|
|
|
// Check if user is superuser - cannot delete admin accounts
|
|
if user.IsSuperuser != nil && *user.IsSuperuser {
|
|
return fmt.Errorf("Cannot delete admin account")
|
|
}
|
|
|
|
// Get user-tenant relations
|
|
tenants, err := s.userTenantDAO.GetByUserIDAll(user.ID)
|
|
if err != nil {
|
|
logger.Warn("failed to get user-tenant relations", zap.Error(err))
|
|
}
|
|
|
|
// Find owned tenant (role = "owner")
|
|
var ownedTenantID string
|
|
for _, t := range tenants {
|
|
if t.Role == "owner" {
|
|
ownedTenantID = t.TenantID
|
|
break
|
|
}
|
|
}
|
|
|
|
// Start transaction for cascade delete
|
|
tx := dao.DB.Begin()
|
|
if tx.Error != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", tx.Error)
|
|
}
|
|
|
|
// Rollback helper function
|
|
rollbackTx := func() {
|
|
if rbErr := tx.Rollback(); rbErr.Error != nil {
|
|
logger.Error("failed to rollback transaction", rbErr.Error)
|
|
}
|
|
}
|
|
|
|
// Delete owned tenant data
|
|
if ownedTenantID != "" {
|
|
// 1. Get knowledge base IDs
|
|
kbIDs, err := s.kbDAO.GetKBIDsByTenantIDSimple(ownedTenantID)
|
|
if err != nil {
|
|
logger.Warn("failed to get knowledge base IDs", zap.Error(err))
|
|
}
|
|
|
|
if len(kbIDs) > 0 {
|
|
// 2. Get document IDs
|
|
docIDs, err := s.documentDAO.GetAllDocIDsByKBIDs(kbIDs)
|
|
if err != nil {
|
|
logger.Warn("failed to get document IDs", zap.Error(err))
|
|
}
|
|
|
|
// 3. Delete tasks by document IDs
|
|
if len(docIDs) > 0 {
|
|
docIDList := make([]string, len(docIDs))
|
|
for i, d := range docIDs {
|
|
docIDList[i] = d["id"]
|
|
}
|
|
if delErr := tx.Unscoped().Where("doc_id IN ?", docIDList).Delete(&model.Task{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete tasks", zap.Error(delErr.Error))
|
|
}
|
|
}
|
|
|
|
// 4. Delete documents
|
|
if delErr := tx.Unscoped().Where("kb_id IN ?", kbIDs).Delete(&model.Document{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete documents", zap.Error(delErr.Error))
|
|
}
|
|
|
|
// 5. Delete knowledge bases
|
|
if delErr := tx.Unscoped().Where("id IN ?", kbIDs).Delete(&model.Knowledgebase{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete knowledge bases", zap.Error(delErr.Error))
|
|
}
|
|
}
|
|
|
|
// 6. Delete files
|
|
if delErr := tx.Unscoped().Where("tenant_id = ?", ownedTenantID).Delete(&model.File{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete files", zap.Error(delErr.Error))
|
|
}
|
|
|
|
// 7. Delete user canvas (agents)
|
|
if delErr := tx.Unscoped().Where("user_id = ?", ownedTenantID).Delete(&model.UserCanvas{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete user canvas", zap.Error(delErr.Error))
|
|
}
|
|
|
|
// 8. Get dialog IDs
|
|
var dialogIDs []string
|
|
if pluckErr := tx.Model(&model.Chat{}).Where("tenant_id = ?", ownedTenantID).Pluck("id", &dialogIDs); pluckErr.Error != nil {
|
|
logger.Warn("failed to get dialog IDs", zap.Error(pluckErr.Error))
|
|
}
|
|
|
|
// 9. Delete chat sessions
|
|
if len(dialogIDs) > 0 {
|
|
if delErr := tx.Unscoped().Where("dialog_id IN ?", dialogIDs).Delete(&model.ChatSession{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete chat sessions", zap.Error(delErr.Error))
|
|
}
|
|
}
|
|
|
|
// 10. Delete chats/dialogs
|
|
if delErr := tx.Unscoped().Where("tenant_id = ?", ownedTenantID).Delete(&model.Chat{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete chats", zap.Error(delErr.Error))
|
|
}
|
|
|
|
// 11. Delete API tokens
|
|
if delErr := tx.Unscoped().Where("tenant_id = ?", ownedTenantID).Delete(&model.APIToken{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete API tokens", zap.Error(delErr.Error))
|
|
}
|
|
|
|
// 12. Delete API4Conversations
|
|
if len(dialogIDs) > 0 {
|
|
if delErr := tx.Unscoped().Where("dialog_id IN ?", dialogIDs).Delete(&model.API4Conversation{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete API4Conversations", zap.Error(delErr.Error))
|
|
}
|
|
}
|
|
|
|
// 13. Delete tenant LLM configurations
|
|
if delErr := tx.Unscoped().Where("tenant_id = ?", ownedTenantID).Delete(&model.TenantLLM{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete tenant LLM", zap.Error(delErr.Error))
|
|
}
|
|
|
|
// 14. Delete tenant
|
|
if delErr := tx.Unscoped().Where("id = ?", ownedTenantID).Delete(&model.Tenant{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete tenant", zap.Error(delErr.Error))
|
|
}
|
|
}
|
|
|
|
// 15. Delete user-tenant relations
|
|
if delErr := tx.Unscoped().Where("user_id = ?", user.ID).Delete(&model.UserTenant{}); delErr.Error != nil {
|
|
logger.Warn("failed to delete user-tenant relations", zap.Error(delErr.Error))
|
|
}
|
|
|
|
// 16. Finally, hard delete user
|
|
if delErr := tx.Unscoped().Where("id = ?", user.ID).Delete(&model.User{}); delErr.Error != nil {
|
|
rollbackTx()
|
|
return fmt.Errorf("failed to delete user: %w", delErr.Error)
|
|
}
|
|
|
|
// Commit transaction
|
|
if commitErr := tx.Commit(); commitErr.Error != nil {
|
|
return fmt.Errorf("failed to commit transaction: %w", commitErr.Error)
|
|
}
|
|
|
|
logger.Info("Delete user success with all related data", zap.String("username", username))
|
|
|
|
return nil
|
|
}
|
|
|
|
// ChangePassword change user password
|
|
// Parameters:
|
|
// - username: email address of the user
|
|
// - newPassword: new encrypted password (base64 encoded RSA encrypted)
|
|
//
|
|
// Returns:
|
|
// - error: error message
|
|
func (s *Service) ChangePassword(username, newPassword string) error {
|
|
userList, err := s.userDAO.ListByEmail(username)
|
|
if err != nil || len(userList) == 0 {
|
|
return fmt.Errorf("User '%s' not found", username)
|
|
}
|
|
|
|
if len(userList) > 1 {
|
|
return fmt.Errorf("Exist more than 1 user: %s!", username)
|
|
}
|
|
|
|
user := userList[0]
|
|
|
|
decryptedPassword, err := DecryptPassword(newPassword)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt password: %w", err)
|
|
}
|
|
|
|
if user.Password != nil && CheckWerkzeugPassword(decryptedPassword, *user.Password) {
|
|
return nil
|
|
}
|
|
|
|
hashedPassword, err := GenerateWerkzeugPasswordHash(decryptedPassword, 150000)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
|
|
user.Password = &hashedPassword
|
|
now := time.Now().Unix()
|
|
user.UpdateTime = &now
|
|
|
|
if err := s.userDAO.Update(user); err != nil {
|
|
return fmt.Errorf("failed to update user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateUserActivateStatus update user activate status
|
|
// Parameters:
|
|
// - username: email address of the user
|
|
// - isActive: true to activate, false to deactivate
|
|
//
|
|
// Returns:
|
|
// - error: error message
|
|
func (s *Service) UpdateUserActivateStatus(username string, isActive bool) error {
|
|
userList, err := s.userDAO.ListByEmail(username)
|
|
if err != nil || len(userList) == 0 {
|
|
return fmt.Errorf("User '%s' not found", username)
|
|
}
|
|
|
|
if len(userList) > 1 {
|
|
return fmt.Errorf("Exist more than 1 user: %s!", username)
|
|
}
|
|
|
|
user := userList[0]
|
|
|
|
targetStatus := "0"
|
|
if isActive {
|
|
targetStatus = "1"
|
|
}
|
|
|
|
if user.IsActive == targetStatus {
|
|
return nil
|
|
}
|
|
|
|
user.IsActive = targetStatus
|
|
now := time.Now().Unix()
|
|
user.UpdateTime = &now
|
|
|
|
if err := s.userDAO.Update(user); err != nil {
|
|
return fmt.Errorf("failed to update user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GrantAdmin grant admin privileges
|
|
// Parameters:
|
|
// - username: email address of the user
|
|
//
|
|
// Returns:
|
|
// - error: error message
|
|
func (s *Service) GrantAdmin(username string) error {
|
|
userList, err := s.userDAO.ListByEmail(username)
|
|
if err != nil || len(userList) == 0 {
|
|
return fmt.Errorf("User '%s' not found", username)
|
|
}
|
|
|
|
if len(userList) > 1 {
|
|
return fmt.Errorf("Exist more than 1 user: %s!", username)
|
|
}
|
|
|
|
user := userList[0]
|
|
|
|
if user.IsSuperuser != nil && *user.IsSuperuser {
|
|
return nil
|
|
}
|
|
|
|
isSuperuser := true
|
|
user.IsSuperuser = &isSuperuser
|
|
now := time.Now().Unix()
|
|
user.UpdateTime = &now
|
|
|
|
if err := s.userDAO.Update(user); err != nil {
|
|
return fmt.Errorf("failed to update user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RevokeAdmin revoke admin privileges
|
|
// Parameters:
|
|
// - username: email address of the user
|
|
//
|
|
// Returns:
|
|
// - error: error message
|
|
func (s *Service) RevokeAdmin(username string) error {
|
|
userList, err := s.userDAO.ListByEmail(username)
|
|
if err != nil || len(userList) == 0 {
|
|
return fmt.Errorf("User '%s' not found", username)
|
|
}
|
|
|
|
if len(userList) > 1 {
|
|
return fmt.Errorf("Exist more than 1 user: %s!", username)
|
|
}
|
|
|
|
user := userList[0]
|
|
|
|
if user.IsSuperuser == nil || !*user.IsSuperuser {
|
|
return nil
|
|
}
|
|
|
|
isSuperuser := false
|
|
user.IsSuperuser = &isSuperuser
|
|
now := time.Now().Unix()
|
|
user.UpdateTime = &now
|
|
|
|
if err := s.userDAO.Update(user); err != nil {
|
|
return fmt.Errorf("failed to update user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUserDatasets get user datasets
|
|
func (s *Service) GetUserDatasets(username string) ([]map[string]interface{}, error) {
|
|
// TODO: Implement get user datasets
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
|
|
// GetUserAgents get user agents
|
|
func (s *Service) GetUserAgents(username string) ([]map[string]interface{}, error) {
|
|
// TODO: Implement get user agents
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
|
|
// API Key methods
|
|
|
|
// GetUserAPIKeys get user API keys
|
|
func (s *Service) GetUserAPIKeys(username string) ([]map[string]interface{}, error) {
|
|
// TODO: Implement get API keys
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
|
|
// GenerateUserAPIKey generate API key for user
|
|
func (s *Service) GenerateUserAPIKey(username string) (map[string]interface{}, error) {
|
|
// TODO: Implement generate API key
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// DeleteUserAPIKey delete user API key
|
|
func (s *Service) DeleteUserAPIKey(username, key string) error {
|
|
// TODO: Implement delete API key
|
|
return nil
|
|
}
|
|
|
|
// Role management methods
|
|
|
|
// ListRoles list all roles
|
|
func (s *Service) ListRoles() ([]map[string]interface{}, error) {
|
|
// TODO: Implement list roles
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
|
|
// CreateRole create a new role
|
|
func (s *Service) CreateRole(roleName, description string) (map[string]interface{}, error) {
|
|
// TODO: Implement create role
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// GetRole get role details
|
|
func (s *Service) GetRole(roleName string) (map[string]interface{}, error) {
|
|
// TODO: Implement get role
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// UpdateRole update role
|
|
func (s *Service) UpdateRole(roleName, description string) (map[string]interface{}, error) {
|
|
// TODO: Implement update role
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// DeleteRole delete role
|
|
func (s *Service) DeleteRole(roleName string) error {
|
|
// TODO: Implement delete role
|
|
return nil
|
|
}
|
|
|
|
// GetRolePermission get role permissions
|
|
func (s *Service) GetRolePermission(roleName string) ([]map[string]interface{}, error) {
|
|
// TODO: Implement get role permissions
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
|
|
// GrantRolePermission grant permission to role
|
|
func (s *Service) GrantRolePermission(roleName string, actions []string, resource string) (map[string]interface{}, error) {
|
|
// TODO: Implement grant role permission
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// RevokeRolePermission revoke permission from role
|
|
func (s *Service) RevokeRolePermission(roleName string, actions []string, resource string) (map[string]interface{}, error) {
|
|
// TODO: Implement revoke role permission
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// UpdateUserRole update user role
|
|
func (s *Service) UpdateUserRole(username, roleName string) ([]map[string]interface{}, error) {
|
|
// TODO: Implement update user role
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
|
|
// GetUserPermission get user permissions
|
|
func (s *Service) GetUserPermission(username string) ([]map[string]interface{}, error) {
|
|
// TODO: Implement get user permissions
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
|
|
// ListServices get all services
|
|
func (s *Service) ListServices() ([]map[string]interface{}, error) {
|
|
allConfigs := server.GetAllConfigs()
|
|
|
|
var result []map[string]interface{}
|
|
for _, configDict := range allConfigs {
|
|
serviceType := configDict["service_type"]
|
|
if serviceType != "ragflow_server" {
|
|
// Get service details to check status
|
|
serviceDetail, err := s.GetServiceDetails(configDict)
|
|
if err == nil {
|
|
if status, ok := serviceDetail["status"]; ok {
|
|
configDict["status"] = status
|
|
} else {
|
|
configDict["status"] = "timeout"
|
|
}
|
|
} else {
|
|
configDict["status"] = "timeout"
|
|
}
|
|
result = append(result, configDict)
|
|
}
|
|
|
|
}
|
|
|
|
id := len(result)
|
|
serverList := GlobalServerStatusStore.GetAllStatuses()
|
|
for _, serverStatus := range serverList {
|
|
serverItem := make(map[string]interface{})
|
|
serverItem["name"] = serverStatus.ServerName
|
|
serverItem["service_type"] = serverStatus.ServerType
|
|
serverItem["id"] = id
|
|
id++
|
|
serverItem["host"] = serverStatus.Host
|
|
serverItem["port"] = serverStatus.Port
|
|
serverItem["status"] = "alive"
|
|
result = append(result, serverItem)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetServicesByType get services by type
|
|
func (s *Service) GetServicesByType(serviceType string) ([]map[string]interface{}, error) {
|
|
return nil, errors.New("get_services_by_type: not implemented")
|
|
}
|
|
|
|
// GetServiceDetails get service details
|
|
func (s *Service) GetServiceDetails(configDict map[string]interface{}) (map[string]interface{}, error) {
|
|
serviceType, _ := configDict["service_type"].(string)
|
|
name, _ := configDict["name"].(string)
|
|
|
|
// Call detail function based on service type
|
|
switch serviceType {
|
|
case "meta_data":
|
|
return s.getMySQLStatus(name)
|
|
case "message_queue":
|
|
return s.getRedisInfo(name)
|
|
case "retrieval":
|
|
// Check the extra.retrieval_type to determine which retrieval service
|
|
if extra, ok := configDict["extra"].(map[string]interface{}); ok {
|
|
if retrievalType, ok := extra["retrieval_type"].(string); ok {
|
|
if retrievalType == "infinity" {
|
|
return s.getInfinityStatus(name)
|
|
}
|
|
}
|
|
}
|
|
return s.getESClusterStats(name)
|
|
case "ragflow_server":
|
|
return s.checkRAGFlowServerAlive(name)
|
|
case "file_store":
|
|
return s.checkMinioAlive(name)
|
|
case "task_executor":
|
|
return s.checkTaskExecutorAlive(name)
|
|
default:
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "unknown",
|
|
"message": "Service type not supported",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// getMySQLStatus gets MySQL service status
|
|
func (s *Service) getMySQLStatus(name string) (map[string]interface{}, error) {
|
|
startTime := time.Now()
|
|
|
|
// Check basic connectivity with SELECT 1
|
|
sqlDB, err := dao.DB.DB()
|
|
if err != nil {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"elapsed": fmt.Sprintf("%.1f", time.Since(startTime).Milliseconds()),
|
|
"message": err.Error(),
|
|
}, nil
|
|
}
|
|
|
|
// Execute SELECT 1 to check connectivity
|
|
_, err = sqlDB.Exec("SELECT 1")
|
|
if err != nil {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"elapsed": fmt.Sprintf("%.1f", time.Since(startTime).Milliseconds()),
|
|
"message": err.Error(),
|
|
}, nil
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "alive",
|
|
"elapsed": fmt.Sprintf("%.1f", time.Since(startTime).Milliseconds()),
|
|
"message": "MySQL connection successful",
|
|
}, nil
|
|
}
|
|
|
|
// getRedisInfo gets Redis service info
|
|
func (s *Service) getRedisInfo(name string) (map[string]interface{}, error) {
|
|
startTime := time.Now()
|
|
|
|
redisClient := cache.Get()
|
|
if redisClient == nil {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"elapsed": fmt.Sprintf("%.1f", time.Since(startTime).Milliseconds()),
|
|
"error": "Redis client not initialized",
|
|
}, nil
|
|
}
|
|
|
|
// Check health
|
|
if !redisClient.Health() {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"elapsed": fmt.Sprintf("%.1f", time.Since(startTime).Milliseconds()),
|
|
"error": "Redis health check failed",
|
|
}, nil
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "alive",
|
|
"elapsed": fmt.Sprintf("%.1f", time.Since(startTime).Milliseconds()),
|
|
"message": "Redis connection successful",
|
|
}, nil
|
|
}
|
|
|
|
// getESClusterStats gets Elasticsearch cluster stats
|
|
func (s *Service) getESClusterStats(name string) (map[string]interface{}, error) {
|
|
// Check if Elasticsearch is the doc engine
|
|
docEngine := os.Getenv("DOC_ENGINE")
|
|
if docEngine == "" {
|
|
docEngine = "elasticsearch"
|
|
}
|
|
if docEngine != "elasticsearch" {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"message": "error: Elasticsearch is not in use.",
|
|
}, nil
|
|
}
|
|
|
|
// Get ES config from server config
|
|
cfg := server.GetConfig()
|
|
if cfg == nil || cfg.DocEngine.ES == nil {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"message": "error: Elasticsearch configuration not found",
|
|
}, nil
|
|
}
|
|
|
|
// Create ES engine and get cluster stats
|
|
esEngine, err := elasticsearch.NewEngine(cfg.DocEngine.ES)
|
|
if err != nil {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"message": fmt.Sprintf("error: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
defer esEngine.Close()
|
|
|
|
clusterStats, err := esEngine.GetClusterStats()
|
|
if err != nil {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"message": fmt.Sprintf("error: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "alive",
|
|
"message": clusterStats,
|
|
}, nil
|
|
}
|
|
|
|
// getInfinityStatus gets Infinity service status
|
|
func (s *Service) getInfinityStatus(name string) (map[string]interface{}, error) {
|
|
// TODO: Implement actual Infinity health check
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "unknown",
|
|
"message": "Infinity health check not implemented",
|
|
}, nil
|
|
}
|
|
|
|
// checkRAGFlowServerAlive checks if RAGFlow server is alive
|
|
func (s *Service) checkRAGFlowServerAlive(name string) (map[string]interface{}, error) {
|
|
startTime := time.Now()
|
|
|
|
// Get ragflow config from allConfigs
|
|
var host string
|
|
var port int
|
|
allConfigs := server.GetAllConfigs()
|
|
for _, config := range allConfigs {
|
|
if serviceType, ok := config["service_type"].(string); ok && serviceType == "ragflow_server" {
|
|
if h, ok := config["host"].(string); ok {
|
|
host = h
|
|
}
|
|
if p, ok := config["port"].(int); ok {
|
|
port = p
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Default values
|
|
if host == "" {
|
|
host = "127.0.0.1"
|
|
}
|
|
if port == 0 {
|
|
port = 9380
|
|
}
|
|
|
|
// Replace 0.0.0.0 with 127.0.0.1 for local check
|
|
if host == "0.0.0.0" {
|
|
host = "127.0.0.1"
|
|
}
|
|
|
|
url := fmt.Sprintf("http://%s:%d/v1/system/ping", host, port)
|
|
|
|
// Create HTTP client with timeout
|
|
client := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"message": fmt.Sprintf("error: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
elapsed := time.Since(startTime).Milliseconds()
|
|
if resp.StatusCode == 200 {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "alive",
|
|
"message": fmt.Sprintf("Confirm elapsed: %.1f ms.", float64(elapsed)),
|
|
}, nil
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"message": fmt.Sprintf("Confirm elapsed: %.1f ms.", float64(elapsed)),
|
|
}, nil
|
|
}
|
|
|
|
// checkMinioAlive checks if MinIO is alive
|
|
func (s *Service) checkMinioAlive(name string) (map[string]interface{}, error) {
|
|
startTime := time.Now()
|
|
|
|
// Get minio config from allConfigs
|
|
var host string
|
|
var port int
|
|
var secure bool
|
|
var verify bool = true
|
|
|
|
allConfigs := server.GetAllConfigs()
|
|
for _, config := range allConfigs {
|
|
if serviceType, ok := config["service_type"].(string); ok && serviceType == "file_store" {
|
|
// Get host from config
|
|
if h, ok := config["host"].(string); ok {
|
|
host = h
|
|
}
|
|
|
|
if p, ok := config["port"].(int); ok {
|
|
port = p
|
|
} else if p, ok := config["port"].(float64); ok {
|
|
port = int(p)
|
|
} else if p, ok := config["port"].(string); ok {
|
|
if parsedPort, err := strconv.Atoi(p); err == nil {
|
|
port = parsedPort
|
|
}
|
|
}
|
|
// Get secure from extra config
|
|
if extra, ok := config["extra"].(map[string]interface{}); ok {
|
|
if s, ok := extra["secure"].(bool); ok {
|
|
secure = s
|
|
} else if s, ok := extra["secure"].(string); ok {
|
|
secure = s == "true" || s == "1" || s == "yes"
|
|
}
|
|
if v, ok := extra["verify"].(bool); ok {
|
|
verify = v
|
|
} else if v, ok := extra["verify"].(string); ok {
|
|
verify = !(v == "false" || v == "0" || v == "no")
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Default host
|
|
if host == "" {
|
|
host = "localhost"
|
|
}
|
|
if port == 0 {
|
|
port = 9000
|
|
}
|
|
|
|
// Determine scheme
|
|
scheme := "http"
|
|
if secure {
|
|
scheme = "https"
|
|
}
|
|
|
|
url := fmt.Sprintf("%s://%s:%d/minio/health/live", scheme, host, port)
|
|
|
|
// Create HTTP client with timeout
|
|
client := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
// If verify is false, we need to skip SSL verification
|
|
if !verify && scheme == "https" {
|
|
client.Transport = &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
}
|
|
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"message": fmt.Sprintf("error: %s", err.Error()),
|
|
}, nil
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
elapsed := time.Since(startTime).Milliseconds()
|
|
if resp.StatusCode == 200 {
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "alive",
|
|
"message": fmt.Sprintf("Confirm elapsed: %.1f ms.", float64(elapsed)),
|
|
}, nil
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "timeout",
|
|
"message": fmt.Sprintf("Confirm elapsed: %.1f ms.", float64(elapsed)),
|
|
}, nil
|
|
}
|
|
|
|
// checkTaskExecutorAlive checks if task executor is alive
|
|
func (s *Service) checkTaskExecutorAlive(name string) (map[string]interface{}, error) {
|
|
// TODO: Implement actual task executor health check
|
|
return map[string]interface{}{
|
|
"service_name": name,
|
|
"status": "unknown",
|
|
"message": "Task executor health check not implemented",
|
|
}, nil
|
|
}
|
|
|
|
// ShutdownService shutdown service
|
|
func (s *Service) ShutdownService(serviceID string) (map[string]interface{}, error) {
|
|
// TODO: Implement with proper service manager
|
|
return map[string]interface{}{
|
|
"service_id": serviceID,
|
|
"status": "shutdown",
|
|
}, nil
|
|
}
|
|
|
|
// RestartService restart service
|
|
func (s *Service) RestartService(serviceID string) (map[string]interface{}, error) {
|
|
// TODO: Implement with proper service manager
|
|
return map[string]interface{}{
|
|
"service_id": serviceID,
|
|
"status": "restarted",
|
|
}, nil
|
|
}
|
|
|
|
// Variable/Settings methods
|
|
|
|
// AdminException admin exception error
|
|
type AdminException struct {
|
|
Message string
|
|
Code int
|
|
}
|
|
|
|
// Error implement error interface
|
|
func (e *AdminException) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// NewAdminException create admin exception
|
|
func NewAdminException(message string) *AdminException {
|
|
return &AdminException{
|
|
Message: message,
|
|
Code: 400,
|
|
}
|
|
}
|
|
|
|
// GetVariable get variable by name
|
|
// Returns the system setting with the given name
|
|
// Returns AdminException if the setting is not found
|
|
func (s *Service) GetVariable(varName string) ([]map[string]interface{}, error) {
|
|
settings, err := s.systemSettingsDAO.GetByName(varName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(settings) == 0 {
|
|
return nil, NewAdminException("Can't get setting: " + varName)
|
|
}
|
|
|
|
result := make([]map[string]interface{}, 0, len(settings))
|
|
for _, setting := range settings {
|
|
result = append(result, map[string]interface{}{
|
|
"name": setting.Name,
|
|
"source": setting.Source,
|
|
"data_type": setting.DataType,
|
|
"value": setting.Value,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetAllVariables get all variables
|
|
// Returns all system settings from database
|
|
func (s *Service) GetAllVariables() ([]map[string]interface{}, error) {
|
|
settings, err := s.systemSettingsDAO.GetAll()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]map[string]interface{}, 0, len(settings))
|
|
for _, setting := range settings {
|
|
result = append(result, map[string]interface{}{
|
|
"name": setting.Name,
|
|
"source": setting.Source,
|
|
"data_type": setting.DataType,
|
|
"value": setting.Value,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// SetVariable set variable
|
|
// Creates or updates a system setting
|
|
// If the setting exists, updates it; otherwise creates a new one
|
|
func (s *Service) SetVariable(varName, varValue string) error {
|
|
settings, err := s.systemSettingsDAO.GetByName(varName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(settings) == 1 {
|
|
setting := &settings[0]
|
|
setting.Value = varValue
|
|
return s.systemSettingsDAO.UpdateByName(varName, setting)
|
|
} else if len(settings) > 1 {
|
|
return NewAdminException("Can't update more than 1 setting: " + varName)
|
|
}
|
|
|
|
// Create new setting if it doesn't exist
|
|
// Determine data_type based on name and value
|
|
dataType := "string"
|
|
if len(varName) >= 7 && varName[:7] == "sandbox" {
|
|
dataType = "json"
|
|
} else if len(varName) >= 9 && varName[len(varName)-9:] == ".enabled" {
|
|
dataType = "boolean"
|
|
}
|
|
|
|
newSetting := &model.SystemSettings{
|
|
Name: varName,
|
|
Value: varValue,
|
|
Source: "admin",
|
|
DataType: dataType,
|
|
}
|
|
return s.systemSettingsDAO.Create(newSetting)
|
|
}
|
|
|
|
// Config methods
|
|
|
|
// GetAllConfigs get all configs
|
|
// Returns all service configurations from the config file
|
|
func (s *Service) GetAllConfigs() ([]map[string]interface{}, error) {
|
|
result := server.GetAllConfigs()
|
|
return result, nil
|
|
}
|
|
|
|
// Environment methods
|
|
|
|
// GetAllEnvironments get all environments
|
|
// Returns important environment variables
|
|
func (s *Service) GetAllEnvironments() ([]map[string]interface{}, error) {
|
|
result := make([]map[string]interface{}, 0)
|
|
|
|
// DOC_ENGINE
|
|
docEngine := os.Getenv("DOC_ENGINE")
|
|
if docEngine == "" {
|
|
docEngine = "elasticsearch"
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"env": "DOC_ENGINE",
|
|
"value": docEngine,
|
|
})
|
|
|
|
// DEFAULT_SUPERUSER_EMAIL
|
|
defaultSuperuserEmail := os.Getenv("DEFAULT_SUPERUSER_EMAIL")
|
|
if defaultSuperuserEmail == "" {
|
|
defaultSuperuserEmail = "admin@ragflow.io"
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"env": "DEFAULT_SUPERUSER_EMAIL",
|
|
"value": defaultSuperuserEmail,
|
|
})
|
|
|
|
// DB_TYPE
|
|
dbType := os.Getenv("DB_TYPE")
|
|
if dbType == "" {
|
|
dbType = "mysql"
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"env": "DB_TYPE",
|
|
"value": dbType,
|
|
})
|
|
|
|
// DEVICE
|
|
device := os.Getenv("DEVICE")
|
|
if device == "" {
|
|
device = "cpu"
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"env": "DEVICE",
|
|
"value": device,
|
|
})
|
|
|
|
// STORAGE_IMPL
|
|
storageImpl := os.Getenv("STORAGE_IMPL")
|
|
if storageImpl == "" {
|
|
storageImpl = "MINIO"
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"env": "STORAGE_IMPL",
|
|
"value": storageImpl,
|
|
})
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Version methods
|
|
|
|
// GetVersion get RAGFlow version
|
|
func (s *Service) GetVersion() string {
|
|
return utility.GetRAGFlowVersion()
|
|
}
|
|
|
|
// Sandbox methods
|
|
|
|
// ListSandboxProviders list sandbox providers
|
|
func (s *Service) ListSandboxProviders() ([]map[string]interface{}, error) {
|
|
// TODO: Implement with sandbox manager
|
|
return []map[string]interface{}{}, nil
|
|
}
|
|
|
|
// GetSandboxProviderSchema get sandbox provider schema
|
|
func (s *Service) GetSandboxProviderSchema(providerID string) (map[string]interface{}, error) {
|
|
// TODO: Implement with sandbox manager
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// GetSandboxConfig get sandbox config
|
|
func (s *Service) GetSandboxConfig() (map[string]interface{}, error) {
|
|
// TODO: Implement with sandbox manager
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// SetSandboxConfig set sandbox config
|
|
func (s *Service) SetSandboxConfig(providerType string, config map[string]interface{}, setActive bool) (map[string]interface{}, error) {
|
|
// TODO: Implement with sandbox manager
|
|
return map[string]interface{}{
|
|
"provider_type": providerType,
|
|
"config": config,
|
|
"set_active": setActive,
|
|
}, nil
|
|
}
|
|
|
|
// TestSandboxConnection test sandbox connection
|
|
func (s *Service) TestSandboxConnection(providerType string, config map[string]interface{}) (map[string]interface{}, error) {
|
|
// TODO: Implement with sandbox manager
|
|
return map[string]interface{}{
|
|
"provider_type": providerType,
|
|
"config": config,
|
|
"connected": true,
|
|
}, nil
|
|
}
|
|
|
|
var heartBeatCount int64 = 0
|
|
|
|
// HandleHeartbeat handle heartbeat
|
|
func (s *Service) HandleHeartbeat(message *common.BaseMessage) (common.ErrorCode, string) {
|
|
heartBeatCount++
|
|
|
|
status := &common.BaseMessage{
|
|
ServerName: message.ServerName,
|
|
ServerType: message.ServerType,
|
|
Host: message.Host,
|
|
Port: message.Port,
|
|
Version: message.Version,
|
|
Timestamp: message.Timestamp,
|
|
Ext: message.Ext,
|
|
}
|
|
GlobalServerStatusStore.UpdateStatus(message.ServerName, status)
|
|
return common.CodeLicenseValid, ""
|
|
}
|
|
|
|
// InitDefaultAdmin initialize default admin user
|
|
// This matches Python's init_default_admin behavior
|
|
func (s *Service) InitDefaultAdmin() error {
|
|
// Default superuser settings (matching Python's DEFAULT_SUPERUSER_* defaults)
|
|
defaultNickname := "admin"
|
|
defaultEmail := "admin@ragflow.io"
|
|
defaultPassword := "admin"
|
|
|
|
// Query superusers
|
|
var users []*model.User
|
|
err := dao.DB.Where("is_superuser = ? AND status = ?", true, "1").Find(&users).Error
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query superusers: %w", err)
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
now := time.Now().Unix()
|
|
nowDate := time.Now().Truncate(time.Second)
|
|
userID := utility.GenerateToken()
|
|
accessToken := utility.GenerateToken()
|
|
status := "1"
|
|
loginChannel := "password"
|
|
isSuperuser := true
|
|
|
|
// Python: password = encode_to_base64(password) = base64.b64encode(password)
|
|
// Then: generate_password_hash(base64_password) creates werkzeug hash
|
|
password := base64.StdEncoding.EncodeToString([]byte(defaultPassword))
|
|
hashedPassword, err := GenerateWerkzeugPasswordHash(password, 150000)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
|
|
user := &model.User{
|
|
ID: userID,
|
|
Email: defaultEmail,
|
|
Nickname: defaultNickname,
|
|
Password: &hashedPassword,
|
|
AccessToken: &accessToken,
|
|
Status: &status,
|
|
IsActive: "1",
|
|
IsAuthenticated: "1",
|
|
IsAnonymous: "0",
|
|
LoginChannel: &loginChannel,
|
|
IsSuperuser: &isSuperuser,
|
|
BaseModel: model.BaseModel{
|
|
CreateTime: &now,
|
|
CreateDate: &nowDate,
|
|
UpdateTime: &now,
|
|
UpdateDate: &nowDate,
|
|
},
|
|
}
|
|
|
|
if err := dao.DB.Create(user).Error; err != nil {
|
|
return fmt.Errorf("can't init admin: %w", err)
|
|
}
|
|
|
|
if err := s.addTenantForAdmin(userID, defaultNickname); err != nil {
|
|
return fmt.Errorf("failed to add tenant for admin: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
for _, user := range users {
|
|
if user.IsActive != "1" {
|
|
return fmt.Errorf("no active admin. Please update 'is_active' in db manually")
|
|
}
|
|
}
|
|
|
|
for _, user := range users {
|
|
if user.Email == defaultEmail {
|
|
// Check if tenant exists
|
|
var count int64
|
|
dao.DB.Model(&model.UserTenant{}).Where("user_id = ? AND status = ?", user.ID, "1").Count(&count)
|
|
if count == 0 {
|
|
nickname := defaultNickname
|
|
if user.Nickname != "" {
|
|
nickname = user.Nickname
|
|
}
|
|
if err := s.addTenantForAdmin(user.ID, nickname); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addTenantForAdmin add tenant for admin user
|
|
func (s *Service) addTenantForAdmin(userID, nickname string) error {
|
|
now := time.Now().Unix()
|
|
nowDate := time.Now().Truncate(time.Second)
|
|
status := "1"
|
|
role := "owner"
|
|
tenantName := nickname + "'s Kingdom"
|
|
|
|
tenant := &model.Tenant{
|
|
ID: userID,
|
|
Name: &tenantName,
|
|
BaseModel: model.BaseModel{
|
|
CreateTime: &now,
|
|
CreateDate: &nowDate,
|
|
UpdateTime: &now,
|
|
UpdateDate: &nowDate,
|
|
},
|
|
}
|
|
|
|
if err := dao.DB.Create(tenant).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
userTenant := &model.UserTenant{
|
|
TenantID: userID,
|
|
UserID: userID,
|
|
InvitedBy: userID,
|
|
Role: role,
|
|
Status: &status,
|
|
BaseModel: model.BaseModel{
|
|
CreateTime: &now,
|
|
CreateDate: &nowDate,
|
|
UpdateTime: &now,
|
|
UpdateDate: &nowDate,
|
|
},
|
|
}
|
|
|
|
return dao.DB.Create(userTenant).Error
|
|
}
|