mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-20 11:56:23 +00:00
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
package usersignup
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
)
|
|
|
|
// This file holds the one-time migration of the pre-actor signup tokens.
|
|
// The "actor tokens" migration freezes the signup_tokens table (and its user-group associations) into a JSON document stored in the "kv" table under the "signup_tokens_migrated" key.
|
|
// It's loaded here to create the per-token actors on first startup.
|
|
|
|
// signupTokensMigratedKey is the kv key under which the pre-actor signup tokens were frozen.
|
|
const signupTokensMigratedKey = "signup_tokens_migrated" // #nosec G101 -- database key name, not a credential
|
|
|
|
// migratedSignupToken is the JSON shape of a signup token frozen into the kv table by the migration.
|
|
// All timestamps are expressed as Unix seconds.
|
|
type migratedSignupToken struct {
|
|
ID string `json:"id"`
|
|
Token string `json:"token"`
|
|
ExpiresAt int64 `json:"expiresAt"`
|
|
UsageLimit int `json:"usageLimit"`
|
|
UsageCount int `json:"usageCount"`
|
|
UserGroupIDs []string `json:"userGroupIds"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
}
|
|
|
|
// migrateSignupTokens creates an actor for every signup token frozen into the kv table by the migration.
|
|
// It requires the actor state store to be available, so it must run after the actor host is ready.
|
|
// It is idempotent: tokens that have already been migrated are left untouched, so a token that has been used since it was migrated is never reset.
|
|
func (s *Service) migrateSignupTokens(ctx context.Context) error {
|
|
migrated, err := loadMigratedSignupTokens(ctx, s.db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(migrated) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var count int
|
|
for _, m := range migrated {
|
|
// Skip tokens that have already expired, since there would be nothing left to store
|
|
expiresAt := time.Unix(m.ExpiresAt, 0)
|
|
if !expiresAt.After(time.Now()) {
|
|
continue
|
|
}
|
|
|
|
state := SignupTokenState{
|
|
ID: m.ID,
|
|
ExpiresAt: expiresAt,
|
|
UsageLimit: m.UsageLimit,
|
|
UsageCount: m.UsageCount,
|
|
UserGroupIDs: m.UserGroupIDs,
|
|
CreatedAt: time.Unix(m.CreatedAt, 0),
|
|
}
|
|
|
|
// The token's value is the actor's ID
|
|
// The "migrate" method only writes the state if the actor doesn't have one already
|
|
_, err = s.actorService.Invoke(ctx, SignupTokenActorType, m.Token, signupTokenMethodMigrate, state)
|
|
if err != nil {
|
|
return fmt.Errorf("error migrating signup token '%s': %w", m.ID, err)
|
|
}
|
|
count++
|
|
}
|
|
|
|
slog.InfoContext(ctx, "Migrated signup tokens to actors", slog.Int("count", count))
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadMigratedSignupTokens reads the signup tokens frozen into the kv table by the migration
|
|
// It returns nil if there's nothing to migrate
|
|
func loadMigratedSignupTokens(ctx context.Context, db *gorm.DB) ([]migratedSignupToken, error) {
|
|
row := model.KV{
|
|
Key: signupTokensMigratedKey,
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
err := db.WithContext(ctx).First(&row).Error
|
|
switch {
|
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
|
// There are no migrated signup tokens in the database, nothing to do
|
|
return nil, nil
|
|
case err != nil:
|
|
return nil, fmt.Errorf("failed to load migrated signup tokens from the database: %w", err)
|
|
case row.Value == nil || len(*row.Value) == 0:
|
|
// Also no migrated signup tokens, nothing to do
|
|
return nil, nil
|
|
}
|
|
|
|
var migrated []migratedSignupToken
|
|
err = json.Unmarshal([]byte(*row.Value), &migrated)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing migrated signup tokens: %w", err)
|
|
}
|
|
|
|
return migrated, nil
|
|
}
|