Skip to content

Commit

Permalink
feat: minecraft builds (#836)
Browse files Browse the repository at this point in the history
  • Loading branch information
andyksaw authored Nov 28, 2024
1 parent ac97be0 commit 8f49888
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 4 deletions.
136 changes: 136 additions & 0 deletions app/Http/Controllers/Api/v2/Minecraft/MinecraftBuildController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

namespace App\Http\Controllers\Api\v2\Minecraft;

use App\Core\Domains\MinecraftUUID\Data\MinecraftUUID;
use App\Core\Domains\MinecraftUUID\Rules\MinecraftUUIDRule;
use App\Http\Controllers\ApiController;
use App\Models\MinecraftBuild;
use App\Models\MinecraftBuildVote;
use App\Models\MinecraftPlayer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;

final class MinecraftBuildController extends ApiController
{
public function index(Request $request)
{
$request->validate([
'page_size' => ['integer', 'gt:0'],
]);

$defaultSize = 25;
$pageSize = min($defaultSize, $request->get('page_size', $defaultSize));

return MinecraftBuild::orderBy('votes', 'desc')
->paginate($pageSize);
}

public function show(Request $request, MinecraftBuild $build)
{
$build->load('player');
return $build;
}

public function store(Request $request)
{
$input = $request->validate([
'player_uuid' => ['required', new MinecraftUUIDRule],
'name' => ['required', Rule::unique(MinecraftBuild::tableName())],
'world' => 'required|string',
'x' => 'required|numeric',
'y' => 'required|numeric',
'z' => 'required|numeric',
'pitch' => 'required|numeric',
'yaw' => 'required|numeric',
]);

$player = MinecraftPlayer::firstOrCreate(
uuid: new MinecraftUUID($input['player_uuid']),
alias: $request->get('alias'),
);
$input['player_id'] = $player->getKey();

return MinecraftBuild::create($input);
}

public function update(Request $request, MinecraftBuild $build)
{
$input = $request->validate([
'player_uuid' => ['required', new MinecraftUUIDRule],
'name' => ['required', Rule::unique(MinecraftBuild::tableName())->ignore($build)],
'world' => 'required|string',
'x' => 'required|numeric',
'y' => 'required|numeric',
'z' => 'required|numeric',
'pitch' => 'required|numeric',
'yaw' => 'required|numeric',
]);

$this->assertHasWriteAccess(build: $build, uuid: $input['player_uuid']);

$build->update($request->all());

return $build;
}

public function patch(Request $request, MinecraftBuild $build)
{
$input = $request->validate([
'player_uuid' => ['required', new MinecraftUUIDRule],
'name' => [Rule::unique(MinecraftBuild::tableName())->ignore($build)],
]);

$this->assertHasWriteAccess(build: $build, uuid: $input['player_uuid']);

if ($request->has('name')) {
$name = $request->get('name');
$build->name = empty($name) ? null : $name;
}
if ($request->has('description')) {
$description = $request->get('description');
$build->description = empty($description) ? null : $description;
}
if ($request->has('lore')) {
$lore = $request->get('lore');
$build->lore = empty($lore) ? null : $lore;
}
$build->save();

return $build;
}

public function destroy(Request $request, MinecraftBuild $build)
{
$input = $request->validate([
'player_uuid' => ['required', new MinecraftUUIDRule],
]);

$this->assertHasWriteAccess(build: $build, uuid: $input['player_uuid']);

DB::transaction(function () use ($build) {
MinecraftBuildVote::where('build_id', $build->getKey())->delete();
$build->delete();
});

return response()->json();
}

/**
* Ensures the given player UUID has the ability to modify the given build.
* Basically checks the UUID is the build owner or a staff member.
*/
private function assertHasWriteAccess(MinecraftBuild $build, string $uuid): void
{
$uuid = new MinecraftUUID($uuid);
$player = MinecraftPlayer::whereUuid($uuid)->with('account.groups')->first();
abort_if($player === null, 403, "Player not found");

$isBuildOwner = $build->player_id === $player->getKey();

$groups = $player->account?->groups ?? collect();
$isStaff = $groups->where('group_type', 'staff')->isNotEmpty();
abort_if(!$isBuildOwner && !$isStaff, 403, "Only the build owner can edit this build");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Http\Controllers\Api\v2\Minecraft;

use App\Http\Controllers\ApiController;
use App\Models\MinecraftBuild;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

final class MinecraftBuildNameController extends ApiController
{
public function index(Request $request)
{
return MinecraftBuild::get(['id', 'name']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace App\Http\Controllers\Api\v2\Minecraft;

use App\Core\Domains\MinecraftUUID\Data\MinecraftUUID;
use App\Core\Domains\MinecraftUUID\Rules\MinecraftUUIDRule;
use App\Http\Controllers\ApiController;
use App\Models\MinecraftBuild;
use App\Models\MinecraftBuildVote;
use App\Models\MinecraftPlayer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class MinecraftBuildVoteController extends ApiController
{
public function store(Request $request, MinecraftBuild $build)
{
$input = $request->validate([
'player_uuid' => ['required', new MinecraftUUIDRule],
'alias' => ['string'],
]);

$player = MinecraftPlayer::firstOrCreate(
uuid: MinecraftUUID::tryParse($input['player_uuid']),
alias: $request->get('alias'),
);

if ($build->player_id === $player->getKey()) {
throw ValidationException::withMessages([
'error' => 'You cannot vote for your own build',
]);
}

$alreadyVoted = MinecraftBuildVote::where('player_id', $player->getKey())
->where('build_id', $build->getKey())
->exists();

if ($alreadyVoted) {
throw ValidationException::withMessages([
'error' => 'You have already voted for this build',
]);
}

DB::transaction(function () use ($player, $build) {
$build->votes = $build->votes + 1;
$build->save();

MinecraftBuildVote::create([
'player_id' => $player->getKey(),
'build_id' => $build->getKey(),
]);
});

return response()->json($build);
}

public function destroy(Request $request, MinecraftBuild $build)
{
$input = $request->validate([
'player_uuid' => ['required', new MinecraftUUIDRule],
]);

$player = MinecraftPlayer::whereUuid(MinecraftUUID::tryParse($input['player_uuid']))
->first();

$vote = MinecraftBuildVote::where('player_id', $player->getKey())
->where('build_id', $build->getKey())
->first();

if ($vote === null) {
throw ValidationException::withMessages([
'error' => 'You have not voted for this build',
]);
}

DB::transaction(function () use ($vote, $build) {
$build->votes = $build->votes - 1;
$build->save();

$vote->delete();
});

return response()->json($build);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use App\Http\Controllers\ApiController;
use App\Models\MinecraftWarp;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

Expand All @@ -18,7 +17,7 @@ public function index(Request $request)
public function store(Request $request)
{
$request->validate([
'name' => ['required', 'string', 'alpha_dash', Rule::unique('minecraft_warps')],
'name' => ['required', 'string', 'alpha_dash', Rule::unique(MinecraftWarp::tableName())],
'world' => 'required|string',
'x' => 'required|numeric',
'y' => 'required|numeric',
Expand All @@ -33,7 +32,7 @@ public function store(Request $request)
public function update(Request $request, MinecraftWarp $warp)
{
$request->validate([
'name' => ['required', 'string', 'alpha_dash', Rule::unique('minecraft_warps')->ignore($warp)],
'name' => ['required', 'string', 'alpha_dash', Rule::unique(MinecraftWarp::tableName())->ignore($warp)],
'world' => 'required|string',
'x' => 'required|numeric',
'y' => 'required|numeric',
Expand Down
46 changes: 46 additions & 0 deletions app/Models/MinecraftBuild.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Models;

use App\Core\Utilities\Traits\HasStaticTable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class MinecraftBuild extends Model
{
use HasFactory;
use HasStaticTable;

protected $table = 'minecraft_builds';

protected $fillable = [
'player_id',
'name',
'description',
'lore',
'votes',
'world',
'x',
'y',
'z',
'pitch',
'yaw',
'created_at',
'updated_at',
];

protected $casts = [
'pitch' => 'decimal:1',
'yaw' => 'decimal:1',
];

public function player(): BelongsTo
{
return $this->belongsTo(
related: MinecraftPlayer::class,
foreignKey: 'player_id',
ownerKey: MinecraftPlayer::primaryKey(),
);
}
}
22 changes: 22 additions & 0 deletions app/Models/MinecraftBuildVote.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace App\Models;

use App\Core\Utilities\Traits\HasStaticTable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class MinecraftBuildVote extends Model
{
use HasFactory;
use HasStaticTable;

protected $table = 'minecraft_build_votes';

protected $fillable = [
'build_id',
'player_id',
'created_at',
'updated_at',
];
}
25 changes: 25 additions & 0 deletions database/factories/MinecraftBuildFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Database\Factories;

use App\Models\MinecraftBuild;

class MinecraftBuildFactory extends Factory
{
protected $model = MinecraftBuild::class;

public function definition()
{
return [
'name' => strtolower($this->faker->unique()->name),
'world' => $this->faker->randomElement(['creative', 'survival', 'monarch']),
'x' => $this->faker->numberBetween(0, 20000),
'y' => $this->faker->numberBetween(0, 20000),
'z' => $this->faker->numberBetween(0, 20000),
'pitch' => $this->faker->randomFloat(nbMaxDecimals: 1, min: 0, max: 100),
'yaw' => $this->faker->randomFloat(nbMaxDecimals: 1, min: 0, max: 100),
'created_at' => $this->faker->dateTime,
'updated_at' => $this->faker->dateTime,
];
}
}
Loading

0 comments on commit 8f49888

Please sign in to comment.