mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-03 10:56:06 -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
|
||||
|
||||
swag:
|
||||
$(SWAG) init --generalInfo api.go --output . --outputTypes json
|
||||
$(SWAG) init --generalInfo api.go --output ./assets/ --outputTypes json
|
||||
|
||||
prebuild: npm-install swag
|
||||
node esbuild.config.js
|
||||
|
404
api.go
404
api.go
@ -11,6 +11,8 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -279,6 +281,14 @@ func (app *App) inviteToAPIInvite(invite *Invite) (APIInvite, error) {
|
||||
}, 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
|
||||
//
|
||||
// @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
|
||||
//
|
||||
// @Summary Get user by UUID
|
||||
// @Description Get details of a user by their UUID. Requires admin privileges.
|
||||
// @Summary Get user details
|
||||
// @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
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -349,23 +337,34 @@ func (app *App) APIGetSelf() func(c echo.Context) error {
|
||||
// @Failure 404 {object} APIError
|
||||
// @Failure 500 {object} APIError
|
||||
// @Router /drasl/api/v2/users/{uuid} [get]
|
||||
// @Router /drasl/api/v2/user [get]
|
||||
func (app *App) APIGetUser() func(c echo.Context) error {
|
||||
return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
|
||||
uuid_ := c.Param("uuid")
|
||||
_, err := uuid.Parse(uuid_)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||
}
|
||||
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||
targetUser := caller
|
||||
|
||||
var profileUser User
|
||||
if err := app.DB.First(&profileUser, "uuid = ?", uuid_).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
|
||||
uuidParam := c.Param("uuid")
|
||||
if uuidParam != "" {
|
||||
if !caller.IsAdmin && (caller.UUID != uuidParam) {
|
||||
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 {
|
||||
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.
|
||||
Password *string `json:"password" example:"hunter2"` // Plaintext password. Not needed if OIDCIdentitySpecs are supplied.
|
||||
OIDCIdentitySpecs []APIOIDCIdentitySpec `json:"oidcIdentities"`
|
||||
IsAdmin bool `json:"isAdmin" example:"true"` // Whether the user is an admin
|
||||
IsLocked bool `json:"isLocked" example:"false"` // Whether the user is locked (disabled)
|
||||
RequestAPIToken bool `json:"requestApiToken" example:"true"` // Whether to include an API token for the user in the response
|
||||
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the player of the new user. If omitted, a random UUID will be generated.
|
||||
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new user's player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
|
||||
InviteCode *string `json:"inviteCode" example:"rqjJwh0yMjO"` // Invite code to use. Optional even if the `RequireInvite` configuration option is set; admin API users can bypass `RequireInvite`.
|
||||
PlayerName *string `json:"playerName" example:"MyPlayerName"` // Optional. Player name. Can be different from the user's username. If omitted, the user's username will be used.
|
||||
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.
|
||||
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`.
|
||||
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`.
|
||||
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.
|
||||
IsAdmin bool `json:"isAdmin" example:"true"` // Whether the user is an admin
|
||||
IsLocked bool `json:"isLocked" example:"false"` // Whether the user is locked (disabled)
|
||||
RequestAPIToken bool `json:"requestApiToken" example:"true"` // Whether to include an API token for the user in the response
|
||||
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the player of the new user. If omitted, a random UUID will be generated.
|
||||
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new user's player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
|
||||
InviteCode *string `json:"inviteCode" example:"rqjJwh0yMjO"` // Invite code to use. Optional even if the `RequireInvite` configuration option is set; admin API users can bypass `RequireInvite`.
|
||||
PlayerName *string `json:"playerName" example:"MyPlayerName"` // Optional. Player name. Can be different from the user's username. If omitted, the user's username will be used.
|
||||
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.
|
||||
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`.
|
||||
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`.
|
||||
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.
|
||||
}
|
||||
|
||||
type APICreateUserResponse struct {
|
||||
@ -407,7 +406,7 @@ type APICreateUserResponse struct {
|
||||
// APICreateUser godoc
|
||||
//
|
||||
// @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
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -487,19 +486,19 @@ func (app *App) APICreateUser() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
type APIUpdateUserRequest struct {
|
||||
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.
|
||||
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
|
||||
ResetMinecraftToken bool `json:"resetMinecraftToken" example:"true"` // 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.
|
||||
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.
|
||||
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.
|
||||
IsLocked *bool `json:"isLocked" example:"false"` // Optional. Pass `true` to lock (disable), `false` to unlock user.
|
||||
ResetAPIToken bool `json:"resetApiToken" example:"false"` // Pass `true` to reset the user's API 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.
|
||||
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
|
||||
//
|
||||
// @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
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -509,79 +508,44 @@ type APIUpdateUserRequest struct {
|
||||
// @Failure 400 {object} APIError
|
||||
// @Failure 403 {object} APIError
|
||||
// @Failure 404 {object} APIError
|
||||
// @Failure 429 {object} APIError
|
||||
// @Failure 500 {object} APIError
|
||||
// @Router /drasl/api/v2/users/{uuid} [patch]
|
||||
// @Router /drasl/api/v2/user [patch]
|
||||
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)
|
||||
if err := c.Bind(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uuid_ := c.Param("uuid")
|
||||
_, err := uuid.Parse(uuid_)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||
}
|
||||
|
||||
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")
|
||||
targetUser := caller
|
||||
uuidParam := c.Param("uuid")
|
||||
if uuidParam != "" {
|
||||
if !caller.IsAdmin && (caller.UUID != uuidParam) {
|
||||
return NewUserError(http.StatusForbidden, "You are not authorized to update 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
|
||||
}
|
||||
|
||||
updatedUser, err := app.UpdateUser(
|
||||
app.DB,
|
||||
caller,
|
||||
targetUser, // user
|
||||
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,
|
||||
*targetUser,
|
||||
req.Password,
|
||||
req.IsAdmin,
|
||||
req.IsLocked,
|
||||
@ -605,7 +569,7 @@ func (app *App) APIUpdateSelf() func(c echo.Context) error {
|
||||
// APIDeleteUser godoc
|
||||
//
|
||||
// @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
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -614,45 +578,34 @@ func (app *App) APIUpdateSelf() func(c echo.Context) error {
|
||||
// @Failure 403 {object} APIError
|
||||
// @Failure 404 {object} APIError
|
||||
// @Failure 500 {object} APIError
|
||||
// @Router /drasl/api/v2/user [delete]
|
||||
// @Router /drasl/api/v2/users/{uuid} [delete]
|
||||
func (app *App) APIDeleteUser() func(c echo.Context) error {
|
||||
return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
|
||||
uuid_ := c.Param("uuid")
|
||||
_, err := uuid.Parse(uuid_)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||
targetUser := caller
|
||||
uuidParam := c.Param("uuid")
|
||||
if uuidParam != "" {
|
||||
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
|
||||
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)
|
||||
err := app.DeleteUser(caller, targetUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -738,23 +691,23 @@ func (app *App) APIGetPlayers() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
type APICreatePlayerRequest struct {
|
||||
Name string `json:"name" example:"MyPlayerName"` // Player name.
|
||||
UserUUID *string `json:"userUuid" example:"f9b9af62-da83-4ec7-aeea-de48c621822c"` // Optional. UUID of the owning user. If omitted, the player will be added to the calling user's account.
|
||||
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.
|
||||
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
|
||||
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
|
||||
ChallengeToken *string `json:"challengeToken" example:"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.
|
||||
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`.
|
||||
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`.
|
||||
CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.
|
||||
Name string `json:"name" example:"MyPlayerName"` // Player name.
|
||||
UserUUID *string `json:"userUuid" example:"f9b9af62-da83-4ec7-aeea-de48c621822c"` // Optional. UUID of the owning user. If omitted, the player will be added to the calling user's account.
|
||||
ChosenUUID *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"` // Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.
|
||||
ExistingPlayer bool `json:"existingPlayer" example:"false"` // If true, the new player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
|
||||
FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"` // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
|
||||
ChallengeToken *string `json:"challengeToken" example:"iK1B2FzLc5fMP94VmUR3KC"` // Challenge token to use when verifying ownership of another player. Call /drasl/api/v2/challenge-skin first to get a skin and token. See `RequireSkinVerification` in configuration.md.
|
||||
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`.
|
||||
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`.
|
||||
CapeURL *string `json:"capeUrl" example:"https://example.com/cape.png"` // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.
|
||||
}
|
||||
|
||||
// APICreatePlayer godoc
|
||||
//
|
||||
// @Summary Create a new player
|
||||
// @Description Create a new player
|
||||
// @Description Create a new player for an existing Drasl user.
|
||||
// @Tags players
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -817,15 +770,15 @@ func (app *App) APICreatePlayer() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
type APIUpdatePlayerRequest struct {
|
||||
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.
|
||||
SkinModel *string `json:"skinModel" example:"classic"` // Optional. New skin model. Either "classic" or "slim".
|
||||
SkinBase64 *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"`
|
||||
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
|
||||
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
|
||||
DeleteCape bool `json:"deleteCape"` // Pass `true` to delete the user's existing cape
|
||||
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.
|
||||
SkinModel *string `json:"skinModel" example:"classic"` // Optional. New skin model. Either "classic" or "slim".
|
||||
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
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// APIUpdatePlayer godoc
|
||||
@ -905,7 +858,7 @@ func (app *App) APIUpdatePlayer() func(c echo.Context) error {
|
||||
// APIDeletePlayer godoc
|
||||
//
|
||||
// @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
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -942,9 +895,8 @@ func (app *App) APIDeletePlayer() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
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"`
|
||||
Subject string `json:"subject" example:"f85f8c18-9bdf-49ad-a76e-719f9ba3ed25"`
|
||||
Issuer string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
|
||||
Subject string `json:"subject" example:"f85f8c18-9bdf-49ad-a76e-719f9ba3ed25"`
|
||||
}
|
||||
|
||||
// APICreateOIDCIdentity godoc
|
||||
@ -953,12 +905,16 @@ type APICreateOIDCIdentityRequest struct {
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Failure 400 {object} APIError
|
||||
// @Failure 401 {object} APIError
|
||||
// @Failure 403 {object} APIError
|
||||
// @Failure 500 {object} APIError
|
||||
// @Router /drasl/api/v2/oidc-identities [post]
|
||||
// @Param uuid path string true "User UUID"
|
||||
// @Param APICreateOIDCIdentityRequest body APICreateOIDCIdentityRequest true "OIDC identity to link to the user"
|
||||
// @Success 200 {object} APIOIDCIdentity
|
||||
// @Failure 400 {object} APIError
|
||||
// @Failure 401 {object} APIError
|
||||
// @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 {
|
||||
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||
req := new(APICreateOIDCIdentityRequest)
|
||||
@ -967,8 +923,13 @@ func (app *App) APICreateOIDCIdentity() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
userUUID := caller.UUID
|
||||
if req.UserUUID != nil {
|
||||
userUUID = *req.UserUUID
|
||||
uuidParam := c.Param("uuid")
|
||||
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)
|
||||
@ -985,8 +946,7 @@ func (app *App) APICreateOIDCIdentity() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
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
|
||||
@ -995,12 +955,16 @@ type APIDeleteOIDCIdentityRequest struct {
|
||||
// @Tags users
|
||||
// @Accept 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
|
||||
// @Failure 400 {object} APIError
|
||||
// @Failure 401 {object} APIError
|
||||
// @Failure 403 {object} APIError
|
||||
// @Failure 404 {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 {
|
||||
return app.withAPIToken(true, func(c echo.Context, caller *User) error {
|
||||
req := new(APIDeleteOIDCIdentityRequest)
|
||||
@ -1009,8 +973,13 @@ func (app *App) APIDeleteOIDCIdentity() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
userUUID := caller.UUID
|
||||
if req.UserUUID != nil {
|
||||
userUUID = *req.UserUUID
|
||||
uuidParam := c.Param("uuid")
|
||||
if uuidParam != "" {
|
||||
_, err := uuid.Parse(uuidParam)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
|
||||
}
|
||||
userUUID = uuidParam
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
ChallengeSkinBase64 string `json:"challengeSkinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Base64-encoded skin PNG. Example value truncated for brevity.
|
||||
ChallengeToken string `json:"challengeToken" example:"iK1B2FzLc5fMP94VmUR3KC"` // Challenge token that must be passed when registering with a challenge skin
|
||||
}
|
||||
|
||||
// APIGetChallengeSkin godoc
|
||||
//
|
||||
// @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
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} APIChallenge
|
||||
// @Failure 500 {object} APIError
|
||||
// @Param APIGetChallengeSkinRequest body APIGetChallengeSkinRequest true "Player name for the challenge skin"
|
||||
// @Success 200 {object} APIChallenge
|
||||
// @Success 400 {object} APIError
|
||||
// @Failure 500 {object} APIError
|
||||
// @Router /drasl/api/v2/challenge-skin [get]
|
||||
func (app *App) APIGetChallengeSkin() func(c echo.Context) 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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
challengeSkinBytes, err := app.GetChallengeSkin(username, challengeToken)
|
||||
challengeSkinBytes, err := app.GetChallengeSkin(playerName, challengeToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1151,8 +1130,8 @@ func (app *App) APIGetChallengeSkin() func(c echo.Context) error {
|
||||
}
|
||||
|
||||
type APILoginResponse struct {
|
||||
User APIUser `json:"user"` // The logged-in user
|
||||
APIToken string `json:"token" example:"Bq608AtLeG7emJOdvXHYxL"` // An API token for the user
|
||||
User APIUser `json:"user"` // The logged-in user
|
||||
APIToken string `json:"apiToken" example:"Bq608AtLeG7emJOdvXHYxL"` // An API token for the user
|
||||
}
|
||||
|
||||
type APILoginRequest struct {
|
||||
@ -1162,24 +1141,25 @@ type APILoginRequest struct {
|
||||
|
||||
// APILogin godoc
|
||||
//
|
||||
// @Summary Get a token
|
||||
// @Description Get a token for login credentials.
|
||||
// @Tags users, auth
|
||||
// @Summary Authenticate and receive an API token
|
||||
// @Description Authenticate with username and password and receive an API token. Can be called without an API token.
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} APILoginResponse
|
||||
// @Failure 400 {object} APIError
|
||||
// @Failure 401 {object} APIError
|
||||
// @Failure 403 {object} APIError
|
||||
// @Failure 429 {object} APIError
|
||||
// @Failure 500 {object} APIError
|
||||
// @Param APILoginRequest body APILoginRequest true "Drasl username and password"
|
||||
// @Success 200 {object} APILoginResponse
|
||||
// @Failure 400 {object} APIError
|
||||
// @Failure 401 {object} APIError
|
||||
// @Failure 403 {object} APIError
|
||||
// @Failure 429 {object} APIError
|
||||
// @Failure 500 {object} APIError
|
||||
// @Router /drasl/api/v2/login [post]
|
||||
func (app *App) APILogin() func(c echo.Context) error {
|
||||
return app.withAPIToken(false, func(c echo.Context, _ *User) error {
|
||||
var req APILoginRequest
|
||||
err := c.Bind(&req)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformed JSON request")
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := app.AuthenticateUser(req.Username, req.Password)
|
||||
|
90
api_test.go
90
api_test.go
@ -1,12 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -302,12 +302,14 @@ func (ts *TestSuite) testAPIGetChallengeSkin(t *testing.T) {
|
||||
username := "user"
|
||||
user, _ := ts.CreateTestUser(t, ts.App, ts.Server, username)
|
||||
|
||||
ts.Get(t, ts.Server, DRASL_API_PREFIX+"/challenge-skin", nil, &user.APIToken)
|
||||
req := httptest.NewRequest(http.MethodGet, DRASL_API_PREFIX+"/challenge-skin", nil)
|
||||
payload := APIGetChallengeSkinRequest{
|
||||
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.URL.RawQuery = url.Values{
|
||||
"username": {"foo"},
|
||||
}.Encode()
|
||||
rec := httptest.NewRecorder()
|
||||
ts.Server.ServeHTTP(rec, req)
|
||||
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
|
||||
payload := APICreateOIDCIdentityRequest{
|
||||
UserUUID: &admin.UUID,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
Subject: provider1Subject1,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
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)
|
||||
var apiOIDCIdentity 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,
|
||||
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)
|
||||
var apiOIDCIdentity 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
|
||||
payload := APICreateOIDCIdentityRequest{
|
||||
UserUUID: &user.UUID,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
Subject: provider1Subject2,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
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)
|
||||
var apiOIDCIdentity 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
|
||||
payload := APICreateOIDCIdentityRequest{
|
||||
UserUUID: &admin.UUID,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
Subject: provider1Subject1,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
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)
|
||||
var apiError 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
|
||||
payload := APICreateOIDCIdentityRequest{
|
||||
UserUUID: &admin.UUID,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
Subject: provider1Subject3,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
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)
|
||||
var apiError 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
|
||||
payload := APICreateOIDCIdentityRequest{
|
||||
UserUUID: &admin.UUID,
|
||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||
Subject: provider2Subject3,
|
||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||
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)
|
||||
var apiError 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
|
||||
payload := APICreateOIDCIdentityRequest{
|
||||
UserUUID: &user.UUID,
|
||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||
Subject: provider2Subject2,
|
||||
Issuer: fakeOIDCProvider2.Config.Issuer,
|
||||
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)
|
||||
var apiOIDCIdentity 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
|
||||
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)
|
||||
}
|
||||
{
|
||||
// Add the identity back for future tests...
|
||||
payload := APICreateOIDCIdentityRequest{
|
||||
UserUUID: &user.UUID,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
Subject: provider1Subject2,
|
||||
Issuer: fakeOIDCProvider1.Config.Issuer,
|
||||
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)
|
||||
}
|
||||
{
|
||||
// Non-admin user should not be able to delete OIDC identity for other users
|
||||
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)
|
||||
var apiError 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
|
||||
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)
|
||||
}
|
||||
{
|
||||
// Can't delete nonexistent OIDC identity
|
||||
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)
|
||||
var apiError 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
|
||||
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)
|
||||
var apiError APIError
|
||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&apiError))
|
||||
|
@ -16,7 +16,7 @@
|
||||
"paths": {
|
||||
"/drasl/api/v2/challenge-skin": {
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -28,6 +28,17 @@
|
||||
"players"
|
||||
],
|
||||
"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": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -35,6 +46,12 @@
|
||||
"$ref": "#/definitions/main.APIChallenge"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.APIError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
@ -164,7 +181,7 @@
|
||||
},
|
||||
"/drasl/api/v2/login": {
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -172,10 +189,20 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"users",
|
||||
"auth"
|
||||
"users"
|
||||
],
|
||||
"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": {
|
||||
"200": {
|
||||
"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": {
|
||||
"get": {
|
||||
"description": "Get details of all players. Requires admin privileges.",
|
||||
@ -344,7 +287,7 @@
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Create a new player",
|
||||
"description": "Create a new player for an existing Drasl user.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@ -462,7 +405,7 @@
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -584,7 +527,7 @@
|
||||
},
|
||||
"/drasl/api/v2/user": {
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -594,7 +537,7 @@
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"summary": "Get own account",
|
||||
"summary": "Get user details",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@ -602,14 +545,26 @@
|
||||
"$ref": "#/definitions/main.APIUser"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too Many Requests",
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.APIError"
|
||||
}
|
||||
@ -623,7 +578,7 @@
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -633,19 +588,19 @@
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"summary": "Delete own account",
|
||||
"summary": "Delete user",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.APIError"
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too Many Requests",
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.APIError"
|
||||
}
|
||||
@ -659,7 +614,7 @@
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -669,7 +624,7 @@
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"summary": "Update own account",
|
||||
"summary": "Update a user",
|
||||
"parameters": [
|
||||
{
|
||||
"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": {
|
||||
"get": {
|
||||
"description": "Get details of all users. Requires admin privileges.",
|
||||
@ -765,7 +841,7 @@
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Create a new user. Requires admin privileges.",
|
||||
"description": "Register and create a new user. Can be called without an API token.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@ -829,7 +905,7 @@
|
||||
},
|
||||
"/drasl/api/v2/users/{uuid}": {
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -839,7 +915,7 @@
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"summary": "Get user by UUID",
|
||||
"summary": "Get user details",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@ -889,7 +965,7 @@
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -934,7 +1010,7 @@
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
@ -988,6 +1064,147 @@
|
||||
"$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": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
@ -1005,12 +1222,25 @@
|
||||
"challengeSkinBase64": {
|
||||
"description": "Base64-encoded skin PNG. Example value truncated for brevity.",
|
||||
"type": "string",
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."
|
||||
},
|
||||
"challengeToken": {
|
||||
"description": "Challenge token that must be passed when registering with a challenge skin",
|
||||
"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": {
|
||||
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.",
|
||||
"type": "string",
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."
|
||||
},
|
||||
"capeUrl": {
|
||||
"description": "Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.",
|
||||
@ -1030,7 +1260,7 @@
|
||||
"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.",
|
||||
"type": "string",
|
||||
"example": "a484528c86725b7b5ac3b47e2f973efd"
|
||||
"example": "iK1B2FzLc5fMP94VmUR3KC"
|
||||
},
|
||||
"chosenUuid": {
|
||||
"description": "Optional. Specify a UUID for the new player. If omitted, a random UUID will be generated.",
|
||||
@ -1055,7 +1285,7 @@
|
||||
"skinBase64": {
|
||||
"description": "Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.",
|
||||
"type": "string",
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."
|
||||
},
|
||||
"skinModel": {
|
||||
"description": "Skin model. Either \"classic\" or \"slim\". If omitted, `\"classic\"` will be assumed.",
|
||||
@ -1080,7 +1310,7 @@
|
||||
"capeBase64": {
|
||||
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.",
|
||||
"type": "string",
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."
|
||||
},
|
||||
"capeUrl": {
|
||||
"description": "Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.",
|
||||
@ -1151,7 +1381,7 @@
|
||||
"skinBase64": {
|
||||
"description": "Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.",
|
||||
"type": "string",
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."
|
||||
},
|
||||
"skinModel": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -1197,6 +1436,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.APIGetChallengeSkinRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"playerName": {
|
||||
"type": "string",
|
||||
"example": "Notch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.APIInvite": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -1217,10 +1465,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.APILoginRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "hunter2"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"example": "Notch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.APILoginResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"apiToken": {
|
||||
"description": "An API token for the user",
|
||||
"type": "string",
|
||||
"example": "Bq608AtLeG7emJOdvXHYxL"
|
||||
@ -1330,7 +1591,7 @@
|
||||
"capeBase64": {
|
||||
"description": "Optional. Base64-encoded cape PNG. Example value truncated for brevity.",
|
||||
"type": "string",
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf"
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."
|
||||
},
|
||||
"capeUrl": {
|
||||
"description": "Optional. URL to cape file",
|
||||
@ -1356,8 +1617,9 @@
|
||||
"example": "MyPlayerName"
|
||||
},
|
||||
"skinBase64": {
|
||||
"description": "Optional. Base64-encoded skin PNG. Example value truncated for brevity.",
|
||||
"type": "string",
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI"
|
||||
"example": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."
|
||||
},
|
||||
"skinModel": {
|
||||
"description": "Optional. New skin model. Either \"classic\" or \"slim\".",
|
||||
@ -1402,12 +1664,12 @@
|
||||
"resetApiToken": {
|
||||
"description": "Pass `true` to reset the user's API token",
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
"example": false
|
||||
},
|
||||
"resetMinecraftToken": {
|
||||
"description": "Pass `true` to reset the user's Minecraft token",
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
"example": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1460,4 +1722,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ package main
|
||||
const VERSION = "3.0.0"
|
||||
|
||||
const REPOSITORY_URL = "https://github.com/unmojang/drasl"
|
||||
const SWAGGER_UI_URL = "https://doc.drasl.unmojang.org"
|
||||
|
||||
const LICENSE = "GPLv3"
|
||||
const LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||
|
@ -110,6 +110,7 @@ type ConstantsType struct {
|
||||
License string
|
||||
LicenseURL string
|
||||
RepositoryURL string
|
||||
SwaggerUIURL string
|
||||
}
|
||||
|
||||
var Constants = &ConstantsType{
|
||||
@ -122,6 +123,7 @@ var Constants = &ConstantsType{
|
||||
License: LICENSE,
|
||||
LicenseURL: LICENSE_URL,
|
||||
RepositoryURL: REPOSITORY_URL,
|
||||
SwaggerUIURL: SWAGGER_UI_URL,
|
||||
}
|
||||
|
||||
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 {
|
||||
FrontEndURL string
|
||||
PublicURL string
|
||||
APIURL string
|
||||
AuthURL string
|
||||
AccountURL string
|
||||
ServicesURL string
|
||||
@ -155,6 +156,17 @@ func (app *App) MakeServer() *echo.Echo {
|
||||
limit := fmt.Sprintf("%dKIB", app.Config.BodyLimit.SizeLimitKiB)
|
||||
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 {
|
||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
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"))
|
||||
|
||||
// Drasl API
|
||||
e.DELETE(DRASL_API_PREFIX+"/invites/:code", app.APIDeleteInvite())
|
||||
e.DELETE(DRASL_API_PREFIX+"/oidc-identities", app.APIDeleteOIDCIdentity())
|
||||
e.DELETE(DRASL_API_PREFIX+"/players/:uuid", app.APIDeletePlayer())
|
||||
e.DELETE(DRASL_API_PREFIX+"/user", app.APIDeleteSelf())
|
||||
e.DELETE(DRASL_API_PREFIX+"/users/:uuid", app.APIDeleteUser())
|
||||
apiSwagger := app.APISwagger()
|
||||
e.GET(DRASL_API_PREFIX+"/swagger.json", apiSwagger)
|
||||
e.GET(DRASL_API_PREFIX+"/openapi.json", apiSwagger)
|
||||
|
||||
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+"/invites", app.APIGetInvites())
|
||||
e.GET(DRASL_API_PREFIX+"/players", app.APIGetPlayers())
|
||||
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/: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+"/user", app.APIUpdateSelf())
|
||||
e.PATCH(DRASL_API_PREFIX+"/users/:uuid", app.APIUpdateUser())
|
||||
e.PATCH(DRASL_API_PREFIX+"/user", 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+"/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+"/user/oidc-identities", apiCreateOIDCIdentity)
|
||||
e.POST(DRASL_API_PREFIX+"/users", app.APICreateUser())
|
||||
e.POST(DRASL_API_PREFIX+"/users/:uuid/oidc-identities", apiCreateOIDCIdentity)
|
||||
|
||||
// authlib-injector
|
||||
e.GET("/authlib-injector", AuthlibInjectorRoot(app))
|
||||
@ -511,6 +533,7 @@ func setup(config *Config) *App {
|
||||
ServicesURL: Unwrap(url.JoinPath(config.BaseURL, "services")),
|
||||
SessionURL: Unwrap(url.JoinPath(config.BaseURL, "session")),
|
||||
AuthlibInjectorURL: Unwrap(url.JoinPath(config.BaseURL, "authlib-injector")),
|
||||
APIURL: Unwrap(url.JoinPath(config.BaseURL, DRASL_API_PREFIX)),
|
||||
VerificationSkinTemplate: verificationSkinTemplate,
|
||||
OIDCProvidersByName: oidcProvidersByName,
|
||||
OIDCProvidersByIssuer: oidcProvidersByIssuer,
|
||||
|
@ -505,7 +505,7 @@ func (app *App) ValidateChallenge(playerName string, challengeToken *string) (*P
|
||||
}
|
||||
|
||||
func MakeChallengeToken() (string, error) {
|
||||
return RandomHex(16)
|
||||
return RandomBase62(16)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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 {
|
||||
|
@ -247,6 +247,9 @@
|
||||
</p>
|
||||
<p>
|
||||
<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
|
||||
type="text"
|
||||
name="apiToken"
|
||||
|
Loading…
x
Reference in New Issue
Block a user