Commit 2192d7ca authored by Jacob Priddy's avatar Jacob Priddy 👌

Documentation for door and auth routes. Also make in memory

implmentation of overrides more realistic
parent 0ab32134
......@@ -13,7 +13,6 @@ use Source\UseCases\Users\Authenticate\APIPresenter;
use Source\UseCases\Users\Authenticate\AuthenticateUseCase;
use Source\UseCases\Users\Authenticate\UserCreationException;
/**
* @group Authentication
*
......
......@@ -14,6 +14,12 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCase;
use Source\UseCases\Door\StatusResponse\StatusResponseUseCase;
/**
* @group Door Routes
*
* Set of routes for door clients wanting to protect access to doors.
* Includes routes for verifying a doorcode as well as getting open mode times from schedules and overrides
*/
class DoorController extends ApiController
{
/**
......@@ -35,15 +41,22 @@ class DoorController extends ApiController
/**
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
*/
protected function respondStatus(): JsonResponse
{
$this->validate($this->request, [
'foresight' => 'integer',
]);
$foresight = $this->request->input('foresight') ?? config('app.status_foresight');
$presenter = new JsonPresenter();
$this->response->getStatusForDoor(
$this->doorGuard->id(),
Carbon::now(),
Carbon::now()->addMinutes(config('app.status_foresight')),
Carbon::now()->addMinutes($foresight),
$presenter
);
......@@ -51,11 +64,27 @@ class DoorController extends ApiController
}
/**
* Door Access
*
* Checks to see if the given doorcode can currently access the authenticated door.
* Also processes commands that are separated by a '*' from the doorcode. If a command is accepted a 200 is
* returned. If a command is rejected, a 403 is given.
*
* @authenticated
* @urlParam doorcode required The doorcode to query. Example: 123456*00110
* @queryParam Number of minutes ahead of now to get the open mode times for
*
* @response 422 {"message":"The given data was invalid.","errors":{"foresight":["The foresight must be an
* integer."]}}
* @response 403
* {"events":[{"begins_at":"2020-06-03T11:54:07-07:00","ends_at":"2020-06-03T11:55:07-07:00"},{"begins_at":"2020-06-03T11:55:07-07:00","ends_at":"2020-06-03T12:15:07-07:00"},{"begins_at":"2020-06-03T21:54:07-07:00","ends_at":"2020-06-03T21:55:07-07:00"}]}
*
* @param string $doorcode
* @param \Source\UseCases\Door\Access\AccessUseCase $access
* @return \Illuminate\Http\JsonResponse
* @throws \Source\Exceptions\AuthenticationException
* @throws \Source\Exceptions\AuthorizationException
* @throws \Illuminate\Validation\ValidationException
*/
public function access(string $doorcode, AccessUseCase $access): JsonResponse
{
......@@ -65,7 +94,20 @@ class DoorController extends ApiController
}
/**
* Door Open Times
*
* This route returns the times when the authenticated door client is supposed to go into open mode accepting any
* key press as a valid door unlock. Retrieves the open mode times for the next interval. Includes open mode
* schedules as well as overrides. The door to get the times for is based off of the authenticated door.
*
* @authenticated
* @queryParam Number of minutes ahead of now to get the open mode times for
*
* @response 422 {"message":"The given data was invalid.","errors":{"foresight":["The foresight must be an
* integer."]}}
*
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
*/
public function status(): JsonResponse
{
......@@ -73,6 +115,15 @@ class DoorController extends ApiController
}
/**
* Door Update
*
* This route returns the newest binary that the door controllers should be running based upon the authenticated
* door. If there are no binaries on record, a 404 response is returned.
*
* @authenticated
*
* @response 404 {"status":"error","code":404,"message":"Entity not found"}
*
* @param \Source\UseCases\Door\UpdateBinary\UpdateBinaryUseCase $updateBinaryUseCase
* @param \App\Guards\DoorGuard $doorGuard
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
......
......@@ -9,9 +9,7 @@ use Source\UseCases\Schedules\SchedulesGet\SchedulesGetUseCase;
use Source\UseCases\Schedules\APIPresenter as ScheduleApiPresenter;
use Source\UseCases\Schedules\ScheduleCreate\ScheduleCreateUseCase;
use Source\UseCases\Schedules\ScheduleUpdate\ScheduleUpdateUseCase;
use Source\UseCases\GroupSchedule\APIPresenter as SchedulesForGroupApiPresenter;
use Source\UseCases\Schedules\SchedulesGet\APIPresenter as SchedulesGetApiPresenter;
use Source\UseCases\GroupSchedule\ActiveSchedulesForGroup\ActiveSchedulesForGroupUseCase;
class SchedulesController extends ApiController
{
......@@ -126,27 +124,4 @@ class SchedulesController extends ApiController
return $this->respondWithData($presenter->getViewModel());
}
/**
* @param string $groupId
* @param \Source\UseCases\GroupSchedule\ActiveSchedulesForGroup\ActiveSchedulesForGroupUseCase $activeForGroup
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
* @throws \Source\Exceptions\AuthorizationException
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function activeForGroup(string $groupId, ActiveSchedulesForGroupUseCase $activeForGroup): JsonResponse
{
$this->authorizer->protect(Permissions::MANAGE_DOORS);
$this->validate($this->request, [
'date' => 'date',
]);
$presenter = new SchedulesForGroupApiPresenter();
$activeForGroup->activeForGroup($groupId, $this->request->input('date'), $presenter);
return $this->respondWithData($presenter->getViewModel());
}
}
......@@ -63,7 +63,6 @@ use Source\UseCases\Schedules\ScheduleUpdate\ScheduleUpdateUseCaseServiceProvide
use Source\UseCases\Doors\GenerateDoorToken\GenerateDoorTokenUseCaseServiceProvider;
use Source\UseCases\DoorGroup\RemoveDoorFromGroup\RemoveDoorFromGroupUseCaseServiceProvider;
use Source\UseCases\GroupUser\RemoveUserFromGroup\RemoveUserFromGroupUseCaseServiceProvider;
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;
......@@ -159,7 +158,6 @@ class AppServiceProvider extends ServiceProvider
SchedulesGetUseCaseServiceProvider::class,
ScheduleCreateUseCaseServiceProvider::class,
ScheduleUpdateUseCaseServiceProvider::class,
ActiveSchedulesForGroupUseCaseServiceProvider::class,
// Overrides
OverrideGetUseCaseServiceProvider::class,
......
......@@ -184,6 +184,97 @@ return [
],
],
/*
*
*
*
* DOOR ROUTES
*
*
*
*
*/
[
/*
* Specify conditions to determine what routes will be parsed in this group.
* A route must fulfill ALL conditions to pass.
*/
'match' => [
/*
* Match only routes whose domains match this pattern (use * as a wildcard to match any characters).
*/
'domains' => [
'*',
// 'domain1.*',
],
/*
* Match only routes whose paths match this pattern (use * as a wildcard to match any characters).
*/
'prefixes' => [
],
],
/*
* Include these routes when generating documentation,
* even if they did not match the rules above.
* Note that the route must be referenced by name here (wildcards are supported).
*/
'include' => [
'door.*',
],
/*
* Exclude these routes when generating documentation,
* even if they matched the rules above.
* Note that the route must be referenced by name here (wildcards are supported).
*/
'exclude' => [
// 'users.create', 'admin.*'
],
/*
* Specify rules to be applied to all the routes in this group when generating documentation
*/
'apply' => [
/*
* Specify headers to be added to the example requests
*/
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Authorization' => 'Bearer door_1_api_token',
// 'Api-Version' => 'v2',
],
/*
* If no @response or @transformer declarations are found for the route,
* we'll try to get a sample response by attempting an API call.
* Configure the settings for the API call here.
*/
'response_calls' => [
/*
* API calls will be made only for routes in this group matching these HTTP methods (GET, POST, etc).
* List the methods here or use '*' to mean all methods. Leave empty to disable API calls.
*/
'methods' => ['*'],
/*
* Laravel config variables which should be set for the API call.
* This is a good place to ensure that notifications, emails
* and other external services are not triggered
* during the documentation API calls
*/
'config' => [
'app.env' => 'memory',
'app.debug' => false,
// 'service.key' => 'value',
],
],
],
],
/*
*
*
......@@ -234,7 +325,8 @@ return [
* Note that the route must be referenced by name here (wildcards are supported).
*/
'exclude' => [
'me.*'
'me.*',
'door.*',
// 'users.create', 'admin.*'
],
......
......@@ -22,50 +22,51 @@ use App\Http\Controllers\SchedulesController;
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::post('login', [AuthController::class, 'login']);
Route::post('login', [AuthController::class, 'login'])->name('login');
Route::post('logout', [AuthController::class, 'samlLogout']);
Route::post('logout', [AuthController::class, 'samlLogout'])->name('logout');
Route::group(['middleware' => 'auth:api'], static function () {
Route::group([
'prefix' => 'users',
], static function () {
Route::get('/', [UsersController::class, 'index']);
Route::post('/', [UsersController::class, 'store']);
Route::get('{userId}', [UsersController::class, 'get']);
Route::patch('{userId}', [UsersController::class, 'update']);
Route::delete('{userId}', [UsersController::class, 'delete']);
Route::get('/', [UsersController::class, 'index'])->name('users.index');
Route::post('/', [UsersController::class, 'store'])->name('users.create');
Route::get('{userId}', [UsersController::class, 'get'])->name('users.get');
Route::patch('{userId}', [UsersController::class, 'update'])->name('users.update');
Route::delete('{userId}', [UsersController::class, 'delete'])->name('users.delete');
Route::get('{userId}/groups', [UsersController::class, 'getGroupsForUser']);
Route::post('{userId}/group/{groupId}', [UsersController::class, 'addUserToGroup']);
Route::delete('{userId}/group/{groupId}', [UsersController::class, 'removeUserFromGroup']);
Route::get('{userId}/doors', [UsersController::class, 'doors']);
Route::post('{userId}/group/{groupId}', [UsersController::class, 'addUserToGroup'])
->name('users.group.add');
Route::delete('{userId}/group/{groupId}', [UsersController::class, 'removeUserFromGroup'])
->name('users.groups.delete');
Route::get('{userId}/doors', [UsersController::class, 'doors'])->name('users.doors');
});
Route::group([
'prefix' => 'groups',
], static function () {
Route::get('/', [GroupsController::class, 'index']);
Route::post('/', [GroupsController::class, 'store']);
Route::get('{groupId}', [GroupsController::class, 'get']);
Route::put('{groupId}', [GroupsController::class, 'update']);
Route::delete('{groupId}', [GroupsController::class, 'delete']);
Route::get('/', [GroupsController::class, 'index'])->name('groups.index');
Route::post('/', [GroupsController::class, 'store'])->name('groups.create');
Route::get('{groupId}', [GroupsController::class, 'get'])->name('groups.get');
Route::put('{groupId}', [GroupsController::class, 'update'])->name('groups.update');
Route::delete('{groupId}', [GroupsController::class, 'delete'])->name('groups.delete');
Route::get('{groupId}/users', [GroupsController::class, 'getUsersForGroup']);
Route::get('{groupId}/doors', [GroupsController::class, 'getDoorsForGroup']);
Route::get('{groupId}/active', [SchedulesController::class, 'activeForGroup']);
});
Route::group([
'prefix' => 'doors',
], static function () {
Route::get('/', [DoorsController::class, 'index']);
Route::post('/', [DoorsController::class, 'store']);
Route::get('{doorId}', [DoorsController::class, 'get']);
Route::patch('{doorId}', [DoorsController::class, 'update']);
Route::post('/{doorId}/regenerate-token', [DoorsController::class, 'regenerateToken']);
Route::delete('{doorId}', [DoorsController::class, 'delete']);
Route::get('/', [DoorsController::class, 'index'])->name('doors.index');
Route::post('/', [DoorsController::class, 'store'])->name('doors.create');
Route::get('{doorId}', [DoorsController::class, 'get'])->name('doors.get');
Route::patch('{doorId}', [DoorsController::class, 'update'])->name('doors.update');
Route::post('/{doorId}/regenerate-token', [DoorsController::class, 'regenerateToken'])
->name('doors.regenerate');
Route::delete('{doorId}', [DoorsController::class, 'delete'])->name('doors.delete');
Route::get('{doorId}/groups', [DoorsController::class, 'getGroupsForDoor']);
Route::post('{doorId}/group/{groupId}', [DoorsController::class, 'addDoorToGroup']);
......@@ -75,41 +76,41 @@ Route::group(['middleware' => 'auth:api'], static function () {
Route::group([
'prefix' => 'schedules',
], static function () {
Route::get('/', [SchedulesController::class, 'index']);
Route::post('/', [SchedulesController::class, 'create']);
Route::get('{scheduleId}', [SchedulesController::class, 'get']);
Route::patch('{scheduleId}', [SchedulesController::class, 'update']);
Route::get('/', [SchedulesController::class, 'index'])->name('schedules.index');
Route::post('/', [SchedulesController::class, 'create'])->name('schedules.create');
Route::get('{scheduleId}', [SchedulesController::class, 'get'])->name('schedules.get');
Route::patch('{scheduleId}', [SchedulesController::class, 'update'])->name('schedules.update');
});
Route::group([
'prefix' => 'tokens',
], static function () {
Route::get('/', [TokensController::class, 'index']);
Route::post('/', [TokensController::class, 'store']);
Route::get('{tokenId}', [TokensController::class, 'get']);
Route::patch('{tokenId}', [TokensController::class, 'update']);
Route::post('{tokenId}/expire', [TokensController::class, 'expire']);
Route::get('/', [TokensController::class, 'index'])->name('tokens.index');
Route::post('/', [TokensController::class, 'store'])->name('tokens.create');
Route::get('{tokenId}', [TokensController::class, 'get'])->name('tokens.get');
Route::patch('{tokenId}', [TokensController::class, 'update'])->name('tokens.update');
Route::post('{tokenId}/expire', [TokensController::class, 'expire'])->name('tokens.expire');
});
Route::group([
'prefix' => 'entries',
], static function () {
Route::get('/', [EntriesController::class, 'index']);
Route::get('/', [EntriesController::class, 'index'])->name('entries.index');
});
Route::group([
'prefix' => 'attempts',
], static function () {
Route::get('/', [AttemptsController::class, 'index']);
Route::get('/', [AttemptsController::class, 'index'])->name('attempts.index');
});
Route::group([
'prefix' => 'overrides',
], static function () {
Route::get('/', [OverridesController::class, 'index']);
Route::post('/', [OverridesController::class, 'create']);
Route::get('{overrideId}', [OverridesController::class, 'get']);
Route::patch('{overrideId}', [OverridesController::class, 'update']);
Route::get('/', [OverridesController::class, 'index'])->name('overrides.index');
Route::post('/', [OverridesController::class, 'create'])->name('overrides.create');
Route::get('{overrideId}', [OverridesController::class, 'get'])->name('overrides.get');
Route::patch('{overrideId}', [OverridesController::class, 'update'])->name('overrides.update');
});
Route::group([
......
......@@ -15,12 +15,8 @@ use App\Http\Controllers\DoorController;
|
*/
Route::get('ping', static function () {
return 'pong';
});
Route::get('access/{doorcode}', [DoorController::class, 'access'])->name('door.access');
Route::get('access/{doorcode}', [DoorController::class, 'access']);
Route::get('status', [DoorController::class, 'status'])->name('door.status');
Route::get('status', [DoorController::class, 'status']);
Route::get('update', [DoorController::class, 'getUpdateBinary']);
Route::get('update', [DoorController::class, 'getUpdateBinary'])->name('door.update');
......@@ -171,7 +171,7 @@ class Override
return $date->isBetween($this->getStart(), $this->getEnd());
}
return $date->isAfter($this->getStart());
return $date->greaterThanOrEqualTo($this->getStart());
}
/**
......
......@@ -42,13 +42,15 @@ class InMemoryOverridesRepository implements OverridesRepository
*/
public function activeOverrideForDoorBetween(string $doorId, Carbon $begin, Carbon $end): ?Override
{
$overrides = array_filter($this->overrides, static function (Override $override) use ($doorId, $begin, $end) {
$overrides = collect(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())));
});
}));
return array_shift($overrides);
return $overrides
->sortBy(fn (Override $item) => $item->getCreatedAt(), SORT_DESC, true)
->first();
}
/**
......@@ -56,6 +58,18 @@ class InMemoryOverridesRepository implements OverridesRepository
*/
public function addOverride(Override $override): ?Override
{
$override = new Override(
$override->getId(),
$override->getReason(),
$override->getUserId(),
$override->getDoorId(),
$override->getType(),
$override->getStart(),
$override->getEnd(),
$override->getCreatedAt() ?? Carbon::now(),
$override->getUpdatedAt() ?? Carbon::now()
);
$this->overrides[] = $override;
return $override;
......
......@@ -13,6 +13,7 @@ class LocalOverridesRepository extends InMemoryOverridesRepository
public function __construct()
{
$date = Carbon::now()->addMinute();
$yesterday = Carbon::now()->subDay();
$this->addOverride(new Override(
1,
'Override amazon door cause idk why just for some reason.',
......@@ -21,8 +22,8 @@ class LocalOverridesRepository extends InMemoryOverridesRepository
Override::TYPE_OPEN,
$date,
$date->clone()->addMinutes(20),
$date,
$date
$yesterday,
$yesterday
));
$this->addOverride(new Override(
......@@ -31,10 +32,10 @@ class LocalOverridesRepository extends InMemoryOverridesRepository
LocalUsersRepository::getAdminUser()->getId(),
LocalDoorsRepository::getTheBatCave()->getId(),
Override::TYPE_LOCKED,
$date->clone()->subDay(),
$yesterday,
$date->clone()->subHours(15),
$date,
$date
$yesterday,
$yesterday
));
}
}
<?php
namespace Source\UseCases\GroupSchedule;
use Source\Entities\Schedule;
use Source\Sanitize\Paginates;
use Source\UseCases\BasePresenter;
class APIPresenter extends BasePresenter implements Presenter
{
use Paginates;
protected array $schedules = [];
/** @inheritDoc */
public function present(ResponseModel $responseModel): void
{
$this->schedules = array_map(function (Schedule $schedule) {
return $this->formatSchedule($schedule);
}, $responseModel->getSchedules());
}
/** @inheritDoc */
public function getViewModel(): array
{
return $this->paginate($this->schedules);
}
}
<?php
namespace Source\UseCases\GroupSchedule\ActiveSchedulesForGroup;
use Carbon\Carbon;
use Source\Gateways\Groups\GroupsRepository;
use Source\UseCases\GroupSchedule\Presenter;
use Source\Exceptions\EntityNotFoundException;
use Source\UseCases\GroupSchedule\ResponseModel;
use Source\Gateways\Schedules\SchedulesRepository;
class ActiveSchedulesForGroup implements ActiveSchedulesForGroupUseCase
{
/**
* @var \Source\Gateways\Schedules\SchedulesRepository
*/
protected SchedulesRepository $schedules;
/**
* @var \Source\Gateways\Groups\GroupsRepository
*/
protected GroupsRepository $groups;
public function __construct(SchedulesRepository $schedules, GroupsRepository $groups)
{
$this->groups = $groups;
$this->schedules = $schedules;
}
/**
* @inheritDoc
*/
public function activeForGroup(string $groupId, ?string $date, Presenter $presenter): void
{
if (!$this->groups->get($groupId)) {
throw new EntityNotFoundException('Group not found.');
}
$cdate = Carbon::now();
if ($date) {
$cdate = new Carbon($date);
}
$schedules = $this->schedules->getActiveForGroupAndDate($groupId, $cdate);
$response = new ResponseModel();
foreach ($schedules as $schedule) {
$response->addSchedule($schedule);
}
$presenter->present($response);
}
}