Files
validator/OmCTF-2025/services/block_game/backend/route/levels.go

322 lines
7.4 KiB
Go

package route
import (
"fmt"
"net/http"
"strconv"
"entgo.io/ent/dialect/sql"
"github.com/gorilla/mux"
"omctf.ru/block-game-backend/auth"
"omctf.ru/block-game-backend/codegen/ent"
"omctf.ru/block-game-backend/codegen/ent/level"
"omctf.ru/block-game-backend/db"
"omctf.ru/block-game-backend/game"
"omctf.ru/block-game-backend/messaging"
"omctf.ru/block-game-backend/schema"
"omctf.ru/block-game-backend/utils"
)
func CreateLevel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := auth.GetUser(ctx)
if err != nil || user == nil {
utils.BailInternalServerError(w, err)
return
}
req, err := utils.GetJSONBody[struct {
Name string `json:"name"`
Description string `json:"description"`
Visibility string `json:"visibility"`
Data schema.LevelData `json:"data"`
Prize string `json:"prize"`
}](r)
if err != nil || req == nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Data.Size > 20 || req.Data.Size <= 0 {
http.Error(w, "size must be between 1 and 20", http.StatusBadRequest)
return
}
if len(req.Data.Tiles) > 256 {
http.Error(w, "max 256 tiles", http.StatusBadRequest)
return
}
level, err := db.Client.Level.CreateFrom(req).
SetOwner(user).
Save(ctx)
if ent.IsValidationError(err) {
http.Error(w, fmt.Sprintf("Invalid request: %s", err), http.StatusBadRequest)
return
}
if ent.IsConstraintError(err) {
http.Error(w, "Name taken", http.StatusConflict)
return
}
if err != nil || level == nil {
utils.BailInternalServerError(w, err)
return
}
w.WriteHeader(http.StatusCreated)
utils.RespondWithJSON(w, map[string]any{
"id": level.ID,
})
}
func getPublicLevelMetadata(level *ent.Level) map[string]any {
return map[string]any{
"id": level.ID,
"name": level.Name,
"description": level.Description,
"visibility": level.Visibility,
}
}
const (
PageSize = 20
)
func ListLevels(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
currentUser, err := auth.GetUser(ctx)
if err != nil || currentUser == nil {
utils.BailInternalServerError(w, err)
return
}
var resultLevels []*ent.Level
pageNumberStr := r.URL.Query().Get("page")
if pageNumberStr == "" {
// non-paged
resultLevels, err = db.Client.Level.Query().Where(
utils.LevelAccessibleBy(currentUser.ID),
).Order(level.ByCreatedAt(sql.OrderDesc())).All(ctx)
if err != nil {
utils.BailInternalServerError(w, err)
return
}
} else {
pageNumber, err := strconv.Atoi(pageNumberStr)
if err != nil || pageNumber < 0 {
http.Error(w, "invalid page number", http.StatusBadRequest)
return
}
resultLevels, err = db.Client.Level.Query().Where(
utils.LevelAccessibleBy(currentUser.ID),
).Order(level.ByCreatedAt(sql.OrderDesc())).Limit(PageSize).Offset(pageNumber * PageSize).All(ctx)
if err != nil {
utils.BailInternalServerError(w, err)
return
}
}
data := make([]any, 0, len(resultLevels))
for _, lvl := range resultLevels {
levelData := getPublicLevelMetadata(lvl)
data = append(data, levelData)
}
utils.RespondWithJSON(w, data)
}
func GetLevel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
currentUser, err := auth.GetUser(ctx)
if err != nil || currentUser == nil {
utils.BailInternalServerError(w, err)
return
}
levelId := mux.Vars(r)["levelId"]
if levelId == "" {
http.Error(w, "levelId missing", http.StatusBadRequest)
return
}
levelIdInt, err := strconv.Atoi(levelId)
if err != nil {
http.Error(w, "levelId invalid", http.StatusBadRequest)
return
}
chosenLevel, err := db.Client.Level.Query().Where(
level.ID(levelIdInt),
utils.LevelAccessibleBy(currentUser.ID),
).WithOwner().Only(ctx)
if ent.IsNotFound(err) {
http.Error(w, "level not found", http.StatusNotFound)
return
}
if err != nil || chosenLevel == nil {
utils.BailInternalServerError(w, err)
return
}
levelOwner, err := chosenLevel.Edges.OwnerOrErr()
if err != nil {
utils.BailInternalServerError(w, err)
return
}
privileged := levelOwner.ID == currentUser.ID
levelData := getPublicLevelMetadata(chosenLevel)
levelData["data"] = chosenLevel.Data
if privileged {
levelData["prize"] = chosenLevel.Prize
}
utils.RespondWithJSON(w, levelData)
}
func FindLevel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
currentUser, err := auth.GetUser(ctx)
if err != nil || currentUser == nil {
utils.BailInternalServerError(w, err)
return
}
query := r.URL.Query().Get("name")
if query == "" {
http.Error(w, "query missing", http.StatusBadRequest)
return
}
foundLevel, err := db.Client.Level.Query().Where(
level.Name(query),
utils.LevelAccessibleBy(currentUser.ID),
).Only(ctx)
if ent.IsNotFound(err) {
http.Error(w, "level not found", http.StatusNotFound)
return
}
if err != nil || foundLevel == nil {
utils.BailInternalServerError(w, err)
return
}
levelData := getPublicLevelMetadata(foundLevel)
utils.RespondWithJSON(w, levelData)
}
func InviteToLevel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
currentUser, err := auth.GetUser(ctx)
if err != nil || currentUser == nil {
utils.BailInternalServerError(w, err)
return
}
req, err := utils.GetJSONBody[struct {
LevelId int `json:"level_id"`
UserId int `json:"user_id"`
}](r)
if err != nil || req == nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
levelToInvite, err := db.Client.Level.Query().Where(
level.ID(req.LevelId),
utils.LevelAccessibleBy(currentUser.ID),
).WithOwner().Only(ctx)
if ent.IsNotFound(err) {
http.Error(w, "level not found", http.StatusNotFound)
return
}
if err != nil || levelToInvite == nil {
utils.BailInternalServerError(w, err)
return
}
levelOwner, err := levelToInvite.Edges.OwnerOrErr()
if err != nil {
utils.BailInternalServerError(w, err)
return
}
if levelOwner.ID != currentUser.ID {
http.Error(w, "only level owner can invite players", http.StatusForbidden)
return
}
userToInvite, err := db.Client.User.Get(ctx, req.UserId)
if ent.IsNotFound(err) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
if err != nil || userToInvite == nil {
utils.BailInternalServerError(w, err)
return
}
err = levelToInvite.Update().
AddInvitedPlayers(userToInvite).
Exec(ctx)
if err != nil {
utils.BailInternalServerError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func PlayLevel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
currentUser, err := auth.GetUser(ctx)
if err != nil || currentUser == nil {
utils.BailInternalServerError(w, err)
return
}
levelId := mux.Vars(r)["levelId"]
if levelId == "" {
http.Error(w, "levelId missing", http.StatusBadRequest)
return
}
levelIdInt, err := strconv.Atoi(levelId)
if err != nil {
http.Error(w, "levelId invalid", http.StatusBadRequest)
return
}
chosenLevel, err := db.Client.Level.Query().Where(
level.ID(levelIdInt),
utils.LevelAccessibleBy(currentUser.ID),
).Only(ctx)
if ent.IsNotFound(err) {
http.Error(w, "level not found", http.StatusNotFound)
return
}
if err != nil || chosenLevel == nil {
utils.BailInternalServerError(w, err)
return
}
conn, err := messaging.InitWebsocket(w, r)
if err != nil {
utils.BailInternalServerError(w, err)
return
}
session := &game.Session{
Conn: conn,
UserId: currentUser.ID,
LevelId: chosenLevel.ID,
Tiles: game.NewTiles(chosenLevel.Data.Size, chosenLevel.Data.Tiles),
}
session.Start()
}