Files
ragflow/internal/admin/service.go
Jin Hai cc7e94ffb6 Use different API route according the ENV (#13597)
### 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>
2026-03-13 19:05:30 +08:00

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
}