mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-11-03 06:24:15 +08:00
Replace the standalone /platform/system/* routes with a single
"系统设置" section inside the canonical Settings modal. The previous
SystemLayout.vue / SystemAdmins.vue surfaces are removed and their
functionality (system admin roster, global settings) is hosted directly
in SystemSettings.vue under the standard `.section-header` /
`.settings-group` skeleton. Legacy URLs redirect to the modal section
so external bookmarks don't 404.
Backend:
- SystemSettingService.Reset + repo.Delete: drop the DB override for a
key so the 3-tier resolver falls back to ENV / built-in default.
Idempotent (resetting a never-persisted key returns nil); emits an
audit row only on real deletions, invalidates the local cache, and
publishes to peers via the existing pubsub channel.
- TenantService.BulkSetStorageQuota: overwrite every tenant's
storage_quota in one statement. Powers POST /system/admin/tenants/
apply-default-storage-quota; bypasses the per-tenant whitelist on
PUT /tenants/:id which intentionally forbids storage_quota edits.
- AuditAction{SystemAdminPromoted,SystemAdminRevoked} constants and
emitAdminAudit() in SystemHandler — promote / revoke now leave an
audit trail with TenantID=0 and {target_email,target_username,
idempotent|changed} in details.
- SystemSetting.LastModifiedByName: derived per-request display label
(username, email fallback) so the UI shows "wizardchen" instead of
a UUID prefix without storing a denormalised column.
Frontend:
- SystemSettings.vue rewritten against the Settings modal skeleton with
auto-persisting controls (switch/select @change, input/inputnumber
@blur, tag-input + per-delta popconfirm for SSRF whitelist and admin
roster). auth.registration_mode change goes through an inline
popconfirm; cancel rolls back. Reset / bulk-apply also inline
popconfirms — no dialog modal for these per-row affordances.
- Priority hint panel surfaces the DB > ENV > default resolver order
so operators can reason about "I set the env but it doesn't show up".
- Router: /platform/system, /platform/system/settings, /platform/system/
admins are now compatibility redirects to /platform/settings?section=
system-global.
- Settings modal sized 900x700 → 1080x780 and content-wrapper 600 → 760
so the wider tables (members, system settings) breathe; <1100px
viewport still flexes to the screen.
- i18n: system.globalSettings.* keys for title / description / loading /
empty / badges / priority hint / per-key labels / reset / bulkApply /
admins (label, placeholder, save messages, popconfirm copy) across
zh-CN, en-US, ko-KR, ru-RU.
Misc:
- internal/utils/filesize.go doc: clarify MAX_FILE_SIZE_MB is a
deploy-time-only knob (nginx + docreader + frontend each cache the
env at startup); a SystemAdmin UI override would mislead operators
because nginx would still 413. Until all four layers can hot-reload
the limit in lockstep, this stays env-only.
- internal/utils/security.go: SSRF whitelist parser/runtime now drives
off SystemSettingService for live updates; ENV remains the fallback
for never-overridden deployments.
540 lines
18 KiB
Go
540 lines
18 KiB
Go
package session
|
|
|
|
import (
|
|
stderrors "errors"
|
|
"net/http"
|
|
|
|
"github.com/Tencent/WeKnora/internal/config"
|
|
"github.com/Tencent/WeKnora/internal/errors"
|
|
"github.com/Tencent/WeKnora/internal/infrastructure/docparser"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
secutils "github.com/Tencent/WeKnora/internal/utils"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// Handler handles all HTTP requests related to conversation sessions
|
|
type Handler struct {
|
|
messageService interfaces.MessageService // Service for managing messages
|
|
sessionService interfaces.SessionService // Service for managing sessions
|
|
streamManager interfaces.StreamManager // Manager for handling streaming responses
|
|
config *config.Config // Application configuration
|
|
knowledgebaseService interfaces.KnowledgeBaseService // Service for managing knowledge bases
|
|
customAgentService interfaces.CustomAgentService // Service for managing custom agents
|
|
tenantService interfaces.TenantService // Service for loading tenant (shared agent context)
|
|
agentShareService interfaces.AgentShareService // Service for resolving shared agents (KB scope in retrieval)
|
|
kbShareService interfaces.KBShareService // Service for resolving shared KB permissions
|
|
fileService interfaces.FileService // Service for file storage (image uploads)
|
|
modelService interfaces.ModelService // Service for model management (VLM access)
|
|
userService interfaces.UserService // Service for resolving per-user preferences (e.g. enable_memory default)
|
|
attachmentProcessor *AttachmentProcessor // Processor for file attachments
|
|
}
|
|
|
|
// NewHandler creates a new instance of Handler with all necessary dependencies
|
|
func NewHandler(
|
|
sessionService interfaces.SessionService,
|
|
messageService interfaces.MessageService,
|
|
streamManager interfaces.StreamManager,
|
|
config *config.Config,
|
|
knowledgebaseService interfaces.KnowledgeBaseService,
|
|
customAgentService interfaces.CustomAgentService,
|
|
tenantService interfaces.TenantService,
|
|
agentShareService interfaces.AgentShareService,
|
|
kbShareService interfaces.KBShareService,
|
|
fileService interfaces.FileService,
|
|
modelService interfaces.ModelService,
|
|
userService interfaces.UserService,
|
|
documentReader interfaces.DocumentReader,
|
|
imageResolver *docparser.ImageResolver,
|
|
) *Handler {
|
|
return &Handler{
|
|
sessionService: sessionService,
|
|
messageService: messageService,
|
|
streamManager: streamManager,
|
|
config: config,
|
|
knowledgebaseService: knowledgebaseService,
|
|
customAgentService: customAgentService,
|
|
tenantService: tenantService,
|
|
agentShareService: agentShareService,
|
|
kbShareService: kbShareService,
|
|
fileService: fileService,
|
|
modelService: modelService,
|
|
userService: userService,
|
|
attachmentProcessor: NewAttachmentProcessor(
|
|
fileService,
|
|
documentReader,
|
|
imageResolver,
|
|
modelService,
|
|
),
|
|
}
|
|
}
|
|
|
|
// CreateSession godoc
|
|
// @Summary 创建会话
|
|
// @Description 创建新的对话会话
|
|
// @Tags 会话
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body CreateSessionRequest true "会话创建请求"
|
|
// @Success 201 {object} map[string]interface{} "创建的会话"
|
|
// @Failure 400 {object} errors.AppError "请求参数错误"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions [post]
|
|
func (h *Handler) CreateSession(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
// Parse and validate the request body
|
|
var request CreateSessionRequest
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
logger.Error(ctx, "Failed to validate session creation parameters", err)
|
|
c.Error(errors.NewBadRequestError(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Get tenant ID from context
|
|
tenantID, exists := c.Get(types.TenantIDContextKey.String())
|
|
if !exists {
|
|
logger.Error(ctx, "Failed to get tenant ID")
|
|
c.Error(errors.NewUnauthorizedError("Unauthorized"))
|
|
return
|
|
}
|
|
|
|
// Sessions are now knowledge-base-independent:
|
|
// - All configuration comes from custom agent at query time
|
|
// - Session only stores basic info (tenant ID, title, description)
|
|
logger.Infof(
|
|
ctx,
|
|
"Processing session creation request, tenant ID: %d",
|
|
tenantID,
|
|
)
|
|
|
|
// Create session object with base properties
|
|
createdSession := &types.Session{
|
|
TenantID: tenantID.(uint64),
|
|
Title: request.Title,
|
|
Description: request.Description,
|
|
}
|
|
// Attach the calling user as the session owner when available.
|
|
// API-key / legacy callers without a user id fall back to tenant-level visibility.
|
|
if userID, ok := types.UserIDFromContext(ctx); ok {
|
|
createdSession.UserID = userID
|
|
}
|
|
|
|
// Call service to create session
|
|
logger.Infof(ctx, "Calling session service to create session")
|
|
createdSession, err := h.sessionService.CreateSession(ctx, createdSession)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Return created session
|
|
logger.Infof(ctx, "Session created successfully, ID: %s", createdSession.ID)
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"success": true,
|
|
"data": createdSession,
|
|
})
|
|
}
|
|
|
|
// GetSession godoc
|
|
// @Summary 获取会话详情
|
|
// @Description 根据ID获取会话详情
|
|
// @Tags 会话
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "会话ID"
|
|
// @Success 200 {object} map[string]interface{} "会话详情"
|
|
// @Failure 404 {object} errors.AppError "会话不存在"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions/{id} [get]
|
|
func (h *Handler) GetSession(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
logger.Info(ctx, "Start retrieving session")
|
|
|
|
// Get session ID from URL parameter
|
|
id := secutils.SanitizeForLog(c.Param("id"))
|
|
if id == "" {
|
|
logger.Error(ctx, "Session ID is empty")
|
|
c.Error(errors.NewBadRequestError(errors.ErrInvalidSessionID.Error()))
|
|
return
|
|
}
|
|
|
|
// Call service to get session details
|
|
logger.Infof(ctx, "Retrieving session, ID: %s", id)
|
|
session, err := h.sessionService.GetSession(ctx, id)
|
|
if err != nil {
|
|
if stderrors.Is(err, errors.ErrSessionNotFound) {
|
|
logger.Warnf(ctx, "Session not found, ID: %s", id)
|
|
c.Error(errors.NewNotFoundError(err.Error()))
|
|
return
|
|
}
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Return session data
|
|
logger.Infof(ctx, "Session retrieved successfully, ID: %s", id)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": session,
|
|
})
|
|
}
|
|
|
|
// GetSessionsByTenant godoc
|
|
// @Summary 获取会话列表
|
|
// @Description 获取当前租户的会话列表,支持分页、关键字搜索、按来源/Agent 筛选
|
|
// @Tags 会话
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param page query int false "页码"
|
|
// @Param page_size query int false "每页数量"
|
|
// @Param keyword query string false "标题模糊搜索"
|
|
// @Param source query string false "来源过滤:web / feishu / wechat / slack / ..."
|
|
// @Param agent_id query string false "按 Agent 过滤(仅对 IM 会话生效)"
|
|
// @Success 200 {object} map[string]interface{} "会话列表"
|
|
// @Failure 400 {object} errors.AppError "请求参数错误"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions [get]
|
|
func (h *Handler) GetSessionsByTenant(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
// Parse pagination parameters from query
|
|
var pagination types.Pagination
|
|
if err := c.ShouldBindQuery(&pagination); err != nil {
|
|
logger.Error(ctx, "Failed to parse pagination parameters", err)
|
|
c.Error(errors.NewBadRequestError(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Response items always include pin state and (when available) IM origin
|
|
// fields so the frontend can render pin icons / source badges without a
|
|
// second roundtrip. Unset filter params behave like "no filter".
|
|
result, err := h.sessionService.ListSessions(ctx, &types.SessionListQuery{
|
|
Keyword: c.Query("keyword"),
|
|
Source: c.Query("source"),
|
|
AgentID: c.Query("agent_id"),
|
|
Page: pagination.Page,
|
|
PageSize: pagination.PageSize,
|
|
})
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": result.Data,
|
|
"total": result.Total,
|
|
"page": result.Page,
|
|
"page_size": result.PageSize,
|
|
})
|
|
}
|
|
|
|
// UpdateSession godoc
|
|
// @Summary 更新会话
|
|
// @Description 更新会话属性
|
|
// @Tags 会话
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "会话ID"
|
|
// @Param request body types.Session true "会话信息"
|
|
// @Success 200 {object} map[string]interface{} "更新后的会话"
|
|
// @Failure 404 {object} errors.AppError "会话不存在"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions/{id} [put]
|
|
func (h *Handler) UpdateSession(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
// Get session ID from URL parameter
|
|
id := secutils.SanitizeForLog(c.Param("id"))
|
|
if id == "" {
|
|
logger.Error(ctx, "Session ID is empty")
|
|
c.Error(errors.NewBadRequestError(errors.ErrInvalidSessionID.Error()))
|
|
return
|
|
}
|
|
|
|
// Verify tenant ID from context for authorization
|
|
tenantID, exists := c.Get(types.TenantIDContextKey.String())
|
|
if !exists {
|
|
logger.Error(ctx, "Failed to get tenant ID")
|
|
c.Error(errors.NewUnauthorizedError("Unauthorized"))
|
|
return
|
|
}
|
|
|
|
// Parse request body to session object
|
|
var session types.Session
|
|
if err := c.ShouldBindJSON(&session); err != nil {
|
|
logger.Error(ctx, "Failed to parse session data", err)
|
|
c.Error(errors.NewBadRequestError(err.Error()))
|
|
return
|
|
}
|
|
|
|
session.ID = id
|
|
session.TenantID = tenantID.(uint64)
|
|
|
|
// Call service to update session
|
|
if err := h.sessionService.UpdateSession(ctx, &session); err != nil {
|
|
if stderrors.Is(err, errors.ErrSessionNotFound) {
|
|
logger.Warnf(ctx, "Session not found, ID: %s", id)
|
|
c.Error(errors.NewNotFoundError(err.Error()))
|
|
return
|
|
}
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Reload session from database to return complete timestamps and stored fields
|
|
updatedSession, err := h.sessionService.GetSession(ctx, id)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Return updated session
|
|
logger.Infof(ctx, "Session updated successfully, ID: %s", id)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": updatedSession,
|
|
})
|
|
}
|
|
|
|
// DeleteSession godoc
|
|
// @Summary 删除会话
|
|
// @Description 删除指定的会话
|
|
// @Tags 会话
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "会话ID"
|
|
// @Success 200 {object} map[string]interface{} "删除成功"
|
|
// @Failure 404 {object} errors.AppError "会话不存在"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions/{id} [delete]
|
|
func (h *Handler) DeleteSession(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
// Get session ID from URL parameter
|
|
id := secutils.SanitizeForLog(c.Param("id"))
|
|
if id == "" {
|
|
logger.Error(ctx, "Session ID is empty")
|
|
c.Error(errors.NewBadRequestError(errors.ErrInvalidSessionID.Error()))
|
|
return
|
|
}
|
|
|
|
// Call service to delete session
|
|
if err := h.sessionService.DeleteSession(ctx, id); err != nil {
|
|
if stderrors.Is(err, errors.ErrSessionNotFound) {
|
|
logger.Warnf(ctx, "Session not found, ID: %s", id)
|
|
c.Error(errors.NewNotFoundError(err.Error()))
|
|
return
|
|
}
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Return success message
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Session deleted successfully",
|
|
})
|
|
}
|
|
|
|
// ClearSessionMessages godoc
|
|
// @Summary 清空会话消息
|
|
// @Description 删除会话中的所有消息,同时清除 LLM 上下文和聊天历史知识库条目。会话本身保留。
|
|
// @Tags 会话
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "会话ID"
|
|
// @Success 200 {object} map[string]interface{} "清空成功"
|
|
// @Failure 400 {object} errors.AppError "请求参数错误"
|
|
// @Failure 404 {object} errors.AppError "会话不存在"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions/{id}/messages [delete]
|
|
func (h *Handler) ClearSessionMessages(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
id := secutils.SanitizeForLog(c.Param("id"))
|
|
if id == "" {
|
|
logger.Error(ctx, "Session ID is empty")
|
|
c.Error(errors.NewBadRequestError(errors.ErrInvalidSessionID.Error()))
|
|
return
|
|
}
|
|
|
|
logger.Infof(ctx, "Clearing all messages for session: %s", id)
|
|
|
|
if err := h.messageService.ClearSessionMessages(ctx, id); err != nil {
|
|
if stderrors.Is(err, errors.ErrSessionNotFound) {
|
|
logger.Warnf(ctx, "Session not found, ID: %s", id)
|
|
c.Error(errors.NewNotFoundError(err.Error()))
|
|
return
|
|
}
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{"session_id": id})
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
|
|
logger.Infof(ctx, "Session messages cleared successfully, ID: %s", id)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Session messages cleared successfully",
|
|
})
|
|
}
|
|
|
|
// batchDeleteRequest represents the request body for batch deleting sessions
|
|
type batchDeleteRequest struct {
|
|
IDs []string `json:"ids"`
|
|
DeleteAll bool `json:"delete_all"`
|
|
}
|
|
|
|
// BatchDeleteSessions godoc
|
|
// @Summary 批量删除会话
|
|
// @Description 根据ID列表批量删除对话会话,或设置 delete_all=true 删除当前租户的所有会话
|
|
// @Tags 会话
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body batchDeleteRequest true "批量删除请求"
|
|
// @Success 200 {object} map[string]interface{} "删除结果"
|
|
// @Failure 400 {object} errors.AppError "请求参数错误"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions/batch [delete]
|
|
func (h *Handler) BatchDeleteSessions(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
var req batchDeleteRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Errorf(ctx, "Invalid batch delete request: %v", err)
|
|
c.Error(errors.NewBadRequestError("invalid request"))
|
|
return
|
|
}
|
|
|
|
if req.DeleteAll {
|
|
if err := h.sessionService.DeleteAllSessions(ctx); err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "All sessions deleted successfully",
|
|
})
|
|
return
|
|
}
|
|
|
|
if len(req.IDs) == 0 {
|
|
c.Error(errors.NewBadRequestError("ids are required when delete_all is false"))
|
|
return
|
|
}
|
|
|
|
// Sanitize all IDs
|
|
sanitizedIDs := make([]string, 0, len(req.IDs))
|
|
for _, id := range req.IDs {
|
|
sanitized := secutils.SanitizeForLog(id)
|
|
if sanitized != "" {
|
|
sanitizedIDs = append(sanitizedIDs, sanitized)
|
|
}
|
|
}
|
|
|
|
if len(sanitizedIDs) == 0 {
|
|
c.Error(errors.NewBadRequestError("no valid session IDs provided"))
|
|
return
|
|
}
|
|
|
|
if err := h.sessionService.BatchDeleteSessions(ctx, sanitizedIDs); err != nil {
|
|
if stderrors.Is(err, errors.ErrSessionNotFound) {
|
|
logger.Warnf(ctx, "No visible sessions found for batch delete")
|
|
c.Error(errors.NewNotFoundError(err.Error()))
|
|
return
|
|
}
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Sessions deleted successfully",
|
|
})
|
|
}
|
|
|
|
// PinSession godoc
|
|
// @Summary 置顶会话
|
|
// @Description 将指定会话置顶(用户维度)
|
|
// @Tags 会话
|
|
// @Produce json
|
|
// @Param session_id path string true "会话ID"
|
|
// @Success 200 {object} map[string]interface{} "置顶成功"
|
|
// @Failure 404 {object} errors.AppError "会话不存在"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions/{session_id}/pin [post]
|
|
func (h *Handler) PinSession(c *gin.Context) {
|
|
h.setSessionPinned(c, true)
|
|
}
|
|
|
|
// UnpinSession godoc
|
|
// @Summary 取消置顶会话
|
|
// @Description 取消指定会话的置顶
|
|
// @Tags 会话
|
|
// @Produce json
|
|
// @Param id path string true "会话ID"
|
|
// @Success 200 {object} map[string]interface{} "取消置顶成功"
|
|
// @Failure 404 {object} errors.AppError "会话不存在"
|
|
// @Security Bearer
|
|
// @Security ApiKeyAuth
|
|
// @Router /sessions/{id}/pin [delete]
|
|
func (h *Handler) UnpinSession(c *gin.Context) {
|
|
h.setSessionPinned(c, false)
|
|
}
|
|
|
|
func (h *Handler) setSessionPinned(c *gin.Context, pinned bool) {
|
|
ctx := c.Request.Context()
|
|
|
|
// POST and DELETE for /sessions/.../pin register under different wildcards
|
|
// (POST :session_id, DELETE :id — see router.go). Accept whichever is set.
|
|
rawID := c.Param("session_id")
|
|
if rawID == "" {
|
|
rawID = c.Param("id")
|
|
}
|
|
id := secutils.SanitizeForLog(rawID)
|
|
if id == "" {
|
|
logger.Error(ctx, "Session ID is empty")
|
|
c.Error(errors.NewBadRequestError(errors.ErrInvalidSessionID.Error()))
|
|
return
|
|
}
|
|
|
|
rows, err := h.sessionService.SetSessionPinned(ctx, id, pinned)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"session_id": id,
|
|
"pinned": pinned,
|
|
})
|
|
c.Error(errors.NewInternalServerError(err.Error()))
|
|
return
|
|
}
|
|
// Zero rows means the session doesn't exist or isn't visible to this user;
|
|
// tell the client rather than reporting success.
|
|
if rows == 0 {
|
|
c.Error(errors.NewNotFoundError(errors.ErrSessionNotFound.Error()))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"is_pinned": pinned,
|
|
})
|
|
}
|