fix: harden identity, live access, drafts and setup (review only)

Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
This commit is contained in:
2026-10-16 12:15:20 +00:00
parent e57610a27c
commit 63e0457886
86 changed files with 5174 additions and 1929 deletions

View File

@@ -1,3 +1,5 @@
// Modified in the independent hardening fork; see docs/hardening.
package services
import (
@@ -317,17 +319,14 @@ func (s *conversationService) CreateNew(externalUser openidentity.ExternalUser,
// activity first, so the widget can offer a request list. A visitor who has not
// talked to us yet has no identity row and therefore an empty list, not an error.
//
// Scoping is by customer, not by channel: a CustomerIdentity is keyed by
// (external_source, external_id) globally, so one signed visitor reaching us
// through two channels is one person with one request history. For a guest the
// external id is issued per channel, so their identities cannot collide across
// channels anyway. Narrowing this to the current channel would silently hide the
// visitor's earlier requests.
// Public identities are scoped to a server-verified issuer and channel. Legacy
// internal integrations may still use their explicitly unscoped identity mapping.
func (s *conversationService) ListCustomerConversations(externalUser openidentity.ExternalUser, cnd *sqls.Cnd) ([]models.Conversation, *sqls.Paging, error) {
if cnd == nil {
cnd = sqls.NewCnd()
}
identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), externalUser.ExternalSource, strings.TrimSpace(externalUser.ExternalID))
identity, err := repositories.CustomerIdentityRepository.GetUniqueBy(sqls.DB(), externalUser.IdentityScope, externalUser.ExternalSource, strings.TrimSpace(externalUser.ExternalID))
if err != nil { return nil, nil, errorsx.UnauthorizedI18n("error.e0161") }
if identity == nil {
paging := &sqls.Paging{}
if cnd.Paging != nil {
@@ -336,6 +335,7 @@ func (s *conversationService) ListCustomerConversations(externalUser openidentit
return []models.Conversation{}, paging, nil
}
cnd.Eq("customer_id", identity.CustomerID).Desc("last_active_at").Desc("id")
if externalUser.ChannelID > 0 { cnd.Eq("channel_id", externalUser.ChannelID) }
list, paging := repositories.ConversationRepository.FindPageByCnd(sqls.DB(), cnd)
if list == nil {
list = []models.Conversation{}
@@ -832,8 +832,9 @@ func (s *conversationService) IsCustomerConversationOwner(conversation *models.C
if extID == "" || strings.TrimSpace(string(externalUser.ExternalSource)) == "" || conversation.CustomerID <= 0 {
return false
}
identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), externalUser.ExternalSource, extID)
if identity == nil {
if externalUser.ChannelID > 0 && externalUser.ChannelID != conversation.ChannelID { return false }
identity, err := repositories.CustomerIdentityRepository.GetUniqueBy(sqls.DB(), externalUser.IdentityScope, externalUser.ExternalSource, extID)
if err != nil || identity == nil {
return false
}
return identity.CustomerID == conversation.CustomerID