File CodeView

This is a code view which proxies my personal git server. On the clearnet the following file is available at:

personal-gemini-capsule/src/branch/main/internal/pocketbase/pb.go

----------------------------

package pocketbase

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"sync"
	"time"
)

type pbRes struct {
	Items json.RawMessage `json:"items"`
	Page  int             `json:"page"`
	Total int             `json:"totalItems"`
}

// AuthResponse represents the authentication response from PocketBase
type authResponse struct {
	Token string `json:"token"`
	User  struct {
		ID       string `json:"id"`
		Username string `json:"username"`
		Email    string `json:"email"`
	} `json:"record"`
}

// TokenCache holds the cached authentication token
type tokenCache struct {
	Token     string
	FetchedAt time.Time
	mu        sync.RWMutex
}

// PocketBaseClient handles communication with PocketBase API
type PocketBaseClient struct {
	host       string
	username   string
	password   string
	tokenCache *tokenCache
	httpClient *http.Client
}

// NewPocketBaseClient creates a new PocketBase client
func NewPocketBaseClient() *PocketBaseClient {
	return &PocketBaseClient{
		host:       getEnvOrDefault("POCKET_BASE_HOST", "http://localhost:8090"),
		username:   getEnvOrDefault("POCKET_BASE_USER", ""),
		password:   getEnvOrDefault("POCKET_BASE_PW", ""),
		tokenCache: &tokenCache{},
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// getEnvOrDefault gets environment variable or returns default value
func getEnvOrDefault(key, defaultValue string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return defaultValue
}

// isOlderThanADay checks if the given time is older than 23 hours
func isOlderThanADay(fetchedAt time.Time) bool {
	if fetchedAt.IsZero() {
		return true
	}
	duration := time.Since(fetchedAt)
	return duration.Hours() > 23
}

// getNewLoginToken fetches a new authentication token from PocketBase
func (c *PocketBaseClient) getNewLoginToken() (string, error) {
	loginURL := fmt.Sprintf("%s/api/collections/users/auth-with-password", c.host)

	loginData := map[string]string{
		"identity": c.username,
		"password": c.password,
	}

	jsonData, err := json.Marshal(loginData)
	if err != nil {
		return "", fmt.Errorf("failed to marshal login data: %w", err)
	}

	resp, err := c.httpClient.Post(loginURL, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to make login request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("login failed with status %d: %s", resp.StatusCode, string(body))
	}

	var authResp authResponse
	if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
		return "", fmt.Errorf("failed to decode auth response: %w", err)
	}

	return authResp.Token, nil
}

// getLoginTokenWithCache gets a cached token or fetches a new one if needed
func (c *PocketBaseClient) getLoginTokenWithCache() (string, error) {
	c.tokenCache.mu.RLock()
	token := c.tokenCache.Token
	fetchedAt := c.tokenCache.FetchedAt
	c.tokenCache.mu.RUnlock()

	if token != "" && !isOlderThanADay(fetchedAt) {
		return token, nil
	}

	c.tokenCache.mu.Lock()
	defer c.tokenCache.mu.Unlock()

	// Double-check after acquiring write lock
	if c.tokenCache.Token != "" && !isOlderThanADay(c.tokenCache.FetchedAt) {
		return c.tokenCache.Token, nil
	}

	newToken, err := c.getNewLoginToken()
	if err != nil {
		return "", err
	}

	c.tokenCache.Token = newToken
	c.tokenCache.FetchedAt = time.Now()

	return newToken, nil
}

// makeAuthenticatedRequest makes an HTTP request with authentication
func (c *PocketBaseClient) makeAuthenticatedRequest(method, url string, params url.Values) (*http.Response, error) {
	token, err := c.getLoginTokenWithCache()
	if err != nil {
		return nil, fmt.Errorf("failed to get auth token: %w", err)
	}

	if params != nil {
		url += "?" + params.Encode()
	}

	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", token)
	req.Header.Set("Content-Type", "application/json")

	return c.httpClient.Do(req)
}

func (c *PocketBaseClient) GetList(
	collection string,
	page int,
	pageSize int,
	sort string) (*pbRes, error) {
	slog.Info(
		"Getting list of items from pocketbase",
		"page", page,
		"pageSize", pageSize,
		"collection", collection)
	params := url.Values{
		"page":      {strconv.Itoa(page)},
		"perPage":   {strconv.Itoa(pageSize)},
		"sort":      {sort},
		"skipTotal": {"true"},
		"filter":    {"source = \"nostr\" || source = \"mastodon\" || source = \"blue_sky\""},
		// TODO: add additional fields like image and tag?
	}
	apiURL := fmt.Sprintf("%s/api/collections/%s/records", c.host, collection)
	resp, err := c.makeAuthenticatedRequest("GET", apiURL, params)
	if err != nil {
		return nil, fmt.Errorf("failed to make request: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
	}

	var res pbRes
	if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}
	return &res, nil
}

func (c *PocketBaseClient) GetRecord(
	collection string,
	id string,
) (*[]byte, error) {
	slog.Info(
		"Getting single record from pocketbase",
		"recordId", id)
	apiURL := fmt.Sprintf("%s/api/collections/%s/records/%s", c.host, collection, id)
	resp, err := c.makeAuthenticatedRequest("GET", apiURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to make request: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}
	defer resp.Body.Close()

	var res pbRes
	if err := json.Unmarshal(body, &res); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}
	return &body, nil

}

-----------------------------

Back to home