Files
2026-05-24 11:27:02 +07:00

167 lines
5.1 KiB
Go

//go:build sqlite || sqliteonly
package sqlitestore
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/crypto"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
type SQLiteBrowserCookieStore struct {
db *sql.DB
encKey string
}
func NewSQLiteBrowserCookieStore(db *sql.DB, encKey string) *SQLiteBrowserCookieStore {
return &SQLiteBrowserCookieStore{db: db, encKey: encKey}
}
func (s *SQLiteBrowserCookieStore) Upsert(ctx context.Context, scope store.BrowserCookieScope, cookies []store.BrowserCookie) (int, error) {
if s.encKey == "" {
return 0, store.ErrBrowserCookieEncryptionRequired
}
if err := scope.Validate(); err != nil {
return 0, err
}
now := time.Now().UTC()
nowStr := now.Format(time.RFC3339Nano)
count := 0
for _, raw := range cookies {
c := store.NormalizeBrowserCookie(raw)
if err := store.ValidateBrowserCookie(c); err != nil {
return count, err
}
encrypted, err := crypto.Encrypt(c.Value, s.encKey)
if err != nil {
return count, fmt.Errorf("encrypt browser cookie: %w", err)
}
id := c.ID
if id == uuid.Nil {
id = store.GenNewID()
}
var expiresAt any
if c.ExpiresAt != nil {
expiresAt = c.ExpiresAt.UTC().Format(time.RFC3339Nano)
}
res, err := s.db.ExecContext(ctx, `
INSERT INTO browser_cookies (
id, tenant_id, user_id, agent_id, domain, name, path, encrypted_value,
secure, http_only, same_site, expires_at, source, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (tenant_id, user_id, agent_id, domain, path, name)
DO UPDATE SET encrypted_value = excluded.encrypted_value,
secure = excluded.secure,
http_only = excluded.http_only,
same_site = excluded.same_site,
expires_at = excluded.expires_at,
source = excluded.source,
updated_at = excluded.updated_at`,
id.String(), scope.TenantID.String(), scope.UserID, scope.AgentID, c.Domain, c.Name, c.Path, encrypted,
c.Secure, c.HTTPOnly, c.SameSite, expiresAt, c.Source, nowStr, nowStr,
)
if err != nil {
return count, err
}
if n, _ := res.RowsAffected(); n > 0 {
count++
}
}
return count, nil
}
func (s *SQLiteBrowserCookieStore) List(ctx context.Context, scope store.BrowserCookieScope, filter store.BrowserCookieFilter) ([]store.BrowserCookie, error) {
if s.encKey == "" {
return nil, store.ErrBrowserCookieEncryptionRequired
}
if err := scope.Validate(); err != nil {
return nil, err
}
query := `SELECT id, tenant_id, user_id, agent_id, domain, name, path, encrypted_value,
secure, http_only, same_site, expires_at, source, created_at, updated_at
FROM browser_cookies
WHERE tenant_id = ? AND user_id = ? AND agent_id = ?
AND (expires_at IS NULL OR expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`
args := []any{scope.TenantID.String(), scope.UserID, scope.AgentID}
if filter.Domain != "" {
query += " AND domain = ?"
args = append(args, strings.ToLower(strings.TrimSpace(filter.Domain)))
}
if filter.Name != "" {
query += " AND name = ?"
args = append(args, strings.TrimSpace(filter.Name))
}
if filter.Path != "" {
query += " AND path = ?"
args = append(args, strings.TrimSpace(filter.Path))
}
query += " ORDER BY domain, path, name"
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return s.scanRows(rows)
}
func (s *SQLiteBrowserCookieStore) Delete(ctx context.Context, scope store.BrowserCookieScope, filter store.BrowserCookieFilter) (int, error) {
if err := scope.Validate(); err != nil {
return 0, err
}
query := `DELETE FROM browser_cookies WHERE tenant_id = ? AND user_id = ? AND agent_id = ?`
args := []any{scope.TenantID.String(), scope.UserID, scope.AgentID}
if filter.Domain != "" {
query += " AND domain = ?"
args = append(args, strings.ToLower(strings.TrimSpace(filter.Domain)))
}
if filter.Name != "" {
query += " AND name = ?"
args = append(args, strings.TrimSpace(filter.Name))
}
if filter.Path != "" {
query += " AND path = ?"
args = append(args, strings.TrimSpace(filter.Path))
}
res, err := s.db.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
n, _ := res.RowsAffected()
return int(n), nil
}
func (s *SQLiteBrowserCookieStore) scanRows(rows *sql.Rows) ([]store.BrowserCookie, error) {
var out []store.BrowserCookie
for rows.Next() {
var c store.BrowserCookie
var id, tenantID, encrypted string
var expiresAt nullSqliteTime
createdAt, updatedAt := scanTimePair()
if err := rows.Scan(&id, &tenantID, &c.UserID, &c.AgentID, &c.Domain, &c.Name, &c.Path,
&encrypted, &c.Secure, &c.HTTPOnly, &c.SameSite, &expiresAt, &c.Source, createdAt, updatedAt); err != nil {
return nil, err
}
c.ID = uuid.MustParse(id)
c.TenantID = uuid.MustParse(tenantID)
if expiresAt.Valid {
c.ExpiresAt = &expiresAt.Time
}
c.CreatedAt = createdAt.Time
c.UpdatedAt = updatedAt.Time
value, err := crypto.Decrypt(encrypted, s.encKey)
if err != nil {
return nil, fmt.Errorf("decrypt browser cookie: %w", err)
}
c.Value = value
out = append(out, c)
}
return out, rows.Err()
}