mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-20 19:51:27 +02:00
Co-authored-by: Elias Schneider <login@eliasschneider.com> Co-authored-by: Claude <noreply@anthropic.com>
163 lines
4.8 KiB
Go
163 lines
4.8 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/italypaleale/francis/builtin/ratelimit"
|
|
"github.com/italypaleale/francis/components/postgres"
|
|
"github.com/italypaleale/francis/host/local"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
"github.com/pocket-id/pocket-id/backend/internal/job"
|
|
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
|
"github.com/pocket-id/pocket-id/backend/internal/service"
|
|
"github.com/pocket-id/pocket-id/backend/internal/storage"
|
|
"github.com/pocket-id/pocket-id/backend/internal/utils/crypto"
|
|
)
|
|
|
|
type NewActorsOpts struct {
|
|
SQLite *sql.DB
|
|
Postgres *pgxpool.Pool
|
|
|
|
EnvConfig *common.EnvConfigSchema
|
|
AppConfig *service.AppConfigService
|
|
HttpClient *http.Client
|
|
DB *gorm.DB
|
|
FileStorage storage.FileStorage
|
|
}
|
|
|
|
func NewActors(o NewActorsOpts) (*local.Host, map[string]*ratelimit.RateLimitService, error) {
|
|
log := slog.Default()
|
|
|
|
// Derive a PSK from the global encryption key
|
|
// The runtime PSK derives the cluster CA used for host-to-host mTLS
|
|
psk, err := o.getPSK()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to derive PSK: %w", err)
|
|
}
|
|
|
|
// Options for the host
|
|
opts := []local.HostOption{
|
|
local.WithAddress(net.JoinHostPort(o.EnvConfig.ActorsHost, o.EnvConfig.ActorsPort)),
|
|
local.WithLogger(log.With("scope", "actor-host")),
|
|
local.WithRuntimePSKs(psk),
|
|
local.WithShutdownGracePeriod(10 * time.Second),
|
|
}
|
|
|
|
// Add all cron jobs
|
|
cronjobs, err := o.getCronJobs()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opts = append(opts, cronjobs...)
|
|
|
|
// Add the rate limiters
|
|
rateLimiters, rateLimiterOpts, err := o.getRateLimiters()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opts = append(opts, rateLimiterOpts...)
|
|
|
|
// Add the database connection
|
|
providerOpt, err := o.getProvider()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opts = append(opts, providerOpt)
|
|
|
|
// Create a new actor host
|
|
h, err := local.NewHost(opts...)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create actor host: %w", err)
|
|
}
|
|
|
|
// Bind a service for each rate limiter so the middleware can invoke them
|
|
rateLimitServices := make(map[string]*ratelimit.RateLimitService, len(rateLimiters))
|
|
for name, rl := range rateLimiters {
|
|
rateLimitServices[name] = rl.Service(h.Service())
|
|
}
|
|
|
|
return h, rateLimitServices, nil
|
|
}
|
|
|
|
// Derive a PSK from the global encryption key
|
|
func (o *NewActorsOpts) getPSK() ([]byte, error) {
|
|
// Note: changing the key derivation or the seed is a breaking change
|
|
return crypto.DeriveKey(o.EnvConfig.EncryptionKey, "pocketid/actors-psk")
|
|
}
|
|
|
|
func (o *NewActorsOpts) getProvider() (local.HostOption, error) {
|
|
switch {
|
|
case o.Postgres != nil && o.SQLite != nil:
|
|
return nil, errors.New("cannot have both Postgres and SQLite connections")
|
|
case o.Postgres != nil:
|
|
return local.WithPostgresProvider(postgres.PostgresProviderOptions{
|
|
DB: o.Postgres,
|
|
}), nil
|
|
case o.SQLite != nil:
|
|
return local.WithSQLiteProvider(local.SQLiteProviderOptions{
|
|
DB: o.SQLite,
|
|
}), nil
|
|
default:
|
|
return nil, errors.New("one of Postgres and SQLite must be set")
|
|
}
|
|
}
|
|
|
|
func (o *NewActorsOpts) getCronJobs() (opts []local.HostOption, err error) {
|
|
// In test mode, we do not register anything
|
|
if common.EnvConfig.AppEnv == "test" {
|
|
return opts, nil
|
|
}
|
|
|
|
// Register the analytics job
|
|
analyticsJob, err := job.GetAnalyticsJob(o.AppConfig, o.HttpClient)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get analytics cron job: %w", err)
|
|
}
|
|
if analyticsJob != nil {
|
|
// This could be nil if analytics are disabled
|
|
opts = append(opts, local.WithBuiltInActor(analyticsJob))
|
|
}
|
|
|
|
// Register the file cleanup jobs
|
|
fileCleanupJobs, err := job.GetFileCleanupJobs(o.DB, o.FileStorage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get file cleanup cron jobs: %w", err)
|
|
}
|
|
for _, j := range fileCleanupJobs {
|
|
opts = append(opts, local.WithBuiltInActor(j))
|
|
}
|
|
|
|
return opts, nil
|
|
}
|
|
|
|
// getRateLimiters creates a built-in rate-limit actor for each middleware policy and returns both the created actors (keyed by policy name) and the host options to register them
|
|
// Unlike cron jobs, rate limiters keep no durable state, so they are registered in every environment
|
|
func (o *NewActorsOpts) getRateLimiters() (actors map[string]*ratelimit.RateLimit, opts []local.HostOption, err error) {
|
|
policies := middleware.RateLimitPolicies()
|
|
actors = make(map[string]*ratelimit.RateLimit, len(policies))
|
|
for _, p := range policies {
|
|
rl, err := ratelimit.New(
|
|
p.Name,
|
|
ratelimit.WithRate(p.Rate),
|
|
ratelimit.WithPer(p.Per),
|
|
ratelimit.WithBurst(p.Burst),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error creating rate limiter %q: %w", p.Name, err)
|
|
}
|
|
actors[p.Name] = rl
|
|
opts = append(opts, local.WithBuiltInActor(rl))
|
|
}
|
|
|
|
return actors, opts, nil
|
|
}
|