diff --git a/api.go b/api.go index 2269581..02a8079 100644 --- a/api.go +++ b/api.go @@ -305,6 +305,7 @@ type APICreateUserRequest struct { SkinURL *string `json:"skinUrl" example:"https://example.com/skin.png"` // Optional. URL to skin file. Do not specify both `skinBase64` and `skinUrl`. CapeBase64 *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"` // Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`. CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`. + MaxPlayerCount *int `json:"maxPlayerCount" example:"3"` // Optional. Maximum number of players a user is allowed to create. -1 means unlimited players. -2 means use the default configured value. } // Create a user (admin only) @@ -354,6 +355,7 @@ func (app *App) APICreateUser() func(c echo.Context) error { req.ExistingPlayer, nil, // challengeToken req.FallbackPlayer, + req.MaxPlayerCount, req.SkinModel, skinReader, req.SkinURL, @@ -379,6 +381,7 @@ type APIUpdateUserRequest struct { IsLocked *bool `json:"isLocked" example:"false"` // Optional. Pass `true` to lock (disable), `false` to unlock user. ResetAPIToken bool `json:"resetApiToken" example:"true"` // Pass `true` to reset the user's API token PreferredLanguage *string `json:"preferredLanguage" example:"en"` // Optional. One of the two-letter codes in https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html. Used by Minecraft. + MaxPlayerCount *int `json:"maxPlayerCount" example:"3"` // Optional. Maximum number of players a user is allowed to create. -1 means unlimited players. -2 means use the default configured value. } // APIUpdateUser godoc @@ -426,7 +429,7 @@ func (app *App) APIUpdateUser() func(c echo.Context) error { req.IsLocked, req.ResetAPIToken, req.PreferredLanguage, - nil, + req.MaxPlayerCount, ) if err != nil { return err @@ -470,7 +473,7 @@ func (app *App) APIUpdateSelf() func(c echo.Context) error { req.IsLocked, req.ResetAPIToken, req.PreferredLanguage, - nil, + req.MaxPlayerCount, ) if err != nil { return err diff --git a/front.go b/front.go index 470bce0..53b5039 100644 --- a/front.go +++ b/front.go @@ -612,7 +612,7 @@ func FrontUpdateUser(app *App) func(c echo.Context) error { password := nilIfEmpty(c.FormValue("password")) resetAPIToken := c.FormValue("resetApiToken") == "on" preferredLanguage := nilIfEmpty(c.FormValue("preferredLanguage")) - // maxPlayerCount := c.FormValue("maxPlayerCount") + maxPlayerCountString := c.FormValue("maxPlayerCount") var targetUser *User if targetUUID == nil || *targetUUID == user.UUID { @@ -629,6 +629,17 @@ func FrontUpdateUser(app *App) func(c echo.Context) error { } } + maxPlayerCount := targetUser.MaxPlayerCount + if maxPlayerCountString == "" { + maxPlayerCount = app.Constants.MaxPlayerCountUseDefault + } else { + var err error + maxPlayerCount, err = strconv.Atoi(maxPlayerCountString) + if err != nil { + return NewWebError(returnURL, "Max player count must be an integer.") + } + } + _, err := app.UpdateUser( app.DB, user, // caller @@ -638,7 +649,7 @@ func FrontUpdateUser(app *App) func(c echo.Context) error { nil, // isLocked resetAPIToken, preferredLanguage, - nil, + &maxPlayerCount, ) if err != nil { var userError *UserError @@ -926,6 +937,7 @@ func FrontRegister(app *App) func(c echo.Context) error { existingPlayer, challengeToken, nil, // fallbackPlayer + nil, // maxPlayerCount nil, // skinModel nil, // skinReader nil, // skinURL diff --git a/front_test.go b/front_test.go index f0f75a5..62821ae 100644 --- a/front_test.go +++ b/front_test.go @@ -924,6 +924,7 @@ func (ts *TestSuite) testUserUpdate(t *testing.T) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) assert.Nil(t, writer.WriteField("uuid", takenUser.UUID)) + assert.Nil(t, writer.WriteField("maxPlayerCount", "3")) assert.Nil(t, writer.WriteField("preferredLanguage", "es")) assert.Nil(t, writer.WriteField("returnUrl", ts.App.FrontEndURL+"/web/user")) assert.Nil(t, writer.Close()) @@ -941,6 +942,17 @@ func (ts *TestSuite) testUserUpdate(t *testing.T) { rec := ts.PostMultipart(t, ts.Server, "/web/update-user", body, writer, []http.Cookie{*takenBrowserTokenCookie}, nil) ts.updateUserShouldFail(t, rec, "You are not an admin.", ts.App.FrontEndURL) } + { + // Non-admin should not be able to increase their max player count + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + assert.Nil(t, writer.WriteField("uuid", takenUser.UUID)) + assert.Nil(t, writer.WriteField("maxPlayerCount", "-1")) + assert.Nil(t, writer.WriteField("returnUrl", ts.App.FrontEndURL+"/web/user")) + assert.Nil(t, writer.Close()) + rec := ts.PostMultipart(t, ts.Server, "/web/update-user", body, writer, []http.Cookie{*takenBrowserTokenCookie}, nil) + ts.updateUserShouldFail(t, rec, "Cannot set a max player count without admin privileges.", ts.App.FrontEndURL+"/web/user") + } { // Invalid preferred language should fail body := &bytes.Buffer{} diff --git a/swagger.json b/swagger.json index b283cc8..d2184b9 100644 --- a/swagger.json +++ b/swagger.json @@ -677,6 +677,11 @@ "type": "boolean", "example": false }, + "maxPlayerCount": { + "description": "Optional. Maximum number of players a user is allowed to create. -1 means unlimited players. -2 means use the default configured value.", + "type": "integer", + "example": 3 + }, "password": { "description": "Plaintext password", "type": "string", @@ -848,6 +853,11 @@ "type": "boolean", "example": false }, + "maxPlayerCount": { + "description": "Optional. Maximum number of players a user is allowed to create. -1 means unlimited players. -2 means use the default configured value.", + "type": "integer", + "example": 3 + }, "password": { "description": "Optional. New plaintext password", "type": "string", diff --git a/user.go b/user.go index ed3ed00..b46a42b 100644 --- a/user.go +++ b/user.go @@ -31,6 +31,7 @@ func (app *App) CreateUser( existingPlayer bool, challengeToken *string, fallbackPlayer *string, + maxPlayerCount *int, skinModel *string, skinReader *io.Reader, skinURL *string, @@ -159,6 +160,18 @@ func (app *App) CreateUser( return User{}, NewBadRequestUserError("Cannot make a new locked user without admin privileges.") } + maxPlayerCountInt := Constants.MaxPlayerCountUseDefault + if maxPlayerCount != nil { + if !callerIsAdmin { + return User{}, NewBadRequestUserError("Cannot set a max player count without admin privileges.") + } + err := app.ValidateMaxPlayerCount(*maxPlayerCount) + if err != nil { + return User{}, NewBadRequestUserError("Invalid max player count: %s", err) + } + maxPlayerCountInt = *maxPlayerCount + } + apiToken, err := MakeAPIToken() if err != nil { return User{}, err @@ -172,8 +185,8 @@ func (app *App) CreateUser( PasswordSalt: passwordSalt, PasswordHash: passwordHash, PreferredLanguage: app.Config.DefaultPreferredLanguage, + MaxPlayerCount: maxPlayerCountInt, APIToken: apiToken, - MaxPlayerCount: Constants.MaxPlayerCountUseDefault, } // Player @@ -334,6 +347,9 @@ func (app *App) UpdateUser( } if maxPlayerCount != nil { + if !callerIsAdmin { + return User{}, NewBadRequestUserError("Cannot set a max player count without admin privileges.") + } err := app.ValidateMaxPlayerCount(*maxPlayerCount) if err != nil { return User{}, NewBadRequestUserError("Invalid max player count: %s", err) diff --git a/view/user.tmpl b/view/user.tmpl index f97afa5..0b39375 100644 --- a/view/user.tmpl +++ b/view/user.tmpl @@ -153,6 +153,19 @@ method="post" enctype="multipart/form-data" > + {{ if .User.IsAdmin }} +

+
+ + +

+ {{ end }}