Przeglądaj źródła

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.
Mike 2 lat temu
rodzic
commit
a6b3794c54
6 zmienionych plików z 78 dodań i 57 usunięć
  1. 13 9
      api.yml
  2. 43 24
      server/http/mgmt_api.go
  3. 8 11
      server/http/mgmt_api_test.go
  4. 3 7
      server/http/types.go
  5. 5 5
      state/user.go
  6. 6 1
      state/user_store.go

+ 13 - 9
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.
+          description: Bad request. Invalid input data.

+ 43 - 24
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

+ 8 - 11
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

+ 3 - 7
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 {

+ 5 - 5
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.

+ 6 - 1
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 {