From c3cb43885f78260c80a1dde211f075a3a2017cd3 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Sat, 26 Jul 2025 12:37:55 -0400 Subject: [PATCH] Add PlayerUUIDGeneration option --- api.go | 4 ++-- assets/swagger.json | 4 ++-- common.go | 9 +++++++++ config.go | 22 +++++++++++++++------- doc/configuration.md | 5 +++-- doc/recipes.md | 8 +++----- locales/es-US/default.po | 3 +++ model.go | 5 +++++ player.go | 6 +++++- user.go | 5 ++++- view/complete-registration.tmpl | 8 ++------ view/registration.tmpl | 2 +- view/user.tmpl | 2 +- 13 files changed, 55 insertions(+), 28 deletions(-) diff --git a/api.go b/api.go index c8e9891..c8dd3a5 100644 --- a/api.go +++ b/api.go @@ -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. diff --git a/assets/swagger.json b/assets/swagger.json index fc74ffd..5cde4d6 100644 --- a/assets/swagger.json +++ b/assets/swagger.json @@ -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" }, diff --git a/common.go b/common.go index 49a8489..12f7382 100644 --- a/common.go +++ b/common.go @@ -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 + } +} diff --git a/config.go b/config.go index 3ea05fd..a1a37f3 100644 --- a/config.go +++ b/config.go @@ -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.") diff --git a/doc/configuration.md b/doc/configuration.md index ad4f8a7..6bbf9a1 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -63,9 +63,9 @@ Other available options: -- `[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"`. diff --git a/doc/recipes.md b/doc/recipes.md index 661c607..1941a8e 100644 --- a/doc/recipes.md +++ b/doc/recipes.md @@ -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 diff --git a/locales/es-US/default.po b/locales/es-US/default.po index 7effda0..d3aa619 100644 --- a/locales/es-US/default.po +++ b/locales/es-US/default.po @@ -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" diff --git a/model.go b/model.go index 266bf9d..ad43932 100644 --- a/model.go +++ b/model.go @@ -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} diff --git a/player.go b/player.go index 25bb50d..10accc8 100644 --- a/player.go +++ b/player.go @@ -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.") diff --git a/user.go b/user.go index 2582a0c..e624a75 100644 --- a/user.go +++ b/user.go @@ -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.") diff --git a/view/complete-registration.tmpl b/view/complete-registration.tmpl index 86560cf..1f21940 100644 --- a/view/complete-registration.tmpl +++ b/view/complete-registration.tmpl @@ -39,11 +39,7 @@ {{ $dividerNeeded = false }} {{ end }}

{{ call .T "Create a player" }}

- {{ if .App.Config.CreateNewPlayer.AllowChoosingUUID }} -

{{ call .T "Complete registration by creating a new player:" }}

- {{ else }} -

{{ call .T "Complete registration by creating a new player with a random UUID:" }}

- {{ end }} +

{{ call .T "Complete registration by creating a new player:" }}

diff --git a/view/registration.tmpl b/view/registration.tmpl index 1fbd555..b865d49 100644 --- a/view/registration.tmpl +++ b/view/registration.tmpl @@ -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}$" />

diff --git a/view/user.tmpl b/view/user.tmpl index 7a20eb0..822820f 100644 --- a/view/user.tmpl +++ b/view/user.tmpl @@ -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 }}