mirror of
https://github.com/unmojang/drasl.git
synced 2025-09-08 22:56:49 -04:00
Lots of API cleanup, link to Swagger API docs
This commit is contained in:
parent
32b8546276
commit
5252317a53
2
Makefile
2
Makefile
@ -8,7 +8,7 @@ npm-install:
|
|||||||
npm install
|
npm install
|
||||||
|
|
||||||
swag:
|
swag:
|
||||||
$(SWAG) init --generalInfo api.go --output . --outputTypes json
|
$(SWAG) init --generalInfo api.go --output ./assets/ --outputTypes json
|
||||||
|
|
||||||
prebuild: npm-install swag
|
prebuild: npm-install swag
|
||||||
node esbuild.config.js
|
node esbuild.config.js
|
||||||
|
404
api.go
404
api.go
@ -11,6 +11,8 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -279,6 +281,14 @@ func (app *App) inviteToAPIInvite(invite *Invite) (APIInvite, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) APISwagger() func(c echo.Context) error {
|
||||||
|
swaggerPath := path.Join(app.Config.DataDirectory, "assets", "swagger.json")
|
||||||
|
swaggerBlob := Unwrap(os.ReadFile(swaggerPath))
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
return c.JSONBlob(http.StatusOK, swaggerBlob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// APIGetUsers godoc
|
// APIGetUsers godoc
|
||||||
//
|
//
|
||||||
// @Summary Get users
|
// @Summary Get users
|
||||||
@ -312,32 +322,10 @@ func (app *App) APIGetUsers() func(c echo.Context) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIGetSelf godoc
|
|
||||||
//
|
|
||||||
// @Summary Get own account
|
|
||||||
// @Description Get details of your own account
|
|
||||||
// @Tags users
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} APIUser
|
|
||||||
// @Failure 403 {object} APIError
|
|
||||||
// @Failure 429 {object} APIError
|
|
||||||
// @Failure 500 {object} APIError
|
|
||||||
// @Router /drasl/api/v2/user [get]
|
|
||||||
func (app *App) APIGetSelf() func(c echo.Context) error {
|
|
||||||
return app.withAPIToken(true, func(c echo.Context, user *User) error {
|
|
||||||
apiUser, err := app.userToAPIUser(user)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, apiUser)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIGetUser godoc
|
// APIGetUser godoc
|
||||||
//
|
//
|
||||||
// @Summary Get user by UUID
|
// @Summary Get user details
|
||||||
// @Description Get details of a user by their UUID. Requires admin privileges.
|
// @Description Get details of a user, either the calling user (GET /user) or the user with the specified UUID (GET /users/{uuid}). Getting details of another user requires admin privileges.
|
||||||
// @Tags users
|
// @Tags users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@ -349,23 +337,34 @@ func (app *App) APIGetSelf() func(c echo.Context) error {
|
|||||||
// @Failure 404 {object} APIError
|
// @Failure 404 {object} APIError
|
||||||
// @Failure 500 {object} APIError
|
// @Failure 500 {object} APIError
|
||||||
// @Router /drasl/api/v2/users/{uuid} [get]
|
// @Router /drasl/api/v2/users/{uuid} [get]
|
||||||
|
// @Router /drasl/api/v2/user [get]
|
||||||
func (app *App) APIGetUser() func(c echo.Context) error {
|
func (app *App) APIGetUser() func(c echo.Context) error {
|
||||||
return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
|
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||||
uuid_ := c.Param("uuid")
|
targetUser := caller
|
||||||
_, err := uuid.Parse(uuid_)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
|
||||||
}
|
|
||||||
|
|
||||||
var profileUser User
|
uuidParam := c.Param("uuid")
|
||||||
if err := app.DB.First(&profileUser, "uuid = ?", uuid_).Error; err != nil {
|
if uuidParam != "" {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if !caller.IsAdmin && (caller.UUID != uuidParam) {
|
||||||
return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
|
return NewUserError(http.StatusForbidden, "You are not authorized to access that user.")
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
|
_, err := uuid.Parse(uuidParam)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetUserStruct User
|
||||||
|
if err := app.DB.First(&targetUserStruct, "uuid = ?", uuidParam).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUser = &targetUserStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
apiUser, err := app.userToAPIUser(&profileUser)
|
apiUser, err := app.userToAPIUser(targetUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -382,21 +381,21 @@ type APICreateUserRequest struct {
|
|||||||
Username string `json:"username" example:"MyUsername"` // Username of the new user. Can be different from the user's player name.
|
Username string `json:"username" example:"MyUsername"` // Username of the new user. Can be different from the user's player name.
|
||||||
Password *string `json:"password" example:"hunter2"` // Plaintext password. Not needed if OIDCIdentitySpecs are supplied.
|
Password *string `json:"password" example:"hunter2"` // Plaintext password. Not needed if OIDCIdentitySpecs are supplied.
|
||||||
OIDCIdentitySpecs []APIOIDCIdentitySpec `json:"oidcIdentities"`
|
OIDCIdentitySpecs []APIOIDCIdentitySpec `json:"oidcIdentities"`
|
||||||
IsAdmin bool `json:"isAdmin" example:"true"` // Whether the user is an admin
|
IsAdmin bool `json:"isAdmin" example:"true"` // Whether the user is an admin
|
||||||
IsLocked bool `json:"isLocked" example:"false"` // Whether the user is locked (disabled)
|
IsLocked bool `json:"isLocked" example:"false"` // Whether the user is locked (disabled)
|
||||||
RequestAPIToken bool `json:"requestApiToken" example:"true"` // Whether to include an API token for the user in the response
|
RequestAPIToken bool `json:"requestApiToken" example:"true"` // Whether to include an API token for the user in the response
|
||||||
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the player of the new user. If omitted, a random UUID will be generated.
|
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the player of the new user. If omitted, a random UUID will be generated.
|
||||||
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new user's player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
|
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new user's player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
|
||||||
InviteCode *string `json:"inviteCode" example:"rqjJwh0yMjO"` // Invite code to use. Optional even if the `RequireInvite` configuration option is set; admin API users can bypass `RequireInvite`.
|
InviteCode *string `json:"inviteCode" example:"rqjJwh0yMjO"` // Invite code to use. Optional even if the `RequireInvite` configuration option is set; admin API users can bypass `RequireInvite`.
|
||||||
PlayerName *string `json:"playerName" example:"MyPlayerName"` // Optional. Player name. Can be different from the user's username. If omitted, the user's username will be used.
|
PlayerName *string `json:"playerName" example:"MyPlayerName"` // Optional. Player name. Can be different from the user's username. If omitted, the user's username will be used.
|
||||||
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
|
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
|
||||||
PreferredLanguage *string `json:"preferredLanguage" example:"en"` // Optional. One of the two-letter codes in https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html. Used by Minecraft. If omitted, the value of the `DefaultPreferredLanguage` configuration option will be used.
|
PreferredLanguage *string `json:"preferredLanguage" example:"en"` // Optional. One of the two-letter codes in https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html. Used by Minecraft. If omitted, the value of the `DefaultPreferredLanguage` configuration option will be used.
|
||||||
SkinModel *string `json:"skinModel" example:"classic"` // Skin model. Either "classic" or "slim". If omitted, `"classic"` will be assumed.
|
SkinModel *string `json:"skinModel" example:"classic"` // Skin model. Either "classic" or "slim". If omitted, `"classic"` will be assumed.
|
||||||
SkinBase64 *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"` // Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.
|
SkinBase64 *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.
|
||||||
SkinURL *string `json:"skinUrl" example:"https://example.com/skin.png"` // Optional. URL to skin file. Do not specify both `skinBase64` and `skinUrl`.
|
SkinURL *string `json:"skinUrl" example:"https://example.com/skin.png"` // Optional. URL to skin file. Do not specify both `skinBase64` and `skinUrl`.
|
||||||
CapeBase64 *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"` // Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.
|
CapeBase64 *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."` // Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.
|
||||||
CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.
|
CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.
|
||||||
MaxPlayerCount *int `json:"maxPlayerCount" example:"3"` // Optional. Maximum number of players a user is allowed to own. -1 means unlimited players. -2 means use the default configured value.
|
MaxPlayerCount *int `json:"maxPlayerCount" example:"3"` // Optional. Maximum number of players a user is allowed to own. -1 means unlimited players. -2 means use the default configured value.
|
||||||
}
|
}
|
||||||
|
|
||||||
type APICreateUserResponse struct {
|
type APICreateUserResponse struct {
|
||||||
@ -407,7 +406,7 @@ type APICreateUserResponse struct {
|
|||||||
// APICreateUser godoc
|
// APICreateUser godoc
|
||||||
//
|
//
|
||||||
// @Summary Create a new user
|
// @Summary Create a new user
|
||||||
// @Description Create a new user. Requires admin privileges.
|
// @Description Register and create a new user. Can be called without an API token.
|
||||||
// @Tags users
|
// @Tags users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@ -487,19 +486,19 @@ func (app *App) APICreateUser() func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type APIUpdateUserRequest struct {
|
type APIUpdateUserRequest struct {
|
||||||
Password *string `json:"password" example:"hunter2"` // Optional. New plaintext password
|
Password *string `json:"password" example:"hunter2"` // Optional. New plaintext password
|
||||||
IsAdmin *bool `json:"isAdmin" example:"true"` // Optional. Pass`true` to grant, `false` to revoke admin privileges.
|
IsAdmin *bool `json:"isAdmin" example:"true"` // Optional. Pass`true` to grant, `false` to revoke admin privileges.
|
||||||
IsLocked *bool `json:"isLocked" example:"false"` // Optional. Pass `true` to lock (disable), `false` to unlock user.
|
IsLocked *bool `json:"isLocked" example:"false"` // Optional. Pass `true` to lock (disable), `false` to unlock user.
|
||||||
ResetAPIToken bool `json:"resetApiToken" example:"true"` // Pass `true` to reset the user's API token
|
ResetAPIToken bool `json:"resetApiToken" example:"false"` // Pass `true` to reset the user's API token
|
||||||
ResetMinecraftToken bool `json:"resetMinecraftToken" example:"true"` // Pass `true` to reset the user's Minecraft token
|
ResetMinecraftToken bool `json:"resetMinecraftToken" example:"false"` // Pass `true` to reset the user's Minecraft token
|
||||||
PreferredLanguage *string `json:"preferredLanguage" example:"en"` // Optional. One of the two-letter codes in https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html. Used by Minecraft.
|
PreferredLanguage *string `json:"preferredLanguage" example:"en"` // Optional. One of the two-letter codes in https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html. Used by Minecraft.
|
||||||
MaxPlayerCount *int `json:"maxPlayerCount" example:"3"` // Optional. Maximum number of players a user is allowed to own. -1 means unlimited players. -2 means use the default configured value.
|
MaxPlayerCount *int `json:"maxPlayerCount" example:"3"` // Optional. Maximum number of players a user is allowed to own. -1 means unlimited players. -2 means use the default configured value.
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIUpdateUser godoc
|
// APIUpdateUser godoc
|
||||||
//
|
//
|
||||||
// @Summary Update a user
|
// @Summary Update a user
|
||||||
// @Description Update an existing user. Requires admin privileges.
|
// @Description Update an existing user, either the calling user (PATCH /user) or the user with the specified UUID (PATCH /users/{uuid}). Updating another user requires admin privileges.
|
||||||
// @Tags users
|
// @Tags users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@ -509,79 +508,44 @@ type APIUpdateUserRequest struct {
|
|||||||
// @Failure 400 {object} APIError
|
// @Failure 400 {object} APIError
|
||||||
// @Failure 403 {object} APIError
|
// @Failure 403 {object} APIError
|
||||||
// @Failure 404 {object} APIError
|
// @Failure 404 {object} APIError
|
||||||
|
// @Failure 429 {object} APIError
|
||||||
// @Failure 500 {object} APIError
|
// @Failure 500 {object} APIError
|
||||||
// @Router /drasl/api/v2/users/{uuid} [patch]
|
// @Router /drasl/api/v2/users/{uuid} [patch]
|
||||||
|
// @Router /drasl/api/v2/user [patch]
|
||||||
func (app *App) APIUpdateUser() func(c echo.Context) error {
|
func (app *App) APIUpdateUser() func(c echo.Context) error {
|
||||||
return app.withAPITokenAdmin(func(c echo.Context, caller *User) error {
|
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||||
req := new(APIUpdateUserRequest)
|
req := new(APIUpdateUserRequest)
|
||||||
if err := c.Bind(req); err != nil {
|
if err := c.Bind(req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid_ := c.Param("uuid")
|
targetUser := caller
|
||||||
_, err := uuid.Parse(uuid_)
|
uuidParam := c.Param("uuid")
|
||||||
if err != nil {
|
if uuidParam != "" {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
if !caller.IsAdmin && (caller.UUID != uuidParam) {
|
||||||
}
|
return NewUserError(http.StatusForbidden, "You are not authorized to update that user.")
|
||||||
|
|
||||||
var targetUser User
|
|
||||||
if err := app.DB.First(&targetUser, "uuid = ?", uuid_).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
|
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
|
_, err := uuid.Parse(uuidParam)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetUserStruct User
|
||||||
|
if err := app.DB.First(&targetUserStruct, "uuid = ?", uuidParam).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUser = &targetUserStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedUser, err := app.UpdateUser(
|
updatedUser, err := app.UpdateUser(
|
||||||
app.DB,
|
app.DB,
|
||||||
caller,
|
caller,
|
||||||
targetUser, // user
|
*targetUser,
|
||||||
req.Password,
|
|
||||||
req.IsAdmin,
|
|
||||||
req.IsLocked,
|
|
||||||
req.ResetAPIToken,
|
|
||||||
req.ResetMinecraftToken,
|
|
||||||
req.PreferredLanguage,
|
|
||||||
req.MaxPlayerCount,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
apiUser, err := app.userToAPIUser(&updatedUser)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, apiUser)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIUpdateSelf godoc
|
|
||||||
//
|
|
||||||
// @Summary Update own account
|
|
||||||
// @Description Update details of your own account.
|
|
||||||
// @Tags users
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param APIUpdateUserRequest body APIUpdateUserRequest true "New properties of the user"
|
|
||||||
// @Success 200 {object} APIUser
|
|
||||||
// @Failure 400 {object} APIError
|
|
||||||
// @Failure 403 {object} APIError
|
|
||||||
// @Failure 404 {object} APIError
|
|
||||||
// @Failure 429 {object} APIError
|
|
||||||
// @Failure 500 {object} APIError
|
|
||||||
// @Router /drasl/api/v2/user [patch]
|
|
||||||
func (app *App) APIUpdateSelf() func(c echo.Context) error {
|
|
||||||
return app.withAPIToken(true, func(c echo.Context, user *User) error {
|
|
||||||
req := new(APIUpdateUserRequest)
|
|
||||||
if err := c.Bind(req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedUser, err := app.UpdateUser(
|
|
||||||
app.DB,
|
|
||||||
user, // caller
|
|
||||||
*user,
|
|
||||||
req.Password,
|
req.Password,
|
||||||
req.IsAdmin,
|
req.IsAdmin,
|
||||||
req.IsLocked,
|
req.IsLocked,
|
||||||
@ -605,7 +569,7 @@ func (app *App) APIUpdateSelf() func(c echo.Context) error {
|
|||||||
// APIDeleteUser godoc
|
// APIDeleteUser godoc
|
||||||
//
|
//
|
||||||
// @Summary Delete user
|
// @Summary Delete user
|
||||||
// @Description Delete a user. This action cannot be undone. Requires admin privileges.
|
// @Description Delete a user, either the calling user (DELETE /user) or the user with the specified UUID (DELETE /users/{uuid}). This action cannot be undone. Deleting another user requires admin privileges.
|
||||||
// @Tags users
|
// @Tags users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@ -614,45 +578,34 @@ func (app *App) APIUpdateSelf() func(c echo.Context) error {
|
|||||||
// @Failure 403 {object} APIError
|
// @Failure 403 {object} APIError
|
||||||
// @Failure 404 {object} APIError
|
// @Failure 404 {object} APIError
|
||||||
// @Failure 500 {object} APIError
|
// @Failure 500 {object} APIError
|
||||||
|
// @Router /drasl/api/v2/user [delete]
|
||||||
// @Router /drasl/api/v2/users/{uuid} [delete]
|
// @Router /drasl/api/v2/users/{uuid} [delete]
|
||||||
func (app *App) APIDeleteUser() func(c echo.Context) error {
|
func (app *App) APIDeleteUser() func(c echo.Context) error {
|
||||||
return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
|
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||||
uuid_ := c.Param("uuid")
|
targetUser := caller
|
||||||
_, err := uuid.Parse(uuid_)
|
uuidParam := c.Param("uuid")
|
||||||
if err != nil {
|
if uuidParam != "" {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
if !caller.IsAdmin && (caller.UUID != uuidParam) {
|
||||||
|
return NewUserError(http.StatusForbidden, "You are not authorized to update that user.")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := uuid.Parse(uuidParam)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetUserStruct User
|
||||||
|
if err := app.DB.First(&targetUserStruct, "uuid = ?", uuidParam).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUser = &targetUserStruct
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetUser User
|
err := app.DeleteUser(caller, targetUser)
|
||||||
result := app.DB.First(&targetUser, "uuid = ?", uuid_)
|
|
||||||
if result.Error != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = app.DeleteUser(user, &targetUser)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.NoContent(http.StatusNoContent)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIDeleteSelf godoc
|
|
||||||
//
|
|
||||||
// @Summary Delete own account
|
|
||||||
// @Description Delete your own account. This action cannot be undone.
|
|
||||||
// @Tags users
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 204
|
|
||||||
// @Failure 401 {object} APIError
|
|
||||||
// @Failure 429 {object} APIError
|
|
||||||
// @Failure 500 {object} APIError
|
|
||||||
// @Router /drasl/api/v2/user [delete]
|
|
||||||
func (app *App) APIDeleteSelf() func(c echo.Context) error {
|
|
||||||
return app.withAPIToken(true, func(c echo.Context, user *User) error {
|
|
||||||
err := app.DeleteUser(user, user)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -738,23 +691,23 @@ func (app *App) APIGetPlayers() func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type APICreatePlayerRequest struct {
|
type APICreatePlayerRequest struct {
|
||||||
Name string `json:"name" example:"MyPlayerName"` // Player name.
|
Name string `json:"name" example:"MyPlayerName"` // Player name.
|
||||||
UserUUID *string `json:"userUuid" example:"f9b9af62-da83-4ec7-aeea-de48c621822c"` // Optional. UUID of the owning user. If omitted, the player will be added to the calling user's account.
|
UserUUID *string `json:"userUuid" example:"f9b9af62-da83-4ec7-aeea-de48c621822c"` // Optional. UUID of the owning user. If omitted, the player will be added to the calling user's account.
|
||||||
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.
|
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.
|
||||||
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
|
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
|
||||||
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
|
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
|
||||||
ChallengeToken *string `json:"challengeToken" example:"a484528c86725b7b5ac3b47e2f973efd"` // Challenge token to use when verifying ownership of another player. Call /drasl/api/v2/challenge-skin first to get a skin and token. See `RequireSkinVerification` in configuration.md.
|
ChallengeToken *string `json:"challengeToken" example:"iK1B2FzLc5fMP94VmUR3KC"` // Challenge token to use when verifying ownership of another player. Call /drasl/api/v2/challenge-skin first to get a skin and token. See `RequireSkinVerification` in configuration.md.
|
||||||
SkinModel *string `json:"skinModel" example:"classic"` // Skin model. Either "classic" or "slim". If omitted, `"classic"` will be assumed.
|
SkinModel *string `json:"skinModel" example:"classic"` // Skin model. Either "classic" or "slim". If omitted, `"classic"` will be assumed.
|
||||||
SkinBase64 *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"` // Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.
|
SkinBase64 *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.
|
||||||
SkinURL *string `json:"skinUrl" example:"https://example.com/skin.png"` // Optional. URL to skin file. Do not specify both `skinBase64` and `skinUrl`.
|
SkinURL *string `json:"skinUrl" example:"https://example.com/skin.png"` // Optional. URL to skin file. Do not specify both `skinBase64` and `skinUrl`.
|
||||||
CapeBase64 *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"` // Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.
|
CapeBase64 *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."` // Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.
|
||||||
CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.
|
CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.
|
||||||
}
|
}
|
||||||
|
|
||||||
// APICreatePlayer godoc
|
// APICreatePlayer godoc
|
||||||
//
|
//
|
||||||
// @Summary Create a new player
|
// @Summary Create a new player
|
||||||
// @Description Create a new player
|
// @Description Create a new player for an existing Drasl user.
|
||||||
// @Tags players
|
// @Tags players
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@ -817,15 +770,15 @@ func (app *App) APICreatePlayer() func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type APIUpdatePlayerRequest struct {
|
type APIUpdatePlayerRequest struct {
|
||||||
Name *string `json:"name" example:"MyPlayerName"` // Optional. New player name. Can be different from the user's username.
|
Name *string `json:"name" example:"MyPlayerName"` // Optional. New player name. Can be different from the user's username.
|
||||||
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Optional. New fallback player. Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
|
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Optional. New fallback player. Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
|
||||||
SkinModel *string `json:"skinModel" example:"classic"` // Optional. New skin model. Either "classic" or "slim".
|
SkinModel *string `json:"skinModel" example:"classic"` // Optional. New skin model. Either "classic" or "slim".
|
||||||
SkinBase64 *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"`
|
SkinBase64 *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Optional. Base64-encoded skin PNG. Example value truncated for brevity.
|
||||||
SkinURL *string `json:"skinUrl" example:"https://example.com/skin.png"` // Optional. URL to skin file
|
SkinURL *string `json:"skinUrl" example:"https://example.com/skin.png"` // Optional. URL to skin file
|
||||||
DeleteSkin bool `json:"deleteSkin"` // Pass `true` to delete the user's existing skin
|
DeleteSkin bool `json:"deleteSkin"` // Pass `true` to delete the user's existing skin
|
||||||
CapeBase64 *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"` // Optional. Base64-encoded cape PNG. Example value truncated for brevity.
|
CapeBase64 *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."` // Optional. Base64-encoded cape PNG. Example value truncated for brevity.
|
||||||
CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file
|
CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file
|
||||||
DeleteCape bool `json:"deleteCape"` // Pass `true` to delete the user's existing cape
|
DeleteCape bool `json:"deleteCape"` // Pass `true` to delete the user's existing cape
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIUpdatePlayer godoc
|
// APIUpdatePlayer godoc
|
||||||
@ -905,7 +858,7 @@ func (app *App) APIUpdatePlayer() func(c echo.Context) error {
|
|||||||
// APIDeletePlayer godoc
|
// APIDeletePlayer godoc
|
||||||
//
|
//
|
||||||
// @Summary Delete player
|
// @Summary Delete player
|
||||||
// @Description Delete a player. This action cannot be undone.
|
// @Description Delete a player. This action cannot be undone. Requires admin privileges unless you own the player.
|
||||||
// @Tags players
|
// @Tags players
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@ -942,9 +895,8 @@ func (app *App) APIDeletePlayer() func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type APICreateOIDCIdentityRequest struct {
|
type APICreateOIDCIdentityRequest struct {
|
||||||
UserUUID *string `json:"userUUID" example:"f9b9af62-da83-4ec7-aeea-de48c621822c"`
|
Issuer string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
|
||||||
Issuer string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
|
Subject string `json:"subject" example:"f85f8c18-9bdf-49ad-a76e-719f9ba3ed25"`
|
||||||
Subject string `json:"subject" example:"f85f8c18-9bdf-49ad-a76e-719f9ba3ed25"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// APICreateOIDCIdentity godoc
|
// APICreateOIDCIdentity godoc
|
||||||
@ -953,12 +905,16 @@ type APICreateOIDCIdentityRequest struct {
|
|||||||
// @Tags users
|
// @Tags users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200
|
// @Param uuid path string true "User UUID"
|
||||||
// @Failure 400 {object} APIError
|
// @Param APICreateOIDCIdentityRequest body APICreateOIDCIdentityRequest true "OIDC identity to link to the user"
|
||||||
// @Failure 401 {object} APIError
|
// @Success 200 {object} APIOIDCIdentity
|
||||||
// @Failure 403 {object} APIError
|
// @Failure 400 {object} APIError
|
||||||
// @Failure 500 {object} APIError
|
// @Failure 401 {object} APIError
|
||||||
// @Router /drasl/api/v2/oidc-identities [post]
|
// @Failure 403 {object} APIError
|
||||||
|
// @Failure 404 {object} APIError
|
||||||
|
// @Failure 500 {object} APIError
|
||||||
|
// @Router /drasl/api/v2/user/oidc-identities [post]
|
||||||
|
// @Router /drasl/api/v2/users/{uuid}/oidc-identities [post]
|
||||||
func (app *App) APICreateOIDCIdentity() func(c echo.Context) error {
|
func (app *App) APICreateOIDCIdentity() func(c echo.Context) error {
|
||||||
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||||
req := new(APICreateOIDCIdentityRequest)
|
req := new(APICreateOIDCIdentityRequest)
|
||||||
@ -967,8 +923,13 @@ func (app *App) APICreateOIDCIdentity() func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userUUID := caller.UUID
|
userUUID := caller.UUID
|
||||||
if req.UserUUID != nil {
|
uuidParam := c.Param("uuid")
|
||||||
userUUID = *req.UserUUID
|
if uuidParam != "" {
|
||||||
|
_, err := uuid.Parse(uuidParam)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||||
|
}
|
||||||
|
userUUID = uuidParam
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcIdentity, err := app.CreateOIDCIdentity(caller, userUUID, req.Issuer, req.Subject)
|
oidcIdentity, err := app.CreateOIDCIdentity(caller, userUUID, req.Issuer, req.Subject)
|
||||||
@ -985,8 +946,7 @@ func (app *App) APICreateOIDCIdentity() func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type APIDeleteOIDCIdentityRequest struct {
|
type APIDeleteOIDCIdentityRequest struct {
|
||||||
UserUUID *string `json:"userUUID" example:"f9b9af62-da83-4ec7-aeea-de48c621822c"`
|
Issuer string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
|
||||||
Issuer string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIDeleteOIDCIdentity godoc
|
// APIDeleteOIDCIdentity godoc
|
||||||
@ -995,12 +955,16 @@ type APIDeleteOIDCIdentityRequest struct {
|
|||||||
// @Tags users
|
// @Tags users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
// @Param uuid path string true "User UUID"
|
||||||
|
// @Param APIDeleteOIDCIdentityRequest body APIDeleteOIDCIdentityRequest true "Issuer of the OIDC provider to unlink from the user"
|
||||||
// @Success 204
|
// @Success 204
|
||||||
|
// @Failure 400 {object} APIError
|
||||||
// @Failure 401 {object} APIError
|
// @Failure 401 {object} APIError
|
||||||
// @Failure 403 {object} APIError
|
// @Failure 403 {object} APIError
|
||||||
// @Failure 404 {object} APIError
|
// @Failure 404 {object} APIError
|
||||||
// @Failure 500 {object} APIError
|
// @Failure 500 {object} APIError
|
||||||
// @Router /drasl/api/v2/oidc-identities [delete]
|
// @Router /drasl/api/v2/user/oidc-identities [delete]
|
||||||
|
// @Router /drasl/api/v2/users/{uuid}/oidc-identities [delete]
|
||||||
func (app *App) APIDeleteOIDCIdentity() func(c echo.Context) error {
|
func (app *App) APIDeleteOIDCIdentity() func(c echo.Context) error {
|
||||||
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||||
req := new(APIDeleteOIDCIdentityRequest)
|
req := new(APIDeleteOIDCIdentityRequest)
|
||||||
@ -1009,8 +973,13 @@ func (app *App) APIDeleteOIDCIdentity() func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userUUID := caller.UUID
|
userUUID := caller.UUID
|
||||||
if req.UserUUID != nil {
|
uuidParam := c.Param("uuid")
|
||||||
userUUID = *req.UserUUID
|
if uuidParam != "" {
|
||||||
|
_, err := uuid.Parse(uuidParam)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||||
|
}
|
||||||
|
userUUID = uuidParam
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcProvider, ok := app.OIDCProvidersByIssuer[req.Issuer]
|
oidcProvider, ok := app.OIDCProvidersByIssuer[req.Issuer]
|
||||||
@ -1113,31 +1082,41 @@ func (app *App) APIDeleteInvite() func(c echo.Context) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type APIGetChallengeSkinRequest struct {
|
||||||
|
PlayerName string `json:"playerName" example:"Notch"`
|
||||||
|
}
|
||||||
|
|
||||||
type APIChallenge struct {
|
type APIChallenge struct {
|
||||||
ChallengeSkinBase64 string `json:"challengeSkinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"` // Base64-encoded skin PNG. Example value truncated for brevity.
|
ChallengeSkinBase64 string `json:"challengeSkinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Base64-encoded skin PNG. Example value truncated for brevity.
|
||||||
ChallengeToken string `json:"challengeToken" example:"414cc23d6eebee3b17a453d6b9800be3e5a4627fd3b0ee54d7c37d03b2596e44"` // Challenge token that must be passed when registering with a challenge skin
|
ChallengeToken string `json:"challengeToken" example:"iK1B2FzLc5fMP94VmUR3KC"` // Challenge token that must be passed when registering with a challenge skin
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIGetChallengeSkin godoc
|
// APIGetChallengeSkin godoc
|
||||||
//
|
//
|
||||||
// @Summary Get a challenge skin/token
|
// @Summary Get a challenge skin/token
|
||||||
// @Description Get a challenge skin and challenge token for a username, for registration purposes. See the `RequireSkinVerification` configuration option.
|
// @Description Get a challenge skin and challenge token for a player name for registration or player creation purposes. See the `ImportExistingPlayer.RequireSkinVerification` configuration option.
|
||||||
// @Tags users, players
|
// @Tags users, players
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} APIChallenge
|
// @Param APIGetChallengeSkinRequest body APIGetChallengeSkinRequest true "Player name for the challenge skin"
|
||||||
// @Failure 500 {object} APIError
|
// @Success 200 {object} APIChallenge
|
||||||
|
// @Success 400 {object} APIError
|
||||||
|
// @Failure 500 {object} APIError
|
||||||
// @Router /drasl/api/v2/challenge-skin [get]
|
// @Router /drasl/api/v2/challenge-skin [get]
|
||||||
func (app *App) APIGetChallengeSkin() func(c echo.Context) error {
|
func (app *App) APIGetChallengeSkin() func(c echo.Context) error {
|
||||||
return app.withAPIToken(false, func(c echo.Context, _ *User) error {
|
return app.withAPIToken(false, func(c echo.Context, _ *User) error {
|
||||||
username := c.QueryParam("username")
|
req := new(APIGetChallengeSkinRequest)
|
||||||
|
if err := c.Bind(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
playerName := req.PlayerName
|
||||||
|
|
||||||
challengeToken, err := MakeChallengeToken()
|
challengeToken, err := MakeChallengeToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
challengeSkinBytes, err := app.GetChallengeSkin(username, challengeToken)
|
challengeSkinBytes, err := app.GetChallengeSkin(playerName, challengeToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1151,8 +1130,8 @@ func (app *App) APIGetChallengeSkin() func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type APILoginResponse struct {
|
type APILoginResponse struct {
|
||||||
User APIUser `json:"user"` // The logged-in user
|
User APIUser `json:"user"` // The logged-in user
|
||||||
APIToken string `json:"token" example:"Bq608AtLeG7emJOdvXHYxL"` // An API token for the user
|
APIToken string `json:"apiToken" example:"Bq608AtLeG7emJOdvXHYxL"` // An API token for the user
|
||||||
}
|
}
|
||||||
|
|
||||||
type APILoginRequest struct {
|
type APILoginRequest struct {
|
||||||
@ -1162,24 +1141,25 @@ type APILoginRequest struct {
|
|||||||
|
|
||||||
// APILogin godoc
|
// APILogin godoc
|
||||||
//
|
//
|
||||||
// @Summary Get a token
|
// @Summary Authenticate and receive an API token
|
||||||
// @Description Get a token for login credentials.
|
// @Description Authenticate with username and password and receive an API token. Can be called without an API token.
|
||||||
// @Tags users, auth
|
// @Tags users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} APILoginResponse
|
// @Param APILoginRequest body APILoginRequest true "Drasl username and password"
|
||||||
// @Failure 400 {object} APIError
|
// @Success 200 {object} APILoginResponse
|
||||||
// @Failure 401 {object} APIError
|
// @Failure 400 {object} APIError
|
||||||
// @Failure 403 {object} APIError
|
// @Failure 401 {object} APIError
|
||||||
// @Failure 429 {object} APIError
|
// @Failure 403 {object} APIError
|
||||||
// @Failure 500 {object} APIError
|
// @Failure 429 {object} APIError
|
||||||
|
// @Failure 500 {object} APIError
|
||||||
// @Router /drasl/api/v2/login [post]
|
// @Router /drasl/api/v2/login [post]
|
||||||
func (app *App) APILogin() func(c echo.Context) error {
|
func (app *App) APILogin() func(c echo.Context) error {
|
||||||
return app.withAPIToken(false, func(c echo.Context, _ *User) error {
|
return app.withAPIToken(false, func(c echo.Context, _ *User) error {
|
||||||
var req APILoginRequest
|
var req APILoginRequest
|
||||||
err := c.Bind(&req)
|
err := c.Bind(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformed JSON request")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := app.AuthenticateUser(req.Username, req.Password)
|
user, err := app.AuthenticateUser(req.Username, req.Password)
|
||||||
|
90
api_test.go
90
api_test.go
@ -1,12 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -302,12 +302,14 @@ func (ts *TestSuite) testAPIGetChallengeSkin(t *testing.T) {
|
|||||||
username := "user"
|
username := "user"
|
||||||
user, _ := ts.CreateTestUser(t, ts.App, ts.Server, username)
|
user, _ := ts.CreateTestUser(t, ts.App, ts.Server, username)
|
||||||
|
|
||||||
ts.Get(t, ts.Server, DRASL_API_PREFIX+"/challenge-skin", nil, &user.APIToken)
|
payload := APIGetChallengeSkinRequest{
|
||||||
req := httptest.NewRequest(http.MethodGet, DRASL_API_PREFIX+"/challenge-skin", nil)
|
PlayerName: "foo",
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
req := httptest.NewRequest(http.MethodGet, DRASL_API_PREFIX+"/challenge-skin", bytes.NewBuffer(body))
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
req.Header.Add("Authorization", "Bearer "+user.APIToken)
|
req.Header.Add("Authorization", "Bearer "+user.APIToken)
|
||||||
req.URL.RawQuery = url.Values{
|
|
||||||
"username": {"foo"},
|
|
||||||
}.Encode()
|
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
ts.Server.ServeHTTP(rec, req)
|
ts.Server.ServeHTTP(rec, req)
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
@ -616,11 +618,10 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// admin should be able to create OIDC identities for themself
|
// admin should be able to create OIDC identities for themself
|
||||||
payload := APICreateOIDCIdentityRequest{
|
payload := APICreateOIDCIdentityRequest{
|
||||||
UserUUID: &admin.UUID,
|
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
Subject: provider1Subject1,
|
||||||
Subject: provider1Subject1,
|
|
||||||
}
|
}
|
||||||
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &admin.APIToken)
|
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/users/"+admin.UUID+"/oidc-identities", payload, nil, &admin.APIToken)
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
var apiOIDCIdentity APIOIDCIdentity
|
var apiOIDCIdentity APIOIDCIdentity
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiOIDCIdentity))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiOIDCIdentity))
|
||||||
@ -638,7 +639,7 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||||
Subject: provider2Subject1,
|
Subject: provider2Subject1,
|
||||||
}
|
}
|
||||||
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &admin.APIToken)
|
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/users/"+admin.UUID+"/oidc-identities", payload, nil, &admin.APIToken)
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
var apiOIDCIdentity APIOIDCIdentity
|
var apiOIDCIdentity APIOIDCIdentity
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiOIDCIdentity))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiOIDCIdentity))
|
||||||
@ -648,11 +649,10 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// admin should be able to create OIDC identities for other users
|
// admin should be able to create OIDC identities for other users
|
||||||
payload := APICreateOIDCIdentityRequest{
|
payload := APICreateOIDCIdentityRequest{
|
||||||
UserUUID: &user.UUID,
|
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
Subject: provider1Subject2,
|
||||||
Subject: provider1Subject2,
|
|
||||||
}
|
}
|
||||||
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &admin.APIToken)
|
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/users/"+user.UUID+"/oidc-identities", payload, nil, &admin.APIToken)
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
var apiOIDCIdentity APIOIDCIdentity
|
var apiOIDCIdentity APIOIDCIdentity
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiOIDCIdentity))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiOIDCIdentity))
|
||||||
@ -662,11 +662,10 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Duplicate issuer and subject should fail
|
// Duplicate issuer and subject should fail
|
||||||
payload := APICreateOIDCIdentityRequest{
|
payload := APICreateOIDCIdentityRequest{
|
||||||
UserUUID: &admin.UUID,
|
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
Subject: provider1Subject1,
|
||||||
Subject: provider1Subject1,
|
|
||||||
}
|
}
|
||||||
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &admin.APIToken)
|
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/users/"+admin.UUID+"/oidc-identities", payload, nil, &admin.APIToken)
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
var apiError APIError
|
var apiError APIError
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
||||||
@ -675,11 +674,10 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Duplicate issuer on the same user should fail
|
// Duplicate issuer on the same user should fail
|
||||||
payload := APICreateOIDCIdentityRequest{
|
payload := APICreateOIDCIdentityRequest{
|
||||||
UserUUID: &admin.UUID,
|
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
Subject: provider1Subject3,
|
||||||
Subject: provider1Subject3,
|
|
||||||
}
|
}
|
||||||
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &admin.APIToken)
|
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/users/"+admin.UUID+"/oidc-identities", payload, nil, &admin.APIToken)
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
var apiError APIError
|
var apiError APIError
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
||||||
@ -688,11 +686,10 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Non-admin should not be able to link an OIDC identity for another user
|
// Non-admin should not be able to link an OIDC identity for another user
|
||||||
payload := APICreateOIDCIdentityRequest{
|
payload := APICreateOIDCIdentityRequest{
|
||||||
UserUUID: &admin.UUID,
|
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
Subject: provider2Subject3,
|
||||||
Subject: provider2Subject3,
|
|
||||||
}
|
}
|
||||||
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &user.APIToken)
|
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/users/"+admin.UUID+"/oidc-identities", payload, nil, &user.APIToken)
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
var apiError APIError
|
var apiError APIError
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
||||||
@ -701,11 +698,10 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Non-admin should be able to link an OIDC identity for themself
|
// Non-admin should be able to link an OIDC identity for themself
|
||||||
payload := APICreateOIDCIdentityRequest{
|
payload := APICreateOIDCIdentityRequest{
|
||||||
UserUUID: &user.UUID,
|
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
Subject: provider2Subject2,
|
||||||
Subject: provider2Subject2,
|
|
||||||
}
|
}
|
||||||
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &user.APIToken)
|
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/users/"+user.UUID+"/oidc-identities", payload, nil, &user.APIToken)
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
var apiOIDCIdentity APIOIDCIdentity
|
var apiOIDCIdentity APIOIDCIdentity
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiOIDCIdentity))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiOIDCIdentity))
|
||||||
@ -715,29 +711,26 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// admin should be able to delete OIDC identity for other users
|
// admin should be able to delete OIDC identity for other users
|
||||||
payload := APIDeleteOIDCIdentityRequest{
|
payload := APIDeleteOIDCIdentityRequest{
|
||||||
UserUUID: &user.UUID,
|
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
|
||||||
}
|
}
|
||||||
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &admin.APIToken)
|
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/users/"+user.UUID+"/oidc-identities", payload, nil, &admin.APIToken)
|
||||||
assert.Equal(t, http.StatusNoContent, rec.Code)
|
assert.Equal(t, http.StatusNoContent, rec.Code)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// Add the identity back for future tests...
|
// Add the identity back for future tests...
|
||||||
payload := APICreateOIDCIdentityRequest{
|
payload := APICreateOIDCIdentityRequest{
|
||||||
UserUUID: &user.UUID,
|
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
Subject: provider1Subject2,
|
||||||
Subject: provider1Subject2,
|
|
||||||
}
|
}
|
||||||
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &admin.APIToken)
|
rec := ts.PostJSON(t, ts.Server, DRASL_API_PREFIX+"/users/"+user.UUID+"/oidc-identities", payload, nil, &admin.APIToken)
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// Non-admin user should not be able to delete OIDC identity for other users
|
// Non-admin user should not be able to delete OIDC identity for other users
|
||||||
payload := APIDeleteOIDCIdentityRequest{
|
payload := APIDeleteOIDCIdentityRequest{
|
||||||
UserUUID: &admin.UUID,
|
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
|
||||||
}
|
}
|
||||||
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &user.APIToken)
|
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/users/"+admin.UUID+"/oidc-identities", payload, nil, &user.APIToken)
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
var apiError APIError
|
var apiError APIError
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
||||||
@ -746,19 +739,17 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Non-admin user should be able to delete OIDC identity for themself
|
// Non-admin user should be able to delete OIDC identity for themself
|
||||||
payload := APIDeleteOIDCIdentityRequest{
|
payload := APIDeleteOIDCIdentityRequest{
|
||||||
UserUUID: &user.UUID,
|
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
|
||||||
}
|
}
|
||||||
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &user.APIToken)
|
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/users/"+user.UUID+"/oidc-identities", payload, nil, &user.APIToken)
|
||||||
assert.Equal(t, http.StatusNoContent, rec.Code)
|
assert.Equal(t, http.StatusNoContent, rec.Code)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// Can't delete nonexistent OIDC identity
|
// Can't delete nonexistent OIDC identity
|
||||||
payload := APIDeleteOIDCIdentityRequest{
|
payload := APIDeleteOIDCIdentityRequest{
|
||||||
UserUUID: &user.UUID,
|
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
|
||||||
}
|
}
|
||||||
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &user.APIToken)
|
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/users/"+user.UUID+"/oidc-identities", payload, nil, &user.APIToken)
|
||||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||||
var apiError APIError
|
var apiError APIError
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
||||||
@ -767,10 +758,9 @@ func (ts *TestSuite) testAPICreateDeleteOIDCIdentity(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Can't delete last OIDC identity
|
// Can't delete last OIDC identity
|
||||||
payload := APIDeleteOIDCIdentityRequest{
|
payload := APIDeleteOIDCIdentityRequest{
|
||||||
UserUUID: &user.UUID,
|
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
|
||||||
}
|
}
|
||||||
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/oidc-identities", payload, nil, &user.APIToken)
|
rec := ts.Delete(t, ts.Server, DRASL_API_PREFIX+"/users/"+user.UUID+"/oidc-identities", payload, nil, &user.APIToken)
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
var apiError APIError
|
var apiError APIError
|
||||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"/drasl/api/v2/challenge-skin": {
|
"/drasl/api/v2/challenge-skin": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get a challenge skin and challenge token for a username, for registration purposes. See the `RequireSkinVerification` configuration option.",
|
"description": "Get a challenge skin and challenge token for a player name for registration or player creation purposes. See the `ImportExistingPlayer.RequireSkinVerification` configuration option.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -28,6 +28,17 @@
|
|||||||
"players"
|
"players"
|
||||||
],
|
],
|
||||||
"summary": "Get a challenge skin/token",
|
"summary": "Get a challenge skin/token",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Player name for the challenge skin",
|
||||||
|
"name": "APIGetChallengeSkinRequest",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIGetChallengeSkinRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@ -35,6 +46,12 @@
|
|||||||
"$ref": "#/definitions/main.APIChallenge"
|
"$ref": "#/definitions/main.APIChallenge"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -164,7 +181,7 @@
|
|||||||
},
|
},
|
||||||
"/drasl/api/v2/login": {
|
"/drasl/api/v2/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Get a token for login credentials.",
|
"description": "Authenticate with username and password and receive an API token. Can be called without an API token.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -172,10 +189,20 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"users",
|
"users"
|
||||||
"auth"
|
],
|
||||||
|
"summary": "Authenticate and receive an API token",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Drasl username and password",
|
||||||
|
"name": "APILoginRequest",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APILoginRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"summary": "Get a token",
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@ -216,90 +243,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/drasl/api/v2/oidc-identities": {
|
|
||||||
"post": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"users"
|
|
||||||
],
|
|
||||||
"summary": "Link an OIDC identity to a user",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK"
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/main.APIError"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/main.APIError"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/main.APIError"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/main.APIError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"users"
|
|
||||||
],
|
|
||||||
"summary": "Unlink an OIDC identity from a user",
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"description": "No Content"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/main.APIError"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/main.APIError"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/main.APIError"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/main.APIError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/drasl/api/v2/players": {
|
"/drasl/api/v2/players": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get details of all players. Requires admin privileges.",
|
"description": "Get details of all players. Requires admin privileges.",
|
||||||
@ -344,7 +287,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Create a new player",
|
"description": "Create a new player for an existing Drasl user.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -462,7 +405,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"description": "Delete a player. This action cannot be undone.",
|
"description": "Delete a player. This action cannot be undone. Requires admin privileges unless you own the player.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -584,7 +527,7 @@
|
|||||||
},
|
},
|
||||||
"/drasl/api/v2/user": {
|
"/drasl/api/v2/user": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get details of your own account",
|
"description": "Get details of a user, either the calling user (GET /user) or the user with the specified UUID (GET /users/{uuid}). Getting details of another user requires admin privileges.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -594,7 +537,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"users"
|
"users"
|
||||||
],
|
],
|
||||||
"summary": "Get own account",
|
"summary": "Get user details",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@ -602,14 +545,26 @@
|
|||||||
"$ref": "#/definitions/main.APIUser"
|
"$ref": "#/definitions/main.APIUser"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"description": "Forbidden",
|
"description": "Forbidden",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/main.APIError"
|
"$ref": "#/definitions/main.APIError"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"429": {
|
"404": {
|
||||||
"description": "Too Many Requests",
|
"description": "Not Found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/main.APIError"
|
"$ref": "#/definitions/main.APIError"
|
||||||
}
|
}
|
||||||
@ -623,7 +578,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"description": "Delete your own account. This action cannot be undone.",
|
"description": "Delete a user, either the calling user (DELETE /user) or the user with the specified UUID (DELETE /users/{uuid}). This action cannot be undone. Deleting another user requires admin privileges.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -633,19 +588,19 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"users"
|
"users"
|
||||||
],
|
],
|
||||||
"summary": "Delete own account",
|
"summary": "Delete user",
|
||||||
"responses": {
|
"responses": {
|
||||||
"204": {
|
"204": {
|
||||||
"description": "No Content"
|
"description": "No Content"
|
||||||
},
|
},
|
||||||
"401": {
|
"403": {
|
||||||
"description": "Unauthorized",
|
"description": "Forbidden",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/main.APIError"
|
"$ref": "#/definitions/main.APIError"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"429": {
|
"404": {
|
||||||
"description": "Too Many Requests",
|
"description": "Not Found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/main.APIError"
|
"$ref": "#/definitions/main.APIError"
|
||||||
}
|
}
|
||||||
@ -659,7 +614,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"patch": {
|
"patch": {
|
||||||
"description": "Update details of your own account.",
|
"description": "Update an existing user, either the calling user (PATCH /user) or the user with the specified UUID (PATCH /users/{uuid}). Updating another user requires admin privileges.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -669,7 +624,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"users"
|
"users"
|
||||||
],
|
],
|
||||||
"summary": "Update own account",
|
"summary": "Update a user",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "New properties of the user",
|
"description": "New properties of the user",
|
||||||
@ -721,6 +676,127 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/drasl/api/v2/user/oidc-identities": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Link an OIDC identity to a user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "OIDC identity to link to the user",
|
||||||
|
"name": "APICreateOIDCIdentityRequest",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APICreateOIDCIdentityRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIOIDCIdentity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Unlink an OIDC identity from a user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Issuer of the OIDC provider to unlink from the user",
|
||||||
|
"name": "APIDeleteOIDCIdentityRequest",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIDeleteOIDCIdentityRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/drasl/api/v2/users": {
|
"/drasl/api/v2/users": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get details of all users. Requires admin privileges.",
|
"description": "Get details of all users. Requires admin privileges.",
|
||||||
@ -765,7 +841,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Create a new user. Requires admin privileges.",
|
"description": "Register and create a new user. Can be called without an API token.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -829,7 +905,7 @@
|
|||||||
},
|
},
|
||||||
"/drasl/api/v2/users/{uuid}": {
|
"/drasl/api/v2/users/{uuid}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get details of a user by their UUID. Requires admin privileges.",
|
"description": "Get details of a user, either the calling user (GET /user) or the user with the specified UUID (GET /users/{uuid}). Getting details of another user requires admin privileges.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -839,7 +915,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"users"
|
"users"
|
||||||
],
|
],
|
||||||
"summary": "Get user by UUID",
|
"summary": "Get user details",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -889,7 +965,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"description": "Delete a user. This action cannot be undone. Requires admin privileges.",
|
"description": "Delete a user, either the calling user (DELETE /user) or the user with the specified UUID (DELETE /users/{uuid}). This action cannot be undone. Deleting another user requires admin privileges.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -934,7 +1010,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"patch": {
|
"patch": {
|
||||||
"description": "Update an existing user. Requires admin privileges.",
|
"description": "Update an existing user, either the calling user (PATCH /user) or the user with the specified UUID (PATCH /users/{uuid}). Updating another user requires admin privileges.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -988,6 +1064,147 @@
|
|||||||
"$ref": "#/definitions/main.APIError"
|
"$ref": "#/definitions/main.APIError"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"429": {
|
||||||
|
"description": "Too Many Requests",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/drasl/api/v2/users/{uuid}/oidc-identities": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Link an OIDC identity to a user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User UUID",
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "OIDC identity to link to the user",
|
||||||
|
"name": "APICreateOIDCIdentityRequest",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APICreateOIDCIdentityRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIOIDCIdentity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Unlink an OIDC identity from a user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User UUID",
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Issuer of the OIDC provider to unlink from the user",
|
||||||
|
"name": "APIDeleteOIDCIdentityRequest",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIDeleteOIDCIdentityRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.APIError"
|
||||||
|
}
|
||||||
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -1005,12 +1222,25 @@
|
|||||||
"challengeSkinBase64": {
|
"challengeSkinBase64": {
|
||||||
"description": "Base64-encoded skin PNG. Example value truncated for brevity.",
|
"description": "Base64-encoded skin PNG. Example value truncated for brevity.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"
|
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."
|
||||||
},
|
},
|
||||||
"challengeToken": {
|
"challengeToken": {
|
||||||
"description": "Challenge token that must be passed when registering with a challenge skin",
|
"description": "Challenge token that must be passed when registering with a challenge skin",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "414cc23d6eebee3b17a453d6b9800be3e5a4627fd3b0ee54d7c37d03b2596e44"
|
"example": "iK1B2FzLc5fMP94VmUR3KC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main.APICreateOIDCIdentityRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"issuer": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "https://idm.example.com/oauth2/openid/drasl"
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "f85f8c18-9bdf-49ad-a76e-719f9ba3ed25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1020,7 +1250,7 @@
|
|||||||
"capeBase64": {
|
"capeBase64": {
|
||||||
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.",
|
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"
|
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."
|
||||||
},
|
},
|
||||||
"capeUrl": {
|
"capeUrl": {
|
||||||
"description": "Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.",
|
"description": "Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.",
|
||||||
@ -1030,7 +1260,7 @@
|
|||||||
"challengeToken": {
|
"challengeToken": {
|
||||||
"description": "Challenge token to use when verifying ownership of another player. Call /drasl/api/v2/challenge-skin first to get a skin and token. See `RequireSkinVerification` in configuration.md.",
|
"description": "Challenge token to use when verifying ownership of another player. Call /drasl/api/v2/challenge-skin first to get a skin and token. See `RequireSkinVerification` in configuration.md.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "a484528c86725b7b5ac3b47e2f973efd"
|
"example": "iK1B2FzLc5fMP94VmUR3KC"
|
||||||
},
|
},
|
||||||
"chosenUuid": {
|
"chosenUuid": {
|
||||||
"description": "Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.",
|
"description": "Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.",
|
||||||
@ -1055,7 +1285,7 @@
|
|||||||
"skinBase64": {
|
"skinBase64": {
|
||||||
"description": "Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.",
|
"description": "Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"
|
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."
|
||||||
},
|
},
|
||||||
"skinModel": {
|
"skinModel": {
|
||||||
"description": "Skin model. Either \"classic\" or \"slim\". If omitted, `\"classic\"` will be assumed.",
|
"description": "Skin model. Either \"classic\" or \"slim\". If omitted, `\"classic\"` will be assumed.",
|
||||||
@ -1080,7 +1310,7 @@
|
|||||||
"capeBase64": {
|
"capeBase64": {
|
||||||
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.",
|
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"
|
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."
|
||||||
},
|
},
|
||||||
"capeUrl": {
|
"capeUrl": {
|
||||||
"description": "Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.",
|
"description": "Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.",
|
||||||
@ -1151,7 +1381,7 @@
|
|||||||
"skinBase64": {
|
"skinBase64": {
|
||||||
"description": "Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.",
|
"description": "Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"
|
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."
|
||||||
},
|
},
|
||||||
"skinModel": {
|
"skinModel": {
|
||||||
"description": "Skin model. Either \"classic\" or \"slim\". If omitted, `\"classic\"` will be assumed.",
|
"description": "Skin model. Either \"classic\" or \"slim\". If omitted, `\"classic\"` will be assumed.",
|
||||||
@ -1188,6 +1418,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"main.APIDeleteOIDCIdentityRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"issuer": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "https://idm.example.com/oauth2/openid/drasl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.APIError": {
|
"main.APIError": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -1197,6 +1436,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"main.APIGetChallengeSkinRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"playerName": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.APIInvite": {
|
"main.APIInvite": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -1217,10 +1465,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"main.APILoginRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "hunter2"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Notch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.APILoginResponse": {
|
"main.APILoginResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
"apiToken": {
|
||||||
"description": "An API token for the user",
|
"description": "An API token for the user",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Bq608AtLeG7emJOdvXHYxL"
|
"example": "Bq608AtLeG7emJOdvXHYxL"
|
||||||
@ -1330,7 +1591,7 @@
|
|||||||
"capeBase64": {
|
"capeBase64": {
|
||||||
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity.",
|
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"
|
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."
|
||||||
},
|
},
|
||||||
"capeUrl": {
|
"capeUrl": {
|
||||||
"description": "Optional. URL to cape file",
|
"description": "Optional. URL to cape file",
|
||||||
@ -1356,8 +1617,9 @@
|
|||||||
"example": "MyPlayerName"
|
"example": "MyPlayerName"
|
||||||
},
|
},
|
||||||
"skinBase64": {
|
"skinBase64": {
|
||||||
|
"description": "Optional. Base64-encoded skin PNG. Example value truncated for brevity.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"
|
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."
|
||||||
},
|
},
|
||||||
"skinModel": {
|
"skinModel": {
|
||||||
"description": "Optional. New skin model. Either \"classic\" or \"slim\".",
|
"description": "Optional. New skin model. Either \"classic\" or \"slim\".",
|
||||||
@ -1402,12 +1664,12 @@
|
|||||||
"resetApiToken": {
|
"resetApiToken": {
|
||||||
"description": "Pass `true` to reset the user's API token",
|
"description": "Pass `true` to reset the user's API token",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"example": true
|
"example": false
|
||||||
},
|
},
|
||||||
"resetMinecraftToken": {
|
"resetMinecraftToken": {
|
||||||
"description": "Pass `true` to reset the user's Minecraft token",
|
"description": "Pass `true` to reset the user's Minecraft token",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"example": true
|
"example": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1460,4 +1722,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ package main
|
|||||||
const VERSION = "3.0.0"
|
const VERSION = "3.0.0"
|
||||||
|
|
||||||
const REPOSITORY_URL = "https://github.com/unmojang/drasl"
|
const REPOSITORY_URL = "https://github.com/unmojang/drasl"
|
||||||
|
const SWAGGER_UI_URL = "https://doc.drasl.unmojang.org"
|
||||||
|
|
||||||
const LICENSE = "GPLv3"
|
const LICENSE = "GPLv3"
|
||||||
const LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
const LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||||
|
@ -110,6 +110,7 @@ type ConstantsType struct {
|
|||||||
License string
|
License string
|
||||||
LicenseURL string
|
LicenseURL string
|
||||||
RepositoryURL string
|
RepositoryURL string
|
||||||
|
SwaggerUIURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
var Constants = &ConstantsType{
|
var Constants = &ConstantsType{
|
||||||
@ -122,6 +123,7 @@ var Constants = &ConstantsType{
|
|||||||
License: LICENSE,
|
License: LICENSE,
|
||||||
LicenseURL: LICENSE_URL,
|
LicenseURL: LICENSE_URL,
|
||||||
RepositoryURL: REPOSITORY_URL,
|
RepositoryURL: REPOSITORY_URL,
|
||||||
|
SwaggerUIURL: SWAGGER_UI_URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeRequestCacheKey(url string, method string, body []byte) []byte {
|
func MakeRequestCacheKey(url string, method string, body []byte) []byte {
|
||||||
|
43
main.go
43
main.go
@ -40,6 +40,7 @@ var bodyDump = middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte)
|
|||||||
type App struct {
|
type App struct {
|
||||||
FrontEndURL string
|
FrontEndURL string
|
||||||
PublicURL string
|
PublicURL string
|
||||||
|
APIURL string
|
||||||
AuthURL string
|
AuthURL string
|
||||||
AccountURL string
|
AccountURL string
|
||||||
ServicesURL string
|
ServicesURL string
|
||||||
@ -155,6 +156,17 @@ func (app *App) MakeServer() *echo.Echo {
|
|||||||
limit := fmt.Sprintf("%dKIB", app.Config.BodyLimit.SizeLimitKiB)
|
limit := fmt.Sprintf("%dKIB", app.Config.BodyLimit.SizeLimitKiB)
|
||||||
e.Use(middleware.BodyLimit(limit))
|
e.Use(middleware.BodyLimit(limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||||
|
AllowOrigins: []string{"*"},
|
||||||
|
Skipper: func(c echo.Context) bool {
|
||||||
|
return !Contains([]string{
|
||||||
|
DRASL_API_PREFIX + "/swagger.json",
|
||||||
|
DRASL_API_PREFIX + "/openapi.json",
|
||||||
|
}, c.Path())
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
if len(app.Config.CORSAllowOrigins) > 0 {
|
if len(app.Config.CORSAllowOrigins) > 0 {
|
||||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||||
AllowOrigins: app.Config.CORSAllowOrigins,
|
AllowOrigins: app.Config.CORSAllowOrigins,
|
||||||
@ -201,29 +213,39 @@ func (app *App) MakeServer() *echo.Echo {
|
|||||||
e.Static("/web/texture/skin", path.Join(app.Config.StateDirectory, "skin"))
|
e.Static("/web/texture/skin", path.Join(app.Config.StateDirectory, "skin"))
|
||||||
|
|
||||||
// Drasl API
|
// Drasl API
|
||||||
e.DELETE(DRASL_API_PREFIX+"/invites/:code", app.APIDeleteInvite())
|
apiSwagger := app.APISwagger()
|
||||||
e.DELETE(DRASL_API_PREFIX+"/oidc-identities", app.APIDeleteOIDCIdentity())
|
e.GET(DRASL_API_PREFIX+"/swagger.json", apiSwagger)
|
||||||
e.DELETE(DRASL_API_PREFIX+"/players/:uuid", app.APIDeletePlayer())
|
e.GET(DRASL_API_PREFIX+"/openapi.json", apiSwagger)
|
||||||
e.DELETE(DRASL_API_PREFIX+"/user", app.APIDeleteSelf())
|
|
||||||
e.DELETE(DRASL_API_PREFIX+"/users/:uuid", app.APIDeleteUser())
|
|
||||||
|
|
||||||
|
apiDeleteUser := app.APIDeleteUser()
|
||||||
|
e.DELETE(DRASL_API_PREFIX+"/invites/:code", app.APIDeleteInvite())
|
||||||
|
e.DELETE(DRASL_API_PREFIX+"/players/:uuid", app.APIDeletePlayer())
|
||||||
|
e.DELETE(DRASL_API_PREFIX+"/user", apiDeleteUser)
|
||||||
|
e.DELETE(DRASL_API_PREFIX+"/user/oidc-identities", app.APIDeleteOIDCIdentity())
|
||||||
|
e.DELETE(DRASL_API_PREFIX+"/users/:uuid", apiDeleteUser)
|
||||||
|
e.DELETE(DRASL_API_PREFIX+"/users/:uuid/oidc-identities", app.APIDeleteOIDCIdentity())
|
||||||
|
|
||||||
|
apiGetUser := app.APIGetUser()
|
||||||
e.GET(DRASL_API_PREFIX+"/challenge-skin", app.APIGetChallengeSkin())
|
e.GET(DRASL_API_PREFIX+"/challenge-skin", app.APIGetChallengeSkin())
|
||||||
e.GET(DRASL_API_PREFIX+"/invites", app.APIGetInvites())
|
e.GET(DRASL_API_PREFIX+"/invites", app.APIGetInvites())
|
||||||
e.GET(DRASL_API_PREFIX+"/players", app.APIGetPlayers())
|
e.GET(DRASL_API_PREFIX+"/players", app.APIGetPlayers())
|
||||||
e.GET(DRASL_API_PREFIX+"/players/:uuid", app.APIGetPlayer())
|
e.GET(DRASL_API_PREFIX+"/players/:uuid", app.APIGetPlayer())
|
||||||
e.GET(DRASL_API_PREFIX+"/user", app.APIGetSelf())
|
e.GET(DRASL_API_PREFIX+"/user", apiGetUser)
|
||||||
e.GET(DRASL_API_PREFIX+"/users", app.APIGetUsers())
|
e.GET(DRASL_API_PREFIX+"/users", app.APIGetUsers())
|
||||||
e.GET(DRASL_API_PREFIX+"/users/:uuid", app.APIGetUser())
|
e.GET(DRASL_API_PREFIX+"/users/:uuid", apiGetUser)
|
||||||
|
|
||||||
|
apiUpdateUser := app.APIUpdateUser()
|
||||||
e.PATCH(DRASL_API_PREFIX+"/players/:uuid", app.APIUpdatePlayer())
|
e.PATCH(DRASL_API_PREFIX+"/players/:uuid", app.APIUpdatePlayer())
|
||||||
e.PATCH(DRASL_API_PREFIX+"/user", app.APIUpdateSelf())
|
e.PATCH(DRASL_API_PREFIX+"/user", apiUpdateUser)
|
||||||
e.PATCH(DRASL_API_PREFIX+"/users/:uuid", app.APIUpdateUser())
|
e.PATCH(DRASL_API_PREFIX+"/users/:uuid", apiUpdateUser)
|
||||||
|
|
||||||
|
apiCreateOIDCIdentity := app.APICreateOIDCIdentity()
|
||||||
e.POST(DRASL_API_PREFIX+"/invites", app.APICreateInvite())
|
e.POST(DRASL_API_PREFIX+"/invites", app.APICreateInvite())
|
||||||
e.POST(DRASL_API_PREFIX+"/login", app.APILogin())
|
e.POST(DRASL_API_PREFIX+"/login", app.APILogin())
|
||||||
e.POST(DRASL_API_PREFIX+"/oidc-identities", app.APICreateOIDCIdentity())
|
|
||||||
e.POST(DRASL_API_PREFIX+"/players", app.APICreatePlayer())
|
e.POST(DRASL_API_PREFIX+"/players", app.APICreatePlayer())
|
||||||
|
e.POST(DRASL_API_PREFIX+"/user/oidc-identities", apiCreateOIDCIdentity)
|
||||||
e.POST(DRASL_API_PREFIX+"/users", app.APICreateUser())
|
e.POST(DRASL_API_PREFIX+"/users", app.APICreateUser())
|
||||||
|
e.POST(DRASL_API_PREFIX+"/users/:uuid/oidc-identities", apiCreateOIDCIdentity)
|
||||||
|
|
||||||
// authlib-injector
|
// authlib-injector
|
||||||
e.GET("/authlib-injector", AuthlibInjectorRoot(app))
|
e.GET("/authlib-injector", AuthlibInjectorRoot(app))
|
||||||
@ -511,6 +533,7 @@ func setup(config *Config) *App {
|
|||||||
ServicesURL: Unwrap(url.JoinPath(config.BaseURL, "services")),
|
ServicesURL: Unwrap(url.JoinPath(config.BaseURL, "services")),
|
||||||
SessionURL: Unwrap(url.JoinPath(config.BaseURL, "session")),
|
SessionURL: Unwrap(url.JoinPath(config.BaseURL, "session")),
|
||||||
AuthlibInjectorURL: Unwrap(url.JoinPath(config.BaseURL, "authlib-injector")),
|
AuthlibInjectorURL: Unwrap(url.JoinPath(config.BaseURL, "authlib-injector")),
|
||||||
|
APIURL: Unwrap(url.JoinPath(config.BaseURL, DRASL_API_PREFIX)),
|
||||||
VerificationSkinTemplate: verificationSkinTemplate,
|
VerificationSkinTemplate: verificationSkinTemplate,
|
||||||
OIDCProvidersByName: oidcProvidersByName,
|
OIDCProvidersByName: oidcProvidersByName,
|
||||||
OIDCProvidersByIssuer: oidcProvidersByIssuer,
|
OIDCProvidersByIssuer: oidcProvidersByIssuer,
|
||||||
|
@ -505,7 +505,7 @@ func (app *App) ValidateChallenge(playerName string, challengeToken *string) (*P
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MakeChallengeToken() (string, error) {
|
func MakeChallengeToken() (string, error) {
|
||||||
return RandomHex(16)
|
return RandomBase62(16)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) GetChallenge(playerName string, token string) []byte {
|
func (app *App) GetChallenge(playerName string, token string) []byte {
|
||||||
|
2
user.go
2
user.go
@ -433,7 +433,7 @@ func (app *App) UpdateUser(
|
|||||||
callerIsAdmin := caller.IsAdmin
|
callerIsAdmin := caller.IsAdmin
|
||||||
|
|
||||||
if user.UUID != caller.UUID && !callerIsAdmin {
|
if user.UUID != caller.UUID && !callerIsAdmin {
|
||||||
return User{}, NewBadRequestUserError("You are not an admin.")
|
return User{}, NewBadRequestUserError("You are not authorized to update that user.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if password != nil {
|
if password != nil {
|
||||||
|
@ -247,6 +247,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="apiToken">API Token</label><br />
|
<label for="apiToken">API Token</label><br />
|
||||||
|
{{ if ne .App.Constants.SwaggerUIURL "" }}
|
||||||
|
<small>See the <a href="{{ .App.Constants.SwaggerUIURL }}/?url={{ .App.APIURL }}/swagger.json">{{ .App.Config.ApplicationName }} API documentation</a>.</small><br />
|
||||||
|
{{ end }}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="apiToken"
|
name="apiToken"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user