Skip to content

Commit

Permalink
Add unverified users cleanup job
Browse files Browse the repository at this point in the history
  • Loading branch information
devplayer0 committed Jun 15, 2021
1 parent a3f3101 commit 9dd8a62
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 7 deletions.
2 changes: 2 additions & 0 deletions cmd/iamd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ func init() {
viper.SetDefault("root_password_file", "")

viper.SetDefault("reserved_usernames", []string{})
viper.SetDefault("cleanup.interval", 2*time.Hour)
viper.SetDefault("cleanup.max_age", 72*time.Hour)

viper.SetDefault("ma1sd.http_address", "")
viper.SetDefault("ma1sd.base_url", "/_ma1sd/backend/api/v1")
Expand Down
19 changes: 15 additions & 4 deletions config.sample.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
log_level: INFO
http:
listen_address: ':8080'
cors_allow_origin: '*'
db:

postgresql:
host: db
user: iamd
password: hunter2
Expand All @@ -11,27 +9,40 @@ db:
timezone: 'Europe/Dublin'
dsn_extra: ''
soft_delete: false

mail:
from: '"Netsoc IAM" <iam@netsoc.ie>'
reply_to: '"Netsoc support" <support@netsoc.ie>'
verify_url: 'https://account.netsoc.ie/verify?token={{.Token}}'
reset_url: 'https://account.netsoc.ie/reset?token={{.Token}}'

smtp:
host: mail
port: 587
username: iam@netsoc.ie
password: hunter2
password_file: /run/secrets/smtp.txt
tls: true

http:
listen_address: ':8080'
cors_allow_origin: '*'

jwt:
key: QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=
key_file: /run/secrets/jwt_key.bin
issuer: netsoc
login_validity: '8760h'
email_validity: '24h'

root_password: hunter22
root_password_file: /run/secrets/root.txt

reserved_usernames: []
cleanup:
interval: 2h
max_age: 72h

ma1sd:
http_address: ''
base_url: '/_ma1sd/backend/api/v1'
Expand Down
6 changes: 6 additions & 0 deletions pkg/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ type JWTConfig struct {
EmailValidity time.Duration `mapstructure:"email_validity"`
}

type CleanupConfig struct {
Interval time.Duration
MaxAge time.Duration `mapstructure:"max_age"`
}

type MA1SDConfig struct {
HTTPAddress string `mapstructure:"http_address"`
BaseURL string `mapstructure:"base_url"`
Expand Down Expand Up @@ -97,6 +102,7 @@ type Config struct {
RootPasswordFile string `mapstructure:"root_password_file"`

ReservedUsernames []string `mapstructure:"reserved_usernames"`
Cleanup CleanupConfig

MA1SD MA1SDConfig
}
Expand Down
19 changes: 19 additions & 0 deletions pkg/server/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,25 @@ func (b argBcrypt) Match(v driver.Value) bool {
return true
}

type argTimeWithin struct {
Expected time.Time
Duration time.Duration
}

func (d argTimeWithin) Match(v driver.Value) bool {
t, ok := v.(time.Time)
if !ok {
return false
}

dt := d.Expected.Sub(t)
if dt < -d.Duration || dt > d.Duration {
return false
}

return true
}

type endpointsTestSuite struct {
suite.Suite

Expand Down
18 changes: 15 additions & 3 deletions pkg/server/iamd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package server
import (
"fmt"
"net/http"
"time"

httpswagger "github.com/devplayer0/http-swagger"
"github.com/gorilla/handlers"
Expand All @@ -20,9 +21,10 @@ import (
type Server struct {
config Config

db *gorm.DB
email email.Sender
http *http.Server
db *gorm.DB
email email.Sender
http *http.Server
stopCleanup chan struct{}

router *mux.Router
ma1sd *ma1sd.MA1SD
Expand Down Expand Up @@ -146,3 +148,13 @@ func NewServer(config Config) (*Server, error) {
func (s *Server) healthCheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}

func (s *Server) CleanupUnverified() (int64, error) {
tx := s.db
if !s.config.PostgreSQL.SoftDelete {
tx = s.db.Unscoped()
}

tx = tx.Delete(&models.User{}, "verified = ? AND created < ?", false, time.Now().Add(-s.config.Cleanup.MaxAge))
return tx.RowsAffected, tx.Error
}
29 changes: 29 additions & 0 deletions pkg/server/iamd_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,33 @@ func (s *Server) Start() error {
}
}

if s.config.Cleanup.Interval != 0 {
go func() {
t := time.NewTicker(s.config.Cleanup.Interval)
defer t.Stop()

s.stopCleanup = make(chan struct{})
for {
select {
case <-t.C:
log.Debug("Running unverified user cleanup")

cleaned, err := s.CleanupUnverified()
if err != nil {
log.WithError(err).Error("Failed to clean up unverified users")
continue
}

if cleaned > 0 {
log.WithField("count", cleaned).Info("Cleaned up unverified users")
}
case <-s.stopCleanup:
return
}
}
}()
}

eChan := make(chan error)
go func() {
if err := s.http.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
Expand Down Expand Up @@ -100,6 +127,8 @@ func (s *Server) Stop() error {
return fmt.Errorf("failed to shut down HTTP server: %w", err)
}

close(s.stopCleanup)

db, err := s.db.DB()
if err != nil {
return fmt.Errorf("failed to get SQL DB: %w", err)
Expand Down
38 changes: 38 additions & 0 deletions pkg/server/iamd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package server

import (
"time"

"github.com/DATA-DOG/go-sqlmock"
)

func (s *endpointsTestSuite) TestCleanup() {
age := 24 * time.Hour
toClean := int64(123)

s.server.config.Cleanup.Interval = 30 * time.Second
s.server.config.Cleanup.MaxAge = age

s.dbMock.ExpectExec(`^DELETE FROM "users" WHERE verified = \$1 AND created < \$2$`).
WithArgs(false, argTimeWithin{Expected: time.Now().Add(-age), Duration: 1 * time.Second}).
WillReturnResult(sqlmock.NewResult(1, toClean))

cleaned, err := s.server.CleanupUnverified()
s.NoError(err)
s.Equal(toClean, cleaned)
}

func (s *endpointsTestSuite) TestCleanupNone() {
age := 24 * time.Hour

s.server.config.Cleanup.Interval = 30 * time.Second
s.server.config.Cleanup.MaxAge = age

s.dbMock.ExpectExec(`^DELETE FROM "users" WHERE verified = \$1 AND created < \$2$`).
WithArgs(false, argTimeWithin{Expected: time.Now().Add(-age), Duration: 1 * time.Second}).
WillReturnResult(sqlmock.NewResult(1, 0))

cleaned, err := s.server.CleanupUnverified()
s.NoError(err)
s.Zero(cleaned)
}

0 comments on commit 9dd8a62

Please sign in to comment.