mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 00:14:33 +00:00
167 lines
5.1 KiB
Go
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()
|
|
}
|