drasl/user.go
Evan Goode e286356cd7 Lint
2024-12-28 21:07:32 -05:00

432 lines
11 KiB
Go

package main
import (
"crypto/rand"
"errors"
"github.com/google/uuid"
"gorm.io/gorm"
"io"
"time"
)
// Must be in a region of the skin that supports translucency
const SKIN_WINDOW_X_MIN = 40
const SKIN_WINDOW_X_MAX = 48
const SKIN_WINDOW_Y_MIN = 9
const SKIN_WINDOW_Y_MAX = 11
var InviteNotFoundError error = NewBadRequestUserError("Invite not found.")
var InviteMissingError error = NewBadRequestUserError("Registration requires an invite.")
func (app *App) CreateUser(
caller *User,
username string,
password string,
isAdmin bool,
isLocked bool,
inviteCode *string,
preferredLanguage *string,
playerName *string,
chosenUUID *string,
existingPlayer bool,
challengeToken *string,
fallbackPlayer *string,
maxPlayerCount *int,
skinModel *string,
skinReader *io.Reader,
skinURL *string,
capeReader *io.Reader,
capeURL *string,
) (User, error) {
callerIsAdmin := caller != nil && caller.IsAdmin
if err := app.ValidateUsername(username); err != nil {
return User{}, NewBadRequestUserError("Invalid username: %s", err)
}
if err := app.ValidatePassword(password); err != nil {
return User{}, NewBadRequestUserError("Invalid password: %s", err)
}
if playerName == nil {
playerName = &username
} else {
if *playerName != username && !app.Config.AllowChangingPlayerName && !callerIsAdmin {
return User{}, NewBadRequestUserError("Choosing a player name different from your username is not allowed.")
}
if err := app.ValidatePlayerName(*playerName); err != nil {
return User{}, NewBadRequestUserError("Invalid player name: %s", err)
}
}
if preferredLanguage == nil {
preferredLanguage = &app.Config.DefaultPreferredLanguage
}
if !IsValidPreferredLanguage(*preferredLanguage) {
return User{}, NewBadRequestUserError("Invalid preferred language.")
}
getInvite := func(requireInvite bool) (*Invite, error) {
var invite Invite
if inviteCode == nil {
if requireInvite && !callerIsAdmin {
return nil, InviteMissingError
}
return nil, nil
} else {
result := app.DB.First(&invite, "code = ?", *inviteCode)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, InviteNotFoundError
}
return nil, result.Error
}
return &invite, nil
}
}
var invite *Invite
var playerUUID string
if existingPlayer {
// Existing player registration
if !app.Config.RegistrationExistingPlayer.Allow && !callerIsAdmin {
return User{}, NewBadRequestUserError("Registration from an existing player is not allowed.")
}
var err error
invite, err = getInvite(app.Config.RegistrationExistingPlayer.RequireInvite)
if err != nil {
return User{}, err
}
if err := app.ValidatePlayerName(*playerName); err != nil {
return User{}, NewBadRequestUserError("Invalid player name: %s", err)
}
details, err := app.ValidateChallenge(*playerName, challengeToken)
if err != nil {
if app.Config.RegistrationExistingPlayer.RequireSkinVerification {
return User{}, NewBadRequestUserError("Couldn't verify your skin, maybe try again: %s", err)
} else {
return User{}, NewBadRequestUserError("Couldn't find your account, maybe try again: %s", err)
}
}
playerName = &details.Username
if err := app.ValidatePlayerName(*playerName); err != nil {
return User{}, NewBadRequestUserError("Invalid player name: %s", err)
}
playerUUID = details.UUID
} else {
// New player registration
if !app.Config.RegistrationNewPlayer.Allow && !callerIsAdmin {
return User{}, NewBadRequestUserError("Registration without some existing player is not allowed.")
}
var err error
invite, err = getInvite(app.Config.RegistrationNewPlayer.RequireInvite)
if err != nil {
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)
_, err := rand.Read(passwordSalt)
if err != nil {
return User{}, err
}
passwordHash, err := HashPassword(password, passwordSalt)
if err != nil {
return User{}, err
}
if isAdmin && !callerIsAdmin {
return User{}, NewBadRequestUserError("Cannot make a new admin user without having admin privileges yourself.")
}
if isLocked && !callerIsAdmin {
return User{}, NewBadRequestUserError("Cannot make a new locked user without admin privileges.")
}
maxPlayerCountInt := Constants.MaxPlayerCountUseDefault
if maxPlayerCount != nil {
if !callerIsAdmin {
return User{}, NewBadRequestUserError("Cannot set a max player count without admin privileges.")
}
err := app.ValidateMaxPlayerCount(*maxPlayerCount)
if err != nil {
return User{}, NewBadRequestUserError("Invalid max player count: %s", err)
}
maxPlayerCountInt = *maxPlayerCount
}
apiToken, err := MakeAPIToken()
if err != nil {
return User{}, err
}
user := User{
IsAdmin: Contains(app.Config.DefaultAdmins, username) || isAdmin,
IsLocked: isLocked,
UUID: uuid.New().String(),
Username: username,
PasswordSalt: passwordSalt,
PasswordHash: passwordHash,
PreferredLanguage: app.Config.DefaultPreferredLanguage,
MaxPlayerCount: maxPlayerCountInt,
APIToken: apiToken,
}
// Player
offlineUUID, err := OfflineUUID(*playerName)
if err != nil {
return User{}, err
}
if fallbackPlayer == nil {
fallbackPlayer = &playerUUID
}
if err := app.ValidatePlayerNameOrUUID(*fallbackPlayer); err != nil {
return User{}, NewBadRequestUserError("Invalid fallback player: %s", err)
}
if skinModel == nil {
skinModel = Ptr(SkinModelClassic)
}
if !IsValidSkinModel(*skinModel) {
return User{}, NewBadRequestUserError("Invalid skin model.")
}
skinHash, skinBuf, err := app.getTexture("skin", caller, skinReader, skinURL)
if err != nil {
return User{}, err
}
capeHash, capeBuf, err := app.getTexture("cape", caller, capeReader, capeURL)
if err != nil {
return User{}, err
}
tx := app.DB.Begin()
defer tx.Rollback()
if err := tx.Create(&user).Error; err != nil {
if IsErrorUniqueFailedField(err, "users.username") {
return User{}, NewBadRequestUserError("That username is taken.")
} else if IsErrorUsernameTakenByPlayerName(err) {
return User{}, NewBadRequestUserError("That username is in use as the name of another user's player.")
} else {
return User{}, err
}
}
player := Player{
UUID: playerUUID,
UserUUID: user.UUID,
Clients: []Client{},
Name: *playerName,
OfflineUUID: offlineUUID,
FallbackPlayer: *fallbackPlayer,
SkinModel: *skinModel,
SkinHash: MakeNullString(skinHash),
CapeHash: MakeNullString(capeHash),
CreatedAt: time.Now(),
NameLastChangedAt: time.Now(),
}
user.Players = append(user.Players, player)
if err := tx.Create(&player).Error; err != nil {
if IsErrorUniqueFailedField(err, "players.name") {
return User{}, NewBadRequestUserError("That player name is taken.")
} else if IsErrorUniqueFailedField(err, "players.uuid") {
return User{}, NewBadRequestUserError("That UUID is taken.")
} else if IsErrorPlayerNameTakenByUsername(err) {
return User{}, NewBadRequestUserError("That player name is in use as another user's username.")
} else {
return User{}, err
}
}
if invite != nil {
if err := tx.Delete(invite).Error; err != nil {
return User{}, err
}
}
if err := tx.Commit().Error; err != nil {
return User{}, err
}
if skinHash != nil {
err = app.WriteSkin(*skinHash, skinBuf)
if err != nil {
return user, NewBadRequestUserError("Error saving the skin.")
}
}
if capeHash != nil {
err = app.WriteCape(*capeHash, capeBuf)
if err != nil {
return user, NewBadRequestUserError("Error saving the cape.")
}
}
return user, nil
}
func (app *App) UpdateUser(
db *gorm.DB,
caller *User,
user User,
password *string,
isAdmin *bool,
isLocked *bool,
resetAPIToken bool,
preferredLanguage *string,
maxPlayerCount *int,
) (User, error) {
if caller == nil {
return User{}, NewBadRequestUserError("Caller cannot be null.")
}
callerIsAdmin := caller.IsAdmin
if user.UUID != caller.UUID && !callerIsAdmin {
return User{}, NewBadRequestUserError("You are not an admin.")
}
if password != nil {
if err := app.ValidatePassword(*password); err != nil {
return User{}, NewBadRequestUserError("Invalid password: %s", err)
}
passwordSalt := make([]byte, 16)
_, err := rand.Read(passwordSalt)
if err != nil {
return User{}, err
}
user.PasswordSalt = passwordSalt
passwordHash, err := HashPassword(*password, passwordSalt)
if err != nil {
return User{}, err
}
user.PasswordHash = passwordHash
}
if isAdmin != nil {
if !callerIsAdmin {
return User{}, NewBadRequestUserError("Cannot change admin status of user without having admin privileges yourself.")
}
user.IsAdmin = *isAdmin
}
if preferredLanguage != nil {
if !IsValidPreferredLanguage(*preferredLanguage) {
return User{}, NewBadRequestUserError("Invalid preferred language.")
}
user.PreferredLanguage = *preferredLanguage
}
if resetAPIToken {
apiToken, err := MakeAPIToken()
if err != nil {
return User{}, err
}
user.APIToken = apiToken
}
if maxPlayerCount != nil {
if !callerIsAdmin {
return User{}, NewBadRequestUserError("Cannot set a max player count without admin privileges.")
}
err := app.ValidateMaxPlayerCount(*maxPlayerCount)
if err != nil {
return User{}, NewBadRequestUserError("Invalid max player count: %s", err)
}
user.MaxPlayerCount = *maxPlayerCount
}
err := db.Transaction(func(tx *gorm.DB) error {
if isLocked != nil {
if !callerIsAdmin {
return NewBadRequestUserError("Cannot change locked status of user without having admin privileges yourself.")
}
err := app.SetIsLocked(tx, &user, *isLocked)
if err != nil {
return err
}
}
if err := tx.Save(&user).Error; err != nil {
return err
}
return nil
})
if err != nil {
return User{}, err
}
return user, nil
}
func (app *App) SetIsLocked(db *gorm.DB, user *User, isLocked bool) error {
user.IsLocked = isLocked
if isLocked {
user.BrowserToken = MakeNullString(nil)
err := app.InvalidateUser(db, user)
if err != nil {
return err
}
}
return nil
}
func (app *App) DeleteUser(caller *User, user *User) error {
if !caller.IsAdmin && caller.UUID != user.UUID {
return NewForbiddenUserError("You are not an admin.")
}
oldSkinHashes := make([]*string, 0, len(user.Players))
oldCapeHashes := make([]*string, 0, len(user.Players))
var players []Player
if err := app.DB.Where("user_uuid = ?", user.UUID).Find(&players).Error; err != nil {
return err
}
for _, player := range players {
oldSkinHashes = append(oldSkinHashes, UnmakeNullString(&player.SkinHash))
oldCapeHashes = append(oldCapeHashes, UnmakeNullString(&player.CapeHash))
}
if err := app.DB.Delete(user).Error; err != nil {
return err
}
for _, oldSkinHash := range oldSkinHashes {
err := app.DeleteSkinIfUnused(oldSkinHash)
if err != nil {
return err
}
}
for _, oldCapeHash := range oldCapeHashes {
err := app.DeleteCapeIfUnused(oldCapeHash)
if err != nil {
return err
}
}
return nil
}