Переглянути джерело

Sync topic (begin), rename user fields

binwiederhier 3 роки тому
батько
коміт
7e528d9c10

+ 3 - 2
cmd/user.go

@@ -16,7 +16,8 @@ import (
 )
 
 const (
-	tierReset = "-"
+	tierReset    = "-"
+	createdByCLI = "cli"
 )
 
 func init() {
@@ -196,7 +197,7 @@ func execUserAdd(c *cli.Context) error {
 
 		password = p
 	}
-	if err := manager.AddUser(username, password, role); err != nil {
+	if err := manager.AddUser(username, password, role, createdByCLI); err != nil {
 		return err
 	}
 	fmt.Fprintf(c.App.ErrWriter, "user %s added with role %s\n", username, role)

+ 1 - 0
server/message_cache_test.go

@@ -536,6 +536,7 @@ func TestSqliteCache_Migration_From9(t *testing.T) {
 	// Create cache to trigger migration
 	cacheDuration := 17 * time.Hour
 	c, err := newSqliteCache(filename, "", cacheDuration, 0, 0, false)
+	require.Nil(t, err)
 	checkSchemaVersion(t, c.db)
 
 	// Check version

+ 6 - 3
server/server.go

@@ -38,14 +38,18 @@ import (
 	TODO
 		Limits & rate limiting:
 			login/account endpoints
-		purge accounts that were not logged int o in X
 		reset daily Limits for users
+			- set last_stats_reset in migration
+		set sync_topic in migration
+		update last_seen when API is accessed
 		Make sure account endpoints make sense for admins
+
 		UI:
 		- flicker of upgrade banner
 		- JS constants
 		Sync:
 			- "account topic" sync mechanism
+				- subscribe to sync topic in UI
 			- "mute" setting
 			- figure out what settings are "web" or "phone"
 		Delete visitor when tier is changed to refresh rate limiters
@@ -54,10 +58,9 @@ import (
 		- Message rate limiting and reset tests
 		Docs:
 		- "expires" field in message
+		- server.yml: enable-X flags
 		Refactor:
 		- rename /access -> /reservation
-		Later:
-		- Pricing
 */
 
 // Server is the main server, providing the UI and API for ntfy

+ 3 - 1
server/server_account.go

@@ -10,6 +10,7 @@ import (
 const (
 	jsonBodyBytesLimit   = 4096
 	subscriptionIDLength = 16
+	createdByAPI         = "api"
 )
 
 func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
@@ -31,7 +32,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
 	if v.accountLimiter != nil && !v.accountLimiter.Allow() {
 		return errHTTPTooManyRequestsLimitAccountCreation
 	}
-	if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User
+	if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser, createdByAPI); err != nil { // TODO this should return a User
 		return err
 	}
 	w.Header().Set("Content-Type", "application/json")
@@ -70,6 +71,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
 	if v.user != nil {
 		response.Username = v.user.Name
 		response.Role = string(v.user.Role)
+		response.SyncTopic = v.user.SyncTopic
 		if v.user.Prefs != nil {
 			if v.user.Prefs.Language != "" {
 				response.Language = v.user.Prefs.Language

+ 9 - 9
server/server_account_test.go

@@ -67,8 +67,8 @@ func TestAccount_Signup_AsUser(t *testing.T) {
 	conf.EnableSignup = true
 	s := newTestServer(t, conf)
 
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
-	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, "unit-test"))
+	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, "unit-test"))
 
 	rr := request(t, s, "POST", "/v1/account", `{"username":"emma", "password":"emma"}`, map[string]string{
 		"Authorization": util.BasicAuth("phil", "phil"),
@@ -133,7 +133,7 @@ func TestAccount_Get_Anonymous(t *testing.T) {
 
 func TestAccount_ChangeSettings(t *testing.T) {
 	s := newTestServer(t, newTestConfigWithAuthFile(t))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 	user, _ := s.userManager.User("phil")
 	token, _ := s.userManager.CreateToken(user)
 
@@ -160,7 +160,7 @@ func TestAccount_ChangeSettings(t *testing.T) {
 
 func TestAccount_Subscription_AddUpdateDelete(t *testing.T) {
 	s := newTestServer(t, newTestConfigWithAuthFile(t))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 
 	rr := request(t, s, "POST", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def"}`, map[string]string{
 		"Authorization": util.BasicAuth("phil", "phil"),
@@ -210,7 +210,7 @@ func TestAccount_Subscription_AddUpdateDelete(t *testing.T) {
 
 func TestAccount_ChangePassword(t *testing.T) {
 	s := newTestServer(t, newTestConfigWithAuthFile(t))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 
 	rr := request(t, s, "POST", "/v1/account/password", `{"password": "new password"}`, map[string]string{
 		"Authorization": util.BasicAuth("phil", "phil"),
@@ -237,7 +237,7 @@ func TestAccount_ChangePassword_NoAccount(t *testing.T) {
 
 func TestAccount_ExtendToken(t *testing.T) {
 	s := newTestServer(t, newTestConfigWithAuthFile(t))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 
 	rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{
 		"Authorization": util.BasicAuth("phil", "phil"),
@@ -260,7 +260,7 @@ func TestAccount_ExtendToken(t *testing.T) {
 
 func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) {
 	s := newTestServer(t, newTestConfigWithAuthFile(t))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 
 	rr := request(t, s, "PATCH", "/v1/account/token", "", map[string]string{
 		"Authorization": util.BasicAuth("phil", "phil"), // Not Bearer!
@@ -271,7 +271,7 @@ func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) {
 
 func TestAccount_DeleteToken(t *testing.T) {
 	s := newTestServer(t, newTestConfigWithAuthFile(t))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 
 	rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{
 		"Authorization": util.BasicAuth("phil", "phil"),
@@ -360,7 +360,7 @@ func TestAccount_Reservation_AddAdminSuccess(t *testing.T) {
 	conf := newTestConfigWithAuthFile(t)
 	conf.EnableSignup = true
 	s := newTestServer(t, conf)
-	require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin))
+	require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin, "unit-test"))
 
 	rr := request(t, s, "POST", "/v1/account/access", `{"topic":"mytopic","everyone":"deny-all"}`, map[string]string{
 		"Authorization": util.BasicAuth("phil", "adminpass"),

+ 10 - 10
server/server_test.go

@@ -626,7 +626,7 @@ func TestServer_Auth_Success_Admin(t *testing.T) {
 	c.AuthFile = filepath.Join(t.TempDir(), "user.db")
 	s := newTestServer(t, c)
 
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, "unit-test"))
 
 	response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
 		"Authorization": basicAuth("phil:phil"),
@@ -641,7 +641,7 @@ func TestServer_Auth_Success_User(t *testing.T) {
 	c.AuthDefault = user.PermissionDenyAll
 	s := newTestServer(t, c)
 
-	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, "unit-test"))
 	require.Nil(t, s.userManager.AllowAccess("", "ben", "mytopic", true, true))
 
 	response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
@@ -656,7 +656,7 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
 	c.AuthDefault = user.PermissionDenyAll
 	s := newTestServer(t, c)
 
-	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, "unit-test"))
 	require.Nil(t, s.userManager.AllowAccess("", "ben", "mytopic", true, true))
 	require.Nil(t, s.userManager.AllowAccess("", "ben", "anothertopic", true, true))
 
@@ -677,7 +677,7 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
 	c.AuthDefault = user.PermissionDenyAll
 	s := newTestServer(t, c)
 
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, "unit-test"))
 
 	response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
 		"Authorization": basicAuth("phil:INVALID"),
@@ -691,7 +691,7 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
 	c.AuthDefault = user.PermissionDenyAll
 	s := newTestServer(t, c)
 
-	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, "unit-test"))
 	require.Nil(t, s.userManager.AllowAccess("", "ben", "sometopic", true, true)) // Not mytopic!
 
 	response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
@@ -706,7 +706,7 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
 	c.AuthDefault = user.PermissionReadWrite // Open by default
 	s := newTestServer(t, c)
 
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, "unit-test"))
 	require.Nil(t, s.userManager.AllowAccess("", user.Everyone, "private", false, false))
 	require.Nil(t, s.userManager.AllowAccess("", user.Everyone, "announcements", true, false))
 
@@ -737,7 +737,7 @@ func TestServer_Auth_ViaQuery(t *testing.T) {
 	c.AuthDefault = user.PermissionDenyAll
 	s := newTestServer(t, c)
 
-	require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin))
+	require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin, "unit-test"))
 
 	u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:some pass"))))
 	response := request(t, s, "GET", u, "", nil)
@@ -1100,7 +1100,7 @@ func TestServer_PublishWithTierBasedMessageLimitAndExpiry(t *testing.T) {
 		MessagesLimit:          5,
 		MessagesExpiryDuration: -5 * time.Second, // Second, what a hack!
 	}))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 	require.Nil(t, s.userManager.ChangeTier("phil", "test"))
 
 	// Publish to reach message limit
@@ -1332,7 +1332,7 @@ func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) {
 		AttachmentTotalSizeLimit: 200_000,
 		AttachmentExpiryDuration: sevenDays, // 7 days
 	}))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 	require.Nil(t, s.userManager.ChangeTier("phil", "test"))
 
 	// Publish and make sure we can retrieve it
@@ -1376,7 +1376,7 @@ func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) {
 		AttachmentTotalSizeLimit: 200_000,
 		AttachmentExpiryDuration: 30 * time.Second,
 	}))
-	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
+	require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, "unit-test"))
 	require.Nil(t, s.userManager.ChangeTier("phil", "test"))
 
 	// Publish small file as anonymous

+ 1 - 0
server/types.go

@@ -271,6 +271,7 @@ type apiAccountReservation struct {
 type apiAccountResponse struct {
 	Username      string                   `json:"username"`
 	Role          string                   `json:"role,omitempty"`
+	SyncTopic     string                   `json:"sync_topic,omitempty"`
 	Language      string                   `json:"language,omitempty"`
 	Notification  *user.NotificationPrefs  `json:"notification,omitempty"`
 	Subscriptions []*user.Subscription     `json:"subscriptions,omitempty"`

+ 43 - 31
user/manager.go

@@ -20,7 +20,8 @@ const (
 	userStatsQueueWriterInterval = 33 * time.Second
 	tokenLength                  = 32
 	tokenExpiryDuration          = 72 * time.Hour // Extend tokens by this much
-	tokenMaxCount                = 10             // Only keep this many tokens in the table per user
+	syncTopicLength              = 16
+	tokenMaxCount                = 10 // Only keep this many tokens in the table per user
 )
 
 var (
@@ -50,10 +51,15 @@ const (
 			tier_id INT,
 			user TEXT NOT NULL,
 			pass TEXT NOT NULL,
-			role TEXT NOT NULL,
-			messages INT NOT NULL DEFAULT (0),
-			emails INT NOT NULL DEFAULT (0),			
-			settings JSON,
+			role TEXT CHECK (role IN ('anonymous', 'admin', 'user')) NOT NULL,
+			prefs JSON NOT NULL DEFAULT '{}',
+			sync_topic TEXT NOT NULL,
+			stats_messages INT NOT NULL DEFAULT (0),
+			stats_emails INT NOT NULL DEFAULT (0),
+			created_by TEXT NOT NULL,
+			created_at INT NOT NULL,
+			last_seen INT NOT NULL,
+			last_stats_reset INT NOT NULL DEFAULT (0),
 		    FOREIGN KEY (tier_id) REFERENCES tier (id)
 		);
 		CREATE UNIQUE INDEX idx_user ON user (user);
@@ -78,7 +84,9 @@ const (
 			id INT PRIMARY KEY,
 			version INT NOT NULL
 		);
-		INSERT INTO user (id, user, pass, role) VALUES (1, '*', '', 'anonymous') ON CONFLICT (id) DO NOTHING;
+		INSERT INTO user (id, user, pass, role, sync_topic, created_by, created_at, last_seen)
+		VALUES (1, '*', '', 'anonymous', '', 'system', UNIXEPOCH(), 0) 
+		ON CONFLICT (id) DO NOTHING;
 	`
 	createTablesQueries   = `BEGIN; ` + createTablesQueriesNoTx + ` COMMIT;`
 	builtinStartupQueries = `
@@ -86,13 +94,13 @@ const (
 	`
 
 	selectUserByNameQuery = `
-		SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
+		SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
 		FROM user u
 		LEFT JOIN tier p on p.id = u.tier_id
 		WHERE user = ?		
 	`
 	selectUserByTokenQuery = `
-		SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
+		SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
 		FROM user u
 		JOIN user_token t on u.id = t.user_id
 		LEFT JOIN tier p on p.id = u.tier_id
@@ -106,7 +114,10 @@ const (
 		ORDER BY u.user DESC
 	`
 
-	insertUserQuery      = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
+	insertUserQuery = `
+		INSERT INTO user (user, pass, role, sync_topic, created_by, created_at, last_seen) 
+		VALUES (?, ?, ?, ?, ?, ?, ?)
+	`
 	selectUsernamesQuery = `
 		SELECT user 
 		FROM user 
@@ -117,11 +128,11 @@ const (
 				ELSE 2
 			END, user
 	`
-	updateUserPassQuery     = `UPDATE user SET pass = ? WHERE user = ?`
-	updateUserRoleQuery     = `UPDATE user SET role = ? WHERE user = ?`
-	updateUserSettingsQuery = `UPDATE user SET settings = ? WHERE user = ?`
-	updateUserStatsQuery    = `UPDATE user SET messages = ?, emails = ? WHERE user = ?`
-	deleteUserQuery         = `DELETE FROM user WHERE user = ?`
+	updateUserPassQuery  = `UPDATE user SET pass = ? WHERE user = ?`
+	updateUserRoleQuery  = `UPDATE user SET role = ? WHERE user = ?`
+	updateUserPrefsQuery = `UPDATE user SET prefs = ? WHERE user = ?`
+	updateUserStatsQuery = `UPDATE user SET stats_messages = ?, stats_emails = ? WHERE user = ?`
+	deleteUserQuery      = `DELETE FROM user WHERE user = ?`
 
 	upsertUserAccessQuery = `
 		INSERT INTO user_access (user_id, topic, read, write, owner_user_id) 
@@ -210,8 +221,8 @@ const (
 		ALTER TABLE user RENAME TO user_old;
 	`
 	migrate1To2InsertFromOldTablesAndDropNoTx = `
-		INSERT INTO user (user, pass, role) 
-		SELECT user, pass, role FROM user_old;
+		INSERT INTO user (user, pass, role, sync_topic, created_by, created_at, last_seen) 
+		SELECT user, pass, role, '', 'admin', UNIXEPOCH(), UNIXEPOCH() FROM user_old;
 
 		INSERT INTO user_access (user_id, topic, read, write)
 		SELECT u.id, a.topic, a.read, a.write
@@ -371,11 +382,11 @@ func (a *Manager) RemoveExpiredTokens() error {
 
 // ChangeSettings persists the user settings
 func (a *Manager) ChangeSettings(user *User) error {
-	settings, err := json.Marshal(user.Prefs)
+	prefs, err := json.Marshal(user.Prefs)
 	if err != nil {
 		return err
 	}
-	if _, err := a.db.Exec(updateUserSettingsQuery, string(settings), user.Name); err != nil {
+	if _, err := a.db.Exec(updateUserPrefsQuery, string(prefs), user.Name); err != nil {
 		return err
 	}
 	return nil
@@ -462,7 +473,7 @@ func (a *Manager) resolvePerms(base, perm Permission) error {
 }
 
 // AddUser adds a user with the given username, password and role
-func (a *Manager) AddUser(username, password string, role Role) error {
+func (a *Manager) AddUser(username, password string, role Role, createdBy string) error {
 	if !AllowedUsername(username) || !AllowedRole(role) {
 		return ErrInvalidArgument
 	}
@@ -470,7 +481,9 @@ func (a *Manager) AddUser(username, password string, role Role) error {
 	if err != nil {
 		return err
 	}
-	if _, err = a.db.Exec(insertUserQuery, username, hash, role); err != nil {
+	// INSERT INTO user (user, pass, role, sync_topic, created_by, created_at, last_seen)
+	syncTopic, now := util.RandomString(syncTopicLength), time.Now().Unix()
+	if _, err = a.db.Exec(insertUserQuery, username, hash, role, syncTopic, createdBy, now, now); err != nil {
 		return err
 	}
 	return nil
@@ -538,33 +551,32 @@ func (a *Manager) userByToken(token string) (*User, error) {
 
 func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
 	defer rows.Close()
-	var username, hash, role string
-	var settings, tierCode, tierName sql.NullString
+	var username, hash, role, prefs, syncTopic string
+	var tierCode, tierName sql.NullString
 	var paid sql.NullBool
 	var messages, emails int64
 	var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration sql.NullInt64
 	if !rows.Next() {
 		return nil, ErrNotFound
 	}
-	if err := rows.Scan(&username, &hash, &role, &messages, &emails, &settings, &tierCode, &tierName, &paid, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration); err != nil {
+	if err := rows.Scan(&username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &tierCode, &tierName, &paid, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration); err != nil {
 		return nil, err
 	} else if err := rows.Err(); err != nil {
 		return nil, err
 	}
 	user := &User{
-		Name: username,
-		Hash: hash,
-		Role: Role(role),
+		Name:      username,
+		Hash:      hash,
+		Role:      Role(role),
+		Prefs:     &Prefs{},
+		SyncTopic: syncTopic,
 		Stats: &Stats{
 			Messages: messages,
 			Emails:   emails,
 		},
 	}
-	if settings.Valid {
-		user.Prefs = &Prefs{}
-		if err := json.Unmarshal([]byte(settings.String), user.Prefs); err != nil {
-			return nil, err
-		}
+	if err := json.Unmarshal([]byte(prefs), user.Prefs); err != nil {
+		return nil, err
 	}
 	if tierCode.Valid {
 		user.Tier = &Tier{

+ 21 - 19
user/manager_test.go

@@ -13,8 +13,8 @@ const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this sh
 
 func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, "unit-test"))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 	require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true))
 	require.Nil(t, a.AllowAccess("", "ben", "readme", true, false))
 	require.Nil(t, a.AllowAccess("", "ben", "writeme", false, true))
@@ -92,20 +92,20 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
 
 func TestManager_AddUser_Invalid(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Equal(t, ErrInvalidArgument, a.AddUser("  invalid  ", "pass", RoleAdmin))
-	require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role"))
+	require.Equal(t, ErrInvalidArgument, a.AddUser("  invalid  ", "pass", RoleAdmin, "unit-test"))
+	require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role", "unit-test"))
 }
 
 func TestManager_AddUser_Timing(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
 	start := time.Now().UnixMilli()
-	require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
+	require.Nil(t, a.AddUser("user", "pass", RoleAdmin, "unit-test"))
 	require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
 }
 
 func TestManager_Authenticate_Timing(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
+	require.Nil(t, a.AddUser("user", "pass", RoleAdmin, "unit-test"))
 
 	// Timing a correct attempt
 	start := time.Now().UnixMilli()
@@ -128,8 +128,8 @@ func TestManager_Authenticate_Timing(t *testing.T) {
 
 func TestManager_UserManagement(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, "unit-test"))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 	require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true))
 	require.Nil(t, a.AllowAccess("", "ben", "readme", true, false))
 	require.Nil(t, a.AllowAccess("", "ben", "writeme", false, true))
@@ -219,7 +219,7 @@ func TestManager_UserManagement(t *testing.T) {
 
 func TestManager_ChangePassword(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
+	require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, "unit-test"))
 
 	_, err := a.Authenticate("phil", "phil")
 	require.Nil(t, err)
@@ -233,7 +233,7 @@ func TestManager_ChangePassword(t *testing.T) {
 
 func TestManager_ChangeRole(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 	require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true))
 	require.Nil(t, a.AllowAccess("", "ben", "readme", true, false))
 
@@ -270,7 +270,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
 		AttachmentTotalSizeLimit: 524288000,
 		AttachmentExpiryDuration: 24 * time.Hour,
 	}))
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 	require.Nil(t, a.ChangeTier("ben", "pro"))
 	require.Nil(t, a.AllowAccess("ben", "ben", "mytopic", true, true))
 	require.Nil(t, a.AllowAccess("ben", Everyone, "mytopic", false, false))
@@ -312,7 +312,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
 
 func TestManager_Token_Valid(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 
 	u, err := a.User("ben")
 	require.Nil(t, err)
@@ -337,7 +337,7 @@ func TestManager_Token_Valid(t *testing.T) {
 
 func TestManager_Token_Invalid(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 
 	u, err := a.AuthenticateToken(strings.Repeat("x", 32)) // 32 == token length
 	require.Nil(t, u)
@@ -350,7 +350,7 @@ func TestManager_Token_Invalid(t *testing.T) {
 
 func TestManager_Token_Expire(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 
 	u, err := a.User("ben")
 	require.Nil(t, err)
@@ -398,7 +398,7 @@ func TestManager_Token_Expire(t *testing.T) {
 
 func TestManager_Token_Extend(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 
 	// Try to extend token for user without token
 	u, err := a.User("ben")
@@ -425,7 +425,7 @@ func TestManager_Token_Extend(t *testing.T) {
 
 func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
 	a := newTestManager(t, PermissionDenyAll)
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 
 	// Try to extend token for user without token
 	u, err := a.User("ben")
@@ -469,7 +469,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
 func TestManager_EnqueueStats(t *testing.T) {
 	a, err := newManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, 1500*time.Millisecond)
 	require.Nil(t, err)
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 
 	// Baseline: No messages or emails
 	u, err := a.User("ben")
@@ -499,12 +499,14 @@ func TestManager_EnqueueStats(t *testing.T) {
 func TestManager_ChangeSettings(t *testing.T) {
 	a, err := newManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, 1500*time.Millisecond)
 	require.Nil(t, err)
-	require.Nil(t, a.AddUser("ben", "ben", RoleUser))
+	require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
 
 	// No settings
 	u, err := a.User("ben")
 	require.Nil(t, err)
-	require.Nil(t, u.Prefs)
+	require.Nil(t, u.Prefs.Subscriptions)
+	require.Nil(t, u.Prefs.Notification)
+	require.Equal(t, "", u.Prefs.Language)
 
 	// Save with new settings
 	u.Prefs = &Prefs{

+ 10 - 7
user/types.go

@@ -9,13 +9,16 @@ import (
 
 // User is a struct that represents a user
 type User struct {
-	Name  string
-	Hash  string // password hash (bcrypt)
-	Token string // Only set if token was used to log in
-	Role  Role
-	Prefs *Prefs
-	Tier  *Tier
-	Stats *Stats
+	Name      string
+	Hash      string // password hash (bcrypt)
+	Token     string // Only set if token was used to log in
+	Role      Role
+	Prefs     *Prefs
+	Tier      *Tier
+	Stats     *Stats
+	SyncTopic string
+	Created   time.Time
+	LastSeen  time.Time
 }
 
 // Auther is an interface for authentication and authorization