Skip to content

Commit

Permalink
mgmt api: add user ID to repsonse body
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mk6i committed Jul 8, 2024
1 parent 107c6d3 commit a6b3794
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 57 deletions.
22 changes: 13 additions & 9 deletions api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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.
67 changes: 43 additions & 24 deletions server/http/mgmt_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -166,34 +170,50 @@ 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
}
}

// 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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 8 additions & 11 deletions server/http/mgmt_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
Expand Down Expand Up @@ -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,
},
{
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
10 changes: 3 additions & 7 deletions server/http/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions state/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion state/user_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit a6b3794

Please sign in to comment.