mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-15 13:46:09 +00:00
Added inern app integration
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Enums;
|
||||
enum RoomStateChange: string
|
||||
{
|
||||
case PlayerJoined = 'player_joined';
|
||||
case PlayerUpdated = 'player_updated';
|
||||
case VoteChanged = 'vote_changed';
|
||||
case CardsRevealed = 'cards_revealed';
|
||||
case VotesReset = 'votes_reset';
|
||||
@@ -12,4 +13,5 @@ enum RoomStateChange: string
|
||||
case RoomReopened = 'room_reopened';
|
||||
case RoomClosed = 'room_closed';
|
||||
case RoomDeleted = 'room_deleted';
|
||||
case RoomSettingsUpdated = 'room_settings_updated';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class EmbeddedLogoutController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*/
|
||||
public function __invoke(Request $request): RedirectResponse
|
||||
{
|
||||
$joinCode = $request->string('join_code')->trim()->upper()->toString();
|
||||
$playerName = $request->string('player_name')->trim()->toString();
|
||||
|
||||
Auth::guard('web')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return to_route('embedded.room', [
|
||||
'join_code' => $joinCode,
|
||||
'player_name' => $playerName,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ShowEmbeddedRoomRequest;
|
||||
use App\Http\Resources\CurrentPlayerResource;
|
||||
use App\Http\Resources\RoomResource;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use App\Services\Rooms\RoomPageService;
|
||||
use App\Services\Rooms\RoomParticipationService;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class EmbeddedRoomController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomParticipationService $roomParticipation,
|
||||
private readonly RoomPageService $roomPage,
|
||||
) {}
|
||||
|
||||
public function __invoke(ShowEmbeddedRoomRequest $request): Response
|
||||
{
|
||||
/** @var array{join_code: string, player_name: string} $validated */
|
||||
$validated = $request->validated();
|
||||
$user = $request->user();
|
||||
|
||||
abort_unless($user instanceof User, 403);
|
||||
|
||||
$room = Room::query()
|
||||
->where('join_code', $validated['join_code'])
|
||||
->firstOrFail();
|
||||
$player = $this->roomParticipation->joinEmbedded(
|
||||
$room,
|
||||
$user,
|
||||
$validated['player_name'],
|
||||
);
|
||||
|
||||
$request->session()->put("room_players.{$room->id}", $player->id);
|
||||
$request->session()->forget('embedded_login');
|
||||
|
||||
$pageData = $this->roomPage->prepare($room, $player, $user);
|
||||
|
||||
return Inertia::render('embedded/Room', [
|
||||
'room' => (new RoomResource($pageData))->resolve($request),
|
||||
'currentPlayer' => (new CurrentPlayerResource($pageData))->resolve($request),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UpdateRoomSettingsRequest;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use App\Services\Rooms\RoomSettingsService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class RoomSettingsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomSettingsService $roomSettings,
|
||||
) {}
|
||||
|
||||
public function edit(Room $room, Request $request): Response
|
||||
{
|
||||
abort_unless($request->user()?->id === $room->owner_id, 403);
|
||||
|
||||
return Inertia::render('rooms/Settings', [
|
||||
'room' => [
|
||||
'id' => $room->id,
|
||||
'title' => $room->title,
|
||||
'join_code' => $room->join_code,
|
||||
'allow_participants_to_manage_rounds' => $room->allow_participants_to_manage_rounds,
|
||||
'is_persistent' => $room->is_persistent,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateRoomSettingsRequest $request, Room $room): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
abort_unless($user instanceof User, 403);
|
||||
|
||||
/** @var array{allow_participants_to_manage_rounds: bool, is_persistent: bool} $settings */
|
||||
$settings = $request->validated();
|
||||
$this->roomSettings->update($room, $user, $settings);
|
||||
|
||||
Inertia::flash('toast', [
|
||||
'type' => 'success',
|
||||
'message' => 'Die Raumeinstellungen wurden gespeichert.',
|
||||
]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class RoundController extends Controller
|
||||
|
||||
public function reset(Request $request, Room $room): Response
|
||||
{
|
||||
$this->ensureModerator($request, $room);
|
||||
$this->ensureCanManageRounds($request, $room);
|
||||
$this->rounds->reset($room);
|
||||
|
||||
return response()->noContent();
|
||||
@@ -24,7 +24,7 @@ class RoundController extends Controller
|
||||
|
||||
public function reveal(Request $request, Room $room): Response
|
||||
{
|
||||
$this->ensureModerator($request, $room);
|
||||
$this->ensureCanManageRounds($request, $room);
|
||||
$this->rounds->reveal($room);
|
||||
|
||||
return response()->noContent();
|
||||
@@ -32,20 +32,20 @@ class RoundController extends Controller
|
||||
|
||||
public function nextRound(Request $request, Room $room): Response
|
||||
{
|
||||
$this->ensureModerator($request, $room);
|
||||
$this->ensureCanManageRounds($request, $room);
|
||||
$this->rounds->startNext($room);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
private function ensureModerator(Request $request, Room $room): void
|
||||
private function ensureCanManageRounds(Request $request, Room $room): void
|
||||
{
|
||||
$player = $request->attributes->get('currentPlayer');
|
||||
|
||||
abort_unless(
|
||||
$player instanceof Player
|
||||
&& $player->room_id === $room->id
|
||||
&& $player->is_moderator,
|
||||
&& ($player->is_moderator || $room->allow_participants_to_manage_rounds),
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShowEmbeddedRoomRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'join_code' => $this->string('join_code')->trim()->upper()->toString(),
|
||||
'player_name' => $this->string('player_name')->trim()->toString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'join_code' => [
|
||||
'bail',
|
||||
'required',
|
||||
'string',
|
||||
'size:6',
|
||||
Rule::exists(Room::class, 'join_code')->whereNull('deleted_at'),
|
||||
],
|
||||
'player_name' => ['required', 'string', 'max:50'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<callable(Validator): void> */
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator): void {
|
||||
if ($validator->errors()->has('join_code')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$room = Room::query()
|
||||
->where('join_code', $this->input('join_code'))
|
||||
->first();
|
||||
|
||||
if ($room?->isClosed()) {
|
||||
$validator->errors()->add('join_code', 'Dieser Raum ist geschlossen.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($room === null || $this->user() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nameIsTaken = Player::query()
|
||||
->where('room_id', $room->id)
|
||||
->where('name', $this->input('player_name'))
|
||||
->where(function ($query): void {
|
||||
$query->whereNull('user_id')
|
||||
->orWhere('user_id', '!=', $this->user()->id);
|
||||
})
|
||||
->exists();
|
||||
|
||||
if ($nameIsTaken) {
|
||||
$validator->errors()->add(
|
||||
'player_name',
|
||||
'Dieser Anzeigename ist in dem Raum bereits vorhanden.',
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'join_code.exists' => 'Dieser Raumcode existiert nicht.',
|
||||
'join_code.size' => 'Der Raumcode muss aus sechs Zeichen bestehen.',
|
||||
'player_name.required' => 'Bitte hinterlege in der Hub-App einen Anzeigenamen.',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator): never
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
Inertia::render('embedded/Error', [
|
||||
'message' => $validator->errors()->first(),
|
||||
])->toResponse($this)->setStatusCode(422),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Room;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateRoomSettingsRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$room = $this->route('room');
|
||||
|
||||
return $room instanceof Room
|
||||
&& $this->user()?->id === $room->owner_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'allow_participants_to_manage_rounds' => ['required', 'boolean'],
|
||||
'is_persistent' => ['required', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ class RoomResource extends JsonResource
|
||||
'current_round' => $room->current_round,
|
||||
'revealed' => $room->revealed_at !== null,
|
||||
'state_version' => $room->state_version,
|
||||
'allow_participants_to_manage_rounds' => $room->allow_participants_to_manage_rounds,
|
||||
'is_persistent' => $room->is_persistent,
|
||||
'statistics' => $pageData->statistics,
|
||||
'deck' => (new RoomDeckResource($room->deck_config))->resolve($request),
|
||||
'players' => $room->players
|
||||
|
||||
@@ -21,6 +21,7 @@ class CloseInactiveRooms implements ShouldQueue
|
||||
|
||||
Room::query()
|
||||
->whereNull('closed_at')
|
||||
->where('is_persistent', false)
|
||||
->where(function ($query) use ($guestCutoff, $accountCutoff): void {
|
||||
$query
|
||||
->where(function ($query) use ($guestCutoff): void {
|
||||
@@ -40,7 +41,12 @@ class CloseInactiveRooms implements ShouldQueue
|
||||
->lockForUpdate()
|
||||
->find((string) $roomId);
|
||||
|
||||
if ($room === null || $room->isClosed() || $room->last_activity_at === null) {
|
||||
if (
|
||||
$room === null
|
||||
|| $room->isClosed()
|
||||
|| $room->is_persistent
|
||||
|| $room->last_activity_at === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property CarbonImmutable|null $last_activity_at
|
||||
* @property CarbonImmutable|null $closed_at
|
||||
* @property CarbonImmutable|null $deleted_at
|
||||
* @property bool $allow_participants_to_manage_rounds
|
||||
* @property bool $is_persistent
|
||||
* @property int $players_count
|
||||
*/
|
||||
class Room extends Model
|
||||
@@ -37,6 +39,13 @@ class Room extends Model
|
||||
'state_version',
|
||||
'last_activity_at',
|
||||
'closed_at',
|
||||
'allow_participants_to_manage_rounds',
|
||||
'is_persistent',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'allow_participants_to_manage_rounds' => false,
|
||||
'is_persistent' => false,
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -48,6 +57,8 @@ class Room extends Model
|
||||
'last_activity_at' => 'immutable_datetime',
|
||||
'closed_at' => 'immutable_datetime',
|
||||
'deck_config' => 'array',
|
||||
'allow_participants_to_manage_rounds' => 'boolean',
|
||||
'is_persistent' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
Fortify::loginView(fn (Request $request) => Inertia::render('auth/Login', [
|
||||
'canResetPassword' => Features::enabled(Features::resetPasswords()),
|
||||
'status' => $request->session()->get('status'),
|
||||
'rememberByDefault' => $request->session()->get('embedded_login', false),
|
||||
]));
|
||||
|
||||
Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/ResetPassword', [
|
||||
|
||||
@@ -101,4 +101,82 @@ class RoomParticipationService
|
||||
|
||||
return [$room, $player];
|
||||
}
|
||||
|
||||
public function joinEmbedded(Room $room, User $user, string $playerName): Player
|
||||
{
|
||||
[$player, $change, $stateVersion] = DB::transaction(function () use ($room, $user, $playerName): array {
|
||||
$lockedRoom = Room::query()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
if ($lockedRoom->isClosed()) {
|
||||
throw ValidationException::withMessages([
|
||||
'join_code' => 'Dieser Raum ist geschlossen.',
|
||||
]);
|
||||
}
|
||||
|
||||
$nameIsTaken = $lockedRoom->players()
|
||||
->where('name', $playerName)
|
||||
->where(function ($query) use ($user): void {
|
||||
$query->whereNull('user_id')
|
||||
->orWhere('user_id', '!=', $user->id);
|
||||
})
|
||||
->exists();
|
||||
|
||||
if ($nameIsTaken) {
|
||||
throw ValidationException::withMessages([
|
||||
'player_name' => 'Dieser Anzeigename ist in dem Raum bereits vorhanden.',
|
||||
]);
|
||||
}
|
||||
|
||||
$player = $lockedRoom->players()
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
$change = null;
|
||||
|
||||
if ($player === null) {
|
||||
$player = $lockedRoom->players()->create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $playerName,
|
||||
'is_moderator' => false,
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
$change = RoomStateChange::PlayerJoined;
|
||||
} else {
|
||||
if ($player->inactive_at !== null) {
|
||||
$change = RoomStateChange::PlayerJoined;
|
||||
} elseif ($player->name !== $playerName) {
|
||||
$change = RoomStateChange::PlayerUpdated;
|
||||
}
|
||||
|
||||
$player->update([
|
||||
'name' => $playerName,
|
||||
'last_seen_at' => now(),
|
||||
'inactive_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($change === null) {
|
||||
return [$player, null, null];
|
||||
}
|
||||
|
||||
$lockedRoom->update([
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
|
||||
return [$player, $change, $lockedRoom->state_version];
|
||||
});
|
||||
|
||||
if ($change instanceof RoomStateChange && is_int($stateVersion)) {
|
||||
RoomStateChanged::dispatch(
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
$change,
|
||||
$this->realtimeState->for($room),
|
||||
);
|
||||
}
|
||||
|
||||
return $player;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class RoomRealtimeStateService
|
||||
* current_round: int,
|
||||
* revealed: bool,
|
||||
* state_version: int,
|
||||
* allow_participants_to_manage_rounds: bool,
|
||||
* statistics: array{average: string|null, median: string|null}|null,
|
||||
* players: array<int, array{
|
||||
* id: int,
|
||||
@@ -44,6 +45,7 @@ class RoomRealtimeStateService
|
||||
'current_round' => $room->current_round,
|
||||
'revealed' => $room->revealed_at !== null,
|
||||
'state_version' => $room->state_version,
|
||||
'allow_participants_to_manage_rounds' => $room->allow_participants_to_manage_rounds,
|
||||
'statistics' => $this->statistics->currentRound($room),
|
||||
'players' => $room->players
|
||||
->map(function ($player) use ($room): array {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Rooms;
|
||||
|
||||
use App\Enums\RoomStateChange;
|
||||
use App\Events\RoomStateChanged;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RoomSettingsService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomRealtimeStateService $realtimeState,
|
||||
) {}
|
||||
|
||||
/** @param array{allow_participants_to_manage_rounds: bool, is_persistent: bool} $settings */
|
||||
public function update(Room $room, User $user, array $settings): void
|
||||
{
|
||||
$stateVersion = DB::transaction(function () use ($room, $user, $settings): ?int {
|
||||
$lockedRoom = Room::query()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
abort_unless($lockedRoom->owner_id === $user->id, 403);
|
||||
|
||||
$lockedRoom->fill($settings);
|
||||
|
||||
if (! $lockedRoom->isDirty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lockedRoom->state_version++;
|
||||
$lockedRoom->save();
|
||||
|
||||
return $lockedRoom->state_version;
|
||||
});
|
||||
|
||||
if ($stateVersion === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RoomStateChanged::dispatch(
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
RoomStateChange::RoomSettingsUpdated,
|
||||
$this->realtimeState->for($room),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user