From a6b3794c54b39504bef7a62d1d3245aebacc423e Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 7 Jul 2024 20:50:33 -0400 Subject: [PATCH] mgmt api: add user ID to repsonse body This commit updates the following management API endpoints: - GET /user - GET /session These endpoints now include the user ID. Consumers of this endpoint must no longer treat the "screen_name" field as the user identifier because the value can change when users modify their screen name formats. --- api.yml | 22 +++++++----- server/http/mgmt_api.go | 67 +++++++++++++++++++++++------------- server/http/mgmt_api_test.go | 19 +++++----- server/http/types.go | 10 ++---- state/user.go | 10 +++--- state/user_store.go | 7 +++- 6 files changed, 78 insertions(+), 57 deletions(-) diff --git a/api.yml b/api.yml index 512082f8..96091b6e 100644 --- a/api.yml +++ b/api.yml @@ -18,9 +18,12 @@ paths: items: type: object properties: + id: + type: string + description: User's unique identifier. screen_name: type: string - description: The user's screen name. + description: User's screen name. post: summary: Create a new user description: Create a new user account with a screen name and password. @@ -83,9 +86,12 @@ paths: items: type: object properties: + id: + type: string + description: User's unique identifier. screen_name: type: string - description: The screen name associated with the session. + description: User's screen name. /user/password: put: @@ -141,10 +147,10 @@ paths: properties: id: type: string - description: Unique identifier of the participant. + description: User's unique identifier. screen_name: type: string - description: Screen name of the participant. + description: User's screen name. post: summary: Create a new public chat room @@ -199,10 +205,10 @@ paths: properties: id: type: string - description: Unique identifier of the participant. + description: User's unique identifier. screen_name: type: string - description: Screen name of the participant. + description: User's screen name. /instant-message: post: @@ -228,6 +234,4 @@ paths: '200': description: Message sent successfully. '400': - description: Bad request. Invalid input data. - '404': - description: User not found. \ No newline at end of file + description: Bad request. Invalid input data. \ No newline at end of file diff --git a/server/http/mgmt_api.go b/server/http/mgmt_api.go index 6de522f3..e1b72863 100644 --- a/server/http/mgmt_api.go +++ b/server/http/mgmt_api.go @@ -81,7 +81,7 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManag return } - err = manager.DeleteUser(user.DisplayScreenName.IdentScreenName()) + err = manager.DeleteUser(state.NewIdentScreenName(user.ScreenName)) switch { case errors.Is(err, state.ErrNoUser): http.Error(w, "user does not exist", http.StatusNotFound) @@ -107,21 +107,24 @@ func userPasswordHandler(w http.ResponseWriter, r *http.Request, userManager Use // putUserPasswordHandler handles the PUT /user/password endpoint. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) { - user, err := userFromBody(r) + input, err := userFromBody(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - user.AuthKey = newUUID().String() - user.IdentScreenName = user.DisplayScreenName.IdentScreenName() - if err := user.HashPassword(user.Password); err != nil { + user := state.User{ + AuthKey: newUUID().String(), + IdentScreenName: state.NewIdentScreenName(input.ScreenName), + } + + if err := user.HashPassword(input.Password); err != nil { logger.Error("error hashing user password in PUT /user/password", "err", err.Error()) http.Error(w, "internal server error", http.StatusInternalServerError) return } - if err := userManager.SetUserPassword(user.User); err != nil { + if err := userManager.SetUserPassword(user); err != nil { switch { case errors.Is(err, state.ErrNoUser): http.Error(w, "user does not exist", http.StatusNotFound) @@ -148,13 +151,14 @@ func sessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever Ses ou := onlineUsers{ Count: len(allUsers), - Sessions: make([]userSession, 0), + Sessions: make([]userHandle, len(allUsers)), } - for _, s := range allUsers { - ou.Sessions = append(ou.Sessions, userSession{ + for i, s := range allUsers { + ou.Sessions[i] = userHandle{ + ID: s.IdentScreenName().String(), ScreenName: s.DisplayScreenName().String(), - }) + } } if err := json.NewEncoder(w).Encode(ou); err != nil { @@ -166,13 +170,23 @@ func sessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever Ses // getUserHandler handles the GET /user endpoint. func getUserHandler(w http.ResponseWriter, _ *http.Request, userManager UserManager, logger *slog.Logger) { w.Header().Set("Content-Type", "application/json") + users, err := userManager.AllUsers() if err != nil { logger.Error("error in GET /user", "err", err.Error()) http.Error(w, "internal server error", http.StatusInternalServerError) return } - if err := json.NewEncoder(w).Encode(users); err != nil { + + out := make([]userHandle, len(users)) + for i, u := range users { + out[i] = userHandle{ + ID: u.IdentScreenName.String(), + ScreenName: u.DisplayScreenName.String(), + } + } + + if err := json.NewEncoder(w).Encode(out); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -180,20 +194,26 @@ func getUserHandler(w http.ResponseWriter, _ *http.Request, userManager UserMana // postUserHandler handles the POST /user endpoint. func postUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) { - user, err := userFromBody(r) + input, err := userFromBody(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - user.AuthKey = newUUID().String() - if err := user.HashPassword(user.Password); err != nil { + sn := state.DisplayScreenName(input.ScreenName) + user := state.User{ + AuthKey: newUUID().String(), + DisplayScreenName: sn, + IdentScreenName: sn.IdentScreenName(), + } + + if err := user.HashPassword(input.Password); err != nil { logger.Error("error hashing user password in POST /user", "err", err.Error()) http.Error(w, "internal server error", http.StatusInternalServerError) return } - err = userManager.InsertUser(user.User) + err = userManager.InsertUser(user) switch { case errors.Is(err, state.ErrDupUser): http.Error(w, "user already exists", http.StatusConflict) @@ -213,7 +233,6 @@ func userFromBody(r *http.Request) (userWithPassword, error) { if err := json.NewDecoder(r.Body).Decode(&user); err != nil { return userWithPassword{}, errors.New("malformed input") } - user.IdentScreenName = user.DisplayScreenName.IdentScreenName() return user, nil } @@ -306,14 +325,14 @@ func getPublicChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetrie cr := chatRoom{ CreateTime: room.CreateTime, Name: room.Name, - Participants: make([]userHandle, 0, len(sessions)), + Participants: make([]userHandle, len(sessions)), URL: room.URL().String(), } - for _, sess := range sessions { - cr.Participants = append(cr.Participants, userHandle{ + for j, sess := range sessions { + cr.Participants[j] = userHandle{ ID: sess.IdentScreenName().String(), ScreenName: sess.DisplayScreenName().String(), - }) + } } out[i] = cr @@ -375,14 +394,14 @@ func getPrivateChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetri CreateTime: room.CreateTime, CreatorID: room.Creator.String(), Name: room.Name, - Participants: make([]userHandle, 0, len(sessions)), + Participants: make([]userHandle, len(sessions)), URL: room.URL().String(), } - for _, sess := range sessions { - cr.Participants = append(cr.Participants, userHandle{ + for j, sess := range sessions { + cr.Participants[j] = userHandle{ ID: sess.IdentScreenName().String(), ScreenName: sess.DisplayScreenName().String(), - }) + } } out[i] = cr diff --git a/server/http/mgmt_api_test.go b/server/http/mgmt_api_test.go index 092f56a4..6ea6b2e9 100644 --- a/server/http/mgmt_api_test.go +++ b/server/http/mgmt_api_test.go @@ -43,7 +43,7 @@ func TestSessionHandler_GET(t *testing.T) { fnNewSess("userA"), fnNewSess("userB"), }, - want: `{"count":2,"sessions":[{"screen_name":"userA"},{"screen_name":"userB"}]}`, + want: `{"count":2,"sessions":[{"id":"usera","screen_name":"userA"},{"id":"userb","screen_name":"userB"}]}`, statusCode: http.StatusOK, }, } @@ -114,7 +114,7 @@ func TestUserHandler_GET(t *testing.T) { IdentScreenName: state.NewIdentScreenName("userB"), }, }, - want: `[{"screen_name":"userA"},{"screen_name":"userB"}]`, + want: `[{"id":"usera","screen_name":"userA"},{"id":"userb","screen_name":"userB"}]`, statusCode: http.StatusOK, }, { @@ -332,9 +332,8 @@ func TestUserPasswordHandler_PUT(t *testing.T) { UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"), user: func() state.User { user := state.User{ - AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(), - DisplayScreenName: "userA", - IdentScreenName: state.NewIdentScreenName("userA"), + AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(), + IdentScreenName: state.NewIdentScreenName("userA"), } assert.NoError(t, user.HashPassword("thepassword")) return user @@ -355,9 +354,8 @@ func TestUserPasswordHandler_PUT(t *testing.T) { UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"), user: func() state.User { user := state.User{ - AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(), - DisplayScreenName: "userA", - IdentScreenName: state.NewIdentScreenName("userA"), + AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(), + IdentScreenName: state.NewIdentScreenName("userA"), } assert.NoError(t, user.HashPassword("thepassword")) return user @@ -372,9 +370,8 @@ func TestUserPasswordHandler_PUT(t *testing.T) { UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"), user: func() state.User { user := state.User{ - AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(), - DisplayScreenName: "userA", - IdentScreenName: state.NewIdentScreenName("userA"), + AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(), + IdentScreenName: state.NewIdentScreenName("userA"), } assert.NoError(t, user.HashPassword("thepassword")) return user diff --git a/server/http/types.go b/server/http/types.go index 284acd02..2cc7509f 100644 --- a/server/http/types.go +++ b/server/http/types.go @@ -37,17 +37,13 @@ type MessageRelayer interface { } type userWithPassword struct { - state.User - Password string `json:"password,omitempty"` -} - -type userSession struct { ScreenName string `json:"screen_name"` + Password string `json:"password,omitempty"` } type onlineUsers struct { - Count int `json:"count"` - Sessions []userSession `json:"sessions"` + Count int `json:"count"` + Sessions []userHandle `json:"sessions"` } type userHandle struct { diff --git a/state/user.go b/state/user.go index abef2a26..996fb2c7 100644 --- a/state/user.go +++ b/state/user.go @@ -87,16 +87,16 @@ func NewStubUser(screenName DisplayScreenName) (User, error) { // User represents a user account. type User struct { // IdentScreenName is the AIM screen name. - IdentScreenName IdentScreenName `json:"-"` + IdentScreenName IdentScreenName // DisplayScreenName is the formatted screen name. - DisplayScreenName DisplayScreenName `json:"screen_name"` + DisplayScreenName DisplayScreenName // AuthKey is the salt for the MD5 password hash. - AuthKey string `json:"-"` + AuthKey string // StrongMD5Pass is the MD5 password hash format used by AIM v4.8-v5.9. - StrongMD5Pass []byte `json:"-"` + StrongMD5Pass []byte // WeakMD5Pass is the MD5 password hash format used by AIM v3.5-v4.7. This // hash is used to authenticate roasted passwords for AIM v1.0-v3.0. - WeakMD5Pass []byte `json:"-"` + WeakMD5Pass []byte } // ValidateHash checks if md5Hash is identical to one of the password hashes. diff --git a/state/user_store.go b/state/user_store.go index 5c665a7f..5c11f211 100644 --- a/state/user_store.go +++ b/state/user_store.go @@ -166,7 +166,12 @@ func (f SQLiteUserStore) DeleteUser(screenName IdentScreenName) error { return nil } -// SetUserPassword sets the user's password hashes and auth key. +// SetUserPassword sets the user's password hashes and auth key. The following +// fields must be set on u: +// - AuthKey +// - WeakMD5Pass +// - StrongMD5Pass +// - IdentScreenName func (f SQLiteUserStore) SetUserPassword(u User) (err error) { tx, err := f.db.Begin() if err != nil {