binwiederhier 3 лет назад
Родитель
Сommit
e6bb5f484c

+ 4 - 4
cmd/tier.go

@@ -61,7 +61,7 @@ var cmdTier = &cli.Command{
 Tiers can be used to grant users higher limits, such as daily message limits, attachment size, or
 Tiers can be used to grant users higher limits, such as daily message limits, attachment size, or
 make it possible for users to reserve topics.
 make it possible for users to reserve topics.
 
 
-This is a server-only command. It directly reads from the user.db as defined in the server config
+This is a server-only command. It directly reads from user.db as defined in the server config
 file server.yml. The command only works if 'auth-file' is properly defined.
 file server.yml. The command only works if 'auth-file' is properly defined.
 
 
 Examples:
 Examples:
@@ -102,7 +102,7 @@ Examples:
 After updating a tier, you may have to restart the ntfy server to apply them 
 After updating a tier, you may have to restart the ntfy server to apply them 
 to all visitors. 
 to all visitors. 
 
 
-This is a server-only command. It directly reads from the user.db as defined in the server config
+This is a server-only command. It directly reads from user.db as defined in the server config
 file server.yml. The command only works if 'auth-file' is properly defined.
 file server.yml. The command only works if 'auth-file' is properly defined.
 
 
 Examples:
 Examples:
@@ -124,7 +124,7 @@ Examples:
 You cannot remove a tier if there are users associated with a tier. Use "ntfy user change-tier"
 You cannot remove a tier if there are users associated with a tier. Use "ntfy user change-tier"
 to remove or switch their tier first.
 to remove or switch their tier first.
 
 
-This is a server-only command. It directly reads from the user.db as defined in the server config
+This is a server-only command. It directly reads from user.db as defined in the server config
 file server.yml. The command only works if 'auth-file' is properly defined.
 file server.yml. The command only works if 'auth-file' is properly defined.
 
 
 Example:
 Example:
@@ -138,7 +138,7 @@ Example:
 			Action:  execTierList,
 			Action:  execTierList,
 			Description: `Shows a list of all configured tiers.
 			Description: `Shows a list of all configured tiers.
 
 
-This is a server-only command. It directly reads from the user.db as defined in the server config
+This is a server-only command. It directly reads from user.db as defined in the server config
 file server.yml. The command only works if 'auth-file' is properly defined.
 file server.yml. The command only works if 'auth-file' is properly defined.
 `,
 `,
 		},
 		},

+ 19 - 1
cmd/tier_test.go

@@ -27,8 +27,26 @@ func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
 	require.Contains(t, stderr.String(), "- Message limit: 1234")
 	require.Contains(t, stderr.String(), "- Message limit: 1234")
 
 
 	app, _, _, stderr = newTestApp()
 	app, _, _, stderr = newTestApp()
-	require.Nil(t, runTierCommand(app, conf, "change", "--message-limit", "999", "pro"))
+	require.Nil(t, runTierCommand(app, conf, "change",
+		"--message-limit=999",
+		"--message-expiry-duration=99h",
+		"--email-limit=91",
+		"--reservation-limit=98",
+		"--attachment-file-size-limit=100m",
+		"--attachment-expiry-duration=7h",
+		"--attachment-total-size-limit=10G",
+		"--attachment-bandwidth-limit=100G",
+		"--stripe-price-id=price_991",
+		"pro",
+	))
 	require.Contains(t, stderr.String(), "- Message limit: 999")
 	require.Contains(t, stderr.String(), "- Message limit: 999")
+	require.Contains(t, stderr.String(), "- Message expiry duration: 99h")
+	require.Contains(t, stderr.String(), "- Email limit: 91")
+	require.Contains(t, stderr.String(), "- Reservation limit: 98")
+	require.Contains(t, stderr.String(), "- Attachment file size limit: 100.0 MB")
+	require.Contains(t, stderr.String(), "- Attachment expiry duration: 7h")
+	require.Contains(t, stderr.String(), "- Attachment total size limit: 10.0 GB")
+	require.Contains(t, stderr.String(), "- Stripe price: price_991")
 
 
 	app, _, _, stderr = newTestApp()
 	app, _, _, stderr = newTestApp()
 	require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
 	require.Nil(t, runTierCommand(app, conf, "remove", "pro"))

+ 4 - 1
cmd/token.go

@@ -42,6 +42,9 @@ User access tokens can be used to publish, subscribe, or perform any other user-
 Tokens have full access, and can perform any task a user can do. They are meant to be used to 
 Tokens have full access, and can perform any task a user can do. They are meant to be used to 
 avoid spreading the password to various places.
 avoid spreading the password to various places.
 
 
+This is a server-only command. It directly reads from user.db as defined in the server config
+file server.yml. The command only works if 'auth-file' is properly defined.
+
 Examples:
 Examples:
   ntfy token add phil                   # Create token for user phil which never expires
   ntfy token add phil                   # Create token for user phil which never expires
   ntfy token add --expires=2d phil      # Create token for user phil which expires in 2 days
   ntfy token add --expires=2d phil      # Create token for user phil which expires in 2 days
@@ -66,7 +69,7 @@ Example:
 			Action:  execTokenList,
 			Action:  execTokenList,
 			Description: `Shows a list of all tokens.
 			Description: `Shows a list of all tokens.
 
 
-This is a server-only command. It directly reads from the user.db as defined in the server config
+This is a server-only command. It directly reads from user.db as defined in the server config
 file server.yml. The command only works if 'auth-file' is properly defined.`,
 file server.yml. The command only works if 'auth-file' is properly defined.`,
 		},
 		},
 	},
 	},

+ 1 - 1
cmd/user.go

@@ -141,7 +141,7 @@ Example:
 
 
 This command is an alias to calling 'ntfy access' (display access control list).
 This command is an alias to calling 'ntfy access' (display access control list).
 
 
-This is a server-only command. It directly reads from the user.db as defined in the server config
+This is a server-only command. It directly reads from user.db as defined in the server config
 file server.yml. The command only works if 'auth-file' is properly defined.
 file server.yml. The command only works if 'auth-file' is properly defined.
 `,
 `,
 		},
 		},

+ 8 - 0
log/event.go

@@ -13,6 +13,7 @@ import (
 const (
 const (
 	tagField        = "tag"
 	tagField        = "tag"
 	errorField      = "error"
 	errorField      = "error"
+	timeTakenField  = "time_taken_ms"
 	exitCodeField   = "exit_code"
 	exitCodeField   = "exit_code"
 	timestampFormat = "2006-01-02T15:04:05.999Z07:00"
 	timestampFormat = "2006-01-02T15:04:05.999Z07:00"
 )
 )
@@ -80,6 +81,13 @@ func (e *Event) Time(t time.Time) *Event {
 	return e
 	return e
 }
 }
 
 
+// Timing runs f and records the time if took to execute it in "time_taken_ms"
+func (e *Event) Timing(f func()) *Event {
+	start := time.Now()
+	f()
+	return e.Field(timeTakenField, time.Since(start).Milliseconds())
+}
+
 // Err adds an "error" field to the log event
 // Err adds an "error" field to the log event
 func (e *Event) Err(err error) *Event {
 func (e *Event) Err(err error) *Event {
 	if err == nil {
 	if err == nil {

+ 5 - 0
log/log.go

@@ -78,6 +78,11 @@ func Time(time time.Time) *Event {
 	return newEvent().Time(time)
 	return newEvent().Time(time)
 }
 }
 
 
+// Timing runs f and records the time if took to execute it in "time_taken_ms"
+func Timing(f func()) *Event {
+	return newEvent().Timing(f)
+}
+
 // CurrentLevel returns the current log level
 // CurrentLevel returns the current log level
 func CurrentLevel() Level {
 func CurrentLevel() Level {
 	mu.Lock()
 	mu.Lock()

+ 20 - 0
log/log_test.go

@@ -2,6 +2,7 @@ package log
 
 
 import (
 import (
 	"bytes"
 	"bytes"
+	"encoding/json"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/require"
 	"os"
 	"os"
 	"testing"
 	"testing"
@@ -131,6 +132,25 @@ func TestLog_NoAllocIfNotPrinted(t *testing.T) {
 	require.Equal(t, expected, out.String())
 	require.Equal(t, expected, out.String())
 }
 }
 
 
+func TestLog_Timing(t *testing.T) {
+	t.Cleanup(resetState)
+
+	var out bytes.Buffer
+	SetOutput(&out)
+	SetFormat(JSONFormat)
+
+	Timing(func() { time.Sleep(300 * time.Millisecond) }).
+		Time(time.Unix(12, 0).UTC()).
+		Info("A thing that takes a while")
+
+	var ev struct {
+		TimeTakenMs int64 `json:"time_taken_ms"`
+	}
+	require.Nil(t, json.Unmarshal(out.Bytes(), &ev))
+	require.True(t, ev.TimeTakenMs >= 300)
+	require.Contains(t, out.String(), `{"time":"1970-01-01T00:00:12Z","level":"INFO","message":"A thing that takes a while","time_taken_ms":`)
+}
+
 type fakeError struct {
 type fakeError struct {
 	Code    int
 	Code    int
 	Message string
 	Message string

+ 1 - 0
server/config.go

@@ -164,6 +164,7 @@ func NewConfig() *Config {
 		AttachmentExpiryDuration:             DefaultAttachmentExpiryDuration,
 		AttachmentExpiryDuration:             DefaultAttachmentExpiryDuration,
 		KeepaliveInterval:                    DefaultKeepaliveInterval,
 		KeepaliveInterval:                    DefaultKeepaliveInterval,
 		ManagerInterval:                      DefaultManagerInterval,
 		ManagerInterval:                      DefaultManagerInterval,
+		DisallowedTopics:                     DefaultDisallowedTopics,
 		WebRootIsApp:                         false,
 		WebRootIsApp:                         false,
 		DelayedSenderInterval:                DefaultDelayedSenderInterval,
 		DelayedSenderInterval:                DefaultDelayedSenderInterval,
 		FirebaseKeepaliveInterval:            DefaultFirebaseKeepaliveInterval,
 		FirebaseKeepaliveInterval:            DefaultFirebaseKeepaliveInterval,

+ 5 - 4
server/message_cache.go

@@ -51,6 +51,8 @@ const (
 		CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
 		CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
 		CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
 		CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
+		CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender);
+		CREATE INDEX IF NOT EXISTS idx_user ON messages (user);
 		CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires);
 		CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires);
 		COMMIT;
 		COMMIT;
 	`
 	`
@@ -215,6 +217,8 @@ const (
 		ALTER TABLE messages ADD COLUMN attachment_deleted INT NOT NULL DEFAULT('0');
 		ALTER TABLE messages ADD COLUMN attachment_deleted INT NOT NULL DEFAULT('0');
 		ALTER TABLE messages ADD COLUMN expires INT NOT NULL DEFAULT('0');
 		ALTER TABLE messages ADD COLUMN expires INT NOT NULL DEFAULT('0');
 		CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
 		CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
+		CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender);
+		CREATE INDEX IF NOT EXISTS idx_user ON messages (user);
 		CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires);
 		CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires);
 	`
 	`
 	migrate9To10UpdateMessageExpiryQuery = `UPDATE messages SET expires = time + ?`
 	migrate9To10UpdateMessageExpiryQuery = `UPDATE messages SET expires = time + ?`
@@ -883,8 +887,5 @@ func migrateFrom9(db *sql.DB, cacheDuration time.Duration) error {
 	if _, err := tx.Exec(updateSchemaVersion, 10); err != nil {
 	if _, err := tx.Exec(updateSchemaVersion, 10); err != nil {
 		return err
 		return err
 	}
 	}
-	if err := tx.Commit(); err != nil {
-		return err
-	}
-	return nil // Update this when a new version is added
+	return tx.Commit()
 }
 }

+ 100 - 69
server/server.go

@@ -37,12 +37,13 @@ import (
 - HIGH Docs
 - HIGH Docs
   - tiers
   - tiers
   - api
   - api
+  - tokens
 - HIGH Self-review
 - HIGH Self-review
 - MEDIUM: Test for expiring messages after reservation removal
 - MEDIUM: Test for expiring messages after reservation removal
 - MEDIUM: uploading attachments leads to 404 -- race
 - MEDIUM: uploading attachments leads to 404 -- race
-- MEDIUM: Do not call tiers endoint when payments is not enabled
 - MEDIUM: Test new token endpoints & never-expiring token
 - MEDIUM: Test new token endpoints & never-expiring token
 - LOW: UI: Flickering upgrade banner when logging in
 - LOW: UI: Flickering upgrade banner when logging in
+- LOW: Menu item -> popup click should not open page
 
 
 */
 */
 
 
@@ -140,6 +141,7 @@ const (
 const (
 const (
 	tagStartup      = "startup"
 	tagStartup      = "startup"
 	tagPublish      = "publish"
 	tagPublish      = "publish"
+	tagSubscribe    = "subscribe"
 	tagFirebase     = "firebase"
 	tagFirebase     = "firebase"
 	tagEmail        = "email" // Send email
 	tagEmail        = "email" // Send email
 	tagSMTP         = "smtp"  // Receive email
 	tagSMTP         = "smtp"  // Receive email
@@ -649,7 +651,7 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
 	}
 	}
 	u := v.User()
 	u := v.User()
 	if s.userManager != nil && u != nil && u.Tier != nil {
 	if s.userManager != nil && u != nil && u.Tier != nil {
-		go s.userManager.EnqueueStats(u.ID, v.Stats())
+		go s.userManager.EnqueueUserStats(u.ID, v.Stats())
 	}
 	}
 	s.mu.Lock()
 	s.mu.Lock()
 	s.messages++
 	s.messages++
@@ -956,8 +958,8 @@ func (s *Server) handleSubscribeRaw(w http.ResponseWriter, r *http.Request, v *v
 }
 }
 
 
 func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *visitor, contentType string, encoder messageEncoder) error {
 func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *visitor, contentType string, encoder messageEncoder) error {
-	logvr(v, r).Debug("HTTP stream connection opened")
-	defer logvr(v, r).Debug("HTTP stream connection closed")
+	logvr(v, r).Tag(tagSubscribe).Debug("HTTP stream connection opened")
+	defer logvr(v, r).Tag(tagSubscribe).Debug("HTTP stream connection closed")
 	if !v.SubscriptionAllowed() {
 	if !v.SubscriptionAllowed() {
 		return errHTTPTooManyRequestsLimitSubscriptions
 		return errHTTPTooManyRequestsLimitSubscriptions
 	}
 	}
@@ -1025,7 +1027,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
 		case <-r.Context().Done():
 		case <-r.Context().Done():
 			return nil
 			return nil
 		case <-time.After(s.config.KeepaliveInterval):
 		case <-time.After(s.config.KeepaliveInterval):
-			logvr(v, r).Trace("Sending keepalive message")
+			logvr(v, r).Tag(tagSubscribe).Trace("Sending keepalive message")
 			v.Keepalive()
 			v.Keepalive()
 			if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message
 			if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message
 				return err
 				return err
@@ -1283,70 +1285,86 @@ func (s *Server) topicFromID(id string) (*topic, error) {
 }
 }
 
 
 func (s *Server) execManager() {
 func (s *Server) execManager() {
-	log.Tag(tagManager).Debug("Starting manager")
-	defer log.Tag(tagManager).Debug("Finished manager")
-
 	// WARNING: Make sure to only selectively lock with the mutex, and be aware that this
 	// WARNING: Make sure to only selectively lock with the mutex, and be aware that this
 	//          there is no mutex for the entire function.
 	//          there is no mutex for the entire function.
 
 
 	// Expire visitors from rate visitors map
 	// Expire visitors from rate visitors map
-	s.mu.Lock()
 	staleVisitors := 0
 	staleVisitors := 0
-	for ip, v := range s.visitors {
-		if v.Stale() {
-			log.Tag(tagManager).With(v).Trace("Deleting stale visitor")
-			delete(s.visitors, ip)
-			staleVisitors++
-		}
-	}
-	s.mu.Unlock()
-	log.Tag(tagManager).Field("stale_visitors", staleVisitors).Debug("Deleted %d stale visitor(s)", staleVisitors)
+	log.
+		Tag(tagManager).
+		Timing(func() {
+			s.mu.Lock()
+			defer s.mu.Unlock()
+			for ip, v := range s.visitors {
+				if v.Stale() {
+					log.Tag(tagManager).With(v).Trace("Deleting stale visitor")
+					delete(s.visitors, ip)
+					staleVisitors++
+				}
+			}
+		}).
+		Field("stale_visitors", staleVisitors).
+		Debug("Deleted %d stale visitor(s)", staleVisitors)
 
 
 	// Delete expired user tokens and users
 	// Delete expired user tokens and users
 	if s.userManager != nil {
 	if s.userManager != nil {
-		if err := s.userManager.RemoveExpiredTokens(); err != nil {
-			log.Tag(tagManager).Err(err).Warn("Error expiring user tokens")
-		}
-		if err := s.userManager.RemoveDeletedUsers(); err != nil {
-			log.Tag(tagManager).Err(err).Warn("Error deleting soft-deleted users")
-		}
+		log.
+			Tag(tagManager).
+			Timing(func() {
+				if err := s.userManager.RemoveExpiredTokens(); err != nil {
+					log.Tag(tagManager).Err(err).Warn("Error expiring user tokens")
+				}
+				if err := s.userManager.RemoveDeletedUsers(); err != nil {
+					log.Tag(tagManager).Err(err).Warn("Error deleting soft-deleted users")
+				}
+			}).
+			Debug("Removed expired tokens and users")
 	}
 	}
 
 
 	// Delete expired attachments
 	// Delete expired attachments
 	if s.fileCache != nil {
 	if s.fileCache != nil {
-		ids, err := s.messageCache.AttachmentsExpired()
-		if err != nil {
-			log.Tag(tagManager).Err(err).Warn("Error retrieving expired attachments")
-		} else if len(ids) > 0 {
-			if log.Tag(tagManager).IsDebug() {
-				log.Tag(tagManager).Debug("Deleting attachments %s", strings.Join(ids, ", "))
-			}
-			if err := s.fileCache.Remove(ids...); err != nil {
-				log.Tag(tagManager).Err(err).Warn("Error deleting attachments")
-			}
-			if err := s.messageCache.MarkAttachmentsDeleted(ids...); err != nil {
-				log.Tag(tagManager).Err(err).Warn("Error marking attachments deleted")
-			}
-		} else {
-			log.Tag(tagManager).Debug("No expired attachments to delete")
-		}
+		log.
+			Tag(tagManager).
+			Timing(func() {
+				ids, err := s.messageCache.AttachmentsExpired()
+				if err != nil {
+					log.Tag(tagManager).Err(err).Warn("Error retrieving expired attachments")
+				} else if len(ids) > 0 {
+					if log.Tag(tagManager).IsDebug() {
+						log.Tag(tagManager).Debug("Deleting attachments %s", strings.Join(ids, ", "))
+					}
+					if err := s.fileCache.Remove(ids...); err != nil {
+						log.Tag(tagManager).Err(err).Warn("Error deleting attachments")
+					}
+					if err := s.messageCache.MarkAttachmentsDeleted(ids...); err != nil {
+						log.Tag(tagManager).Err(err).Warn("Error marking attachments deleted")
+					}
+				} else {
+					log.Tag(tagManager).Debug("No expired attachments to delete")
+				}
+			}).
+			Debug("Deleted expired attachments")
 	}
 	}
 
 
 	// Prune messages
 	// Prune messages
-	log.Tag(tagManager).Debug("Manager: Pruning messages")
-	expiredMessageIDs, err := s.messageCache.MessagesExpired()
-	if err != nil {
-		log.Tag(tagManager).Err(err).Warn("Error retrieving expired messages")
-	} else if len(expiredMessageIDs) > 0 {
-		if err := s.fileCache.Remove(expiredMessageIDs...); err != nil {
-			log.Tag(tagManager).Err(err).Warn("Error deleting attachments for expired messages")
-		}
-		if err := s.messageCache.DeleteMessages(expiredMessageIDs...); err != nil {
-			log.Tag(tagManager).Err(err).Warn("Error marking attachments deleted")
-		}
-	} else {
-		log.Tag(tagManager).Debug("No expired messages to delete")
-	}
+	log.
+		Tag(tagManager).
+		Timing(func() {
+			expiredMessageIDs, err := s.messageCache.MessagesExpired()
+			if err != nil {
+				log.Tag(tagManager).Err(err).Warn("Error retrieving expired messages")
+			} else if len(expiredMessageIDs) > 0 {
+				if err := s.fileCache.Remove(expiredMessageIDs...); err != nil {
+					log.Tag(tagManager).Err(err).Warn("Error deleting attachments for expired messages")
+				}
+				if err := s.messageCache.DeleteMessages(expiredMessageIDs...); err != nil {
+					log.Tag(tagManager).Err(err).Warn("Error marking attachments deleted")
+				}
+			} else {
+				log.Tag(tagManager).Debug("No expired messages to delete")
+			}
+		}).
+		Debug("Pruned messages")
 
 
 	// Message count per topic
 	// Message count per topic
 	var messagesCached int
 	var messagesCached int
@@ -1360,20 +1378,26 @@ func (s *Server) execManager() {
 	}
 	}
 
 
 	// Remove subscriptions without subscribers
 	// Remove subscriptions without subscribers
-	s.mu.Lock()
-	var subscribers int
-	for _, t := range s.topics {
-		subs := t.SubscribersCount()
-		log.Tag(tagManager).Trace("- topic %s: %d subscribers", t.ID, subs)
-		msgs, exists := messageCounts[t.ID]
-		if subs == 0 && (!exists || msgs == 0) {
-			log.Tag(tagManager).Trace("Deleting empty topic %s", t.ID)
-			delete(s.topics, t.ID)
-			continue
-		}
-		subscribers += subs
-	}
-	s.mu.Unlock()
+	var emptyTopics, subscribers int
+	log.
+		Tag(tagManager).
+		Timing(func() {
+			s.mu.Lock()
+			defer s.mu.Unlock()
+			for _, t := range s.topics {
+				subs := t.SubscribersCount()
+				log.Tag(tagManager).Trace("- topic %s: %d subscribers", t.ID, subs)
+				msgs, exists := messageCounts[t.ID]
+				if subs == 0 && (!exists || msgs == 0) {
+					log.Tag(tagManager).Trace("Deleting empty topic %s", t.ID)
+					emptyTopics++
+					delete(s.topics, t.ID)
+					continue
+				}
+				subscribers += subs
+			}
+		}).
+		Debug("Removed %d empty topic(s)", emptyTopics)
 
 
 	// Mail stats
 	// Mail stats
 	var receivedMailTotal, receivedMailSuccess, receivedMailFailure int64
 	var receivedMailTotal, receivedMailSuccess, receivedMailFailure int64
@@ -1407,6 +1431,10 @@ func (s *Server) execManager() {
 		Info("Server stats")
 		Info("Server stats")
 }
 }
 
 
+func (s *Server) expireVisitors() {
+
+}
+
 func (s *Server) runSMTPServer() error {
 func (s *Server) runSMTPServer() error {
 	s.smtpServerBackend = newMailBackend(s.config, s.handle)
 	s.smtpServerBackend = newMailBackend(s.config, s.handle)
 	s.smtpServer = smtp.NewServer(s.smtpServerBackend)
 	s.smtpServer = smtp.NewServer(s.smtpServerBackend)
@@ -1424,7 +1452,10 @@ func (s *Server) runManager() {
 	for {
 	for {
 		select {
 		select {
 		case <-time.After(s.config.ManagerInterval):
 		case <-time.After(s.config.ManagerInterval):
-			s.execManager()
+			log.
+				Tag(tagManager).
+				Timing(s.execManager).
+				Debug("Manager finished")
 		case <-s.closeChan:
 		case <-s.closeChan:
 			return
 			return
 		}
 		}

+ 10 - 7
server/server_account.go

@@ -314,7 +314,7 @@ func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Requ
 		}
 		}
 	}
 	}
 	logvr(v, r).Tag(tagAccount).Debug("Changing account settings for user %s", u.Name)
 	logvr(v, r).Tag(tagAccount).Debug("Changing account settings for user %s", u.Name)
-	if err := s.userManager.ChangeSettings(u); err != nil {
+	if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
 		return err
 		return err
 	}
 	}
 	return s.writeJSON(w, newSuccessResponse())
 	return s.writeJSON(w, newSuccessResponse())
@@ -338,7 +338,8 @@ func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Req
 	}
 	}
 	if newSubscription.ID == "" {
 	if newSubscription.ID == "" {
 		newSubscription.ID = util.RandomStringPrefix(subscriptionIDPrefix, subscriptionIDLength)
 		newSubscription.ID = util.RandomStringPrefix(subscriptionIDPrefix, subscriptionIDLength)
-		u.Prefs.Subscriptions = append(u.Prefs.Subscriptions, newSubscription)
+		prefs := u.Prefs
+		prefs.Subscriptions = append(prefs.Subscriptions, newSubscription)
 		logvr(v, r).
 		logvr(v, r).
 			Tag(tagAccount).
 			Tag(tagAccount).
 			Fields(log.Context{
 			Fields(log.Context{
@@ -346,7 +347,7 @@ func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Req
 				"topic":    newSubscription.Topic,
 				"topic":    newSubscription.Topic,
 			}).
 			}).
 			Debug("Adding subscription for user %s", u.Name)
 			Debug("Adding subscription for user %s", u.Name)
-		if err := s.userManager.ChangeSettings(u); err != nil {
+		if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
 			return err
 			return err
 		}
 		}
 	}
 	}
@@ -367,8 +368,9 @@ func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.
 	if u.Prefs == nil || u.Prefs.Subscriptions == nil {
 	if u.Prefs == nil || u.Prefs.Subscriptions == nil {
 		return errHTTPNotFound
 		return errHTTPNotFound
 	}
 	}
+	prefs := u.Prefs
 	var subscription *user.Subscription
 	var subscription *user.Subscription
-	for _, sub := range u.Prefs.Subscriptions {
+	for _, sub := range prefs.Subscriptions {
 		if sub.ID == subscriptionID {
 		if sub.ID == subscriptionID {
 			sub.DisplayName = updatedSubscription.DisplayName
 			sub.DisplayName = updatedSubscription.DisplayName
 			subscription = sub
 			subscription = sub
@@ -386,7 +388,7 @@ func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.
 			"display_name": subscription.DisplayName,
 			"display_name": subscription.DisplayName,
 		}).
 		}).
 		Debug("Changing subscription for user %s", u.Name)
 		Debug("Changing subscription for user %s", u.Name)
-	if err := s.userManager.ChangeSettings(u); err != nil {
+	if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
 		return err
 		return err
 	}
 	}
 	return s.writeJSON(w, subscription)
 	return s.writeJSON(w, subscription)
@@ -417,8 +419,9 @@ func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.
 		}
 		}
 	}
 	}
 	if len(newSubscriptions) < len(u.Prefs.Subscriptions) {
 	if len(newSubscriptions) < len(u.Prefs.Subscriptions) {
-		u.Prefs.Subscriptions = newSubscriptions
-		if err := s.userManager.ChangeSettings(u); err != nil {
+		prefs := u.Prefs
+		prefs.Subscriptions = newSubscriptions
+		if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
 			return err
 			return err
 		}
 		}
 	}
 	}

+ 1 - 1
server/server_account_test.go

@@ -724,5 +724,5 @@ func TestAccount_Persist_UserStats_After_Tier_Change(t *testing.T) {
 	time.Sleep(300 * time.Millisecond)
 	time.Sleep(300 * time.Millisecond)
 	u, err = s.userManager.User("phil")
 	u, err = s.userManager.User("phil")
 	require.Nil(t, err)
 	require.Nil(t, err)
-	require.Equal(t, int64(0), u.Stats.Messages) // v.EnqueueStats had run!
+	require.Equal(t, int64(0), u.Stats.Messages) // v.EnqueueUserStats had run!
 }
 }

+ 1 - 1
server/server_test.go

@@ -938,7 +938,7 @@ func TestServer_DailyMessageQuotaFromDatabase(t *testing.T) {
 
 
 	u, err := s.userManager.User("phil")
 	u, err := s.userManager.User("phil")
 	require.Nil(t, err)
 	require.Nil(t, err)
-	s.userManager.EnqueueStats(u.ID, &user.Stats{
+	s.userManager.EnqueueUserStats(u.ID, &user.Stats{
 		Messages: 123456,
 		Messages: 123456,
 		Emails:   999,
 		Emails:   999,
 	})
 	})

+ 1 - 1
server/topic.go

@@ -88,7 +88,7 @@ func (t *topic) CancelSubscribers(exceptUserID string) {
 	defer t.mu.Unlock()
 	defer t.mu.Unlock()
 	for _, s := range t.subscribers {
 	for _, s := range t.subscribers {
 		if s.userID != exceptUserID {
 		if s.userID != exceptUserID {
-			log.Field("topic", t.ID).Trace("Canceling subscriber %s", s.userID)
+			log.Tag(tagSubscribe).Field("topic", t.ID).Debug("Canceling subscriber %s", s.userID)
 			s.cancel()
 			s.cancel()
 		}
 		}
 	}
 	}

+ 3 - 3
server/visitor.go

@@ -27,7 +27,7 @@ const (
 )
 )
 
 
 // Constants used to convert a tier-user's MessageLimit (see user.Tier) into adequate request limiter
 // Constants used to convert a tier-user's MessageLimit (see user.Tier) into adequate request limiter
-// values (token bucket).
+// values (token bucket). This is only used to increase the values in server.yml, never decrease them.
 //
 //
 // Example: Assuming a user.Tier's MessageLimit is 10,000:
 // Example: Assuming a user.Tier's MessageLimit is 10,000:
 // - the allowed burst is 500 (= 10,000 * 5%), which is < 1000 (the max)
 // - the allowed burst is 500 (= 10,000 * 5%), which is < 1000 (the max)
@@ -59,7 +59,7 @@ type visitor struct {
 	subscriptionLimiter *util.FixedLimiter // Fixed limiter for active subscriptions (ongoing connections)
 	subscriptionLimiter *util.FixedLimiter // Fixed limiter for active subscriptions (ongoing connections)
 	bandwidthLimiter    *util.RateLimiter  // Limiter for attachment bandwidth downloads
 	bandwidthLimiter    *util.RateLimiter  // Limiter for attachment bandwidth downloads
 	accountLimiter      *rate.Limiter      // Rate limiter for account creation, may be nil
 	accountLimiter      *rate.Limiter      // Rate limiter for account creation, may be nil
-	authLimiter         *rate.Limiter      // Limiter for incorrect login attempts
+	authLimiter         *rate.Limiter      // Limiter for incorrect login attempts, may be nil
 	firebase            time.Time          // Next allowed Firebase message
 	firebase            time.Time          // Next allowed Firebase message
 	seen                time.Time          // Last seen time of this visitor (needed for removal of stale visitors)
 	seen                time.Time          // Last seen time of this visitor (needed for removal of stale visitors)
 	mu                  sync.Mutex
 	mu                  sync.Mutex
@@ -360,7 +360,7 @@ func (v *visitor) resetLimitersNoLock(messages, emails int64, enqueueUpdate bool
 		v.authLimiter = nil    // Users are already logged in, no need to limit requests
 		v.authLimiter = nil    // Users are already logged in, no need to limit requests
 	}
 	}
 	if enqueueUpdate && v.user != nil {
 	if enqueueUpdate && v.user != nil {
-		go v.userManager.EnqueueStats(v.user.ID, &user.Stats{
+		go v.userManager.EnqueueUserStats(v.user.ID, &user.Stats{
 			Messages: messages,
 			Messages: messages,
 			Emails:   emails,
 			Emails:   emails,
 		})
 		})

+ 60 - 46
user/manager.go

@@ -1,3 +1,4 @@
+// Package user deals with authentication and authorization against topics
 package user
 package user
 
 
 import (
 import (
@@ -28,7 +29,7 @@ const (
 	tokenPrefix                     = "tk_"
 	tokenPrefix                     = "tk_"
 	tokenLength                     = 32
 	tokenLength                     = 32
 	tokenMaxCount                   = 20 // Only keep this many tokens in the table per user
 	tokenMaxCount                   = 20 // Only keep this many tokens in the table per user
-	tagManager                      = "user_manager"
+	tag                             = "user_manager"
 )
 )
 
 
 // Default constants that may be overridden by configs
 // Default constants that may be overridden by configs
@@ -47,7 +48,7 @@ var (
 const (
 const (
 	createTablesQueriesNoTx = `
 	createTablesQueriesNoTx = `
 		CREATE TABLE IF NOT EXISTS tier (
 		CREATE TABLE IF NOT EXISTS tier (
-			id TEXT PRIMARY KEY,		
+			id TEXT PRIMARY KEY,
 			code TEXT NOT NULL,
 			code TEXT NOT NULL,
 			name TEXT NOT NULL,
 			name TEXT NOT NULL,
 			messages_limit INT NOT NULL,
 			messages_limit INT NOT NULL,
@@ -89,7 +90,7 @@ const (
 			topic TEXT NOT NULL,
 			topic TEXT NOT NULL,
 			read INT NOT NULL,
 			read INT NOT NULL,
 			write INT NOT NULL,
 			write INT NOT NULL,
-			owner_user_id INT,			
+			owner_user_id INT,
 			PRIMARY KEY (user_id, topic),
 			PRIMARY KEY (user_id, topic),
 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE,
 			FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE,
 		    FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE
 		    FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE
@@ -109,7 +110,7 @@ const (
 			version INT NOT NULL
 			version INT NOT NULL
 		);
 		);
 		INSERT INTO user (id, user, pass, role, sync_topic, created)
 		INSERT INTO user (id, user, pass, role, sync_topic, created)
-		VALUES ('` + everyoneID + `', '*', '', 'anonymous', '', UNIXEPOCH()) 
+		VALUES ('` + everyoneID + `', '*', '', 'anonymous', '', UNIXEPOCH())
 		ON CONFLICT (id) DO NOTHING;
 		ON CONFLICT (id) DO NOTHING;
 	`
 	`
 	createTablesQueries   = `BEGIN; ` + createTablesQueriesNoTx + ` COMMIT;`
 	createTablesQueries   = `BEGIN; ` + createTablesQueriesNoTx + ` COMMIT;`
@@ -121,7 +122,7 @@ const (
 		SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_price_id
 		SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_price_id
 		FROM user u
 		FROM user u
 		LEFT JOIN tier t on t.id = u.tier_id
 		LEFT JOIN tier t on t.id = u.tier_id
-		WHERE u.id = ?		
+		WHERE u.id = ?
 	`
 	`
 	selectUserByNameQuery = `
 	selectUserByNameQuery = `
 		SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_price_id
 		SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_price_id
@@ -151,12 +152,12 @@ const (
 	`
 	`
 
 
 	insertUserQuery = `
 	insertUserQuery = `
-		INSERT INTO user (id, user, pass, role, sync_topic, created) 
+		INSERT INTO user (id, user, pass, role, sync_topic, created)
 		VALUES (?, ?, ?, ?, ?, ?)
 		VALUES (?, ?, ?, ?, ?, ?)
 	`
 	`
 	selectUsernamesQuery = `
 	selectUsernamesQuery = `
-		SELECT user 
-		FROM user 
+		SELECT user
+		FROM user
 		ORDER BY
 		ORDER BY
 			CASE role
 			CASE role
 				WHEN 'admin' THEN 1
 				WHEN 'admin' THEN 1
@@ -166,7 +167,7 @@ const (
 	`
 	`
 	updateUserPassQuery          = `UPDATE user SET pass = ? WHERE user = ?`
 	updateUserPassQuery          = `UPDATE user SET pass = ? WHERE user = ?`
 	updateUserRoleQuery          = `UPDATE user SET role = ? WHERE user = ?`
 	updateUserRoleQuery          = `UPDATE user SET role = ? WHERE user = ?`
-	updateUserPrefsQuery         = `UPDATE user SET prefs = ? WHERE user = ?`
+	updateUserPrefsQuery         = `UPDATE user SET prefs = ? WHERE id = ?`
 	updateUserStatsQuery         = `UPDATE user SET stats_messages = ?, stats_emails = ? WHERE id = ?`
 	updateUserStatsQuery         = `UPDATE user SET stats_messages = ?, stats_emails = ? WHERE id = ?`
 	updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0`
 	updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0`
 	updateUserDeletedQuery       = `UPDATE user SET deleted = ? WHERE id = ?`
 	updateUserDeletedQuery       = `UPDATE user SET deleted = ? WHERE id = ?`
@@ -174,15 +175,15 @@ const (
 	deleteUserQuery              = `DELETE FROM user WHERE user = ?`
 	deleteUserQuery              = `DELETE FROM user WHERE user = ?`
 
 
 	upsertUserAccessQuery = `
 	upsertUserAccessQuery = `
-		INSERT INTO user_access (user_id, topic, read, write, owner_user_id) 
+		INSERT INTO user_access (user_id, topic, read, write, owner_user_id)
 		VALUES ((SELECT id FROM user WHERE user = ?), ?, ?, ?, (SELECT IIF(?='',NULL,(SELECT id FROM user WHERE user=?))))
 		VALUES ((SELECT id FROM user WHERE user = ?), ?, ?, ?, (SELECT IIF(?='',NULL,(SELECT id FROM user WHERE user=?))))
-		ON CONFLICT (user_id, topic) 
+		ON CONFLICT (user_id, topic)
 		DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id
 		DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id
 	`
 	`
 	selectUserAccessQuery = `
 	selectUserAccessQuery = `
 		SELECT topic, read, write
 		SELECT topic, read, write
-		FROM user_access 
-		WHERE user_id = (SELECT id FROM user WHERE user = ?) 
+		FROM user_access
+		WHERE user_id = (SELECT id FROM user WHERE user = ?)
 		ORDER BY write DESC, read DESC, topic
 		ORDER BY write DESC, read DESC, topic
 	`
 	`
 	selectUserReservationsQuery = `
 	selectUserReservationsQuery = `
@@ -201,9 +202,9 @@ const (
 	selectUserHasReservationQuery = `
 	selectUserHasReservationQuery = `
 		SELECT COUNT(*)
 		SELECT COUNT(*)
 		FROM user_access
 		FROM user_access
-		WHERE user_id = owner_user_id 
+		WHERE user_id = owner_user_id
 		  AND owner_user_id = (SELECT id FROM user WHERE user = ?)
 		  AND owner_user_id = (SELECT id FROM user WHERE user = ?)
-		  AND topic = ?	
+		  AND topic = ?
 	`
 	`
 	selectOtherAccessCountQuery = `
 	selectOtherAccessCountQuery = `
 		SELECT COUNT(*)
 		SELECT COUNT(*)
@@ -213,13 +214,13 @@ const (
 	`
 	`
 	deleteAllAccessQuery  = `DELETE FROM user_access`
 	deleteAllAccessQuery  = `DELETE FROM user_access`
 	deleteUserAccessQuery = `
 	deleteUserAccessQuery = `
-		DELETE FROM user_access 
+		DELETE FROM user_access
 		WHERE user_id = (SELECT id FROM user WHERE user = ?)
 		WHERE user_id = (SELECT id FROM user WHERE user = ?)
 		   OR owner_user_id = (SELECT id FROM user WHERE user = ?)
 		   OR owner_user_id = (SELECT id FROM user WHERE user = ?)
 	`
 	`
 	deleteTopicAccessQuery = `
 	deleteTopicAccessQuery = `
-		DELETE FROM user_access 
-	   	WHERE (user_id = (SELECT id FROM user WHERE user = ?) OR owner_user_id = (SELECT id FROM user WHERE user = ?)) 
+		DELETE FROM user_access
+	   	WHERE (user_id = (SELECT id FROM user WHERE user = ?) OR owner_user_id = (SELECT id FROM user WHERE user = ?))
 	   	  AND topic = ?
 	   	  AND topic = ?
   	`
   	`
 
 
@@ -239,7 +240,7 @@ const (
 			SELECT user_id, token
 			SELECT user_id, token
 			FROM user_token
 			FROM user_token
 			WHERE user_id = ?
 			WHERE user_id = ?
-			ORDER BY expires DESC 
+			ORDER BY expires DESC
 			LIMIT ?
 			LIMIT ?
 		)
 		)
 	`
 	`
@@ -249,7 +250,7 @@ const (
 		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 	`
 	`
 	updateTierQuery = `
 	updateTierQuery = `
-		UPDATE tier 
+		UPDATE tier
 		SET name = ?, messages_limit = ?, messages_expiry_duration = ?, emails_limit = ?, reservations_limit = ?, attachment_file_size_limit = ?, attachment_total_size_limit = ?, attachment_expiry_duration = ?, attachment_bandwidth_limit = ?, stripe_price_id = ?
 		SET name = ?, messages_limit = ?, messages_expiry_duration = ?, emails_limit = ?, reservations_limit = ?, attachment_file_size_limit = ?, attachment_total_size_limit = ?, attachment_expiry_duration = ?, attachment_bandwidth_limit = ?, stripe_price_id = ?
 		WHERE code = ?
 		WHERE code = ?
 	`
 	`
@@ -272,7 +273,7 @@ const (
 	deleteTierQuery     = `DELETE FROM tier WHERE code = ?`
 	deleteTierQuery     = `DELETE FROM tier WHERE code = ?`
 
 
 	updateBillingQuery = `
 	updateBillingQuery = `
-		UPDATE user 
+		UPDATE user
 		SET stripe_customer_id = ?, stripe_subscription_id = ?, stripe_subscription_status = ?, stripe_subscription_paid_until = ?, stripe_subscription_cancel_at = ?
 		SET stripe_customer_id = ?, stripe_subscription_id = ?, stripe_subscription_status = ?, stripe_subscription_paid_until = ?, stripe_subscription_cancel_at = ?
 		WHERE user = ?
 		WHERE user = ?
 	`
 	`
@@ -291,7 +292,7 @@ const (
 	`
 	`
 	migrate1To2SelectAllOldUsernamesNoTx = `SELECT user FROM user_old`
 	migrate1To2SelectAllOldUsernamesNoTx = `SELECT user FROM user_old`
 	migrate1To2InsertUserNoTx            = `
 	migrate1To2InsertUserNoTx            = `
-		INSERT INTO user (id, user, pass, role, sync_topic, created) 
+		INSERT INTO user (id, user, pass, role, sync_topic, created)
 		SELECT ?, user, pass, role, ?, UNIXEPOCH() FROM user_old WHERE user = ?
 		SELECT ?, user, pass, role, ?, UNIXEPOCH() FROM user_old WHERE user = ?
 	`
 	`
 	migrate1To2InsertFromOldTablesAndDropNoTx = `
 	migrate1To2InsertFromOldTablesAndDropNoTx = `
@@ -305,6 +306,12 @@ const (
 	`
 	`
 )
 )
 
 
+var (
+	migrations = map[int]func(db *sql.DB) error{
+		1: migrateFrom1,
+	}
+)
+
 // Manager is an implementation of Manager. It stores users and access control list
 // Manager is an implementation of Manager. It stores users and access control list
 // in a SQLite database.
 // in a SQLite database.
 type Manager struct {
 type Manager struct {
@@ -350,15 +357,15 @@ func (a *Manager) Authenticate(username, password string) (*User, error) {
 	}
 	}
 	user, err := a.User(username)
 	user, err := a.User(username)
 	if err != nil {
 	if err != nil {
-		log.Tag(tagManager).Field("user_name", username).Err(err).Trace("Authentication of user failed (1)")
+		log.Tag(tag).Field("user_name", username).Err(err).Trace("Authentication of user failed (1)")
 		bcrypt.CompareHashAndPassword([]byte(userAuthIntentionalSlowDownHash), []byte("intentional slow-down to avoid timing attacks"))
 		bcrypt.CompareHashAndPassword([]byte(userAuthIntentionalSlowDownHash), []byte("intentional slow-down to avoid timing attacks"))
 		return nil, ErrUnauthenticated
 		return nil, ErrUnauthenticated
 	} else if user.Deleted {
 	} else if user.Deleted {
-		log.Tag(tagManager).Field("user_name", username).Trace("Authentication of user failed (2): user marked deleted")
+		log.Tag(tag).Field("user_name", username).Trace("Authentication of user failed (2): user marked deleted")
 		bcrypt.CompareHashAndPassword([]byte(userAuthIntentionalSlowDownHash), []byte("intentional slow-down to avoid timing attacks"))
 		bcrypt.CompareHashAndPassword([]byte(userAuthIntentionalSlowDownHash), []byte("intentional slow-down to avoid timing attacks"))
 		return nil, ErrUnauthenticated
 		return nil, ErrUnauthenticated
 	} else if err := bcrypt.CompareHashAndPassword([]byte(user.Hash), []byte(password)); err != nil {
 	} else if err := bcrypt.CompareHashAndPassword([]byte(user.Hash), []byte(password)); err != nil {
-		log.Tag(tagManager).Field("user_name", username).Err(err).Trace("Authentication of user failed (3)")
+		log.Tag(tag).Field("user_name", username).Err(err).Trace("Authentication of user failed (3)")
 		return nil, ErrUnauthenticated
 		return nil, ErrUnauthenticated
 	}
 	}
 	return user, nil
 	return user, nil
@@ -372,7 +379,7 @@ func (a *Manager) AuthenticateToken(token string) (*User, error) {
 	}
 	}
 	user, err := a.userByToken(token)
 	user, err := a.userByToken(token)
 	if err != nil {
 	if err != nil {
-		log.Tag(tagManager).Field("token", token).Err(err).Trace("Authentication of token failed")
+		log.Tag(tag).Field("token", token).Err(err).Trace("Authentication of token failed")
 		return nil, ErrUnauthenticated
 		return nil, ErrUnauthenticated
 	}
 	}
 	user.Token = token
 	user.Token = token
@@ -532,12 +539,12 @@ func (a *Manager) RemoveDeletedUsers() error {
 }
 }
 
 
 // ChangeSettings persists the user settings
 // ChangeSettings persists the user settings
-func (a *Manager) ChangeSettings(user *User) error {
-	prefs, err := json.Marshal(user.Prefs)
+func (a *Manager) ChangeSettings(userID string, prefs *Prefs) error {
+	b, err := json.Marshal(prefs)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if _, err := a.db.Exec(updateUserPrefsQuery, string(prefs), user.Name); err != nil {
+	if _, err := a.db.Exec(updateUserPrefsQuery, string(b), userID); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
@@ -554,9 +561,9 @@ func (a *Manager) ResetStats() error {
 	return nil
 	return nil
 }
 }
 
 
-// EnqueueStats adds the user to a queue which writes out user stats (messages, emails, ..) in
+// EnqueueUserStats adds the user to a queue which writes out user stats (messages, emails, ..) in
 // batches at a regular interval
 // batches at a regular interval
-func (a *Manager) EnqueueStats(userID string, stats *Stats) {
+func (a *Manager) EnqueueUserStats(userID string, stats *Stats) {
 	a.mu.Lock()
 	a.mu.Lock()
 	defer a.mu.Unlock()
 	defer a.mu.Unlock()
 	a.statsQueue[userID] = stats
 	a.statsQueue[userID] = stats
@@ -574,10 +581,10 @@ func (a *Manager) asyncQueueWriter(interval time.Duration) {
 	ticker := time.NewTicker(interval)
 	ticker := time.NewTicker(interval)
 	for range ticker.C {
 	for range ticker.C {
 		if err := a.writeUserStatsQueue(); err != nil {
 		if err := a.writeUserStatsQueue(); err != nil {
-			log.Tag(tagManager).Err(err).Warn("Writing user stats queue failed")
+			log.Tag(tag).Err(err).Warn("Writing user stats queue failed")
 		}
 		}
 		if err := a.writeTokenUpdateQueue(); err != nil {
 		if err := a.writeTokenUpdateQueue(); err != nil {
-			log.Tag(tagManager).Err(err).Warn("Writing token update queue failed")
+			log.Tag(tag).Err(err).Warn("Writing token update queue failed")
 		}
 		}
 	}
 	}
 }
 }
@@ -586,7 +593,7 @@ func (a *Manager) writeUserStatsQueue() error {
 	a.mu.Lock()
 	a.mu.Lock()
 	if len(a.statsQueue) == 0 {
 	if len(a.statsQueue) == 0 {
 		a.mu.Unlock()
 		a.mu.Unlock()
-		log.Tag(tagManager).Trace("No user stats updates to commit")
+		log.Tag(tag).Trace("No user stats updates to commit")
 		return nil
 		return nil
 	}
 	}
 	statsQueue := a.statsQueue
 	statsQueue := a.statsQueue
@@ -597,10 +604,10 @@ func (a *Manager) writeUserStatsQueue() error {
 		return err
 		return err
 	}
 	}
 	defer tx.Rollback()
 	defer tx.Rollback()
-	log.Tag(tagManager).Debug("Writing user stats queue for %d user(s)", len(statsQueue))
+	log.Tag(tag).Debug("Writing user stats queue for %d user(s)", len(statsQueue))
 	for userID, update := range statsQueue {
 	for userID, update := range statsQueue {
 		log.
 		log.
-			Tag(tagManager).
+			Tag(tag).
 			Fields(log.Context{
 			Fields(log.Context{
 				"user_id":        userID,
 				"user_id":        userID,
 				"messages_count": update.Messages,
 				"messages_count": update.Messages,
@@ -618,7 +625,7 @@ func (a *Manager) writeTokenUpdateQueue() error {
 	a.mu.Lock()
 	a.mu.Lock()
 	if len(a.tokenQueue) == 0 {
 	if len(a.tokenQueue) == 0 {
 		a.mu.Unlock()
 		a.mu.Unlock()
-		log.Tag(tagManager).Trace("No token updates to commit")
+		log.Tag(tag).Trace("No token updates to commit")
 		return nil
 		return nil
 	}
 	}
 	tokenQueue := a.tokenQueue
 	tokenQueue := a.tokenQueue
@@ -629,9 +636,9 @@ func (a *Manager) writeTokenUpdateQueue() error {
 		return err
 		return err
 	}
 	}
 	defer tx.Rollback()
 	defer tx.Rollback()
-	log.Tag(tagManager).Debug("Writing token update queue for %d token(s)", len(tokenQueue))
+	log.Tag(tag).Debug("Writing token update queue for %d token(s)", len(tokenQueue))
 	for tokenID, update := range tokenQueue {
 	for tokenID, update := range tokenQueue {
-		log.Tag(tagManager).Trace("Updating token %s with last access time %v", tokenID, update.LastAccess.Unix())
+		log.Tag(tag).Trace("Updating token %s with last access time %v", tokenID, update.LastAccess.Unix())
 		if _, err := tx.Exec(updateTokenLastAccessQuery, update.LastAccess.Unix(), update.LastOrigin.String(), tokenID); err != nil {
 		if _, err := tx.Exec(updateTokenLastAccessQuery, update.LastAccess.Unix(), update.LastOrigin.String(), tokenID); err != nil {
 			return err
 			return err
 		}
 		}
@@ -718,7 +725,7 @@ func (a *Manager) MarkUserRemoved(user *User) error {
 		return err
 		return err
 	}
 	}
 	defer tx.Rollback()
 	defer tx.Rollback()
-	if _, err := a.db.Exec(deleteUserAccessQuery, user.Name, user.Name); err != nil {
+	if _, err := tx.Exec(deleteUserAccessQuery, user.Name, user.Name); err != nil {
 		return err
 		return err
 	}
 	}
 	if _, err := tx.Exec(deleteAllTokenQuery, user.ID); err != nil {
 	if _, err := tx.Exec(deleteAllTokenQuery, user.ID); err != nil {
@@ -1012,7 +1019,6 @@ func (a *Manager) checkReservationsLimit(username string, reservationsLimit int6
 
 
 // CheckAllowAccess tests if a user may create an access control entry for the given topic.
 // CheckAllowAccess tests if a user may create an access control entry for the given topic.
 // If there are any ACL entries that are not owned by the user, an error is returned.
 // If there are any ACL entries that are not owned by the user, an error is returned.
-// FIXME is this the same as HasReservation?
 func (a *Manager) CheckAllowAccess(username string, topic string) error {
 func (a *Manager) CheckAllowAccess(username string, topic string) error {
 	if (!AllowedUsername(username) && username != Everyone) || !AllowedTopic(topic) {
 	if (!AllowedUsername(username) && username != Everyone) || !AllowedTopic(topic) {
 		return ErrInvalidArgument
 		return ErrInvalidArgument
@@ -1275,10 +1281,18 @@ func setupDB(db *sql.DB) error {
 	// Do migrations
 	// Do migrations
 	if schemaVersion == currentSchemaVersion {
 	if schemaVersion == currentSchemaVersion {
 		return nil
 		return nil
-	} else if schemaVersion == 1 {
-		return migrateFrom1(db)
+	} else if schemaVersion > currentSchemaVersion {
+		return fmt.Errorf("unexpected schema version: version %d is higher than current version %d", schemaVersion, currentSchemaVersion)
+	}
+	for i := schemaVersion; i < currentSchemaVersion; i++ {
+		fn, ok := migrations[i]
+		if !ok {
+			return fmt.Errorf("cannot find migration step from schema version %d to %d", i, i+1)
+		} else if err := fn(db); err != nil {
+			return err
+		}
 	}
 	}
-	return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
+	return nil
 }
 }
 
 
 func setupNewDB(db *sql.DB) error {
 func setupNewDB(db *sql.DB) error {
@@ -1292,7 +1306,7 @@ func setupNewDB(db *sql.DB) error {
 }
 }
 
 
 func migrateFrom1(db *sql.DB) error {
 func migrateFrom1(db *sql.DB) error {
-	log.Tag(tagManager).Info("Migrating user database schema: from 1 to 2")
+	log.Tag(tag).Info("Migrating user database schema: from 1 to 2")
 	tx, err := db.Begin()
 	tx, err := db.Begin()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -1339,7 +1353,7 @@ func migrateFrom1(db *sql.DB) error {
 	if err := tx.Commit(); err != nil {
 	if err := tx.Commit(); err != nil {
 		return err
 		return err
 	}
 	}
-	return nil // Update this when a new version is added
+	return nil
 }
 }
 
 
 func nullString(s string) sql.NullString {
 func nullString(s string) sql.NullString {

+ 3 - 3
user/manager_test.go

@@ -562,7 +562,7 @@ func TestManager_EnqueueStats(t *testing.T) {
 	require.Nil(t, err)
 	require.Nil(t, err)
 	require.Equal(t, int64(0), u.Stats.Messages)
 	require.Equal(t, int64(0), u.Stats.Messages)
 	require.Equal(t, int64(0), u.Stats.Emails)
 	require.Equal(t, int64(0), u.Stats.Emails)
-	a.EnqueueStats(u.ID, &Stats{
+	a.EnqueueUserStats(u.ID, &Stats{
 		Messages: 11,
 		Messages: 11,
 		Emails:   2,
 		Emails:   2,
 	})
 	})
@@ -595,7 +595,7 @@ func TestManager_ChangeSettings(t *testing.T) {
 	require.Nil(t, u.Prefs.Language)
 	require.Nil(t, u.Prefs.Language)
 
 
 	// Save with new settings
 	// Save with new settings
-	u.Prefs = &Prefs{
+	prefs := &Prefs{
 		Language: util.String("de"),
 		Language: util.String("de"),
 		Notification: &NotificationPrefs{
 		Notification: &NotificationPrefs{
 			Sound:       util.String("ding"),
 			Sound:       util.String("ding"),
@@ -610,7 +610,7 @@ func TestManager_ChangeSettings(t *testing.T) {
 			},
 			},
 		},
 		},
 	}
 	}
-	require.Nil(t, a.ChangeSettings(u))
+	require.Nil(t, a.ChangeSettings(u.ID, prefs))
 
 
 	// Read again
 	// Read again
 	u, err = a.User("ben")
 	u, err = a.User("ben")

+ 0 - 1
user/types.go

@@ -1,4 +1,3 @@
-// Package user deals with authentication and authorization against topics
 package user
 package user
 
 
 import (
 import (

+ 1 - 1
util/util.go

@@ -234,7 +234,7 @@ func FormatSize(b int64) string {
 		div *= unit
 		div *= unit
 		exp++
 		exp++
 	}
 	}
-	return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
+	return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
 }
 }
 
 
 // ReadPassword will read a password from STDIN. If the terminal supports it, it will not print the
 // ReadPassword will read a password from STDIN. If the terminal supports it, it will not print the

+ 14 - 13
web/public/static/langs/en.json

@@ -176,24 +176,25 @@
   "account_basics_password_dialog_current_password_label": "Current password",
   "account_basics_password_dialog_current_password_label": "Current password",
   "account_basics_password_dialog_new_password_label": "New password",
   "account_basics_password_dialog_new_password_label": "New password",
   "account_basics_password_dialog_confirm_password_label": "Confirm password",
   "account_basics_password_dialog_confirm_password_label": "Confirm password",
-  "account_basics_password_dialog_button_cancel": "Cancel",
   "account_basics_password_dialog_button_submit": "Change password",
   "account_basics_password_dialog_button_submit": "Change password",
   "account_basics_password_dialog_current_password_incorrect": "Password incorrect",
   "account_basics_password_dialog_current_password_incorrect": "Password incorrect",
   "account_usage_title": "Usage",
   "account_usage_title": "Usage",
   "account_usage_of_limit": "of {{limit}}",
   "account_usage_of_limit": "of {{limit}}",
   "account_usage_unlimited": "Unlimited",
   "account_usage_unlimited": "Unlimited",
   "account_usage_limits_reset_daily": "Usage limits are reset daily at midnight (UTC)",
   "account_usage_limits_reset_daily": "Usage limits are reset daily at midnight (UTC)",
-  "account_usage_tier_title": "Account type",
-  "account_usage_tier_description": "Your account's power level",
-  "account_usage_tier_admin": "Admin",
-  "account_usage_tier_basic": "Basic",
-  "account_usage_tier_free": "Free",
-  "account_usage_tier_upgrade_button": "Upgrade to Pro",
-  "account_usage_tier_change_button": "Change",
-  "account_usage_tier_paid_until": "Subscription paid until {{date}}, and will auto-renew",
-  "account_usage_tier_payment_overdue": "Your payment is overdue. Please update your payment method, or your account will be downgraded soon.",
-  "account_usage_tier_canceled_subscription": "Your subscription was canceled and will be downgraded to a free account on {{date}}.",
-  "account_usage_manage_billing_button": "Manage billing",
+  "account_basics_tier_title": "Account type",
+  "account_basics_tier_description": "Your account's power level",
+  "account_basics_tier_admin": "Admin",
+  "account_basics_tier_admin_suffix_with_tier": "(with {{tier}} tier)",
+  "account_basics_tier_admin_suffix_no_tier": "(no tier)",
+  "account_basics_tier_basic": "Basic",
+  "account_basics_tier_free": "Free",
+  "account_basics_tier_upgrade_button": "Upgrade to Pro",
+  "account_basics_tier_change_button": "Change",
+  "account_basics_tier_paid_until": "Subscription paid until {{date}}, and will auto-renew",
+  "account_basics_tier_payment_overdue": "Your payment is overdue. Please update your payment method, or your account will be downgraded soon.",
+  "account_basics_tier_canceled_subscription": "Your subscription was canceled and will be downgraded to a free account on {{date}}.",
+  "account_basics_tier_manage_billing_button": "Manage billing",
   "account_usage_messages_title": "Published messages",
   "account_usage_messages_title": "Published messages",
   "account_usage_emails_title": "Emails sent",
   "account_usage_emails_title": "Emails sent",
   "account_usage_reservations_title": "Reserved topics",
   "account_usage_reservations_title": "Reserved topics",
@@ -204,7 +205,7 @@
   "account_usage_cannot_create_portal_session": "Unable to open billing portal",
   "account_usage_cannot_create_portal_session": "Unable to open billing portal",
   "account_delete_title": "Delete account",
   "account_delete_title": "Delete account",
   "account_delete_description": "Permanently delete your account",
   "account_delete_description": "Permanently delete your account",
-  "account_delete_dialog_description": "This will permanently delete your account, including all data that is stored on the server. If you really want to proceed, please confirm with your password in the box below.",
+  "account_delete_dialog_description": "This will permanently delete your account, including all data that is stored on the server. After deletion, your username will be unavailable for 7 days. If you really want to proceed, please confirm with your password in the box below.",
   "account_delete_dialog_label": "Password",
   "account_delete_dialog_label": "Password",
   "account_delete_dialog_button_cancel": "Cancel",
   "account_delete_dialog_button_cancel": "Cancel",
   "account_delete_dialog_button_submit": "Permanently delete account",
   "account_delete_dialog_button_submit": "Permanently delete account",

+ 7 - 6
web/src/app/AccountApi.js

@@ -27,6 +27,7 @@ class AccountApi {
     constructor() {
     constructor() {
         this.timer = null;
         this.timer = null;
         this.listener = null; // Fired when account is fetched from remote
         this.listener = null; // Fired when account is fetched from remote
+        this.tiers = null; // Cached
     }
     }
 
 
     registerListener(listener) {
     registerListener(listener) {
@@ -148,11 +149,7 @@ class AccountApi {
         console.log(`[AccountApi] Extending user access token ${url}`);
         console.log(`[AccountApi] Extending user access token ${url}`);
         await fetchOrThrow(url, {
         await fetchOrThrow(url, {
             method: "PATCH",
             method: "PATCH",
-            headers: withBearerAuth({}, session.token()),
-            body: JSON.stringify({
-                token: session.token(),
-                expires: Math.floor(Date.now() / 1000) + 6220800 // FIXME
-            })
+            headers: withBearerAuth({}, session.token())
         });
         });
     }
     }
 
 
@@ -239,10 +236,14 @@ class AccountApi {
     }
     }
 
 
     async billingTiers() {
     async billingTiers() {
+        if (this.tiers) {
+            return this.tiers;
+        }
         const url = tiersUrl(config.base_url);
         const url = tiersUrl(config.base_url);
         console.log(`[AccountApi] Fetching billing tiers`);
         console.log(`[AccountApi] Fetching billing tiers`);
         const response = await fetchOrThrow(url); // No auth needed!
         const response = await fetchOrThrow(url); // No auth needed!
-        return await response.json(); // May throw SyntaxError
+        this.tiers = await response.json(); // May throw SyntaxError
+        return this.tiers;
     }
     }
 
 
     async createBillingSubscription(tier) {
     async createBillingSubscription(tier) {

+ 19 - 17
web/src/components/Account.js

@@ -198,7 +198,7 @@ const ChangePasswordDialog = (props) => {
                 />
                 />
             </DialogContent>
             </DialogContent>
             <DialogFooter status={error}>
             <DialogFooter status={error}>
-                <Button onClick={props.onClose}>{t("account_basics_password_dialog_button_cancel")}</Button>
+                <Button onClick={props.onClose}>{t("common_cancel")}</Button>
                 <Button
                 <Button
                     onClick={handleDialogSubmit}
                     onClick={handleDialogSubmit}
                     disabled={newPassword.length === 0 || currentPassword.length === 0 || newPassword !== confirmPassword}
                     disabled={newPassword.length === 0 || currentPassword.length === 0 || newPassword !== confirmPassword}
@@ -242,10 +242,10 @@ const AccountType = () => {
 
 
     let accountType;
     let accountType;
     if (account.role === Role.ADMIN) {
     if (account.role === Role.ADMIN) {
-        const tierSuffix = (account.tier) ? `(with ${account.tier.name} tier)` : `(no tier)`;
-        accountType = `${t("account_usage_tier_admin")} ${tierSuffix}`;
+        const tierSuffix = (account.tier) ? t("account_basics_tier_admin_suffix_with_tier", { tier: account.tier.name }) : t("account_basics_tier_admin_suffix_no_tier");
+        accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`;
     } else if (!account.tier) {
     } else if (!account.tier) {
-        accountType = (config.enable_payments) ? t("account_usage_tier_free") : t("account_usage_tier_basic");
+        accountType = (config.enable_payments) ? t("account_basics_tier_free") : t("account_basics_tier_basic");
     } else {
     } else {
         accountType = account.tier.name;
         accountType = account.tier.name;
     }
     }
@@ -253,13 +253,13 @@ const AccountType = () => {
     return (
     return (
         <Pref
         <Pref
             alignTop={account.billing?.status === SubscriptionStatus.PAST_DUE || account.billing?.cancel_at > 0}
             alignTop={account.billing?.status === SubscriptionStatus.PAST_DUE || account.billing?.cancel_at > 0}
-            title={t("account_usage_tier_title")}
-            description={t("account_usage_tier_description")}
+            title={t("account_basics_tier_title")}
+            description={t("account_basics_tier_description")}
         >
         >
             <div>
             <div>
                 {accountType}
                 {accountType}
                 {account.billing?.paid_until && !account.billing?.cancel_at &&
                 {account.billing?.paid_until && !account.billing?.cancel_at &&
-                    <Tooltip title={t("account_usage_tier_paid_until", { date: formatShortDate(account.billing?.paid_until) })}>
+                    <Tooltip title={t("account_basics_tier_paid_until", { date: formatShortDate(account.billing?.paid_until) })}>
                         <span><InfoIcon/></span>
                         <span><InfoIcon/></span>
                     </Tooltip>
                     </Tooltip>
                 }
                 }
@@ -270,7 +270,7 @@ const AccountType = () => {
                         startIcon={<CelebrationIcon sx={{ color: "#55b86e" }}/>}
                         startIcon={<CelebrationIcon sx={{ color: "#55b86e" }}/>}
                         onClick={handleUpgradeClick}
                         onClick={handleUpgradeClick}
                         sx={{ml: 1}}
                         sx={{ml: 1}}
-                    >{t("account_usage_tier_upgrade_button")}</Button>
+                    >{t("account_basics_tier_upgrade_button")}</Button>
                 }
                 }
                 {config.enable_payments && account.role === Role.USER && account.billing?.subscription &&
                 {config.enable_payments && account.role === Role.USER && account.billing?.subscription &&
                     <Button
                     <Button
@@ -278,7 +278,7 @@ const AccountType = () => {
                         size="small"
                         size="small"
                         onClick={handleUpgradeClick}
                         onClick={handleUpgradeClick}
                         sx={{ml: 1}}
                         sx={{ml: 1}}
-                    >{t("account_usage_tier_change_button")}</Button>
+                    >{t("account_basics_tier_change_button")}</Button>
                 }
                 }
                 {config.enable_payments && account.role === Role.USER && account.billing?.customer &&
                 {config.enable_payments && account.role === Role.USER && account.billing?.customer &&
                     <Button
                     <Button
@@ -286,19 +286,21 @@ const AccountType = () => {
                         size="small"
                         size="small"
                         onClick={handleManageBilling}
                         onClick={handleManageBilling}
                         sx={{ml: 1}}
                         sx={{ml: 1}}
-                    >{t("account_usage_manage_billing_button")}</Button>
+                    >{t("account_basics_tier_manage_billing_button")}</Button>
+                }
+                {config.enable_payments &&
+                    <UpgradeDialog
+                        key={`upgradeDialogFromAccount${upgradeDialogKey}`}
+                        open={upgradeDialogOpen}
+                        onCancel={() => setUpgradeDialogOpen(false)}
+                    />
                 }
                 }
-                <UpgradeDialog
-                    key={`upgradeDialogFromAccount${upgradeDialogKey}`}
-                    open={upgradeDialogOpen}
-                    onCancel={() => setUpgradeDialogOpen(false)}
-                />
             </div>
             </div>
             {account.billing?.status === SubscriptionStatus.PAST_DUE &&
             {account.billing?.status === SubscriptionStatus.PAST_DUE &&
-                <Alert severity="error" sx={{mt: 1}}>{t("account_usage_tier_payment_overdue")}</Alert>
+                <Alert severity="error" sx={{mt: 1}}>{t("account_basics_tier_payment_overdue")}</Alert>
             }
             }
             {account.billing?.cancel_at > 0 &&
             {account.billing?.cancel_at > 0 &&
-                <Alert severity="warning" sx={{mt: 1}}>{t("account_usage_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })}</Alert>
+                <Alert severity="warning" sx={{mt: 1}}>{t("account_basics_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })}</Alert>
             }
             }
             <Portal>
             <Portal>
                 <Snackbar
                 <Snackbar

+ 1 - 1
web/src/components/UpgradeDialog.js

@@ -212,7 +212,7 @@ const TierCard = (props) => {
                             }}>{labelText}</div>
                             }}>{labelText}</div>
                         }
                         }
                         <Typography variant="h5" component="div">
                         <Typography variant="h5" component="div">
-                            {tier.name || t("account_usage_tier_free")}
+                            {tier.name || t("account_basics_tier_free")}
                         </Typography>
                         </Typography>
                         <List dense>
                         <List dense>
                             {tier.limits.reservations > 0 && <FeatureItem>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations })}</FeatureItem>}
                             {tier.limits.reservations > 0 && <FeatureItem>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations })}</FeatureItem>}

+ 0 - 2
web/src/components/routes.js

@@ -1,8 +1,6 @@
 import config from "../app/config";
 import config from "../app/config";
 import {shortUrl} from "../app/utils";
 import {shortUrl} from "../app/utils";
 
 
-// Remember to also update the "disallowedTopics" list!
-
 const routes = {
 const routes = {
     login: "/login",
     login: "/login",
     signup: "/signup",
     signup: "/signup",