Files

78 lines
3.5 KiB
Go
Raw Permalink Normal View History

package saas
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"data-terminal-cloud/internal/audit"
"github.com/jackc/pgx/v5"
)
type InitialConsentSelection struct {
Types []string `json:"types"`
Confirmed bool `json:"confirmed"`
}
var ErrInitialConsentInvalid = errors.New("initial consent selection is invalid or unconfirmed")
var initialConsentNotices = map[string]string{
ConsentTranscriptStorage: "保存后续聊天的文字记录,便于查看聊天内容。",
ConsentRawAudioStorage: "保存后续聊天的原始录音;播放、下载仍需单独权限,设备支持录音上传时才能生成录音。",
ConsentStoryExtraction: "从后续聊天文字整理故事和摘要,需要同时开启字幕保存。",
ConsentPersonalMemory: "从后续聊天文字整理名字、偏好等记忆,并用于下次聊天;生成新记忆需要同时开启字幕保存。",
ConsentFamilySharing: "允许按后续独立分享规则向家庭成员分享内容;本项不会自动分享全部聊天或录音。",
}
func ValidateInitialConsents(selection InitialConsentSelection) error {
if len(selection.Types) > 0 && !selection.Confirmed {
return ErrInitialConsentInvalid
}
seen := map[string]bool{}
for _, kind := range selection.Types {
if _, ok := initialConsentNotices[kind]; !ok || seen[kind] {
return ErrInitialConsentInvalid
}
seen[kind] = true
}
return nil
}
// InitializeSubjectMemory is called only from new-profile creation, never from
// binding, profile edits, or reads, so a withdrawn permission stays withdrawn.
func InitializeSubjectMemory(ctx context.Context, tx pgx.Tx, tenantID, subjectPublicID, actorID, requestID string, selection InitialConsentSelection) error {
if err := ValidateInitialConsents(selection); err != nil {
return err
}
var subjectID, policy, actorPublicID string
if err := tx.QueryRow(ctx, `SELECT sp.id::text,t.policy_version,ua.public_id::text
FROM subject_profile sp JOIN tenant t ON t.id=sp.tenant_id
JOIN user_account ua ON ua.tenant_id=t.id AND ua.id=$3::uuid
WHERE sp.public_id=$1::uuid AND sp.tenant_id=$2::uuid AND ua.status='active'
FOR UPDATE OF sp`, subjectPublicID, tenantID, actorID).Scan(&subjectID, &policy, &actorPublicID); err != nil {
return fmt.Errorf("resolve initial memory consent: %w", err)
}
for _, kind := range selection.Types {
sum := sha256.Sum256([]byte(initialConsentNotices[kind] + "各项授权适用于后续新通话,可在数据与授权中分别撤回。"))
var consentID string
if err := tx.QueryRow(ctx, `INSERT INTO consent_record
(tenant_id,subject_id,consent_type,scope,policy_version,notice_digest,actor_user_id,evidence_ref)
VALUES($1::uuid,$2::uuid,$3,'{"scope":"future_conversations","source":"subject_creation_selection","notice_version":"creation-consents-v1"}'::jsonb,$4,$5,$6::uuid,$7)
RETURNING public_id::text`, tenantID, subjectID, kind, policy, hex.EncodeToString(sum[:]), actorID, "subject-create:"+requestID).Scan(&consentID); err != nil {
return fmt.Errorf("insert initial memory consent: %w", err)
}
if err := audit.NewRepository().Record(ctx, tx, audit.Event{
TenantID: &tenantID, ActorType: "user", ActorPublicID: &actorPublicID,
Action: "consent.grant", ResourceType: "consent", ResourceID: &consentID,
RequestID: requestID, Result: "success", Detail: map[string]any{
"subject_id": subjectPublicID, "consent_type": kind, "source": "subject_creation_selection", "notice_version": "creation-consents-v1",
},
}); err != nil {
return err
}
}
return createConsentSnapshot(ctx, tx, tenantID, subjectID)
}