Commit 0193b072 authored by Jacob Priddy's avatar Jacob Priddy 👌
Browse files

Merge branch '51-door-response' into 'master'

Resolve "Door Response"

Closes #51 and #61

See merge request !47
parents 0e35ca28 c5cfbb50
Pipeline #8941 passed with stages
in 2 minutes and 57 seconds
......@@ -3,12 +3,16 @@
namespace App\Exceptions;
use Throwable;
use Carbon\Carbon;
use App\Guards\DoorGuard;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\AuthenticationException;
use Source\Exceptions\EntityExistsException;
use Source\Exceptions\AuthorizationException;
use Illuminate\Validation\ValidationException;
use Source\Exceptions\EntityNotFoundException;
use Source\UseCases\Door\StatusResponse\JsonPresenter;
use Source\UseCases\Door\StatusResponse\StatusResponseUseCase;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Source\Exceptions\AuthenticationException as SourceAuthenticationException;
......@@ -57,6 +61,22 @@ class Handler extends ExceptionHandler
}
if ($e instanceof AuthorizationException) {
if ($request->is('api/door/*')) {
/** @var DoorGuard $doorGuard */
$doorGuard = app()->make(DoorGuard::class);
if (!$doorGuard->id()) {
// Should not be authenticated
return response()->json(['message' => $e->getMessage()], 401);
}
$presenter = new JsonPresenter();
/** @var StatusResponseUseCase $useCase */
$useCase = app()->make(StatusResponseUseCase::class);
$useCase->getStatusForDoor($doorGuard->id(), Carbon::now(), Carbon::now()->addMinutes(config('app.status_foresight')), $presenter);
return new JsonResponse($presenter->getViewModel(), 403);
}
return response()->json(['message' => $e->getMessage()], 403);
}
......
......@@ -2,24 +2,62 @@
namespace App\Http\Controllers;
use Carbon\Carbon;
use App\Guards\DoorGuard;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Source\Authorization\Authorizer;
use Source\UseCases\Door\Access\AccessUseCase;
use Source\UseCases\Door\StatusResponse\JsonPresenter;
use Source\UseCases\Door\StatusResponse\StatusResponseUseCase;
class DoorController extends ApiController
{
/**
* @var \App\Guards\DoorGuard
*/
protected DoorGuard $doorGuard;
/**
* @var \Source\UseCases\Door\StatusResponse\StatusResponseUseCase
*/
protected StatusResponseUseCase $response;
public function __construct(Request $request, Authorizer $authorizer, DoorGuard $doorGuard, StatusResponseUseCase $response)
{
parent::__construct($request, $authorizer);
$this->doorGuard = $doorGuard;
$this->response = $response;
}
/**
* @return \Illuminate\Http\JsonResponse
*/
protected function respondStatus(): JsonResponse
{
$presenter = new JsonPresenter();
$this->response->getStatusForDoor(
$this->doorGuard->id(),
Carbon::now(),
Carbon::now()->addMinutes(config('app.status_foresight')),
$presenter
);
return $this->respondWithData($presenter->getViewModel());
}
/**
* @param string $doorcode
* @param \App\Guards\DoorGuard $guard
* @param \Source\UseCases\Door\Access\AccessUseCase $access
* @return \Illuminate\Http\JsonResponse
* @throws \Source\Exceptions\AuthenticationException
* @throws \Source\Exceptions\AuthorizationException
*/
public function access(string $doorcode, DoorGuard $guard, AccessUseCase $access): JsonResponse
public function access(string $doorcode, AccessUseCase $access): JsonResponse
{
$access->protectUserDoorAccess($guard->id(), $doorcode);
$access->protectUserDoorAccess($this->doorGuard->id(), $doorcode);
return $this->respondSuccess();
return $this->respondStatus();
}
}
......@@ -17,11 +17,11 @@ class OverridesController extends ApiController
{
/**
* @param \Source\UseCases\Overrides\OverrideCreate\OverrideCreateUseCase $overrideCreate
* @param \App\Guards\ApiGuard $apiGuard
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
* @throws \Source\Exceptions\AuthorizationException
* @throws \Source\Exceptions\EntityNotFoundException
* @throws \Exception
*/
public function create(OverrideCreateUseCase $overrideCreate, ApiGuard $apiGuard): JsonResponse
{
......@@ -29,7 +29,7 @@ class OverridesController extends ApiController
$this->validate($this->request, [
'reason' => 'required|string|max:1024',
'door_id' => 'required|integer',
'type' => 'required|integer|between:' . Override::TYPE_OPEN . ',' . Override::TYPE_NORMAL,
'type' => 'required|integer|between:' . Override::TYPE_OPEN . ',' . Override::TYPE_LOCKED,
'start' => 'required|date|before:end',
'end' => 'required|date|after:start',
]);
......
......@@ -38,11 +38,11 @@ use Source\UseCases\Groups\UpdateGroup\UpdateGroupUseCaseServiceProvider;
use Source\UseCases\Tokens\CreateToken\CreateTokenUseCaseServiceProvider;
use Source\UseCases\Tokens\ExpireToken\ExpireTokenUseCaseServiceProvider;
use Source\UseCases\Tokens\UpdateToken\UpdateTokenUseCaseServiceProvider;
use Source\UseCases\Token\Authenticate\AuthenticateUseCaseServiceProvider;
use Source\UseCases\Attempts\GetAttempts\GetAttemptsUseCaseServiceProvider;
use Source\UseCases\Groups\GetAllGroups\GetAllGroupsUseCaseServiceProvider;
use Source\UseCases\Tokens\GetAllTokens\GetAllTokensUseCaseServiceProvider;
use Source\UseCases\Schedules\ScheduleGet\ScheduleGetUseCaseServiceProvider;
use Source\UseCases\Door\StatusResponse\StatusResponseUseCaseServiceProvider;
use Source\UseCases\Schedules\SchedulesGet\SchedulesGetUseCaseServiceProvider;
use Source\UseCases\DoorGroup\GetDoorGroups\GetDoorGroupsUseCaseServiceProvider;
use Source\UseCases\DoorGroup\GetGroupDoors\GetGroupDoorsUseCaseServiceProvider;
......@@ -69,6 +69,7 @@ use Source\UseCases\Entries\GetEntriesForDoorAndUser\GetEntriesForDoorAndUserUse
use Source\UseCases\GroupSchedule\ActiveSchedulesForGroup\ActiveSchedulesForGroupUseCaseServiceProvider;
use Source\UseCases\Door\Authenticate\AuthenticateUseCaseServiceProvider as DoorAuthenticateUseCaseServiceProvider;
use Source\UseCases\Users\Authenticate\AuthenticateUseCaseServiceProvider as UserAuthenticateUseCaseServiceProvider;
use Source\UseCases\Token\Authenticate\AuthenticateUseCaseServiceProvider as TokenAuthenticateUseCaseServiceProvider;
class AppServiceProvider extends ServiceProvider
{
......@@ -125,6 +126,8 @@ class AppServiceProvider extends ServiceProvider
// Door
AccessUseCaseServiceProvider::class,
StatusResponseUseCaseServiceProvider::class,
DoorAuthenticateUseCaseServiceProvider::class,
// Doors
GetDoorUseCaseServiceProvider::class,
......@@ -132,8 +135,6 @@ class AppServiceProvider extends ServiceProvider
DeleteDoorUseCaseServiceProvider::class,
UpdateDoorUseCaseServiceProvider::class,
GetAllDoorsUseCaseServiceProvider::class,
AuthenticateUseCaseServiceProvider::class,
DoorAuthenticateUseCaseServiceProvider::class,
UserAuthenticateUseCaseServiceProvider::class,
GenerateDoorTokenUseCaseServiceProvider::class,
......@@ -143,6 +144,7 @@ class AppServiceProvider extends ServiceProvider
ExpireTokenUseCaseServiceProvider::class,
UpdateTokenUseCaseServiceProvider::class,
GetAllTokensUseCaseServiceProvider::class,
TokenAuthenticateUseCaseServiceProvider::class,
// TokenUser
GetUserTokensUseCaseServiceProvider::class,
......
......@@ -67,7 +67,18 @@ return [
|
*/
'timezone' => 'UTC',
'timezone' => 'America/Los_Angeles',
/*
|--------------------------------------------------------------------------
| Status Response Range
|--------------------------------------------------------------------------
|
| Specify the response range for events when doors make a request to
| the doors api. Units are in minutes.
|
*/
'status_foresight' => 60 * 12,
/*
|--------------------------------------------------------------------------
......
......@@ -13,6 +13,9 @@ class CreateOverridesTable extends Migration
*/
public function up(): void
{
/*
* Only one override can be active at a time
*/
Schema::create('overrides', static function (Blueprint $table) {
$table->id();
$table->string('reason');
......
......@@ -10,7 +10,6 @@ class Override
{
public const TYPE_OPEN = 0;
public const TYPE_LOCKED = 1;
public const TYPE_NORMAL = 2;
protected int $id;
......@@ -53,7 +52,7 @@ class Override
?Carbon $createdAt = null,
?Carbon $updatedAt = null
) {
if ($type > self::TYPE_NORMAL || $type < self::TYPE_OPEN) {
if ($type > self::TYPE_LOCKED || $type < self::TYPE_OPEN) {
throw new InvalidArgumentException('Type not valid override type.');
}
......@@ -161,4 +160,22 @@ class Override
return $date->isAfter($this->getStart());
}
/**
* @param int $type
* @return bool
*/
public function hasTypeOf(int $type): bool
{
return $this->getType() === $type;
}
/**
* @param string $id
* @return bool
*/
public function hasIdOf(string $id): bool
{
return $this->getId() === $id;
}
}
......@@ -178,6 +178,10 @@ class Schedule
return $date->isAfter($this->getStart());
}
/**
* @param int|null $type
* @return bool
*/
public function hasTypeOf(?int $type): bool
{
return $this->type === $type;
......
......@@ -3,6 +3,7 @@
namespace Source\Gateways\DoorSchedule;
use Carbon\Carbon;
use Source\Sanitize\CastsTo;
use Source\Entities\Schedule;
use Illuminate\Database\ConnectionInterface;
......@@ -26,16 +27,17 @@ class DatabaseDoorScheduleRepository implements DoorScheduleRepository
*/
public function getActiveSchedulesForDoor(string $doorId): array
{
$openModeType = Schedule::TYPE_OPEN_MODE;
$type = Schedule::TYPE_OPEN_MODE;
$query = <<<QUERY
select S.id, S.group_id, S.type, S.rset, S.start, S.end, S.duration_ms, S.description, S.created_at, S.updated_at
FROM schedules as S
INNER JOIN door_group AS DG ON DG.group_id = S.group_id AND DG.door_id = ?
WHERE S.type = $openModeType AND ((CURRENT_DATE BETWEEN S.start AND S.end) OR (CURRENT_DATE > S.start AND S.end IS NULL))
INNER JOIN door_group AS DG ON DG.group_id = S.group_id AND DG.door_id = :DOOR_ID
WHERE S.type = :TYPE AND ((CURRENT_DATE BETWEEN S.start AND S.end) OR (CURRENT_DATE > S.start AND S.end IS NULL))
QUERY;
$schedules = $this->db->select($query, [
$this->castToInt($doorId),
':DOOR_ID' => $this->castToInt($doorId),
':TYPE' => $this->castToInt($type),
]);
return array_map(function ($schedule) {
......@@ -46,10 +48,46 @@ QUERY;
$schedule->rset,
$schedule->duration_ms,
$schedule->description,
$this->castToDate($schedule->start),
$this->castToDate($schedule->end),
$this->castToDate($schedule->created_at),
$this->castToDate($schedule->updated_at)
$this->castToCarbon($schedule->start),
$this->castToCarbon($schedule->end),
$this->castToCarbon($schedule->created_at),
$this->castToCarbon($schedule->updated_at)
);
}, $schedules);
}
/**
* @inheritDoc
*/
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, int $type = Schedule::TYPE_OPEN_MODE): array
{
$query = <<<QUERY
select S.id, S.group_id, S.type, S.rset, S.start, S.end, S.duration_ms, S.description, S.created_at, S.updated_at
FROM schedules as S
INNER JOIN door_group AS DG ON DG.group_id = S.group_id AND DG.door_id = :DOOR_ID
WHERE S.type = :TYPE AND (((:BEGIN, :END) OVERLAPS (s.start, S.end))
OR (S.start < :END AND S.end IS NULL))
QUERY;
$schedules = $this->db->select($query, [
':DOOR_ID' => $this->castToInt($doorId),
':TYPE' => $this->castToInt($type),
':BEGIN' => $begin,
':END' => $end,
]);
return array_map(function ($schedule) {
return new Schedule(
$schedule->id,
$schedule->group_id,
$schedule->type,
$schedule->rset,
$schedule->duration_ms,
$schedule->description,
$this->castToCarbon($schedule->start),
$this->castToCarbon($schedule->end),
$this->castToCarbon($schedule->created_at),
$this->castToCarbon($schedule->updated_at)
);
}, $schedules);
}
......
......@@ -3,6 +3,9 @@
namespace Source\Gateways\DoorSchedule;
use Carbon\Carbon;
use Source\Entities\Schedule;
interface DoorScheduleRepository
{
/**
......@@ -10,4 +13,13 @@ interface DoorScheduleRepository
* @return \Source\Entities\Schedule[]
*/
public function getActiveSchedulesForDoor(string $doorId): array;
/**
* @param string $doorId
* @param \Carbon\Carbon $begin
* @param \Carbon\Carbon $end
* @param int $type
* @return \Source\Entities\Schedule[]
*/
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, int $type = Schedule::TYPE_OPEN_MODE): array;
}
......@@ -3,6 +3,7 @@
namespace Source\Gateways\DoorSchedule;
use Carbon\Carbon;
use Source\Entities\Schedule;
class InMemoryDoorScheduleRepository implements DoorScheduleRepository
......@@ -32,4 +33,16 @@ class InMemoryDoorScheduleRepository implements DoorScheduleRepository
return $this->doorScheduleMap[$doorId];
}
/**
* @inheritDoc
*/
public function getSchedulesForDoorBetween(string $doorId, Carbon $begin, Carbon $end, int $type = Schedule::TYPE_OPEN_MODE): array
{
if (!isset($this->doorScheduleMap[$doorId])) {
return [];
}
return $this->doorScheduleMap[$doorId];
}
}
......@@ -43,8 +43,8 @@ QUERY;
$group->id,
$group->title,
$group->description,
$this->castToDate($group->created_at),
$this->castToDate($group->updated_at)
$this->castToCarbon($group->created_at),
$this->castToCarbon($group->updated_at)
);
}, $commonGroups);
}
......
......@@ -6,12 +6,23 @@ namespace Source\Gateways\Overrides;
use Carbon\Carbon;
use Source\Sanitize\CastsTo;
use Source\Entities\Override;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\ConnectionInterface;
use Source\Exceptions\EntityNotFoundException;
class DatabaseOverridesRepository implements OverridesRepository
{
use CastsTo;
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected ConnectionInterface $db;
public function __construct(ConnectionInterface $db)
{
$this->db = $db;
}
/**
* @param \App\Override $override
* @return \Source\Entities\Override
......@@ -31,6 +42,25 @@ class DatabaseOverridesRepository implements OverridesRepository
);
}
/**
* @param $override
* @return \Source\Entities\Override
*/
protected function toOverrideFromRaw($override): Override
{
return new Override(
$override->id,
$override->reason,
$override->user_id,
$override->door_id,
$override->type,
$this->castToCarbon($override->start),
$this->castToCarbon($override->end),
$this->castToCarbon($override->created_at),
$this->castToCarbon($override->updated_at)
);
}
/**
* @inheritDoc
*/
......@@ -38,6 +68,7 @@ class DatabaseOverridesRepository implements OverridesRepository
{
$overrides = \App\Override::query()
->where('door_id', $this->castToInt($doorId))
->orderByDesc('created_at')
->get()->values()->all();
return array_map(static function (\App\Override $override) {
......@@ -50,37 +81,48 @@ class DatabaseOverridesRepository implements OverridesRepository
*/
public function overrideHistoryBetween(Carbon $begin, Carbon $end): array
{
$overrides = \App\Override::query()
->whereBetween('start', [$begin, $end])
->orWhereBetween('end', [$begin, $end])
->orWhere(static function (Builder $query) use ($begin, $end) {
$query->where('start', '<', $begin)
->where('end', '>', $end);
})->get()->values()->all();
return array_map(static function (\App\Override $override) {
return self::toOverride($override);
$query = <<<QUERY
select A.id, A.reason, A.user_id, A.door_id, A.type, A.start, A.end, A.created_at, A.updated_at
from overrides as A
WHERE ((:BEGIN, :END) OVERLAPS (A.start, A.end))
ORDER BY A.created_at DESC
QUERY;
$overrides = $this->db->select($query, [
':BEGIN' => $begin,
':END' => $end,
]);
return array_map(function ($override) {
return $this->toOverrideFromRaw($override);
}, $overrides);
}
/**
* @inheritDoc
*/
public function activeOverrideForDoor(string $doorId, Carbon $date): ?Override
public function activeOverrideForDoorBetween(string $doorId, Carbon $begin, Carbon $end): ?Override
{
/** @var \App\Override|null $override */
$override = \App\Override::query()
->where('door_id', $this->castToInt($doorId))
->where('start', '<', $date)
->where('end', '>', $date)
->orderByDesc('created_at')
->first();
$query = <<<QUERY
select A.id, A.reason, A.user_id, A.door_id, A.type, A.start, A.end, A.created_at, A.updated_at
from overrides as A
WHERE A.door_id = :DOOR_ID
AND ((:BEGIN, :END) OVERLAPS (A.start, A.end))
ORDER BY A.created_at DESC
LIMIT 1
QUERY;
$override = $this->db->selectOne($query, [
':DOOR_ID' => $this->castToInt($doorId),
':BEGIN' => $begin,
':END' => $end,
]);
if (!$override) {
return null;
}
return self::toOverride($override);
return $this->toOverrideFromRaw($override);
}
/**
......@@ -106,4 +148,27 @@ class DatabaseOverridesRepository implements OverridesRepository
return self::toOverride($o);
}
/**
* @inheritDoc
*/
public function updateOverride(string $overrideId, Override $override): ?Override
{
/** @var \App\Override $o */
$o = \App\Override::query()->find($this->castToInt($overrideId));
if (!$o) {
throw new EntityNotFoundException('Override with id "' . $overrideId . '" does not exist.');
}
$o->setAttribute('start', $override->getStart());
$o->setAttribute('end', $override->getEnd());
$o->setAttribute('reason', $override->getReason());
if (!$o->save()) {
return null;
}
return self::toOverride($o);
}
}
......@@ -8,6 +8,9 @@ use Source\Entities\Override;
class InMemoryOverridesRepository implements OverridesRepository
{
/**
* @var \Source\Entities\Override[]
*/
protected array $overrides = [];
/**
......@@ -33,17 +36,15 @@ class InMemoryOverridesRepository implements OverridesRepository
/**
* @inheritDoc
*/
public function activeOverrideForDoor(string $doorId, Carbon $date): ?Override
public function activeOverrideForDoorBetween(string $doorId, Carbon $begin, Carbon $end): ?Override
{
$overrides = array_filter($this->overrides, static function (Override $override) use ($doorId, $date) {
return $override->hasDoorIdOf($doorId) && $override->isActiveForDate($date);
$overrides = array_filter($this->overrides, static function (Override $override) use ($doorId, $begin, $end) {
return $override->hasDoorIdOf($doorId) &&
($override->isActiveForDate($begin) || $override->isActiveForDate($end) ||
($begin->isBefore($override->getStart()) && $end->isAfter($override->getEnd())));
});
if (count($overrides) > 0) {
return $overrides[0];
}
return null;
return array_shift($overrides);
}
/**
......@@ -55,4 +56,18 @@ class InMemoryOverridesRepository implements OverridesRepository
return $override;