Files
pocket-id/backend/internal/bootstrap/services_bootstrap.go
Claude 7c59dcdb82 refactor: use actors for one-time access and signup tokens
Move one-time access tokens and signup tokens onto the Francis actor
framework, so their state is coordinated cluster-wide and expired entries
are purged without the periodic db_cleanup job.

One-time access tokens:
- Each token is its own actor, keyed by the token value, storing the user
  ID and optional device token in actor state with a TTL matching the
  token's expiration. Expired state is purged automatically.
- Exchange consumes the token by invoking the actor (atomic validate +
  delete) outside of any DB transaction, then loads the user and issues an
  access token. On a later failure it compensates by restoring the token
  (best-effort, using a non-cancelable context).
- The one_time_access_tokens table is dropped; the CLI writes token state
  through a minimal, non-running actor host.

Signup tokens:
- A singleton actor holds every signup token in its state and keeps a
  single cleanup alarm scheduled for the earliest expiration; when it
  fires it purges expired tokens and reschedules. Listing uses Peek.
- Sign-up consumes a token by invoking the actor to atomically increment
  its usage count, performs user creation in a transaction, and, on
  failure, compensates by releasing the token (best-effort).
- Existing tokens are migrated from the database into the actor state on
  first startup; the tables are retained only for that migration.

Also removes ClearOneTimeAccessTokens and ClearSignupTokens, and lets the
importer skip tables no longer present in the schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBMz9v9s8S8RPYNWzshkBu
2026-07-20 21:12:46 +00:00

160 lines
5.4 KiB
Go

package bootstrap
import (
"context"
"fmt"
"net/http"
"github.com/italypaleale/francis/host/local"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/api"
"github.com/pocket-id/pocket-id/backend/internal/apikey"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"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/oidc"
"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/usersignup"
"github.com/pocket-id/pocket-id/backend/internal/webauthn"
)
type services struct {
appConfigService *appconfig.AppConfigService
appImagesService *service.AppImagesService
emailService *service.EmailService
geoLiteService *service.GeoLiteService
auditLogService *service.AuditLogService
jwtService *service.JwtService
scimService *service.ScimService
userService *service.UserService
customClaimService *service.CustomClaimService
oidcService *service.OidcService
userGroupService *service.UserGroupService
ldapService *service.LdapService
versionService *service.VersionService
fileStorage storage.FileStorage
appLockService *service.AppLockService
oneTimeAccessService *service.OneTimeAccessService
apiKeyModule *apikey.Module
oidcModule *oidc.Module
webauthnModule *webauthn.Module
userSignUpModule *usersignup.Module
apiModule *api.Module
// actors is the actor host, retained so E2E test helpers can seed and reset actor-backed state
actors *local.Host
}
// Initializes all services
func initServices(
ctx context.Context,
db *gorm.DB,
instanceID string,
actors *local.Host,
httpClient *http.Client,
imageExtensions map[string]string,
fileStorage storage.FileStorage,
scheduler *job.Scheduler,
) (svc *services, err error) {
svc = &services{
actors: actors,
}
// Init the app config service
svc.appConfigService, err = appconfig.NewService(ctx, actors, db)
if err != nil {
return nil, fmt.Errorf("failed to create app config service: %w", err)
}
svc.fileStorage = fileStorage
svc.appImagesService = service.NewAppImagesService(imageExtensions, fileStorage)
svc.appLockService = service.NewAppLockService(db)
svc.emailService, err = service.NewEmailService(db)
if err != nil {
return nil, fmt.Errorf("failed to create email service: %w", err)
}
svc.geoLiteService = service.NewGeoLiteService(httpClient)
svc.auditLogService = service.NewAuditLogService(db, svc.emailService, svc.geoLiteService, svc.appConfigService)
svc.jwtService, err = service.NewJwtService(ctx, db, instanceID)
if err != nil {
return nil, fmt.Errorf("failed to create JWT service: %w", err)
}
svc.customClaimService = service.NewCustomClaimService(db)
svc.webauthnModule, err = webauthn.New(webauthn.Dependencies{
DB: db,
AppURL: common.EnvConfig.AppURL,
Signer: svc.jwtService,
AuditLog: svc.auditLogService,
AppConfig: svc.appConfigService,
})
if err != nil {
return nil, fmt.Errorf("failed to create WebAuthn module: %w", err)
}
svc.scimService = service.NewScimService(db, scheduler, httpClient)
svc.apiModule = api.New(api.Dependencies{DB: db, Issuer: common.EnvConfig.AppURL})
svc.oidcModule, err = oidc.New(ctx, oidc.Dependencies{
DB: db,
HTTPClient: httpClient,
Config: oidc.Config{
BaseURL: common.EnvConfig.AppURL,
TokenBaseURL: common.EnvConfig.AppURL,
Secret: common.EnvConfig.EncryptionKey,
AllowInsecureCallbackURLs: common.EnvConfig.AllowInsecureCallbackURLs,
},
Signer: svc.jwtService,
CustomClaims: svc.customClaimService,
Reauth: svc.webauthnModule,
AuditLog: svc.auditLogService,
APIAccess: svc.apiModule,
})
if err != nil {
return nil, fmt.Errorf("failed to create OIDC module: %w", err)
}
svc.oidcService, err = service.NewOidcService(db, svc.jwtService, svc.oidcModule.Preview, svc.scimService, httpClient, fileStorage)
if err != nil {
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
}
svc.userGroupService = service.NewUserGroupService(db, svc.scimService)
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.customClaimService, svc.appImagesService, svc.scimService, fileStorage)
svc.ldapService = service.NewLdapService(db, httpClient, svc.userService, svc.userGroupService, fileStorage)
svc.apiKeyModule, err = apikey.New(ctx, apikey.Dependencies{
DB: db,
StaticApiKey: common.EnvConfig.StaticApiKey,
})
if err != nil {
return nil, fmt.Errorf("failed to create API key module: %w", err)
}
svc.userSignUpModule, err = usersignup.New(ctx, usersignup.Dependencies{
DB: db,
Actors: actors,
Signer: svc.jwtService,
AuditLog: svc.auditLogService,
UserCreator: svc.userService,
AppConfig: svc.appConfigService,
})
if err != nil {
return nil, fmt.Errorf("failed to create user signup module: %w", err)
}
svc.oneTimeAccessService, err = service.NewOneTimeAccessService(actors, db, svc.userService, svc.jwtService, svc.auditLogService, svc.emailService)
if err != nil {
return nil, fmt.Errorf("failed to create one-time access service: %w", err)
}
svc.versionService = service.NewVersionService(httpClient)
return svc, nil
}