mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-03 19:06:04 -04:00
Allow setting user's max player count in admin panel
This commit is contained in:
parent
f045b9a57e
commit
1da3ac2601
4
api.go
4
api.go
@ -418,6 +418,7 @@ func (app *App) APIUpdateUser() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
updatedUser, err := app.UpdateUser(
|
||||
app.DB,
|
||||
caller,
|
||||
targetUser, // user
|
||||
req.Password,
|
||||
@ -425,6 +426,7 @@ func (app *App) APIUpdateUser() func(c echo.Context) error {
|
||||
req.IsLocked,
|
||||
req.ResetAPIToken,
|
||||
req.PreferredLanguage,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -460,6 +462,7 @@ func (app *App) APIUpdateSelf() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
updatedUser, err := app.UpdateUser(
|
||||
app.DB,
|
||||
user,
|
||||
*user,
|
||||
req.Password,
|
||||
@ -467,6 +470,7 @@ func (app *App) APIUpdateSelf() func(c echo.Context) error {
|
||||
req.IsLocked,
|
||||
req.ResetAPIToken,
|
||||
req.PreferredLanguage,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
64
front.go
64
front.go
@ -13,6 +13,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -388,23 +389,48 @@ func FrontUpdateUsers(app *App) func(c echo.Context) error {
|
||||
defer tx.Rollback()
|
||||
|
||||
anyUnlockedAdmins := false
|
||||
for _, user := range users {
|
||||
shouldBeAdmin := c.FormValue("admin-"+user.Username) == "on"
|
||||
if app.IsDefaultAdmin(&user) {
|
||||
for _, targetUser := range users {
|
||||
shouldBeAdmin := c.FormValue("admin-"+targetUser.UUID) == "on"
|
||||
if app.IsDefaultAdmin(&targetUser) {
|
||||
shouldBeAdmin = true
|
||||
}
|
||||
|
||||
shouldBeLocked := c.FormValue("locked-"+user.Username) == "on"
|
||||
shouldBeLocked := c.FormValue("locked-"+targetUser.UUID) == "on"
|
||||
if shouldBeAdmin && !shouldBeLocked {
|
||||
anyUnlockedAdmins = true
|
||||
}
|
||||
if user.IsAdmin != shouldBeAdmin || user.IsLocked != shouldBeLocked {
|
||||
user.IsAdmin = shouldBeAdmin
|
||||
err := app.SetIsLocked(tx, &user, shouldBeLocked)
|
||||
|
||||
maxPlayerCountString := c.FormValue("max-player-count-" + targetUser.UUID)
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
if targetUser.IsAdmin != shouldBeAdmin || targetUser.IsLocked != shouldBeLocked || targetUser.MaxPlayerCount != maxPlayerCount {
|
||||
_, err := app.UpdateUser(
|
||||
tx,
|
||||
user, // caller
|
||||
targetUser, // user
|
||||
nil,
|
||||
&shouldBeAdmin, // isAdmin
|
||||
&shouldBeLocked, // isLocked
|
||||
false,
|
||||
nil,
|
||||
&maxPlayerCount,
|
||||
)
|
||||
if err != nil {
|
||||
var userError *UserError
|
||||
if errors.As(err, &userError) {
|
||||
return &WebError{ReturnURL: returnURL, Err: userError.Err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
tx.Save(user)
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,7 +438,10 @@ func FrontUpdateUsers(app *App) func(c echo.Context) error {
|
||||
return NewWebError(returnURL, "There must be at least one unlocked admin account.")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
err := tx.Commit().Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
setSuccessMessage(&c, "Changes saved.")
|
||||
return c.Redirect(http.StatusSeeOther, returnURL)
|
||||
@ -583,6 +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")
|
||||
|
||||
var targetUser *User
|
||||
if targetUUID == nil || *targetUUID == user.UUID {
|
||||
@ -600,6 +630,7 @@ func FrontUpdateUser(app *App) func(c echo.Context) error {
|
||||
}
|
||||
|
||||
_, err := app.UpdateUser(
|
||||
app.DB,
|
||||
user, // caller
|
||||
*targetUser, // user
|
||||
password,
|
||||
@ -607,6 +638,7 @@ func FrontUpdateUser(app *App) func(c echo.Context) error {
|
||||
nil, // isLocked
|
||||
resetAPIToken,
|
||||
preferredLanguage,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
var userError *UserError
|
||||
@ -1021,19 +1053,21 @@ func FrontDeleteUser(app *App) func(c echo.Context) error {
|
||||
returnURL := getReturnURL(app, &c)
|
||||
|
||||
var targetUser *User
|
||||
targetUsername := c.FormValue("username")
|
||||
if targetUsername == "" || targetUsername == user.Username {
|
||||
targetUUID := c.FormValue("uuid")
|
||||
if targetUUID == "" || targetUUID == user.UUID {
|
||||
targetUser = user
|
||||
} else {
|
||||
if !user.IsAdmin {
|
||||
return NewWebError(app.FrontEndURL, "You are not an admin.")
|
||||
}
|
||||
var targetUserStruct User
|
||||
result := app.DB.First(&targetUserStruct, "username = ?", targetUsername)
|
||||
targetUser = &targetUserStruct
|
||||
if result.Error != nil {
|
||||
return NewWebError(returnURL, "User not found.")
|
||||
if err := app.DB.First(&targetUserStruct, "uuid = ?", targetUUID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return NewWebError(returnURL, "User not found.")
|
||||
}
|
||||
return err
|
||||
}
|
||||
targetUser = &targetUserStruct
|
||||
}
|
||||
|
||||
err := app.DeleteUser(targetUser)
|
||||
|
@ -1296,10 +1296,10 @@ func (ts *TestSuite) testAdmin(t *testing.T) {
|
||||
user, browserTokenCookie := ts.CreateTestUser(ts.App, ts.Server, username)
|
||||
|
||||
otherUsername := "adminOther"
|
||||
_, otherBrowserTokenCookie := ts.CreateTestUser(ts.App, ts.Server, otherUsername)
|
||||
otherUser, otherBrowserTokenCookie := ts.CreateTestUser(ts.App, ts.Server, otherUsername)
|
||||
|
||||
anotherUsername := "adminAnother"
|
||||
_, _ = ts.CreateTestUser(ts.App, ts.Server, anotherUsername)
|
||||
anotherUser, anotherBrowserTokenCookie := ts.CreateTestUser(ts.App, ts.Server, anotherUsername)
|
||||
|
||||
// Make `username` an admin
|
||||
user.IsAdmin = true
|
||||
@ -1317,37 +1317,51 @@ func (ts *TestSuite) testAdmin(t *testing.T) {
|
||||
assert.Equal(t, returnURL, rec.Header().Get("Location"))
|
||||
}
|
||||
|
||||
// Make `otherUsername` and `anotherUsername` admins and lock their accounts
|
||||
// Make `otherUser` and `anotherUser` admins, lock their accounts, and set max player counts
|
||||
form := url.Values{}
|
||||
form.Set("returnUrl", returnURL)
|
||||
form.Set("admin-"+username, "on")
|
||||
form.Set("admin-"+otherUsername, "on")
|
||||
form.Set("locked-"+otherUsername, "on")
|
||||
form.Set("admin-"+anotherUsername, "on")
|
||||
form.Set("locked-"+anotherUsername, "on")
|
||||
form.Set("admin-"+user.UUID, "on")
|
||||
form.Set("admin-"+otherUser.UUID, "on")
|
||||
form.Set("locked-"+otherUser.UUID, "on")
|
||||
form.Set("admin-"+anotherUser.UUID, "on")
|
||||
form.Set("locked-"+anotherUser.UUID, "on")
|
||||
form.Set("max-player-count-"+otherUser.UUID, "3")
|
||||
form.Set("max-player-count-"+anotherUser.UUID, "-1")
|
||||
rec := ts.PostForm(t, ts.Server, "/web/admin/update-users", form, []http.Cookie{*browserTokenCookie}, nil)
|
||||
|
||||
assert.Equal(t, http.StatusSeeOther, rec.Code)
|
||||
assert.Equal(t, "", getErrorMessage(rec))
|
||||
assert.Equal(t, returnURL, rec.Header().Get("Location"))
|
||||
|
||||
// Check that their account was locked and they were made an admin
|
||||
var other User
|
||||
result = ts.App.DB.First(&other, "username = ?", otherUsername)
|
||||
result = ts.App.DB.First(&otherUser, "uuid = ?", otherUser.UUID)
|
||||
assert.Nil(t, result.Error)
|
||||
assert.True(t, other.IsAdmin)
|
||||
assert.True(t, other.IsLocked)
|
||||
assert.True(t, otherUser.IsAdmin)
|
||||
assert.True(t, otherUser.IsLocked)
|
||||
assert.Equal(t, 3, otherUser.MaxPlayerCount)
|
||||
// `otherUser` should be logged out of the web interface
|
||||
assert.NotEqual(t, "", otherBrowserTokenCookie.Value)
|
||||
assert.Nil(t, UnmakeNullString(&other.BrowserToken))
|
||||
assert.Nil(t, UnmakeNullString(&otherUser.BrowserToken))
|
||||
|
||||
result = ts.App.DB.First(&anotherUser, "uuid = ?", anotherUser.UUID)
|
||||
assert.Nil(t, result.Error)
|
||||
assert.True(t, anotherUser.IsAdmin)
|
||||
assert.True(t, anotherUser.IsLocked)
|
||||
assert.Equal(t, -1, anotherUser.MaxPlayerCount)
|
||||
// `anotherUser` should be logged out of the web interface
|
||||
assert.NotEqual(t, "", anotherBrowserTokenCookie.Value)
|
||||
assert.Nil(t, UnmakeNullString(&anotherUser.BrowserToken))
|
||||
|
||||
// Delete `otherUser`
|
||||
form = url.Values{}
|
||||
form.Set("returnUrl", returnURL)
|
||||
form.Set("username", otherUsername)
|
||||
form.Set("uuid", otherUser.UUID)
|
||||
rec = ts.PostForm(t, ts.Server, "/web/delete-user", form, []http.Cookie{*browserTokenCookie}, nil)
|
||||
|
||||
assert.Equal(t, http.StatusSeeOther, rec.Code)
|
||||
assert.Equal(t, "", getErrorMessage(rec))
|
||||
assert.Equal(t, returnURL, rec.Header().Get("Location"))
|
||||
|
||||
err := ts.App.DB.First(&otherUser, "uuid = ?", otherUser.UUID).Error
|
||||
assert.NotNil(t, err)
|
||||
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||
}
|
||||
|
7
model.go
7
model.go
@ -100,6 +100,13 @@ func (app *App) ValidatePlayerNameOrUUID(player string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *App) ValidateMaxPlayerCount(maxPlayerCount int) error {
|
||||
if maxPlayerCount < 0 && maxPlayerCount != app.Constants.MaxPlayerCountUnlimited && maxPlayerCount != app.Constants.MaxPlayerCountUseDefault {
|
||||
return errors.New("must be greater than 0, OR use -1 to indicate unlimited players, OR use -2 to use the system default")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// func MakeTransientUser(app *App, playerName string) (User, error) {
|
||||
// preimage := bytes.Join([][]byte{
|
||||
// []byte("uuid"),
|
||||
|
@ -32,16 +32,17 @@ hr {
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td:not(:first-child) {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
thead {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-lighter);
|
||||
}
|
||||
@ -96,19 +97,27 @@ summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input {
|
||||
color: white;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: gray;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="password"] {
|
||||
margin: 0.5em 0;
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
border: none;
|
||||
border: var(--input-border-width) solid var(--accent);
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
accent-color: var(--accent);
|
||||
input[type="number"] {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:not(:disabled),
|
||||
|
40
user.go
40
user.go
@ -273,6 +273,7 @@ func (app *App) CreateUser(
|
||||
}
|
||||
|
||||
func (app *App) UpdateUser(
|
||||
db *gorm.DB,
|
||||
caller *User,
|
||||
user User,
|
||||
password *string,
|
||||
@ -280,6 +281,7 @@ func (app *App) UpdateUser(
|
||||
isLocked *bool,
|
||||
resetAPIToken bool,
|
||||
preferredLanguage *string,
|
||||
maxPlayerCount *int,
|
||||
) (User, error) {
|
||||
if caller == nil {
|
||||
return User{}, NewBadRequestUserError("Caller cannot be null.")
|
||||
@ -316,13 +318,6 @@ func (app *App) UpdateUser(
|
||||
user.IsAdmin = *isAdmin
|
||||
}
|
||||
|
||||
if isLocked != nil {
|
||||
if !callerIsAdmin {
|
||||
return User{}, NewBadRequestUserError("Cannot change locked status of user without having admin privileges yourself.")
|
||||
}
|
||||
user.IsLocked = *isLocked
|
||||
}
|
||||
|
||||
if preferredLanguage != nil {
|
||||
if !IsValidPreferredLanguage(*preferredLanguage) {
|
||||
return User{}, NewBadRequestUserError("Invalid preferred language.")
|
||||
@ -338,7 +333,28 @@ func (app *App) UpdateUser(
|
||||
user.APIToken = apiToken
|
||||
}
|
||||
|
||||
if err := app.DB.Save(&user).Error; err != nil {
|
||||
if maxPlayerCount != nil {
|
||||
err := app.ValidateMaxPlayerCount(*maxPlayerCount)
|
||||
if err != nil {
|
||||
return User{}, NewBadRequestUserError("Invalid max player count: %s", err)
|
||||
}
|
||||
user.MaxPlayerCount = *maxPlayerCount
|
||||
}
|
||||
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if isLocked != nil {
|
||||
if !callerIsAdmin {
|
||||
return NewBadRequestUserError("Cannot change locked status of user without having admin privileges yourself.")
|
||||
}
|
||||
app.SetIsLocked(tx, &user, *isLocked)
|
||||
}
|
||||
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
@ -349,11 +365,9 @@ func (app *App) SetIsLocked(db *gorm.DB, user *User, isLocked bool) error {
|
||||
user.IsLocked = isLocked
|
||||
if isLocked {
|
||||
user.BrowserToken = MakeNullString(nil)
|
||||
for _, player := range user.Players {
|
||||
err := app.InvalidatePlayer(db, &player)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := app.InvalidateUser(db, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -61,19 +61,18 @@
|
||||
No invites to show.
|
||||
{{ end }}
|
||||
|
||||
|
||||
<h4>All Users</h4>
|
||||
|
||||
<div style="display: none">
|
||||
{{ range $user := .Users }}
|
||||
<form
|
||||
id="delete-{{ $user.Username }}"
|
||||
id="delete-{{ $user.UUID }}"
|
||||
action="{{ $.App.FrontEndURL }}/web/delete-user"
|
||||
method="post"
|
||||
onsubmit="return confirm('Are you sure you want to delete the account “{{ $user.Username }}”? This action is irreversible.');"
|
||||
>
|
||||
<input hidden name="returnUrl" value="{{ $.URL }}" />
|
||||
<input type="text" name="username" value="{{ $user.Username }}" />
|
||||
<input hidden type="text" name="uuid" value="{{ $user.UUID }}" />
|
||||
</form>
|
||||
{{ end }}
|
||||
</div>
|
||||
@ -84,6 +83,7 @@
|
||||
<tr>
|
||||
<td colspan="2">User</td>
|
||||
<td>Players</td>
|
||||
<td>Max # players*</td>
|
||||
<td>Admin</td>
|
||||
<td>Locked</td>
|
||||
<td>Delete Account</td>
|
||||
@ -113,10 +113,18 @@
|
||||
{{ len $user.Players }} players
|
||||
{{ end }}
|
||||
</td>
|
||||
{{/*<td>{{ $user.PlayerName }}</td>*/}}
|
||||
<td>
|
||||
<input
|
||||
name="admin-{{ $user.Username }}"
|
||||
name="max-player-count-{{ $user.UUID }}"
|
||||
type="number"
|
||||
{{ if $user.IsAdmin }}disabled{{ end }}
|
||||
value="{{ if or $user.IsAdmin (eq $user.MaxPlayerCount $.App.Constants.MaxPlayerCountUnlimited) }}-1{{ else if eq $user.MaxPlayerCount $.App.Constants.MaxPlayerCountUseDefault}}{{ else }}{{ $user.MaxPlayerCount }}{{ end }}"
|
||||
placeholder="{{ $.App.Config.DefaultMaxPlayerCount }}"
|
||||
min="-1">
|
||||
</input>
|
||||
<td>
|
||||
<input
|
||||
name="admin-{{ $user.UUID }}"
|
||||
title="Admin?"
|
||||
type="checkbox"
|
||||
{{ if
|
||||
@ -134,7 +142,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
name="locked-{{ $user.Username }}"
|
||||
name="locked-{{ $user.UUID }}"
|
||||
title="Locked?"
|
||||
type="checkbox"
|
||||
{{ if
|
||||
@ -147,7 +155,7 @@
|
||||
<td>
|
||||
<input
|
||||
type="submit"
|
||||
form="delete-{{ $user.Username }}"
|
||||
form="delete-{{ $user.UUID }}"
|
||||
value="× Delete"
|
||||
/>
|
||||
</td>
|
||||
@ -155,6 +163,7 @@
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
<p><small>*Specify -1 to allow an unlimited number of players. Leave blank to use the default max number, which is {{ $.App.Config.DefaultMaxPlayerCount }}.</small></p>
|
||||
<p style="text-align: center">
|
||||
<input hidden name="returnUrl" value="{{ $.URL }}" />
|
||||
<input type="submit" value="Save changes" />
|
||||
|
@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="minimum-scale=1, width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A self-hosted API server for Minecraft"
|
||||
|
@ -71,10 +71,10 @@
|
||||
{{ if .AdminView }}{{ .TargetUser.Username }} is{{ else }}You are{{ end }} not allowed to create players.
|
||||
{{ if .AdminView }}You can override this limit since you're an admin.{{ end }}
|
||||
{{ else if (gt .MaxPlayerCount 0) }}
|
||||
{{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} account can have up to {{ .MaxPlayerCount }} associated player(s).
|
||||
{{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} account can have up to {{ .MaxPlayerCount }} player(s).
|
||||
{{ if .AdminView }}You can override this limit since you're an admin.{{ end }}
|
||||
{{ else }}
|
||||
{{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} account can have an unlimited number of associated players.
|
||||
{{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} account can have an unlimited number of players.
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ if or (lt (len .TargetUser.Players) .MaxPlayerCount) (lt .MaxPlayerCount 0) .AdminView }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user