mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-03 19:06:04 -04:00
Make multiple profiles usable from web front end
This commit is contained in:
parent
f58ce99eae
commit
738d80538f
@ -42,6 +42,8 @@ func NewBadRequestUserError(message string, args ...interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConstantsType struct {
|
type ConstantsType struct {
|
||||||
|
MaxPlayerCountUseDefault int
|
||||||
|
MaxPlayerCountUnlimited int
|
||||||
ConfigDirectory string
|
ConfigDirectory string
|
||||||
MaxPlayerNameLength int
|
MaxPlayerNameLength int
|
||||||
MaxUsernameLength int
|
MaxUsernameLength int
|
||||||
@ -52,6 +54,8 @@ type ConstantsType struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var Constants = &ConstantsType{
|
var Constants = &ConstantsType{
|
||||||
|
MaxPlayerCountUseDefault: -2,
|
||||||
|
MaxPlayerCountUnlimited: -1,
|
||||||
MaxUsernameLength: 16,
|
MaxUsernameLength: 16,
|
||||||
MaxPlayerNameLength: 16,
|
MaxPlayerNameLength: 16,
|
||||||
ConfigDirectory: DEFAULT_CONFIG_DIRECTORY,
|
ConfigDirectory: DEFAULT_CONFIG_DIRECTORY,
|
||||||
@ -462,7 +466,7 @@ func (app *App) DeleteSkinIfUnused(hash *string) error {
|
|||||||
|
|
||||||
var inUse bool
|
var inUse bool
|
||||||
|
|
||||||
err := app.DB.Model(User{}).
|
err := app.DB.Model(Player{}).
|
||||||
Select("count(*) > 0").
|
Select("count(*) > 0").
|
||||||
Where("skin_hash = ?", *hash).
|
Where("skin_hash = ?", *hash).
|
||||||
Find(&inUse).
|
Find(&inUse).
|
||||||
@ -493,7 +497,7 @@ func (app *App) DeleteCapeIfUnused(hash *string) error {
|
|||||||
|
|
||||||
var inUse bool
|
var inUse bool
|
||||||
|
|
||||||
err := app.DB.Model(User{}).
|
err := app.DB.Model(Player{}).
|
||||||
Select("count(*) > 0").
|
Select("count(*) > 0").
|
||||||
Where("cape_hash = ?", *hash).
|
Where("cape_hash = ?", *hash).
|
||||||
Find(&inUse).
|
Find(&inUse).
|
||||||
|
@ -71,6 +71,7 @@ type Config struct {
|
|||||||
DataDirectory string
|
DataDirectory string
|
||||||
DefaultAdmins []string
|
DefaultAdmins []string
|
||||||
DefaultPreferredLanguage string
|
DefaultPreferredLanguage string
|
||||||
|
DefaultMaxPlayerCount int
|
||||||
Domain string
|
Domain string
|
||||||
EnableBackgroundEffect bool
|
EnableBackgroundEffect bool
|
||||||
EnableFooter bool
|
EnableFooter bool
|
||||||
@ -117,6 +118,7 @@ func DefaultConfig() Config {
|
|||||||
DataDirectory: DEFAULT_DATA_DIRECTORY,
|
DataDirectory: DEFAULT_DATA_DIRECTORY,
|
||||||
DefaultAdmins: []string{},
|
DefaultAdmins: []string{},
|
||||||
DefaultPreferredLanguage: "en",
|
DefaultPreferredLanguage: "en",
|
||||||
|
DefaultMaxPlayerCount: 1,
|
||||||
Domain: "",
|
Domain: "",
|
||||||
EnableBackgroundEffect: true,
|
EnableBackgroundEffect: true,
|
||||||
EnableFooter: true,
|
EnableFooter: true,
|
||||||
@ -178,6 +180,9 @@ func CleanConfig(config *Config) error {
|
|||||||
if _, err := os.Open(config.DataDirectory); err != nil {
|
if _, err := os.Open(config.DataDirectory); err != nil {
|
||||||
return fmt.Errorf("Couldn't open DataDirectory: %s", err)
|
return fmt.Errorf("Couldn't open DataDirectory: %s", err)
|
||||||
}
|
}
|
||||||
|
if config.DefaultMaxPlayerCount < 0 {
|
||||||
|
return errors.New("DefaultMaxPlayerCount must be >= 0")
|
||||||
|
}
|
||||||
if config.RegistrationExistingPlayer.Allow {
|
if config.RegistrationExistingPlayer.Allow {
|
||||||
if config.RegistrationExistingPlayer.Nickname == "" {
|
if config.RegistrationExistingPlayer.Nickname == "" {
|
||||||
return errors.New("RegistrationExistingPlayer.Nickname must be set")
|
return errors.New("RegistrationExistingPlayer.Nickname must be set")
|
||||||
|
15
db.go
15
db.go
@ -124,10 +124,12 @@ func migrate(db *gorm.DB, alreadyExisted bool) error {
|
|||||||
userVersion = CURRENT_USER_VERSION
|
userVersion = CURRENT_USER_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
err := db.Transaction(func(tx *gorm.DB) error {
|
initialUserVersion := userVersion
|
||||||
if userVersion < CURRENT_USER_VERSION {
|
if initialUserVersion < CURRENT_USER_VERSION {
|
||||||
log.Printf("Started migration of database version %d to version %d", userVersion, CURRENT_USER_VERSION)
|
log.Printf("Started migration of database version %d to %d", userVersion, CURRENT_USER_VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
if userVersion == 0 {
|
if userVersion == 0 {
|
||||||
// Version 0 to 1
|
// Version 0 to 1
|
||||||
// Add User.OfflineUUID
|
// Add User.OfflineUUID
|
||||||
@ -242,6 +244,7 @@ func migrate(db *gorm.DB, alreadyExisted bool) error {
|
|||||||
APIToken: v3User.APIToken,
|
APIToken: v3User.APIToken,
|
||||||
PreferredLanguage: v3User.PreferredLanguage,
|
PreferredLanguage: v3User.PreferredLanguage,
|
||||||
Players: []Player{player},
|
Players: []Player{player},
|
||||||
|
MaxPlayerCount: Constants.MaxPlayerCountUseDefault,
|
||||||
}
|
}
|
||||||
user.Players = append(user.Players, player)
|
user.Players = append(user.Players, player)
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
@ -249,6 +252,7 @@ func migrate(db *gorm.DB, alreadyExisted bool) error {
|
|||||||
if err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Save(&users).Error; err != nil {
|
if err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Save(&users).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
userVersion += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
err := tx.AutoMigrate(&User{})
|
err := tx.AutoMigrate(&User{})
|
||||||
@ -277,10 +281,13 @@ func migrate(db *gorm.DB, alreadyExisted bool) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if initialUserVersion < CURRENT_USER_VERSION {
|
||||||
|
log.Printf("Finished migration from version %d to %d", initialUserVersion, userVersion)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ Other available options:
|
|||||||
- `DataDirectory`: directory where Drasl's static assets are installed. String. Default value: `"/usr/share/drasl"`.
|
- `DataDirectory`: directory where Drasl's static assets are installed. String. Default value: `"/usr/share/drasl"`.
|
||||||
- `ListenAddress`: IP address and port to listen on. Depending on how you configure your reverse proxy and whether you run Drasl in a container, you should consider setting the listen address to `"127.0.0.1:25585"` to ensure Drasl is only accessible through the reverse proxy. If your reverse proxy is unable to connect to Drasl, try setting this back to the default value. String. Default value: `"0.0.0.0:25585"`.
|
- `ListenAddress`: IP address and port to listen on. Depending on how you configure your reverse proxy and whether you run Drasl in a container, you should consider setting the listen address to `"127.0.0.1:25585"` to ensure Drasl is only accessible through the reverse proxy. If your reverse proxy is unable to connect to Drasl, try setting this back to the default value. String. Default value: `"0.0.0.0:25585"`.
|
||||||
- `DefaultAdmins`: Usernames of the instance's permanent admins. Admin rights can be granted to other accounts using the web UI, but admins defined via `DefaultAdmins` cannot be demoted unless they are removed from the config file. Array of strings. Default value: `[]`.
|
- `DefaultAdmins`: Usernames of the instance's permanent admins. Admin rights can be granted to other accounts using the web UI, but admins defined via `DefaultAdmins` cannot be demoted unless they are removed from the config file. Array of strings. Default value: `[]`.
|
||||||
|
- `DefaultMaxPlayerCount`: Number of players each user is allowed to create by default. Admins can increase or decrease each user's individual limit. Use `-1` to allow creating an unlimited number of players. Integer. Default value: `1`.
|
||||||
- `EnableBackgroundEffect`: Whether to enable the 3D background animation in the web UI. Boolean. Default value: `true`.
|
- `EnableBackgroundEffect`: Whether to enable the 3D background animation in the web UI. Boolean. Default value: `true`.
|
||||||
- `EnableFooter`: Whether to enable the page footer in the web UI. Boolean. Default value: `true`.
|
- `EnableFooter`: Whether to enable the page footer in the web UI. Boolean. Default value: `true`.
|
||||||
- `[RateLimit]`: Rate-limit requests per IP address to limit abuse. Only applies to certain web UI routes, not any Yggdrasil routes. Requests for skins, capes, and web pages are also unaffected. Uses [Echo](https://echo.labstack.com)'s [rate limiter middleware](https://echo.labstack.com/middleware/rate-limiter/).
|
- `[RateLimit]`: Rate-limit requests per IP address to limit abuse. Only applies to certain web UI routes, not any Yggdrasil routes. Requests for skins, capes, and web pages are also unaffected. Uses [Echo](https://echo.labstack.com)'s [rate limiter middleware](https://echo.labstack.com/middleware/rate-limiter/).
|
||||||
|
276
front.go
276
front.go
@ -36,14 +36,15 @@ func NewTemplate(app *App) *Template {
|
|||||||
|
|
||||||
names := []string{
|
names := []string{
|
||||||
"root",
|
"root",
|
||||||
"profile",
|
"user",
|
||||||
|
"player",
|
||||||
"registration",
|
"registration",
|
||||||
"challenge-skin",
|
"challenge",
|
||||||
"admin",
|
"admin",
|
||||||
}
|
}
|
||||||
|
|
||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
"UserSkinURL": app.UserSkinURL,
|
"PlayerSkinURL": app.PlayerSkinURL,
|
||||||
"InviteURL": app.InviteURL,
|
"InviteURL": app.InviteURL,
|
||||||
"IsDefaultAdmin": app.IsDefaultAdmin,
|
"IsDefaultAdmin": app.IsDefaultAdmin,
|
||||||
}
|
}
|
||||||
@ -178,9 +179,10 @@ func getReturnURL(app *App, c *echo.Context) string {
|
|||||||
if (*c).FormValue("returnUrl") != "" {
|
if (*c).FormValue("returnUrl") != "" {
|
||||||
return (*c).FormValue("returnUrl")
|
return (*c).FormValue("returnUrl")
|
||||||
}
|
}
|
||||||
if (*c).QueryParam("returnUrl") != "" {
|
// TODO no idea why this is here
|
||||||
return (*c).QueryParam("username")
|
// if (*c).QueryParam("returnUrl") != "" {
|
||||||
}
|
// return (*c).QueryParam("username")
|
||||||
|
// }
|
||||||
return app.FrontEndURL
|
return app.FrontEndURL
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +190,15 @@ func getReturnURL(app *App, c *echo.Context) string {
|
|||||||
// reference to the user
|
// reference to the user
|
||||||
func withBrowserAuthentication(app *App, requireLogin bool, f func(c echo.Context, user *User) error) func(c echo.Context) error {
|
func withBrowserAuthentication(app *App, requireLogin bool, f func(c echo.Context, user *User) error) func(c echo.Context) error {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
returnURL := getReturnURL(app, &c)
|
destination := c.Request().URL.String()
|
||||||
|
if c.Request().Method != "GET" {
|
||||||
|
destination = getReturnURL(app, &c)
|
||||||
|
}
|
||||||
|
returnURL, err := addDestination(app.FrontEndURL, destination)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
cookie, err := c.Cookie("browserToken")
|
cookie, err := c.Cookie("browserToken")
|
||||||
|
|
||||||
var user User
|
var user User
|
||||||
@ -239,6 +249,7 @@ func FrontRoot(app *App) func(c echo.Context) error {
|
|||||||
App *App
|
App *App
|
||||||
User *User
|
User *User
|
||||||
URL string
|
URL string
|
||||||
|
Destination string
|
||||||
SuccessMessage string
|
SuccessMessage string
|
||||||
WarningMessage string
|
WarningMessage string
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
@ -249,6 +260,7 @@ func FrontRoot(app *App) func(c echo.Context) error {
|
|||||||
App: app,
|
App: app,
|
||||||
User: user,
|
User: user,
|
||||||
URL: c.Request().URL.RequestURI(),
|
URL: c.Request().URL.RequestURI(),
|
||||||
|
Destination: c.QueryParam("destination"),
|
||||||
SuccessMessage: lastSuccessMessage(&c),
|
SuccessMessage: lastSuccessMessage(&c),
|
||||||
WarningMessage: lastWarningMessage(&c),
|
WarningMessage: lastWarningMessage(&c),
|
||||||
ErrorMessage: lastErrorMessage(&c),
|
ErrorMessage: lastErrorMessage(&c),
|
||||||
@ -324,7 +336,7 @@ func FrontAdmin(app *App) func(c echo.Context) error {
|
|||||||
|
|
||||||
return withBrowserAdmin(app, func(c echo.Context, user *User) error {
|
return withBrowserAdmin(app, func(c echo.Context, user *User) error {
|
||||||
var users []User
|
var users []User
|
||||||
result := app.DB.Find(&users)
|
result := app.DB.Preload("Players").Find(&users)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
@ -429,35 +441,42 @@ func FrontNewInvite(app *App) func(c echo.Context) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /drasl/profile
|
// GET /drasl/user
|
||||||
func FrontProfile(app *App) func(c echo.Context) error {
|
// GET /drasl/user/:uuid
|
||||||
type profileContext struct {
|
func FrontUser(app *App) func(c echo.Context) error {
|
||||||
|
type userContext struct {
|
||||||
App *App
|
App *App
|
||||||
User *User
|
User *User
|
||||||
URL string
|
URL string
|
||||||
SuccessMessage string
|
SuccessMessage string
|
||||||
WarningMessage string
|
WarningMessage string
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
ProfileUser *User
|
TargetUser *User
|
||||||
ProfileUserID string
|
TargetUserID string
|
||||||
SkinURL *string
|
SkinURL *string
|
||||||
CapeURL *string
|
CapeURL *string
|
||||||
AdminView bool
|
AdminView bool
|
||||||
|
MaxPlayerCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
return withBrowserAuthentication(app, true, func(c echo.Context, user *User) error {
|
return withBrowserAuthentication(app, true, func(c echo.Context, user *User) error {
|
||||||
var profileUser *User
|
var targetUser *User
|
||||||
profileUsername := c.QueryParam("user")
|
targetUUID := c.Param("uuid")
|
||||||
adminView := false
|
adminView := false
|
||||||
if profileUsername == "" || profileUsername == user.Username {
|
if targetUUID == "" || targetUUID == user.UUID {
|
||||||
profileUser = user
|
var targetUserStruct User
|
||||||
|
result := app.DB.Preload("Players").First(&targetUserStruct, "uuid = ?", user.UUID)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
targetUser = &targetUserStruct
|
||||||
} else {
|
} else {
|
||||||
if !user.IsAdmin {
|
if !user.IsAdmin {
|
||||||
return NewWebError(app.FrontEndURL, "You are not an admin.")
|
return NewWebError(app.FrontEndURL, "You are not an admin.")
|
||||||
}
|
}
|
||||||
var profileUserStruct User
|
adminView = true
|
||||||
result := app.DB.First(&profileUserStruct, "username = ?", profileUsername)
|
var targetUserStruct User
|
||||||
profileUser = &profileUserStruct
|
result := app.DB.Preload("Players").First(&targetUserStruct, "uuid = ?", targetUUID)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
returnURL, err := url.JoinPath(app.FrontEndURL, "web/admin")
|
returnURL, err := url.JoinPath(app.FrontEndURL, "web/admin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -465,16 +484,68 @@ func FrontProfile(app *App) func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
return NewWebError(returnURL, "User not found.")
|
return NewWebError(returnURL, "User not found.")
|
||||||
}
|
}
|
||||||
adminView = true
|
targetUser = &targetUserStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO support multiple players
|
maxPlayerCount := app.GetMaxPlayerCount(targetUser)
|
||||||
player := &profileUser.Players[0]
|
|
||||||
skinURL, err := app.GetSkinURL(player)
|
return c.Render(http.StatusOK, "user", userContext{
|
||||||
|
App: app,
|
||||||
|
User: user,
|
||||||
|
URL: c.Request().URL.RequestURI(),
|
||||||
|
SuccessMessage: lastSuccessMessage(&c),
|
||||||
|
WarningMessage: lastWarningMessage(&c),
|
||||||
|
ErrorMessage: lastErrorMessage(&c),
|
||||||
|
TargetUser: targetUser,
|
||||||
|
// SkinURL: skinURL,
|
||||||
|
// CapeURL: capeURL,
|
||||||
|
AdminView: adminView,
|
||||||
|
MaxPlayerCount: maxPlayerCount,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /drasl/player/:uuid
|
||||||
|
func FrontPlayer(app *App) func(c echo.Context) error {
|
||||||
|
type playerContext struct {
|
||||||
|
App *App
|
||||||
|
User *User
|
||||||
|
URL string
|
||||||
|
SuccessMessage string
|
||||||
|
WarningMessage string
|
||||||
|
ErrorMessage string
|
||||||
|
Player *Player
|
||||||
|
PlayerID string
|
||||||
|
SkinURL *string
|
||||||
|
CapeURL *string
|
||||||
|
AdminView bool
|
||||||
|
}
|
||||||
|
|
||||||
|
return withBrowserAuthentication(app, true, func(c echo.Context, user *User) error {
|
||||||
|
playerUUID := c.Param("uuid")
|
||||||
|
|
||||||
|
var player Player
|
||||||
|
result := app.DB.Preload("User").First(&player, "uuid = ?", playerUUID)
|
||||||
|
if result.Error != nil {
|
||||||
|
returnURL, err := url.JoinPath(app.FrontEndURL, "web/admin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
capeURL, err := app.GetCapeURL(player)
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return NewWebError(returnURL, "Player not found.")
|
||||||
|
}
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if !user.IsAdmin && (player.User.UUID != user.UUID) {
|
||||||
|
return NewWebError(app.FrontEndURL, "You are not an admin.")
|
||||||
|
}
|
||||||
|
adminView := player.User.UUID != user.UUID
|
||||||
|
|
||||||
|
skinURL, err := app.GetSkinURL(&player)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
capeURL, err := app.GetCapeURL(&player)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -484,15 +555,15 @@ func FrontProfile(app *App) func(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "profile", profileContext{
|
return c.Render(http.StatusOK, "player", playerContext{
|
||||||
App: app,
|
App: app,
|
||||||
User: user,
|
User: user,
|
||||||
URL: c.Request().URL.RequestURI(),
|
URL: c.Request().URL.RequestURI(),
|
||||||
SuccessMessage: lastSuccessMessage(&c),
|
SuccessMessage: lastSuccessMessage(&c),
|
||||||
WarningMessage: lastWarningMessage(&c),
|
WarningMessage: lastWarningMessage(&c),
|
||||||
ErrorMessage: lastErrorMessage(&c),
|
ErrorMessage: lastErrorMessage(&c),
|
||||||
ProfileUser: profileUser,
|
Player: &player,
|
||||||
ProfileUserID: id,
|
PlayerID: id,
|
||||||
SkinURL: skinURL,
|
SkinURL: skinURL,
|
||||||
CapeURL: capeURL,
|
CapeURL: capeURL,
|
||||||
AdminView: adminView,
|
AdminView: adminView,
|
||||||
@ -559,7 +630,7 @@ func FrontUpdatePlayer(app *App) func(c echo.Context) error {
|
|||||||
return withBrowserAuthentication(app, true, func(c echo.Context, user *User) error {
|
return withBrowserAuthentication(app, true, func(c echo.Context, user *User) error {
|
||||||
returnURL := getReturnURL(app, &c)
|
returnURL := getReturnURL(app, &c)
|
||||||
|
|
||||||
targetUUID := nilIfEmpty(c.FormValue("uuid"))
|
playerUUID := c.FormValue("uuid")
|
||||||
playerName := nilIfEmpty(c.FormValue("playerName"))
|
playerName := nilIfEmpty(c.FormValue("playerName"))
|
||||||
fallbackPlayer := nilIfEmpty(c.FormValue("fallbackPlayer"))
|
fallbackPlayer := nilIfEmpty(c.FormValue("fallbackPlayer"))
|
||||||
skinModel := nilIfEmpty(c.FormValue("skinModel"))
|
skinModel := nilIfEmpty(c.FormValue("skinModel"))
|
||||||
@ -569,7 +640,7 @@ func FrontUpdatePlayer(app *App) func(c echo.Context) error {
|
|||||||
deleteCape := c.FormValue("deleteCape") == "on"
|
deleteCape := c.FormValue("deleteCape") == "on"
|
||||||
|
|
||||||
var player Player
|
var player Player
|
||||||
result := app.DB.First(&player, "uuid = ?", targetUUID)
|
result := app.DB.Preload("User").First(&player, "uuid = ?", playerUUID)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return NewWebError(returnURL, "Player not found.")
|
return NewWebError(returnURL, "Player not found.")
|
||||||
}
|
}
|
||||||
@ -646,30 +717,59 @@ func FrontLogout(app *App) func(c echo.Context) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /challenge-skin
|
const (
|
||||||
func FrontChallengeSkin(app *App) func(c echo.Context) error {
|
ChallengeActionRegister string = "register"
|
||||||
type challengeSkinContext struct {
|
ChallengeActionCreatePlayer string = "create-player"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /create-player-challenge
|
||||||
|
func FrontCreatePlayerChallenge(app *App) func(c echo.Context) error {
|
||||||
|
return frontChallenge(app, ChallengeActionCreatePlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /register-challenge
|
||||||
|
func FrontRegisterChallenge(app *App) func(c echo.Context) error {
|
||||||
|
return frontChallenge(app, ChallengeActionRegister)
|
||||||
|
}
|
||||||
|
|
||||||
|
func frontChallenge(app *App, action string) func(c echo.Context) error {
|
||||||
|
type challengeContext struct {
|
||||||
App *App
|
App *App
|
||||||
User *User
|
User *User
|
||||||
URL string
|
URL string
|
||||||
SuccessMessage string
|
SuccessMessage string
|
||||||
WarningMessage string
|
WarningMessage string
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
Username string
|
PlayerName string
|
||||||
RegistrationProvider string
|
RegistrationProvider string
|
||||||
SkinBase64 string
|
SkinBase64 string
|
||||||
SkinFilename string
|
SkinFilename string
|
||||||
ChallengeToken string
|
ChallengeToken string
|
||||||
InviteCode string
|
InviteCode string
|
||||||
|
Action string
|
||||||
|
UserUUID *string
|
||||||
}
|
}
|
||||||
|
|
||||||
return withBrowserAuthentication(app, false, func(c echo.Context, user *User) error {
|
return withBrowserAuthentication(app, false, func(c echo.Context, user *User) error {
|
||||||
returnURL := getReturnURL(app, &c)
|
returnURL := getReturnURL(app, &c)
|
||||||
|
|
||||||
|
var playerName string
|
||||||
|
var userUUID *string
|
||||||
|
if action == ChallengeActionRegister {
|
||||||
username := c.QueryParam("username")
|
username := c.QueryParam("username")
|
||||||
if err := app.ValidateUsername(username); err != nil {
|
if err := app.ValidateUsername(username); err != nil {
|
||||||
return NewWebError(returnURL, "Invalid username: %s", err)
|
return NewWebError(returnURL, "Invalid username: %s", err)
|
||||||
}
|
}
|
||||||
|
playerName = username
|
||||||
|
} else if action == ChallengeActionCreatePlayer {
|
||||||
|
playerName = c.QueryParam("playerName")
|
||||||
|
userUUIDString := c.QueryParam("userUuid")
|
||||||
|
userUUID = &userUUIDString
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.ValidatePlayerName(playerName); err != nil {
|
||||||
|
return NewWebError(returnURL, "Invalid player name: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
inviteCode := c.QueryParam("inviteCode")
|
inviteCode := c.QueryParam("inviteCode")
|
||||||
|
|
||||||
@ -692,7 +792,7 @@ func FrontChallengeSkin(app *App) func(c echo.Context) error {
|
|||||||
challengeToken = cookie.Value
|
challengeToken = cookie.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
challengeSkinBytes, err := app.GetChallengeSkin(username, challengeToken)
|
challengeSkinBytes, err := app.GetChallengeSkin(playerName, challengeToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var userError *UserError
|
var userError *UserError
|
||||||
if errors.As(err, &userError) {
|
if errors.As(err, &userError) {
|
||||||
@ -702,25 +802,69 @@ func FrontChallengeSkin(app *App) func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
skinBase64 := base64.StdEncoding.EncodeToString(challengeSkinBytes)
|
skinBase64 := base64.StdEncoding.EncodeToString(challengeSkinBytes)
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "challenge-skin", challengeSkinContext{
|
return c.Render(http.StatusOK, "challenge", challengeContext{
|
||||||
App: app,
|
App: app,
|
||||||
User: user,
|
User: user,
|
||||||
URL: c.Request().URL.RequestURI(),
|
URL: c.Request().URL.RequestURI(),
|
||||||
SuccessMessage: lastSuccessMessage(&c),
|
SuccessMessage: lastSuccessMessage(&c),
|
||||||
WarningMessage: lastWarningMessage(&c),
|
WarningMessage: lastWarningMessage(&c),
|
||||||
ErrorMessage: lastErrorMessage(&c),
|
ErrorMessage: lastErrorMessage(&c),
|
||||||
Username: username,
|
PlayerName: playerName,
|
||||||
SkinBase64: skinBase64,
|
SkinBase64: skinBase64,
|
||||||
SkinFilename: username + "-challenge.png",
|
SkinFilename: playerName + "-challenge.png",
|
||||||
ChallengeToken: challengeToken,
|
ChallengeToken: challengeToken,
|
||||||
InviteCode: inviteCode,
|
InviteCode: inviteCode,
|
||||||
|
Action: action,
|
||||||
|
UserUUID: userUUID,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /create-player
|
||||||
|
func FrontCreatePlayer(app *App) func(c echo.Context) error {
|
||||||
|
return withBrowserAuthentication(app, true, func(c echo.Context, caller *User) error {
|
||||||
|
userUUID := c.FormValue("userUuid")
|
||||||
|
|
||||||
|
playerName := c.FormValue("playerName")
|
||||||
|
chosenUUID := nilIfEmpty(c.FormValue("playerUuid"))
|
||||||
|
existingPlayer := c.FormValue("existingPlayer") == "on"
|
||||||
|
challengeToken := nilIfEmpty(c.FormValue("challengeToken"))
|
||||||
|
|
||||||
|
failureURL := getReturnURL(app, &c)
|
||||||
|
|
||||||
|
player, err := app.CreatePlayer(
|
||||||
|
caller,
|
||||||
|
userUUID,
|
||||||
|
playerName,
|
||||||
|
chosenUUID,
|
||||||
|
existingPlayer,
|
||||||
|
challengeToken,
|
||||||
|
nil, // fallbackPlayer
|
||||||
|
nil, // skinModel
|
||||||
|
nil, // skinReader
|
||||||
|
nil, // skinURL
|
||||||
|
nil, // capeReader
|
||||||
|
nil, // capeURL
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
var userError *UserError
|
||||||
|
if errors.As(err, &userError) {
|
||||||
|
return &WebError{ReturnURL: failureURL, Err: userError.Err}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
returnURL, err := url.JoinPath(app.FrontEndURL, "web/player", player.UUID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Redirect(http.StatusSeeOther, returnURL)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// POST /register
|
// POST /register
|
||||||
func FrontRegister(app *App) func(c echo.Context) error {
|
func FrontRegister(app *App) func(c echo.Context) error {
|
||||||
returnURL := Unwrap(url.JoinPath(app.FrontEndURL, "web/profile"))
|
returnURL := Unwrap(url.JoinPath(app.FrontEndURL, "web/user"))
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
username := c.FormValue("username")
|
username := c.FormValue("username")
|
||||||
honeypot := c.FormValue("email")
|
honeypot := c.FormValue("email")
|
||||||
@ -795,9 +939,23 @@ func FrontRegister(app *App) func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addDestination(url_ string, destination string) (string, error) {
|
||||||
|
if destination == "" {
|
||||||
|
return url_, nil
|
||||||
|
} else {
|
||||||
|
urlStruct, err := url.Parse(url_)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
query := urlStruct.Query()
|
||||||
|
query.Set("destination", destination)
|
||||||
|
urlStruct.RawQuery = query.Encode()
|
||||||
|
return urlStruct.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// POST /login
|
// POST /login
|
||||||
func FrontLogin(app *App) func(c echo.Context) error {
|
func FrontLogin(app *App) func(c echo.Context) error {
|
||||||
returnURL := app.FrontEndURL + "/web/profile"
|
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
failureURL := getReturnURL(app, &c)
|
failureURL := getReturnURL(app, &c)
|
||||||
|
|
||||||
@ -847,6 +1005,14 @@ func FrontLogin(app *App) func(c echo.Context) error {
|
|||||||
user.BrowserToken = MakeNullString(&browserToken)
|
user.BrowserToken = MakeNullString(&browserToken)
|
||||||
app.DB.Save(&user)
|
app.DB.Save(&user)
|
||||||
|
|
||||||
|
returnURL, err := url.JoinPath(app.FrontEndURL, "web/user")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
destination := c.FormValue("destination")
|
||||||
|
if destination != "" {
|
||||||
|
returnURL = destination
|
||||||
|
}
|
||||||
return c.Redirect(http.StatusSeeOther, returnURL)
|
return c.Redirect(http.StatusSeeOther, returnURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -892,3 +1058,33 @@ func FrontDeleteUser(app *App) func(c echo.Context) error {
|
|||||||
return c.Redirect(http.StatusSeeOther, returnURL)
|
return c.Redirect(http.StatusSeeOther, returnURL)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /delete-player
|
||||||
|
func FrontDeletePlayer(app *App) func(c echo.Context) error {
|
||||||
|
return withBrowserAuthentication(app, true, func(c echo.Context, user *User) error {
|
||||||
|
returnURL := getReturnURL(app, &c)
|
||||||
|
|
||||||
|
playerUUID := c.FormValue("uuid")
|
||||||
|
|
||||||
|
var player Player
|
||||||
|
result := app.DB.Preload("User").First(&player, "uuid = ?", playerUUID)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return NewWebError(returnURL, "Player not found.")
|
||||||
|
}
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if !user.IsAdmin && (player.User.UUID != player.User.UUID) {
|
||||||
|
return NewWebError(app.FrontEndURL, "You are not an admin.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := app.DeletePlayer(&player)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccessMessage(&c, fmt.Sprintf("Player \"%s\" deleted", player.Name))
|
||||||
|
|
||||||
|
return c.Redirect(http.StatusSeeOther, returnURL)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
17
main.go
17
main.go
@ -82,6 +82,7 @@ func makeRateLimiter(app *App) echo.MiddlewareFunc {
|
|||||||
switch c.Path() {
|
switch c.Path() {
|
||||||
case "/",
|
case "/",
|
||||||
"/web/delete-user",
|
"/web/delete-user",
|
||||||
|
"/web/delete-player",
|
||||||
"/web/login",
|
"/web/login",
|
||||||
"/web/logout",
|
"/web/logout",
|
||||||
"/web/register",
|
"/web/register",
|
||||||
@ -144,25 +145,31 @@ func (app *App) MakeServer() *echo.Echo {
|
|||||||
t := NewTemplate(app)
|
t := NewTemplate(app)
|
||||||
e.Renderer = t
|
e.Renderer = t
|
||||||
e.GET("/", FrontRoot(app))
|
e.GET("/", FrontRoot(app))
|
||||||
e.GET("/web/manifest.webmanifest", FrontWebManifest(app))
|
|
||||||
e.GET("/web/admin", FrontAdmin(app))
|
e.GET("/web/admin", FrontAdmin(app))
|
||||||
e.GET("/web/challenge-skin", FrontChallengeSkin(app))
|
e.GET("/web/create-player-challenge", FrontCreatePlayerChallenge(app))
|
||||||
e.GET("/web/profile", FrontProfile(app))
|
e.GET("/web/manifest.webmanifest", FrontWebManifest(app))
|
||||||
|
e.GET("/web/player/:uuid", FrontPlayer(app))
|
||||||
|
e.GET("/web/register-challenge", FrontRegisterChallenge(app))
|
||||||
e.GET("/web/registration", FrontRegistration(app))
|
e.GET("/web/registration", FrontRegistration(app))
|
||||||
|
frontUser := FrontUser(app)
|
||||||
|
e.GET("/web/user", frontUser)
|
||||||
|
e.GET("/web/user/:uuid", frontUser)
|
||||||
e.POST("/web/admin/delete-invite", FrontDeleteInvite(app))
|
e.POST("/web/admin/delete-invite", FrontDeleteInvite(app))
|
||||||
e.POST("/web/admin/new-invite", FrontNewInvite(app))
|
e.POST("/web/admin/new-invite", FrontNewInvite(app))
|
||||||
e.POST("/web/admin/update-users", FrontUpdateUsers(app))
|
e.POST("/web/admin/update-users", FrontUpdateUsers(app))
|
||||||
|
e.POST("/web/create-player", FrontCreatePlayer(app))
|
||||||
|
e.POST("/web/delete-player", FrontDeletePlayer(app))
|
||||||
e.POST("/web/delete-user", FrontDeleteUser(app))
|
e.POST("/web/delete-user", FrontDeleteUser(app))
|
||||||
e.POST("/web/login", FrontLogin(app))
|
e.POST("/web/login", FrontLogin(app))
|
||||||
e.POST("/web/logout", FrontLogout(app))
|
e.POST("/web/logout", FrontLogout(app))
|
||||||
e.POST("/web/register", FrontRegister(app))
|
e.POST("/web/register", FrontRegister(app))
|
||||||
e.POST("/web/update-user", FrontUpdateUser(app))
|
|
||||||
e.POST("/web/update-player", FrontUpdatePlayer(app))
|
e.POST("/web/update-player", FrontUpdatePlayer(app))
|
||||||
|
e.POST("/web/update-user", FrontUpdateUser(app))
|
||||||
e.Static("/web/public", path.Join(app.Config.DataDirectory, "public"))
|
e.Static("/web/public", path.Join(app.Config.DataDirectory, "public"))
|
||||||
e.Static("/web/texture/cape", path.Join(app.Config.StateDirectory, "cape"))
|
e.Static("/web/texture/cape", path.Join(app.Config.StateDirectory, "cape"))
|
||||||
e.Static("/web/texture/skin", path.Join(app.Config.StateDirectory, "skin"))
|
|
||||||
e.Static("/web/texture/default-cape", path.Join(app.Config.StateDirectory, "default-cape"))
|
e.Static("/web/texture/default-cape", path.Join(app.Config.StateDirectory, "default-cape"))
|
||||||
e.Static("/web/texture/default-skin", path.Join(app.Config.StateDirectory, "default-skin"))
|
e.Static("/web/texture/default-skin", path.Join(app.Config.StateDirectory, "default-skin"))
|
||||||
|
e.Static("/web/texture/skin", path.Join(app.Config.StateDirectory, "skin"))
|
||||||
|
|
||||||
// Drasl API
|
// Drasl API
|
||||||
e.GET("/drasl/api/v1/users", app.APIGetUsers())
|
e.GET("/drasl/api/v1/users", app.APIGetUsers())
|
||||||
|
21
model.go
21
model.go
@ -69,7 +69,7 @@ func (app *App) ValidatePlayerName(playerName string) error {
|
|||||||
if app.TransientLoginEligible(playerName) {
|
if app.TransientLoginEligible(playerName) {
|
||||||
return errors.New("name is reserved for transient login")
|
return errors.New("name is reserved for transient login")
|
||||||
}
|
}
|
||||||
maxLength := app.Constants.MaxPlayerNameLength
|
maxLength := Constants.MaxPlayerNameLength
|
||||||
if playerName == "" {
|
if playerName == "" {
|
||||||
return errors.New("can't be blank")
|
return errors.New("can't be blank")
|
||||||
}
|
}
|
||||||
@ -137,7 +137,7 @@ func (app *App) ValidatePlayerNameOrUUID(player string) error {
|
|||||||
func (app *App) TransientLoginEligible(playerName string) bool {
|
func (app *App) TransientLoginEligible(playerName string) bool {
|
||||||
return app.Config.TransientUsers.Allow &&
|
return app.Config.TransientUsers.Allow &&
|
||||||
app.TransientUsernameRegex.MatchString(playerName) &&
|
app.TransientUsernameRegex.MatchString(playerName) &&
|
||||||
len(playerName) <= app.Constants.MaxPlayerNameLength
|
len(playerName) <= Constants.MaxPlayerNameLength
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) ValidatePassword(password string) error {
|
func (app *App) ValidatePassword(password string) error {
|
||||||
@ -254,7 +254,7 @@ func (app *App) InviteURL(invite *Invite) (string, error) {
|
|||||||
return url + "?invite=" + invite.Code, nil
|
return url + "?invite=" + invite.Code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) UserSkinURL(player *Player) (*string, error) {
|
func (app *App) PlayerSkinURL(player *Player) (*string, error) {
|
||||||
if !player.SkinHash.Valid {
|
if !player.SkinHash.Valid {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -345,6 +345,16 @@ func (app *App) GetClient(accessToken string, stalePolicy StaleTokenPolicy) *Cli
|
|||||||
return &client
|
return &client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) GetMaxPlayerCount(user *User) int {
|
||||||
|
if user.IsAdmin {
|
||||||
|
return Constants.MaxPlayerCountUnlimited
|
||||||
|
}
|
||||||
|
if user.MaxPlayerCount == Constants.MaxPlayerCountUseDefault {
|
||||||
|
return app.Config.DefaultMaxPlayerCount
|
||||||
|
}
|
||||||
|
return user.MaxPlayerCount
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
IsLocked bool
|
IsLocked bool
|
||||||
@ -355,7 +365,8 @@ type User struct {
|
|||||||
BrowserToken sql.NullString `gorm:"index"`
|
BrowserToken sql.NullString `gorm:"index"`
|
||||||
APIToken string
|
APIToken string
|
||||||
PreferredLanguage string
|
PreferredLanguage string
|
||||||
Players []Player `gorm:"foreignKey:UserUUID"`
|
Players []Player
|
||||||
|
MaxPlayerCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Player struct {
|
type Player struct {
|
||||||
@ -369,7 +380,7 @@ type Player struct {
|
|||||||
CapeHash sql.NullString `gorm:"index"`
|
CapeHash sql.NullString `gorm:"index"`
|
||||||
ServerID sql.NullString
|
ServerID sql.NullString
|
||||||
FallbackPlayer string
|
FallbackPlayer string
|
||||||
Clients []Client `gorm:"foreignKey:PlayerUUID"`
|
Clients []Client
|
||||||
User User
|
User User
|
||||||
UserUUID string `gorm:"not null"`
|
UserUUID string `gorm:"not null"`
|
||||||
}
|
}
|
||||||
|
54
player.go
54
player.go
@ -69,7 +69,7 @@ func (app *App) getTexture(
|
|||||||
|
|
||||||
func (app *App) CreatePlayer(
|
func (app *App) CreatePlayer(
|
||||||
caller *User,
|
caller *User,
|
||||||
user *User,
|
userUUID string,
|
||||||
playerName string,
|
playerName string,
|
||||||
chosenUUID *string,
|
chosenUUID *string,
|
||||||
existingPlayer bool,
|
existingPlayer bool,
|
||||||
@ -87,6 +87,27 @@ func (app *App) CreatePlayer(
|
|||||||
|
|
||||||
callerIsAdmin := caller.IsAdmin
|
callerIsAdmin := caller.IsAdmin
|
||||||
|
|
||||||
|
if userUUID != caller.UUID && !callerIsAdmin {
|
||||||
|
return Player{}, NewBadRequestUserError("Can't create a player belonging to another user unless you're an admin.")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := app.DB.Session(&gorm.Session{FullSaveAssociations: true}).Begin()
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err := tx.Preload("Players").First(&user, "uuid = ?", userUUID).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return Player{}, NewBadRequestUserError("User not found.")
|
||||||
|
}
|
||||||
|
return Player{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
maxPlayerCount := app.GetMaxPlayerCount(&user)
|
||||||
|
log.Println("mpc is", maxPlayerCount, "pc is", len(user.Players))
|
||||||
|
if len(user.Players) >= maxPlayerCount && !callerIsAdmin {
|
||||||
|
return Player{}, NewBadRequestUserError("You are only allowed to create %d player(s).", maxPlayerCount)
|
||||||
|
}
|
||||||
|
|
||||||
if err := app.ValidatePlayerName(playerName); err != nil {
|
if err := app.ValidatePlayerName(playerName); err != nil {
|
||||||
return Player{}, NewBadRequestUserError("Invalid player name: %s", err)
|
return Player{}, NewBadRequestUserError("Invalid player name: %s", err)
|
||||||
}
|
}
|
||||||
@ -168,6 +189,7 @@ func (app *App) CreatePlayer(
|
|||||||
|
|
||||||
player := Player{
|
player := Player{
|
||||||
UUID: playerUUID,
|
UUID: playerUUID,
|
||||||
|
UserUUID: userUUID,
|
||||||
Clients: []Client{},
|
Clients: []Client{},
|
||||||
Name: playerName,
|
Name: playerName,
|
||||||
OfflineUUID: offlineUUID,
|
OfflineUUID: offlineUUID,
|
||||||
@ -178,11 +200,9 @@ func (app *App) CreatePlayer(
|
|||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
NameLastChangedAt: time.Now(),
|
NameLastChangedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
user.Players = append(user.Players, player)
|
||||||
|
|
||||||
tx := app.DB.Begin()
|
if err := tx.Save(&user).Error; err != nil {
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
if err := tx.Create(&player).Error; err != nil {
|
|
||||||
if IsErrorUniqueFailedField(err, "players.name") {
|
if IsErrorUniqueFailedField(err, "players.name") {
|
||||||
return Player{}, NewBadRequestUserError("That player name is taken.")
|
return Player{}, NewBadRequestUserError("That player name is taken.")
|
||||||
} else if IsErrorUniqueFailedField(err, "players.uuid") {
|
} else if IsErrorUniqueFailedField(err, "players.uuid") {
|
||||||
@ -194,10 +214,6 @@ func (app *App) CreatePlayer(
|
|||||||
return Player{}, err
|
return Player{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.DB.Preload("Players").First(&user, user.UUID).Error; err != nil {
|
|
||||||
return Player{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if skinHash != nil {
|
if skinHash != nil {
|
||||||
err = app.WriteSkin(*skinHash, skinBuf)
|
err = app.WriteSkin(*skinHash, skinBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -241,7 +257,7 @@ func (app *App) UpdatePlayer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if playerName != nil && *playerName != player.Name {
|
if playerName != nil && *playerName != player.Name {
|
||||||
if !app.Config.AllowChangingPlayerName && !user.IsAdmin {
|
if !app.Config.AllowChangingPlayerName && !callerIsAdmin {
|
||||||
return Player{}, NewBadRequestUserError("Changing your player name is not allowed.")
|
return Player{}, NewBadRequestUserError("Changing your player name is not allowed.")
|
||||||
}
|
}
|
||||||
if err := app.ValidatePlayerName(*playerName); err != nil {
|
if err := app.ValidatePlayerName(*playerName); err != nil {
|
||||||
@ -551,3 +567,21 @@ func (app *App) InvalidatePlayer(db *gorm.DB, player *Player) error {
|
|||||||
result := db.Model(Client{}).Where("player_uuid = ?", player.UUID).Update("version", gorm.Expr("version + ?", 1))
|
result := db.Model(Client{}).Where("player_uuid = ?", player.UUID).Update("version", gorm.Expr("version + ?", 1))
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) DeletePlayer(player *Player) error {
|
||||||
|
if err := app.DB.Select("Clients").Delete(player).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := app.DeleteSkinIfUnused(UnmakeNullString(&player.SkinHash))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.DeleteCapeIfUnused(UnmakeNullString(&player.CapeHash))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
18
user.go
18
user.go
@ -125,6 +125,19 @@ func (app *App) CreateUser(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return User{}, err
|
return User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if chosenUUID == nil {
|
||||||
|
playerUUID = uuid.New().String()
|
||||||
|
} else {
|
||||||
|
if !app.Config.RegistrationNewPlayer.AllowChoosingUUID && !callerIsAdmin {
|
||||||
|
return User{}, NewBadRequestUserError("Choosing a UUID is not allowed.")
|
||||||
|
}
|
||||||
|
chosenUUIDStruct, err := uuid.Parse(*chosenUUID)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, NewBadRequestUserError("Invalid UUID: %s", err)
|
||||||
|
}
|
||||||
|
playerUUID = chosenUUIDStruct.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
passwordSalt := make([]byte, 16)
|
passwordSalt := make([]byte, 16)
|
||||||
@ -160,6 +173,7 @@ func (app *App) CreateUser(
|
|||||||
PasswordHash: passwordHash,
|
PasswordHash: passwordHash,
|
||||||
PreferredLanguage: app.Config.DefaultPreferredLanguage,
|
PreferredLanguage: app.Config.DefaultPreferredLanguage,
|
||||||
APIToken: apiToken,
|
APIToken: apiToken,
|
||||||
|
MaxPlayerCount: Constants.MaxPlayerCountUseDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player
|
// Player
|
||||||
@ -313,6 +327,10 @@ func (app *App) UpdateUser(
|
|||||||
user.APIToken = apiToken
|
user.APIToken = apiToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := app.DB.Save(&user).Error; err != nil {
|
||||||
|
return User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,8 +82,8 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">Profile</td>
|
<td colspan="2">User</td>
|
||||||
<td>Player Name</td>
|
<td>Players</td>
|
||||||
<td>Admin</td>
|
<td>Admin</td>
|
||||||
<td>Locked</td>
|
<td>Locked</td>
|
||||||
<td>Delete Account</td>
|
<td>Delete Account</td>
|
||||||
@ -95,16 +95,25 @@
|
|||||||
<td style="width: 30px">
|
<td style="width: 30px">
|
||||||
<div
|
<div
|
||||||
class="list-profile-picture"
|
class="list-profile-picture"
|
||||||
style="background-image: url({{ PlayerSkinURL $user }});"
|
{{/*style="background-image: url({{ PlayerSkinURL $user }});"*/}}
|
||||||
></div>
|
></div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
href="{{ $.App.FrontEndURL }}/web/profile?user={{ $user.Username }}"
|
href="{{ $.App.FrontEndURL }}/web/user/{{ $user.UUID }}"
|
||||||
>{{ $user.Username }}</a
|
>{{ $user.Username }}</a
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ $user.PlayerName }}</td>
|
<td>
|
||||||
|
{{ if eq (len $user.Players) 1 }}
|
||||||
|
{{ with $player := index $user.Players 0 }}
|
||||||
|
<a href="{{ $.App.FrontEndURL }}/web/player/{{ $player.UUID }}">{{ $player.Name }}</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ else if gt (len $user.Players) 1 }}
|
||||||
|
{{ len $user.Players }} players
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
{{/*<td>{{ $user.PlayerName }}</td>*/}}
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
name="admin-{{ $user.Username }}"
|
name="admin-{{ $user.Username }}"
|
||||||
@ -148,7 +157,7 @@
|
|||||||
</table>
|
</table>
|
||||||
<p style="text-align: center">
|
<p style="text-align: center">
|
||||||
<input hidden name="returnUrl" value="{{ $.URL }}" />
|
<input hidden name="returnUrl" value="{{ $.URL }}" />
|
||||||
<input type="submit" value="Save Changes" />
|
<input type="submit" value="Save changes" />
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
{{ template "layout" . }}
|
|
||||||
{{ define "content" }}
|
|
||||||
{{ template "header" . }}
|
|
||||||
|
|
||||||
|
|
||||||
<p>
|
|
||||||
We need to verify that you own the
|
|
||||||
{{ .App.Config.RegistrationExistingPlayer.Nickname }} account
|
|
||||||
"{{ .Username }}" before you register its UUID.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{/* prettier-ignore-start */}}
|
|
||||||
<p>
|
|
||||||
Download this image and set it as your skin on your
|
|
||||||
{{ .App.Config.RegistrationExistingPlayer.Nickname }}
|
|
||||||
account{{ if .App.Config.RegistrationExistingPlayer.SetSkinURL }}, <a target="_blank" href="{{ .App.Config.RegistrationExistingPlayer.SetSkinURL }}">here</a>{{ end }}.
|
|
||||||
</p>
|
|
||||||
{{/* prettier-ignore-end */}}
|
|
||||||
|
|
||||||
<div style="text-align: center">
|
|
||||||
<img
|
|
||||||
src="data:image/png;base64,{{ .SkinBase64 }}"
|
|
||||||
width="256"
|
|
||||||
height="256"
|
|
||||||
style="image-rendering: pixelated; width: 256px"
|
|
||||||
alt="{{ .App.Config.ApplicationName }} verification skin"
|
|
||||||
/>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
download="{{ .SkinFilename }}"
|
|
||||||
href="data:image/png;base64,{{ .SkinBase64 }}"
|
|
||||||
>Download skin</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
When you are done, enter a password for your {{ .App.Config.ApplicationName }} account and hit
|
|
||||||
"Register".
|
|
||||||
</p>
|
|
||||||
<form action="{{ .App.FrontEndURL }}/web/register" method="post">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="username"
|
|
||||||
value="{{ .Username }}"
|
|
||||||
required
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<input type="password" name="password" placeholder="Password" required />
|
|
||||||
<input type="checkbox" name="existingPlayer" checked hidden />
|
|
||||||
<input hidden name="challengeToken" value="{{ .ChallengeToken }}" />
|
|
||||||
<input hidden name="inviteCode" value="{{ .InviteCode }}" />
|
|
||||||
<input hidden name="returnUrl" value="{{ .URL }}" />
|
|
||||||
<input type="submit" value="Register" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{ template "footer" . }}
|
|
||||||
{{ end }}
|
|
70
view/challenge.tmpl
Normal file
70
view/challenge.tmpl
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{{ template "layout" . }}
|
||||||
|
{{ define "content" }}
|
||||||
|
{{ template "header" . }}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
We need to verify that you own the
|
||||||
|
{{ .App.Config.RegistrationExistingPlayer.Nickname }} account
|
||||||
|
"{{ .PlayerName }}" before you register its UUID.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Download this image and set it as your skin on your
|
||||||
|
{{ .App.Config.RegistrationExistingPlayer.Nickname }}
|
||||||
|
account{{ if .App.Config.RegistrationExistingPlayer.SetSkinURL }}, <a target="_blank" href="{{ .App.Config.RegistrationExistingPlayer.SetSkinURL }}">here</a>{{ end }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="text-align: center">
|
||||||
|
<img
|
||||||
|
src="data:image/png;base64,{{ .SkinBase64 }}"
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
style="image-rendering: pixelated; width: 256px;"
|
||||||
|
alt="{{ .App.Config.ApplicationName }} verification skin"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
download="{{ .SkinFilename }}"
|
||||||
|
href="data:image/png;base64,{{ .SkinBase64 }}"
|
||||||
|
>Download skin</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{ if eq .Action "register" }}
|
||||||
|
<p>
|
||||||
|
When you are done, enter a password for your {{ .App.Config.ApplicationName }} account and hit
|
||||||
|
"Register".
|
||||||
|
</p>
|
||||||
|
<form action="{{ .App.FrontEndURL }}/web/register" method="post">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
value="{{ .PlayerName }}"
|
||||||
|
required
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<input type="password" name="password" placeholder="Password" required />
|
||||||
|
<input type="checkbox" name="existingPlayer" checked hidden />
|
||||||
|
<input hidden name="challengeToken" value="{{ .ChallengeToken }}" />
|
||||||
|
<input hidden name="inviteCode" value="{{ .InviteCode }}" />
|
||||||
|
<input hidden name="returnUrl" value="{{ .URL }}" />
|
||||||
|
<input type="submit" value="Register" />
|
||||||
|
</form>
|
||||||
|
{{ else if eq .Action "create-player" }}
|
||||||
|
<p>
|
||||||
|
When you are done, hit "Create player".
|
||||||
|
</p>
|
||||||
|
<form action="{{ .App.FrontEndURL }}/web/create-player" method="post">
|
||||||
|
<input hidden name="userUuid" value="{{ .UserUUID }}"/>
|
||||||
|
<input hidden name="playerName" value="{{ .PlayerName }}"/>
|
||||||
|
<input type="checkbox" name="existingPlayer" checked hidden />
|
||||||
|
<input hidden name="challengeToken" value="{{ .ChallengeToken }}" />
|
||||||
|
<input hidden name="returnUrl" value="{{ .URL }}" />
|
||||||
|
<input type="submit" value="Create player" />
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ template "footer" . }}
|
||||||
|
{{ end }}
|
@ -11,21 +11,22 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: right">
|
<div style="text-align: right">
|
||||||
<a href="{{ .App.FrontEndURL }}/web/registration">Register</a>
|
|
||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
{{ if .User.IsAdmin }}
|
{{ if .User.IsAdmin }}
|
||||||
<a href="{{ .App.FrontEndURL }}/web/admin">Admin</a>
|
<a href="{{ .App.FrontEndURL }}/web/admin">Admin</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<a href="{{ .App.FrontEndURL }}/web/profile"
|
<a href="{{ .App.FrontEndURL }}/web/user"
|
||||||
>{{ .User.Username }}'s Profile</a
|
>{{ .User.Username }}'s account</a
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
style="display: inline"
|
style="display: inline"
|
||||||
action="{{ .App.FrontEndURL }}/web/logout"
|
action="{{ .App.FrontEndURL }}/web/logout"
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
<input type="submit" value="Log Out" />
|
<input type="submit" value="Log out" />
|
||||||
</form>
|
</form>
|
||||||
|
{{ else }}
|
||||||
|
<a href="{{ .App.FrontEndURL }}/web/registration">Register</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
181
view/player.tmpl
Normal file
181
view/player.tmpl
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
{{ template "layout" . }}
|
||||||
|
|
||||||
|
{{ define "title" }}{{ .Player.Name }} - {{ .App.Config.ApplicationName }}{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
|
||||||
|
{{ template "header" . }}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ if .AdminView }}
|
||||||
|
<a href="{{ .App.FrontEndURL }}/web/user/{{ .Player.User.UUID }}">Back to {{ .Player.User.Username }}'s account</a>
|
||||||
|
{{ else }}
|
||||||
|
<a href="{{ .App.FrontEndURL }}/web/user">Back to your account</a>
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 style="text-align: center;">{{ .Player.Name }}</h2>
|
||||||
|
{{/* prettier-ignore-start */}}
|
||||||
|
<h6 style="text-align: center;">{{ .Player.UUID }}<br />{{ .PlayerID }}</h6>
|
||||||
|
{{/* prettier-ignore-end */}}
|
||||||
|
{{ if .SkinURL }}
|
||||||
|
<div id="skin-container" style="height: 300px;">
|
||||||
|
<canvas id="skin-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
{{ else }}
|
||||||
|
No skin yet.
|
||||||
|
{{ end }}
|
||||||
|
<form
|
||||||
|
action="{{ .App.FrontEndURL }}/web/update-player"
|
||||||
|
method="post"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
{{ if or .App.Config.AllowChangingPlayerName .User.IsAdmin }}
|
||||||
|
<p>
|
||||||
|
<label for="player-name"
|
||||||
|
>Player Name (can be different from {{ if .AdminView }}{{ .Player.User.Username }}'s{{ else }}your{{ end }} {{ .App.Config.ApplicationName }} username)</label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="playerName"
|
||||||
|
id="player-name"
|
||||||
|
value="{{ .Player.Name }}"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
{{ if or .App.Config.AllowSkins .User.IsAdmin }}
|
||||||
|
<h4>Skin</h4>
|
||||||
|
<p>
|
||||||
|
<label for="skin-file">Upload a skin</label><br />
|
||||||
|
<input type="file" name="skinFile" id="skin-file" />
|
||||||
|
</p>
|
||||||
|
{{ if or .App.Config.AllowTextureFromURL .User.IsAdmin }}
|
||||||
|
<p>
|
||||||
|
<label for="skin-url">or instead, provide a URL to a skin</label><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="skinUrl"
|
||||||
|
id="skin-url"
|
||||||
|
class="long"
|
||||||
|
placeholder="Leave blank to keep"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
<p>
|
||||||
|
<label for="delete-skin"
|
||||||
|
>or instead, check the box to delete the current skin
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" name="deleteSkin" id="delete-skin" />
|
||||||
|
</p>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Skin model</legend>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="skin-model-classic"
|
||||||
|
name="skinModel"
|
||||||
|
value="classic"
|
||||||
|
{{ if eq .Player.SkinModel "classic" }}checked{{ end }}
|
||||||
|
/>
|
||||||
|
<label for="skin-model-classic">Classic</label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="skin-model-slim"
|
||||||
|
name="skinModel"
|
||||||
|
value="slim"
|
||||||
|
{{ if eq .Player.SkinModel "slim" }}checked{{ end }}
|
||||||
|
/>
|
||||||
|
<label for="skin-model-slim">Slim</label>
|
||||||
|
</fieldset>
|
||||||
|
{{ end }}
|
||||||
|
{{ if or .App.Config.AllowCapes .User.IsAdmin }}
|
||||||
|
<h4>Cape</h4>
|
||||||
|
<p>
|
||||||
|
<label for="cape-file">Upload a cape</label><br />
|
||||||
|
<input type="file" name="capeFile" id="cape-file" />
|
||||||
|
</p>
|
||||||
|
{{ if or .App.Config.AllowTextureFromURL .User.IsAdmin }}
|
||||||
|
<p>
|
||||||
|
<label for="cape-url">or instead, provide a URL to a cape</label><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="capeUrl"
|
||||||
|
id="cape-url"
|
||||||
|
class="long"
|
||||||
|
placeholder="Leave blank to keep"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
<p>
|
||||||
|
<label for="delete-cape"
|
||||||
|
>or instead, check the box to delete the current cape
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" name="deleteCape" id="delete-cape" />
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .App.Config.ForwardSkins }}
|
||||||
|
<p>
|
||||||
|
<label for="fallback-player">Fallback Player</label><br />
|
||||||
|
UUID or 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.<br />
|
||||||
|
<input
|
||||||
|
class="long"
|
||||||
|
type="text"
|
||||||
|
name="fallbackPlayer"
|
||||||
|
id="fallback-player"
|
||||||
|
placeholder="{{ .Player.Name }}"
|
||||||
|
value="{{ .Player.FallbackPlayer }}"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
<input hidden name="uuid" value="{{ .Player.UUID }}" />
|
||||||
|
<input hidden name="returnUrl" value="{{ .URL }}" />
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<input type="submit" value="Save changes" />
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
<details>
|
||||||
|
<summary>Delete Player</summary>
|
||||||
|
<form
|
||||||
|
action="{{ .App.FrontEndURL }}/web/delete-player"
|
||||||
|
method="post"
|
||||||
|
onsubmit="return confirm('Are you sure? This action is irreversible.');"
|
||||||
|
>
|
||||||
|
<input hidden name="uuid" value="{{ .Player.UUID }}" />
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
name="returnUrl"
|
||||||
|
value="{{ if .AdminView }}
|
||||||
|
{{ .App.FrontEndURL }}/web/user/{{ .Player.User.UUID }}
|
||||||
|
{{ else }}
|
||||||
|
{{ .App.FrontEndURL }}/web/user
|
||||||
|
{{ end }}"
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Delete Player" />
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ if .SkinURL }}
|
||||||
|
<script type="module">
|
||||||
|
import { skinview3d } from "{{.App.FrontEndURL}}/web/public/bundle.js"
|
||||||
|
const skinCanvas = document.getElementById("skin-canvas");
|
||||||
|
const skinViewer = new skinview3d.SkinViewer({
|
||||||
|
canvas: skinCanvas,
|
||||||
|
width: 200,
|
||||||
|
height: skinCanvas.parentElement.clientHeight,
|
||||||
|
});
|
||||||
|
skinViewer.controls.enableZoom = false;
|
||||||
|
skinViewer.loadSkin({{.SkinURL}}, {
|
||||||
|
model: "{{.Player.SkinModel}}",
|
||||||
|
});
|
||||||
|
{{if .CapeURL}}
|
||||||
|
skinViewer.loadCape({{.CapeURL}});
|
||||||
|
{{end}}
|
||||||
|
skinViewer.render();
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ template "footer" . }}
|
||||||
|
|
||||||
|
{{ end }}
|
@ -1,487 +0,0 @@
|
|||||||
{{ template "layout" . }}
|
|
||||||
|
|
||||||
{{ define "title" }}{{ .ProfileUser.PlayerName }}'s Profile - {{ .App.Config.ApplicationName }}{{ end }}
|
|
||||||
|
|
||||||
{{ define "content" }}
|
|
||||||
|
|
||||||
{{ template "header" . }}
|
|
||||||
|
|
||||||
|
|
||||||
<h2 style="text-align: center;">{{ .ProfileUser.PlayerName }}</h2>
|
|
||||||
{{/* prettier-ignore-start */}}
|
|
||||||
<h6 style="text-align: center;">{{ .ProfileUser.UUID }}<br />{{ .ProfileUserID }}</h6>
|
|
||||||
{{/* prettier-ignore-end */}}
|
|
||||||
{{ if .SkinURL }}
|
|
||||||
<div id="skin-container" style="height: 300px;">
|
|
||||||
<canvas id="skin-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
{{ else }}
|
|
||||||
No skin yet.
|
|
||||||
{{ end }}
|
|
||||||
<form
|
|
||||||
action="{{ .App.FrontEndURL }}/web/update"
|
|
||||||
method="post"
|
|
||||||
enctype="multipart/form-data"
|
|
||||||
>
|
|
||||||
{{ if or .App.Config.AllowChangingPlayerName .User.IsAdmin }}
|
|
||||||
<p>
|
|
||||||
<label for="player-name"
|
|
||||||
>Player Name (can be different from username)</label
|
|
||||||
><br />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="playerName"
|
|
||||||
id="player-name"
|
|
||||||
value="{{ .ProfileUser.PlayerName }}"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
<p>
|
|
||||||
<label for="password">Password</label><br />
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
id="password"
|
|
||||||
class="long"
|
|
||||||
placeholder="Leave blank to keep"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<label for="apiToken">API Token</label><br />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="apiToken"
|
|
||||||
id="api-token"
|
|
||||||
class="long"
|
|
||||||
readonly
|
|
||||||
value="{{ .ProfileUser.APIToken }}"
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<label for="reset-api-token"
|
|
||||||
>check the box to reset your API token
|
|
||||||
</label>
|
|
||||||
<input type="checkbox" name="resetApiToken" id="reset-api-token" />
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<label for="preferred-language"
|
|
||||||
>Preferred Language (used by Minecraft)</label
|
|
||||||
><br />
|
|
||||||
<select
|
|
||||||
name="preferredLanguage"
|
|
||||||
id="preferred-language"
|
|
||||||
value="{{ .ProfileUser.PreferredLanguage }}"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
value="sq"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "sq" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Albanian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="ar"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "ar" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Arabic
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="be"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "be" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Belarusian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="bg"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "bg" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Bulgarian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="ca"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "ca" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Catalan
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="zh"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "zh" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Chinese
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="hr"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "hr" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Croatian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="cs"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "cs" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Czech
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="da"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "da" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Danish
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="nl"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "nl" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Dutch
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="en"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "en" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
English
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="et"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "et" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Estonian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="fi"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "fi" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Finnish
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="fr"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "fr" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
French
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="de"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "de" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
German
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="el"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "el" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Greek
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="iw"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "iw" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Hebrew
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="hi"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "hi" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Hindi
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="hu"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "hu" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Hungarian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="is"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "is" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Icelandic
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="in"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "in" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Indonesian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="ga"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "ga" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Irish
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="it"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "it" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Italian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="ja"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "ja" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Japanese
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="ko"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "ko" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Korean
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="lv"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "lv" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Latvian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="lt"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "lt" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Lithuanian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="mk"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "mk" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Macedonian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="ms"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "ms" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Malay
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="mt"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "mt" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Maltese
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="no"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "no" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Norwegian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="nb"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "nb" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Norwegian Bokmål
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="nn"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "nn" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Norwegian Nynorsk
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="pl"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "pl" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Polish
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="pt"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "pt" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Portuguese
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="ro"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "ro" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Romanian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="ru"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "ru" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Russian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="sr"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "sr" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Serbian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="sk"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "sk" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Slovak
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="sl"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "sl" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Slovenian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="es"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "es" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Spanish
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="sv"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "sv" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Swedish
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="th"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "th" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Thai
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="tr"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "tr" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Turkish
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="uk"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "uk" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Ukrainian
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value="vi"
|
|
||||||
{{ if eq .ProfileUser.PreferredLanguage "vi" }}selected{{ end }}
|
|
||||||
>
|
|
||||||
Vietnamese
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
{{ if or .App.Config.AllowSkins .User.IsAdmin }}
|
|
||||||
<h4>Skin</h4>
|
|
||||||
<p>
|
|
||||||
<label for="skin-file">Upload a skin</label><br />
|
|
||||||
<input type="file" name="skinFile" id="skin-file" />
|
|
||||||
</p>
|
|
||||||
{{ if or .App.Config.AllowTextureFromURL .User.IsAdmin }}
|
|
||||||
<p>
|
|
||||||
<label for="skin-url">or instead, provide a URL to a skin</label><br />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="skinUrl"
|
|
||||||
id="skin-url"
|
|
||||||
class="long"
|
|
||||||
placeholder="Leave blank to keep"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
<p>
|
|
||||||
<label for="delete-skin"
|
|
||||||
>or instead, check the box to delete your current skin
|
|
||||||
</label>
|
|
||||||
<input type="checkbox" name="deleteSkin" id="delete-skin" />
|
|
||||||
</p>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Skin model</legend>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="skin-model-classic"
|
|
||||||
name="skinModel"
|
|
||||||
value="classic"
|
|
||||||
{{ if eq .ProfileUser.SkinModel "classic" }}checked{{ end }}
|
|
||||||
/>
|
|
||||||
<label for="skin-model-classic">Classic</label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="skin-model-slim"
|
|
||||||
name="skinModel"
|
|
||||||
value="slim"
|
|
||||||
{{ if eq .ProfileUser.SkinModel "slim" }}checked{{ end }}
|
|
||||||
/>
|
|
||||||
<label for="skin-model-slim">Slim</label>
|
|
||||||
</fieldset>
|
|
||||||
{{ end }}
|
|
||||||
{{ if or .App.Config.AllowCapes .User.IsAdmin }}
|
|
||||||
<h4>Cape</h4>
|
|
||||||
<p>
|
|
||||||
<label for="cape-file">Upload a cape</label><br />
|
|
||||||
<input type="file" name="capeFile" id="cape-file" />
|
|
||||||
</p>
|
|
||||||
{{ if or .App.Config.AllowTextureFromURL .User.IsAdmin }}
|
|
||||||
<p>
|
|
||||||
<label for="cape-url">or instead, provide a URL to a cape</label><br />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="capeUrl"
|
|
||||||
id="cape-url"
|
|
||||||
class="long"
|
|
||||||
placeholder="Leave blank to keep"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
<p>
|
|
||||||
<label for="delete-cape"
|
|
||||||
>or instead, check the box to delete your current cape
|
|
||||||
</label>
|
|
||||||
<input type="checkbox" name="deleteCape" id="delete-cape" />
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .App.Config.ForwardSkins }}
|
|
||||||
<p>
|
|
||||||
<label for="fallback-player">Fallback Player</label><br />
|
|
||||||
UUID or 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.<br />
|
|
||||||
<input
|
|
||||||
class="long"
|
|
||||||
type="text"
|
|
||||||
name="fallbackPlayer"
|
|
||||||
id="fallback-player"
|
|
||||||
placeholder="{{ .ProfileUser.PlayerName }}"
|
|
||||||
value="{{ .ProfileUser.FallbackPlayer }}"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
<input hidden name="uuid" value="{{ .ProfileUser.UUID }}" />
|
|
||||||
<input hidden name="returnUrl" value="{{ .URL }}" />
|
|
||||||
<p style="text-align: center;">
|
|
||||||
<input type="submit" value="Save Changes" />
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<p>
|
|
||||||
<details>
|
|
||||||
<summary>Delete Account</summary>
|
|
||||||
<form
|
|
||||||
action="{{ .App.FrontEndURL }}/web/delete-user"
|
|
||||||
method="post"
|
|
||||||
onsubmit="return confirm('Are you sure? This action is irreversible.');"
|
|
||||||
>
|
|
||||||
<input hidden name="uuid" value="{{ .ProfileUser.UUID }}" />
|
|
||||||
<input
|
|
||||||
hidden
|
|
||||||
name="returnUrl"
|
|
||||||
value="{{ if .AdminView }}
|
|
||||||
{{ .App.FrontEndURL }}/web/admin
|
|
||||||
{{ else }}
|
|
||||||
{{ .App.FrontEndURL }}
|
|
||||||
{{ end }}"
|
|
||||||
/>
|
|
||||||
<input type="submit" value="🗙 Delete Account" />
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{ if .SkinURL }}
|
|
||||||
<script type="module">
|
|
||||||
import { skinview3d } from "{{.App.FrontEndURL}}/web/public/bundle.js"
|
|
||||||
const skinCanvas = document.getElementById("skin-canvas");
|
|
||||||
const skinViewer = new skinview3d.SkinViewer({
|
|
||||||
canvas: skinCanvas,
|
|
||||||
width: 200,
|
|
||||||
height: skinCanvas.parentElement.clientHeight,
|
|
||||||
});
|
|
||||||
skinViewer.controls.enableZoom = false;
|
|
||||||
skinViewer.loadSkin({{.SkinURL}}, {
|
|
||||||
model: "{{.ProfileUser.SkinModel}}",
|
|
||||||
});
|
|
||||||
{{if .CapeURL}}
|
|
||||||
skinViewer.loadCape({{.CapeURL}});
|
|
||||||
{{end}}
|
|
||||||
skinViewer.render();
|
|
||||||
</script>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ template "footer" . }}
|
|
||||||
|
|
||||||
{{ end }}
|
|
@ -78,11 +78,11 @@
|
|||||||
{{ if .InviteCode }}
|
{{ if .InviteCode }}
|
||||||
<p><em>Using invite code {{ .InviteCode }}</em></p>
|
<p><em>Using invite code {{ .InviteCode }}</em></p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<form action="{{ .App.FrontEndURL }}/web/challenge-skin" method="get">
|
<form action="{{ .App.FrontEndURL }}/web/register-challenge" method="get">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="username"
|
||||||
placeholder="{{ .App.Config.RegistrationExistingPlayer.Nickname }} Player Name"
|
placeholder="{{ .App.Config.RegistrationExistingPlayer.Nickname }} Player name"
|
||||||
maxlength="{{ .App.Constants.MaxUsernameLength }}"
|
maxlength="{{ .App.Constants.MaxUsernameLength }}"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -104,7 +104,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="username"
|
||||||
placeholder="{{ .App.Config.RegistrationExistingPlayer.Nickname }} Player Name"
|
placeholder="{{ .App.Config.RegistrationExistingPlayer.Nickname }} Player name"
|
||||||
maxlength="{{ .App.Constants.MaxUsernameLength }}"
|
maxlength="{{ .App.Constants.MaxUsernameLength }}"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
<h3>Log in</h3>
|
<h3>Log in</h3>
|
||||||
<form action="{{ .App.FrontEndURL }}/web/login" method="post">
|
<form action="{{ .App.FrontEndURL }}/web/login" method="post">
|
||||||
<input type="text" name="username" placeholder="Username" required />
|
<input type="text" name="username" placeholder="Username" required />
|
||||||
|
<input hidden name="returnUrl" value="{{ .URL }}" />
|
||||||
|
<input hidden name="destination" value="{{ .Destination }}" />
|
||||||
<input
|
<input
|
||||||
class="long"
|
class="long"
|
||||||
type="password"
|
type="password"
|
||||||
@ -103,9 +105,7 @@
|
|||||||
-Dminecraft.api.auth.host={{ .App.AuthURL }}
|
-Dminecraft.api.auth.host={{ .App.AuthURL }}
|
||||||
-Dminecraft.api.account.host={{ .App.AccountURL }}
|
-Dminecraft.api.account.host={{ .App.AccountURL }}
|
||||||
-Dminecraft.api.session.host={{ .App.SessionURL }}
|
-Dminecraft.api.session.host={{ .App.SessionURL }}
|
||||||
-Dminecraft.api.services.host={{ .App.ServicesURL }}
|
-Dminecraft.api.services.host={{ .App.ServicesURL }}</pre>
|
||||||
</pre
|
|
||||||
>
|
|
||||||
|
|
||||||
For example, the full command you use to start the server might be:
|
For example, the full command you use to start the server might be:
|
||||||
<pre style="word-wrap: break-word; white-space: pre-wrap; overflow-x: auto">
|
<pre style="word-wrap: break-word; white-space: pre-wrap; overflow-x: auto">
|
||||||
@ -115,8 +115,7 @@ java -Xmx1024M -Xms1024M \
|
|||||||
-Dminecraft.api.account.host={{ .App.AccountURL }} \
|
-Dminecraft.api.account.host={{ .App.AccountURL }} \
|
||||||
-Dminecraft.api.session.host={{ .App.SessionURL }} \
|
-Dminecraft.api.session.host={{ .App.SessionURL }} \
|
||||||
-Dminecraft.api.services.host={{ .App.ServicesURL }} \
|
-Dminecraft.api.services.host={{ .App.ServicesURL }} \
|
||||||
-jar server.jar nogui</pre
|
-jar server.jar nogui</pre>
|
||||||
>
|
|
||||||
|
|
||||||
<h4>Minecraft 1.15.2 and earlier</h4>
|
<h4>Minecraft 1.15.2 and earlier</h4>
|
||||||
|
|
||||||
|
500
view/user.tmpl
Normal file
500
view/user.tmpl
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
{{ template "layout" . }}
|
||||||
|
|
||||||
|
{{ define "title" }}{{ .TargetUser.Username }}'s Account - {{ .App.Config.ApplicationName }}{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
|
||||||
|
{{ template "header" . }}
|
||||||
|
|
||||||
|
<h2 style="text-align: center;">{{ .TargetUser.Username }}</h2>
|
||||||
|
|
||||||
|
<div style="display: none">
|
||||||
|
{{ range $player := .TargetUser.Players }}
|
||||||
|
<form
|
||||||
|
id="delete-{{ $player.UUID }}"
|
||||||
|
action="{{ $.App.FrontEndURL }}/web/delete-player"
|
||||||
|
method="post"
|
||||||
|
onsubmit="return confirm('Are you sure? This action is irreversible.');"
|
||||||
|
>
|
||||||
|
<input hidden name="returnUrl" value="{{ $.URL }}" />
|
||||||
|
<input type="text" name="uuid" value="{{ $player.UUID }}" />
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>{{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} players</h3>
|
||||||
|
{{ if .TargetUser.Players }}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">Player</td>
|
||||||
|
<td>UUID</td>
|
||||||
|
<td>Delete Player</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range $player := .TargetUser.Players }}
|
||||||
|
<tr>
|
||||||
|
<td style="width: 30px">
|
||||||
|
<div
|
||||||
|
class="list-profile-picture"
|
||||||
|
{{ with $playerSkinURL := PlayerSkinURL $player }}
|
||||||
|
{{ if $playerSkinURL }}
|
||||||
|
style="background-image: url({{ PlayerSkinURL $player }});"
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="{{ $.App.FrontEndURL }}/web/player/{{ $player.UUID }}"
|
||||||
|
>{{ $player.Name }}</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>{{ $player.UUID }}</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
form="delete-{{ $player.UUID }}"
|
||||||
|
value="Delete"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{ else }}
|
||||||
|
No players yet.
|
||||||
|
{{ end }}
|
||||||
|
<p>
|
||||||
|
{{ if (eq .MaxPlayerCount 0) }}
|
||||||
|
{{ 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 }}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.
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
{{ if or (lt (len .TargetUser.Players) .MaxPlayerCount) (lt .MaxPlayerCount 0) .AdminView }}
|
||||||
|
{{ if .App.Config.RegistrationNewPlayer.Allow }}
|
||||||
|
{{ if .App.Config.RegistrationNewPlayer.AllowChoosingUUID }}
|
||||||
|
<h4>Create a new player</h4>
|
||||||
|
{{ else }}
|
||||||
|
<p>Create a new player with a random UUID:</p>
|
||||||
|
{{ end }}
|
||||||
|
<form action="{{ .App.FrontEndURL }}/web/create-player" method="post">
|
||||||
|
<input hidden name="userUuid" value="{{ .TargetUser.UUID }}">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="playerName"
|
||||||
|
placeholder="Player name"
|
||||||
|
maxlength="{{ .App.Constants.MaxPlayerNameLength }}"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{{ if .App.Config.RegistrationNewPlayer.AllowChoosingUUID }}
|
||||||
|
<input
|
||||||
|
class="long"
|
||||||
|
type="text"
|
||||||
|
name="playerUuid"
|
||||||
|
placeholder="UUID (leave blank for random)"
|
||||||
|
pattern="^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$"
|
||||||
|
/>
|
||||||
|
{{ end }}
|
||||||
|
<input hidden name="returnUrl" value="{{ .URL }}" />
|
||||||
|
<input type="submit" value="Create player" />
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .App.Config.RegistrationExistingPlayer.Allow }}
|
||||||
|
<h4>Import a(n) {{ .App.Config.RegistrationExistingPlayer.Nickname }} player</h4>
|
||||||
|
{{ if .App.Config.RegistrationExistingPlayer.RequireSkinVerification }}
|
||||||
|
<p>
|
||||||
|
Create a new player with the UUID of an existing
|
||||||
|
{{ .App.Config.RegistrationExistingPlayer.Nickname }} player.
|
||||||
|
Requires verification that you own the account.
|
||||||
|
</p>
|
||||||
|
<form action="{{ .App.FrontEndURL }}/web/create-player-challenge" method="get">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="playerName"
|
||||||
|
placeholder="{{ .App.Config.RegistrationExistingPlayer.Nickname }} player name"
|
||||||
|
maxlength="{{ .App.Constants.MaxUsernameLength }}"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input hidden name="userUuid" value="{{ .TargetUser.UUID }}">
|
||||||
|
<input hidden name="returnUrl" value="{{ .URL }}" />
|
||||||
|
<input type="submit" value="Continue" />
|
||||||
|
</form>
|
||||||
|
{{ else }}
|
||||||
|
<p>
|
||||||
|
Create a new player with the UUID of an existing
|
||||||
|
{{ .App.Config.RegistrationExistingPlayer.Nickname }} player.
|
||||||
|
</p>
|
||||||
|
<form action="{{ .App.FrontEndURL }}/web/create-player" method="post">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="playerName"
|
||||||
|
placeholder="{{ .App.Config.RegistrationExistingPlayer.Nickname }} Player name"
|
||||||
|
maxlength="{{ .App.Constants.MaxPlayerNameLength }}"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input hidden type="checkbox" name="existingPlayer" checked />
|
||||||
|
<input hidden name="userUuid" value="{{ .TargetUser.UUID }}">
|
||||||
|
<input hidden name="returnUrl" value="{{ .URL }}" />
|
||||||
|
<input type="submit" value="Create player" />
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
<h3>Account settings</h3>
|
||||||
|
<form
|
||||||
|
action="{{ .App.FrontEndURL }}/web/update-user"
|
||||||
|
method="post"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<label for="password">Password</label><br />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
class="long"
|
||||||
|
placeholder="Leave blank to keep"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="apiToken">API Token</label><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="apiToken"
|
||||||
|
id="api-token"
|
||||||
|
class="long"
|
||||||
|
readonly
|
||||||
|
value="{{ .TargetUser.APIToken }}"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<label for="reset-api-token"
|
||||||
|
>check the box to reset your API token
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" name="resetApiToken" id="reset-api-token" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="preferred-language"
|
||||||
|
>Preferred Language (used by Minecraft)</label
|
||||||
|
><br />
|
||||||
|
<select
|
||||||
|
name="preferredLanguage"
|
||||||
|
id="preferred-language"
|
||||||
|
value="{{ .TargetUser.PreferredLanguage }}"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="sq"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "sq" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Albanian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="ar"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "ar" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Arabic
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="be"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "be" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Belarusian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="bg"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "bg" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Bulgarian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="ca"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "ca" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Catalan
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="zh"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "zh" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Chinese
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="hr"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "hr" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Croatian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="cs"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "cs" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Czech
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="da"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "da" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Danish
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="nl"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "nl" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Dutch
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="en"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "en" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="et"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "et" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Estonian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="fi"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "fi" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Finnish
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="fr"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "fr" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
French
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="de"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "de" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
German
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="el"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "el" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Greek
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="iw"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "iw" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Hebrew
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="hi"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "hi" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Hindi
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="hu"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "hu" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Hungarian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="is"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "is" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Icelandic
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="in"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "in" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Indonesian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="ga"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "ga" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Irish
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="it"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "it" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Italian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="ja"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "ja" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Japanese
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="ko"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "ko" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Korean
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="lv"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "lv" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Latvian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="lt"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "lt" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Lithuanian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="mk"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "mk" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Macedonian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="ms"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "ms" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Malay
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="mt"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "mt" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Maltese
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="no"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "no" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Norwegian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="nb"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "nb" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Norwegian Bokmål
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="nn"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "nn" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Norwegian Nynorsk
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="pl"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "pl" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Polish
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="pt"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "pt" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Portuguese
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="ro"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "ro" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Romanian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="ru"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "ru" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Russian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="sr"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "sr" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Serbian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="sk"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "sk" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Slovak
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="sl"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "sl" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Slovenian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="es"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "es" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Spanish
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="sv"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "sv" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Swedish
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="th"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "th" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Thai
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="tr"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "tr" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Turkish
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="uk"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "uk" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Ukrainian
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="vi"
|
||||||
|
{{ if eq .TargetUser.PreferredLanguage "vi" }}selected{{ end }}
|
||||||
|
>
|
||||||
|
Vietnamese
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<input hidden name="uuid" value="{{ .TargetUser.UUID }}" />
|
||||||
|
<input hidden name="returnUrl" value="{{ .URL }}" />
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<input type="submit" value="Save changes" />
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
<details>
|
||||||
|
<summary>Delete account</summary>
|
||||||
|
<form
|
||||||
|
action="{{ .App.FrontEndURL }}/web/delete-user"
|
||||||
|
method="post"
|
||||||
|
onsubmit="return confirm('Are you sure? This action is irreversible.');"
|
||||||
|
>
|
||||||
|
<input hidden name="uuid" value="{{ .TargetUser.UUID }}" />
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
name="returnUrl"
|
||||||
|
value="{{ if .AdminView }}
|
||||||
|
{{ .App.FrontEndURL }}/web/admin
|
||||||
|
{{ else }}
|
||||||
|
{{ .App.FrontEndURL }}
|
||||||
|
{{ end }}"
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Delete account" />
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ template "footer" . }}
|
||||||
|
|
||||||
|
{{ end }}
|
Loading…
x
Reference in New Issue
Block a user