feat: add ability to force email verification (#1837)

This commit is contained in:
Elias Schneider
2026-10-13 22:04:33 +02:00
committed by GitHub
parent eb8ea6bed2
commit 0ffeb170cf
81 changed files with 2310 additions and 263 deletions
+55 -5
View File
@@ -17,10 +17,12 @@ const ActorType = "EmailVerification"
const (
// MethodIssue replaces the outstanding verification state for a user
MethodIssue = "issue"
methodConsume = "consume"
methodDiscard = "discard"
methodRestore = "restore"
MethodIssue = "issue"
methodConsume = "consume"
methodDiscard = "discard"
methodRestore = "restore"
methodCancelChange = "cancelChange"
methodPendingChange = "pendingChange"
)
type consumeStatus string
@@ -32,7 +34,14 @@ const (
// State is the persisted verification state for one user
type State struct {
TokenHash string
TokenHash string
Email string
ReplacesEmail string
ExpiresAt time.Time
}
// EmailChange describes an email change that is waiting for the user to confirm it
type EmailChange struct {
Email string
ExpiresAt time.Time
}
@@ -68,11 +77,35 @@ func (a *emailVerificationActor) Invoke(ctx context.Context, method string, data
return nil, a.discard(ctx, data)
case methodRestore:
return nil, a.restore(ctx, data)
case methodCancelChange:
return nil, a.cancelChange(ctx)
default:
return nil, common.ErrUnsupportedActorMethod{Method: method}
}
}
// Peek implements actor.ActorPeek
func (a *emailVerificationActor) Peek(ctx context.Context, method string, _ actor.Envelope) (any, error) {
if method != methodPendingChange {
return nil, common.ErrUnsupportedActorMethod{Method: method}
}
state, err := a.client.GetState(ctx)
if err != nil {
return nil, fmt.Errorf("error retrieving actor state: %w", err)
}
// Only an unexpired email change is pending, a token for the current email is not
if state.ReplacesEmail == "" || state.ExpiresAt.Before(time.Now()) {
return EmailChange{}, nil
}
return EmailChange{
Email: state.Email,
ExpiresAt: state.ExpiresAt,
}, nil
}
func (a *emailVerificationActor) issue(ctx context.Context, data actor.Envelope) error {
state, err := decodeState(data, MethodIssue)
if err != nil {
@@ -134,6 +167,23 @@ func (a *emailVerificationActor) discard(ctx context.Context, data actor.Envelop
return nil
}
func (a *emailVerificationActor) cancelChange(ctx context.Context) error {
state, err := a.client.GetState(ctx)
if err != nil {
return fmt.Errorf("error retrieving actor state: %w", err)
}
if state.ReplacesEmail == "" {
return nil
}
err = a.client.DeleteState(ctx)
if err != nil && !errors.Is(err, actor.ErrStateNotFound) {
return fmt.Errorf("error deleting actor state: %w", err)
}
return nil
}
func (a *emailVerificationActor) restore(ctx context.Context, data actor.Envelope) error {
state, err := decodeState(data, methodRestore)
if err != nil {