diff --git a/cmd/user.go b/cmd/user.go index 73678d3..98a9449 100644 --- a/cmd/user.go +++ b/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) diff --git a/server/message_cache_test.go b/server/message_cache_test.go index a15b343..84f9455 100644 --- a/server/message_cache_test.go +++ b/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 diff --git a/server/server.go b/server/server.go index bc26737..c49d7b1 100644 --- a/server/server.go +++ b/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 diff --git a/server/server_account.go b/server/server_account.go index cc30dd6..0b2df19 100644 --- a/server/server_account.go +++ b/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 diff --git a/server/server_account_test.go b/server/server_account_test.go index 95b89d7..d6c28c8 100644 --- a/server/server_account_test.go +++ b/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"), diff --git a/server/server_test.go b/server/server_test.go index 28a24e2..752ea55 100644 --- a/server/server_test.go +++ b/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 diff --git a/server/types.go b/server/types.go index 21022f3..9b0acde 100644 --- a/server/types.go +++ b/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"` diff --git a/user/manager.go b/user/manager.go index 7037d7a..44ca81b 100644 --- a/user/manager.go +++ b/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{ diff --git a/user/manager_test.go b/user/manager_test.go index d278468..edffd3d 100644 --- a/user/manager_test.go +++ b/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{ diff --git a/user/types.go b/user/types.go index a42a47b..f0247d4 100644 --- a/user/types.go +++ b/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