mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-31 08:11:27 +02:00
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
84 lines
2.8 KiB
Go
84 lines
2.8 KiB
Go
package usersignup
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/italypaleale/francis/host/local"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
)
|
|
|
|
type TokenService interface {
|
|
GenerateAccessToken(user model.User, authenticationMethod string, sessionDuration time.Duration) (string, error)
|
|
}
|
|
|
|
type AuditLogger interface {
|
|
Create(ctx context.Context, event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData, tx *gorm.DB) (model.AuditLog, bool)
|
|
}
|
|
|
|
type UserCreator interface {
|
|
CreateUserInternal(ctx context.Context, dbConfig *appconfig.AppConfigModel, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error)
|
|
}
|
|
|
|
// AppConfigResolver loads the current application configuration, so handlers can pass it explicitly to the service methods that need it
|
|
type AppConfigResolver interface {
|
|
GetConfig(ctx context.Context) (*appconfig.AppConfigModel, error)
|
|
}
|
|
|
|
type Dependencies struct {
|
|
DB *gorm.DB
|
|
Actors *local.Host
|
|
|
|
Signer TokenService
|
|
AuditLog AuditLogger
|
|
UserCreator UserCreator
|
|
AppConfig AppConfigResolver
|
|
}
|
|
|
|
type Module struct {
|
|
service *Service
|
|
handler *handler
|
|
}
|
|
|
|
func New(ctx context.Context, deps Dependencies) (*Module, error) {
|
|
// Load the signup tokens currently stored in the database, so the singleton actor can be seeded (migrated) from them on first startup
|
|
existing, err := loadExistingSignupTokens(ctx, deps.DB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Register the singleton actor that holds all signup tokens
|
|
bootstrapData := &signupTokenBootstrap{Tokens: existing}
|
|
err = deps.Actors.RegisterSingletonActor(
|
|
SignupTokenActorType, NewSignupTokenActor,
|
|
local.WithBootstrapData(bootstrapData),
|
|
local.WithIdleTimeout(-1), // Disable idle timeout for this actor
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error registering the %s actor: %w", SignupTokenActorType, err)
|
|
}
|
|
|
|
service := newService(deps, deps.Actors.Service())
|
|
return &Module{
|
|
service: service,
|
|
handler: newHandler(service, deps.AppConfig),
|
|
}, nil
|
|
}
|
|
|
|
// RegisterRoutes mounts the signup and signup-token management endpoints
|
|
// adminAuth guards the admin token-management routes; signupRateLimit throttles public self-signup
|
|
func (m *Module) RegisterRoutes(apiGroup *gin.RouterGroup, adminAuth, signupRateLimit gin.HandlerFunc) {
|
|
apiGroup.POST("/signup-tokens", adminAuth, m.handler.createSignupToken)
|
|
apiGroup.GET("/signup-tokens", adminAuth, m.handler.listSignupTokens)
|
|
apiGroup.DELETE("/signup-tokens/:id", adminAuth, m.handler.deleteSignupToken)
|
|
apiGroup.POST("/signup", signupRateLimit, m.handler.signup)
|
|
apiGroup.GET("/signup/setup", m.handler.checkInitialAdminSetupAvailable)
|
|
apiGroup.POST("/signup/setup", m.handler.signUpInitialAdmin)
|
|
}
|