1package store
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "os"
8 "path/filepath"
9 "runtime/debug"
10 "time"
11
12 "github.com/mjl-/bstore"
13
14 "github.com/mjl-/mox/metrics"
15 "github.com/mjl-/mox/mlog"
16 "github.com/mjl-/mox/mox-"
17 "github.com/mjl-/mox/moxvar"
18)
19
20// AccountRemove represents the scheduled removal of an account, when its last
21// reference goes away.
22type AccountRemove struct {
23 AccountName string
24}
25
26// AuthDB and AuthDBTypes are exported for ../backup.go.
27var AuthDB *bstore.DB
28var AuthDBTypes = []any{TLSPublicKey{}, LoginAttempt{}, LoginAttemptState{}, AccountRemove{}}
29
30var loginAttemptCleanerStop chan chan struct{}
31
32// Init opens auth.db and starts the login writer.
33func Init(ctx context.Context) error {
34 if AuthDB != nil {
35 return fmt.Errorf("already initialized")
36 }
37 pkglog := mlog.New("store", nil)
38 p := mox.DataDirPath("auth.db")
39 os.MkdirAll(filepath.Dir(p), 0770)
40 opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: moxvar.RegisterLogger(p, pkglog.Logger)}
41 var err error
42 AuthDB, err = bstore.Open(ctx, p, &opts, AuthDBTypes...)
43 if err != nil {
44 return err
45 }
46
47 // List pending account removals, and process them one by one, committing each
48 // individually.
49 removals, err := bstore.QueryDB[AccountRemove](ctx, AuthDB).List()
50 if err != nil {
51 return fmt.Errorf("listing scheduled account removals: %v", err)
52 }
53 for _, removal := range removals {
54 if err := removeAccount(pkglog, removal.AccountName); err != nil {
55 pkglog.Errorx("removing old account", err, slog.String("account", removal.AccountName))
56 }
57 }
58
59 startLoginAttemptWriter()
60 loginAttemptCleanerStop = make(chan chan struct{})
61
62 go func() {
63 defer func() {
64 x := recover()
65 if x == nil {
66 return
67 }
68
69 mlog.New("store", nil).Error("unhandled panic in LoginAttemptCleanup", slog.Any("err", x))
70 debug.PrintStack()
71 metrics.PanicInc(metrics.Store)
72
73 }()
74
75 t := time.NewTicker(24 * time.Hour)
76 for {
77 err := LoginAttemptCleanup(ctx)
78 pkglog.Check(err, "cleaning up old historic login attempts")
79
80 select {
81 case c := <-loginAttemptCleanerStop:
82 c <- struct{}{}
83 return
84 case <-t.C:
85 case <-ctx.Done():
86 return
87 }
88 }
89 }()
90
91 return nil
92}
93
94// Close closes auth.db and stops the login writer.
95func Close() error {
96 if AuthDB == nil {
97 return fmt.Errorf("not open")
98 }
99
100 stopc := make(chan struct{})
101 writeLoginAttemptStop <- stopc
102 <-stopc
103
104 stopc = make(chan struct{})
105 loginAttemptCleanerStop <- stopc
106 <-stopc
107
108 err := AuthDB.Close()
109 AuthDB = nil
110
111 return err
112}
113