Files
pocket-id/backend/internal/usersignup/cleanup_job.go
T

114 lines
3.9 KiB
Go

package usersignup
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/italypaleale/francis/builtin/cronjob"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
)
const (
// cleanupJobInterval is how often overdue signups are deleted, which bounds how long an account can outlive its deadline
cleanupJobInterval = time.Hour
// cleanupJobJitter spreads each occurrence around its scheduled time, so it doesn't coincide with the other cleanup jobs
cleanupJobJitter = 5 * time.Minute
)
// UserDeleter deletes a user together with everything that references it
type UserDeleter interface {
DeleteUser(ctx context.Context, dbConfig *appconfig.AppConfigModel, userID string, allowLdapDelete bool) error
}
type cleanupJob struct {
db *gorm.DB
appConfig appconfig.AppConfigResolver
users UserDeleter
}
func newCleanupJob(db *gorm.DB, appConfig appconfig.AppConfigResolver, users UserDeleter) (*cronjob.CronJob, error) {
job := &cleanupJob{
db: db,
appConfig: appConfig,
users: users,
}
cronActor, err := cronjob.New(
"DeleteUnverifiedSignups",
cronjob.WithJob(job.deleteUnverifiedSignups),
cronjob.WithInterval(cleanupJobInterval),
cronjob.WithJitter(cleanupJobJitter),
// Also run right after the job is first registered, so accounts that missed their deadline while Pocket ID wasn't running are removed at startup
cronjob.WithImmediate(),
cronjob.WithLogger(slog.Default()),
)
if err != nil {
return nil, fmt.Errorf("error creating unverified signup cleanup cron job: %w", err)
}
return cronActor, nil
}
// deleteUnverifiedSignups deletes self-signed-up accounts that did not verify their email before their deadline
func (j *cleanupJob) deleteUnverifiedSignups(ctx context.Context) error {
dbConfig, err := j.appConfig.GetConfig(ctx)
if err != nil {
return fmt.Errorf("error loading app config: %w", err)
}
// Deadlines only apply while verification is required
// Clearing them otherwise keeps accounts that were used in the meantime from being deleted once verification is required again
if dbConfig.EmailVerification.String() != appconfig.EmailVerificationRequired {
st := j.db.
WithContext(ctx).
Model(&model.User{}).
Where("email_verification_deadline IS NOT NULL").
Update("email_verification_deadline", nil)
if st.Error != nil {
return fmt.Errorf("failed to clear email verification deadlines: %w", st.Error)
}
if st.RowsAffected > 0 {
slog.InfoContext(ctx, "Cleared email verification deadlines because verification is no longer required", slog.Int64("count", st.RowsAffected))
}
return nil
}
// Load the overdue accounts
// Admins are exempt from email verification, so an account promoted to admin before its deadline is kept
var userIDs []string
err = j.db.
WithContext(ctx).
Model(&model.User{}).
Where("email_verification_deadline < ? AND email_verified = ? AND is_admin = ?", datatype.DateTime(time.Now()), false, false).
Pluck("id", &userIDs).
Error
if err != nil {
return fmt.Errorf("failed to list accounts that missed their email verification deadline: %w", err)
}
if len(userIDs) == 0 {
return nil
}
// Delete each account through the user service so its client sessions, SCIM peers and profile picture are cleaned up as well
// A failure is logged and the remaining accounts are still processed
deleted := 0
for _, userID := range userIDs {
err = j.users.DeleteUser(ctx, dbConfig, userID, false)
if err != nil {
slog.ErrorContext(ctx, "Failed to delete an account that missed its email verification deadline", slog.String("userID", userID), slog.Any("error", err))
continue
}
deleted++
}
slog.InfoContext(ctx, "Deleted accounts that missed their email verification deadline", slog.Int("count", deleted))
return nil
}