Add PlayerUUIDGeneration option

This commit is contained in:
Evan Goode 2025-07-26 12:37:55 -04:00
parent 79428c5286
commit 98d3b0bb51
13 changed files with 55 additions and 28 deletions

4
api.go
View File

@ -384,7 +384,7 @@ type APICreateUserRequest struct {
IsAdmin bool `json:"isAdmin" example:"true"` // Whether the user is an admin
IsLocked bool `json:"isLocked" example:"false"` // Whether the user is locked (disabled)
RequestAPIToken bool `json:"requestApiToken" example:"true"` // Whether to include an API token for the user in the response
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the player of the new user. If omitted, a random UUID will be generated.
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the player of the new user. If omitted, a UUID will be generated according to the `PlayerUUIDGeneration` configuration option.
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new user's player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
InviteCode *string `json:"inviteCode" example:"rqjJwh0yMjO"` // Invite code to use. Optional even if the `RequireInvite` configuration option is set; admin API users can bypass `RequireInvite`.
PlayerName *string `json:"playerName" example:"MyPlayerName"` // Optional. Player name. Can be different from the user's username. If omitted, the user's username will be used.
@ -693,7 +693,7 @@ func (app *App) APIGetPlayers() func(c echo.Context) error {
type APICreatePlayerRequest struct {
Name string `json:"name" example:"MyPlayerName"` // Player name.
UserUUID *string `json:"userUuid" example:"f9b9af62-da83-4ec7-aeea-de48c621822c"` // Optional. UUID of the owning user. If omitted, the player will be added to the calling user's account.
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the new player. If omitted, a UUID will be generated according to the `PlayerUUIDGeneration` configuration option.
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
ChallengeToken *string `json:"challengeToken" example:"iK1B2FzLc5fMP94VmUR3KC"` // Challenge token to use when verifying ownership of another player. Call /drasl/api/v2/challenge-skin first to get a skin and token. See `RequireSkinVerification` in configuration.md.

View File

@ -1263,7 +1263,7 @@
"example": "iK1B2FzLc5fMP94VmUR3KC"
},
"chosenUuid": {
"description": "Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.",
"description": "Optional. Specify a UUID for the new player. If omitted, a UUID will be generated according to the `PlayerUUIDGeneration` configuration option.",
"type": "string",
"example": "557e0c92-2420-4704-8840-a790ea11551c"
},
@ -1318,7 +1318,7 @@
"example": "https://example.com/cape.png"
},
"chosenUuid": {
"description": "Optional. Specify a UUID for the player of the new user. If omitted, a random UUID will be generated.",
"description": "Optional. Specify a UUID for the player of the new user. If omitted, a UUID will be generated according to the `PlayerUUIDGeneration` configuration option.",
"type": "string",
"example": "557e0c92-2420-4704-8840-a790ea11551c"
},

View File

@ -959,3 +959,12 @@ func NewFallbackAPIServer(config *FallbackAPIServerConfig) (FallbackAPIServer, e
PlayerNameToIDJobCh: make(chan []playerNameToIDJob),
}, nil
}
func (app *App) NewPlayerUUID(playerName string) (string, error) {
switch app.Config.PlayerUUIDGeneration {
case PlayerUUIDGenerationOffline:
return OfflineUUID(playerName)
default:
return uuid.New().String(), nil
}
}

View File

@ -143,6 +143,7 @@ type BaseConfig struct {
ListenAddress string
LogRequests bool
MinPasswordLength int
PlayerUUIDGeneration string
PreMigrationBackups bool
RateLimit rateLimitConfig
RegistrationExistingPlayer registrationExistingPlayerConfig
@ -216,13 +217,14 @@ func DefaultRawConfig() RawConfig {
ImportExistingPlayer: importExistingPlayerConfig{
Allow: false,
},
InstanceName: "Drasl",
ListenAddress: "0.0.0.0:25585",
LogRequests: true,
MinPasswordLength: 8,
OfflineSkins: true,
PreMigrationBackups: true,
RateLimit: defaultRateLimitConfig,
InstanceName: "Drasl",
ListenAddress: "0.0.0.0:25585",
LogRequests: true,
MinPasswordLength: 8,
OfflineSkins: true,
PlayerUUIDGeneration: "random",
PreMigrationBackups: true,
RateLimit: defaultRateLimitConfig,
RegistrationExistingPlayer: registrationExistingPlayerConfig{
Allow: false,
},
@ -379,6 +381,12 @@ func CleanConfig(rawConfig *RawConfig) (Config, error) {
return Config{}, errors.New("If RegisterNewPlayer is allowed, CreateNewPlayer must be allowed.")
}
}
switch config.PlayerUUIDGeneration {
case PlayerUUIDGenerationRandom:
case PlayerUUIDGenerationOffline:
default:
return Config{}, errors.New(`PlayerUUIDGeneration must be either "random" or "offline".`)
}
if config.RegistrationExistingPlayer.Allow {
if !config.ImportExistingPlayer.Allow {
return Config{}, errors.New("If RegistrationExistingPlayer is allowed, ImportExistingPlayer must be allowed.")

View File

@ -63,9 +63,9 @@ Other available options:
<!-- - `UsernameRegex`: If a username matches this regular expression, it will be allowed to log in with the shared password. Use `".*"` to allow transient login for any username. String. Example value: `"[Bot] .*"`. -->
<!-- - `Password`: The shared password for transient login. Not restricted by `MinPasswordLength`. String. Example value: `"hunter2"`. -->
- `[CreateNewPlayer]`: Policy for creating new players, i.e. players with a random or user-specified UUID.
- `[CreateNewPlayer]`: Policy for creating new players.
- `Allow`: Allow users to create players with new UUIDs, up to their individual `MaxPlayerCount` limit. Boolean. Default value: `true`.
- `AllowChoosingUUID`: Allow users to choose a UUID for the new player. If disabled, the new player's UUID will always be randomly chosen. Boolean. Default value: `false`.
- `AllowChoosingUUID`: Allow users to choose a UUID for the new player. If disabled, the new player's UUID will always be generated according to the strategy specified by the `PlayerUUIDGeneration` option. Boolean. Default value: `false`.
- `[RegistrationNewPlayer]`
- `Allow`: Allow users to register a new Drasl account by creating a player with a new UUID. Requires `CreateNewPlayer.Allow = true`. Boolean. Default value: `true`.
- `RequireInvite`: Whether registration requires an invite. If enabled, users will only be able to create a new account if they use an invite link generated by an admin (see `DefaultAdmins`). Boolean. Default value: `false`.
@ -119,3 +119,4 @@ Other available options:
- `AllowTextureFromURL`: Allow users to specify a skin or cape by providing a URL to the texture file. Previously, this option was always allowed; now it is opt-in. Admins can do this regardless of this setting. Boolean. Default value: `false`.
- `ValidPlayerNameRegex`: Regular expression (regex) that player names must match. Currently, Drasl usernames are validated using this regex too. Player names will be limited to a maximum of 16 characters no matter what. Mojang allows the characters `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_`, and by default, Drasl follows suit. Minecraft servers may misbehave if additional characters are allowed. Change to `.+` if you want to allow any player name (that is 16 characters or shorter). String. Default value: `^[a-zA-Z0-9_]+$`.
- `CORSAllowOrigins`: List of origins that may access Drasl API routes. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. Necessary for allowing browsers to access the Drasl API. Set to `["*"]` to allow all origins. Array of strings. Example value: `["https://front-end.example.com"]`. Default value: `[]`.
- `PlayerUUIDGeneration`: How to generate UUIDs for new players. Must be either `"random"` or `"offline"`. `"random"` generates a new random Version 4 UUID. `"offline"` means the player's UUID will be generated from the player's name using the same algorithm Minecraft uses to derive player UUIDs on `online-mode=false` servers. `PlayerUUIDGeneration = "offline"` is useful for migrating `online-mode=false` servers to Drasl since it lets player UUIDs (and thus inventories, permissions, etc.) remain the same when switching from `online-mode=false` to `online-mode=true`. Note: if a player's name is changed, their UUID will not change, even with `PlayerUUIDGeneration = "offline"`. String. Default value: `"random"`.

View File

@ -8,16 +8,14 @@ See [configuration.md](./configuration.md) for detailed documentation of each co
### Example 1: Basic, minimal setup
- Private and standalone: does not interact with any other API servers
- Registering a new account requires an invite from an admin
- Users can choose their player's UUID when they register, useful for migrating from Mojang accounts
- Registering a new account requires an invite from an admin (`RegistrationNewPlayer.RequireInvite`)
- Seamless migration from `online-mode=false` servers: UUIDs of new players will be derived from their player name using the same algorithm used by Minecraft to derive player UUIDs in `online-mode=false` servers (`PlayerUUIDGeneration`)
```
Domain = "drasl.example.com" # CHANGE ME!
BaseURL = "https://drasl.example.com" # CHANGE ME!
DefaultAdmins = ["myusername"] # CHANGE ME!
[CreateNewPlayer]
AllowChoosingUUID = true
PlayerUUIDGeneration = "offline"
[RegistrationNewPlayer]
Allow = true

View File

@ -432,6 +432,9 @@ msgstr "Crear un nuevo jugador con un UUID aleatorio:"
msgid "Player UUID (leave blank for random)"
msgstr "UUID del jugador (déjelo en blanco para uno aleatorio)"
msgid "Player UUID (leave blank for offline UUID)"
msgstr "UUID del jugador (déjelo en blanco para usar el UUID offline)"
msgid "Import a player from %s"
msgstr "Importar un jugador desde %s"

View File

@ -25,6 +25,11 @@ const (
TextureTypeCape string = "cape"
)
const (
PlayerUUIDGenerationRandom string = "random"
PlayerUUIDGenerationOffline string = "offline"
)
func MakeNullString(s *string) sql.NullString {
if s == nil {
return sql.NullString{Valid: false}

View File

@ -157,7 +157,11 @@ func (app *App) CreatePlayer(
}
if chosenUUID == nil {
playerUUID = uuid.New().String()
var err error
playerUUID, err = app.NewPlayerUUID(playerName)
if err != nil {
return Player{}, err
}
} else {
if !app.Config.CreateNewPlayer.AllowChoosingUUID && !callerIsAdmin {
return Player{}, NewBadRequestUserError("Choosing a UUID is not allowed.")

View File

@ -189,7 +189,10 @@ func (app *App) CreateUser(
}
if chosenUUID == nil {
playerUUID = uuid.New().String()
playerUUID, err = app.NewPlayerUUID(*playerName)
if err != nil {
return User{}, err
}
} else {
if !app.Config.CreateNewPlayer.AllowChoosingUUID && !callerIsAdmin {
return User{}, NewBadRequestUserError("Choosing a UUID is not allowed.")

View File

@ -39,11 +39,7 @@
{{ $dividerNeeded = false }}
{{ end }}
<h3>{{ call .T "Create a player" }}</h3>
{{ if .App.Config.CreateNewPlayer.AllowChoosingUUID }}
<p>{{ call .T "Complete registration by creating a new player:" }}</p>
{{ else }}
<p>{{ call .T "Complete registration by creating a new player with a random UUID:" }}</p>
{{ end }}
<p>{{ call .T "Complete registration by creating a new player:" }}</p>
<form action="{{ .App.FrontEndURL }}/web/register" method="post">
<input
required
@ -69,7 +65,7 @@
class="long"
type="text"
name="uuid"
placeholder="{{ call .T "Player UUID (leave blank for random)" }}"
placeholder="{{ if eq .App.Config.PlayerUUIDGeneration "offline" }}{{ call .T "Player UUID (leave blank for offline UUID)" }}{{ else }}{{ call .T "Player UUID (leave blank for random)" }}{{ end }}"
pattern="^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$"
/>
</p>

View File

@ -64,7 +64,7 @@
class="long"
type="text"
name="uuid"
placeholder="{{ call .T "Player UUID (leave blank for random)" }}"
placeholder="{{ if eq .App.Config.PlayerUUIDGeneration "offline" }}{{ call .T "Player UUID (leave blank for offline UUID)" }}{{ else }}{{ call .T "Player UUID (leave blank for random)" }}{{ end }}"
pattern="^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$"
/>
</p>

View File

@ -142,7 +142,7 @@
class="long"
type="text"
name="playerUuid"
placeholder="{{ call .T "Player UUID (leave blank for random)" }}"
placeholder="{{ if eq .App.Config.PlayerUUIDGeneration "offline" }}{{ call .T "Player UUID (leave blank for offline UUID)" }}{{ else }}{{ call .T "Player UUID (leave blank for random)" }}{{ end }}"
pattern="^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$"
/>
{{ end }}