Skip to content

Commit

Permalink
Added time entry api endpoints; Increased phpstan level; renamed to s…
Browse files Browse the repository at this point in the history
…olidtime
  • Loading branch information
korridor committed Feb 26, 2024
1 parent efb06ed commit 6e23e37
Show file tree
Hide file tree
Showing 36 changed files with 1,717 additions and 462 deletions.
7 changes: 4 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
APP_NAME=Laravel
APP_NAME=solidtime
APP_ENV=local
APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
APP_DEBUG=true
APP_URL=http://localhost
APP_URL=https://solidtime.test
APP_FORCE_HTTPS=true

SUPER_ADMINS=[email protected]

Expand Down Expand Up @@ -60,6 +61,6 @@ VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

NGINX_HOST_NAME=timetracker.test
NGINX_HOST_NAME=solidtime.test
NETWORK_NAME=reverse-proxy-docker-traefik_routing

313 changes: 151 additions & 162 deletions LICENSE.txt → LICENSE.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ Additional System Requirements:
Add the following entry to your `/etc/hosts`

```
127.0.0.1 timetracker.test
127.0.0.1 playwright.timetracker.test
127.0.0.1 solidtime.test
127.0.0.1 playwright.solidtime.test
```

## Running E2E Tests

`./vendor/bin/sail up -d ` will automatically start a Playwright UI server that you can access at `https://playwright.timetracker.test`.
`./vendor/bin/sail up -d ` will automatically start a Playwright UI server that you can access at `https://playwright.solidtime.test`.
Make sure that you use HTTPS otherwise the resources will not be loaded correctly.

## Recording E2E Tests
Expand All @@ -49,7 +49,7 @@ To record E2E tests, you need to install and execute playwright locally (outside

```bash
npx playwright install
npx playwright codegen timetracker.test
npx playwright codegen solidtime.test
```

## Contributing
Expand Down
3 changes: 2 additions & 1 deletion app/Actions/Fortify/PasswordValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

namespace App\Actions\Fortify;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Validation\Rules\Password;

trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array|string>
* @return array<int, Rule|string>
*/
protected function passwordRules(): array
{
Expand Down
3 changes: 2 additions & 1 deletion app/Actions/Jetstream/AddOrganizationMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\Organization;
use App\Models\User;
use Closure;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Laravel\Jetstream\Contracts\AddsTeamMembers;
Expand Down Expand Up @@ -55,7 +56,7 @@ protected function validate(Organization $organization, string $email, ?string $
/**
* Get the validation rules for adding a team member.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
* @return array<string, array<Rule|string>>
*/
protected function rules(): array
{
Expand Down
3 changes: 2 additions & 1 deletion app/Actions/Jetstream/InviteOrganizationMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Models\OrganizationInvitation;
use App\Models\User;
use Closure;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Gate;
Expand Down Expand Up @@ -59,7 +60,7 @@ protected function validate(Organization $organization, string $email, ?string $
/**
* Get the validation rules for inviting a team member.
*
* @return array<string, ValidationRule|array|string>
* @return array<string, array<ValidationRule|Rule|string>>
*/
protected function rules(Organization $organization): array
{
Expand Down
24 changes: 24 additions & 0 deletions app/Exceptions/ApiException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace App\Exceptions;

use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ApiException extends Exception
{
/**
* Render the exception into an HTTP response.
*/
public function render(Request $request): JsonResponse
{
return response()
->json([
'error' => true,
'message' => $this->getMessage(),
], 400);
}
}
9 changes: 9 additions & 0 deletions app/Exceptions/TimeEntryStillRunning.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Exceptions;

class TimeEntryStillRunning extends ApiException
{
}
8 changes: 5 additions & 3 deletions app/Filament/Resources/OrganizationResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ class OrganizationResource extends Resource
public static function form(Form $form): Form
{
return $form
->columns(1)
->schema([
Forms\Components\TextInput::make('name')
->label('Name')
->required()
->maxLength(255),
Forms\Components\Toggle::make('Is personal?')
Forms\Components\Toggle::make('personal_team')
->label('Is personal?')
->required(),
Forms\Components\Select::make('owner_id')
Forms\Components\Select::make('user_id')
->relationship(name: 'owner', titleAttribute: 'email')
->searchable(['name', 'email'])
->required(),
Expand All @@ -47,7 +48,8 @@ public static function table(Table $table): Table
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\ToggleColumn::make('is_personal')
Tables\Columns\IconColumn::make('personal_team')
->boolean()
->label('Is personal?')
->sortable(),
Tables\Columns\TextColumn::make('owner.email')
Expand Down
1 change: 1 addition & 0 deletions app/Filament/Resources/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class UserResource extends Resource
public static function form(Form $form): Form
{
return $form
->columns(1)
->schema([
Forms\Components\TextInput::make('id')
->label('ID')
Expand Down
32 changes: 27 additions & 5 deletions app/Http/Controllers/Api/V1/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,22 @@
use App\Models\Organization;
use App\Models\Project;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;

class ProjectController extends Controller
{
protected function checkPermission(Organization $organization, string $permission, ?Project $project = null): void
{
parent::checkPermission($organization, $permission);
if ($project !== null && $project->organization_id !== $organization->id) {
throw new AuthorizationException('Project does not belong to organization');
}
}

/**
* Get projects
*
* @throws AuthorizationException
*/
public function index(Organization $organization): JsonResource
Expand All @@ -29,17 +40,22 @@ public function index(Organization $organization): JsonResource
}

/**
* Get project
*
* @throws AuthorizationException
*/
public function show(Organization $organization, Project $project): JsonResource
{
$this->checkPermission($organization, 'projects:view');
$this->checkPermission($organization, 'projects:view', $project);

$project->load('organization');

return new ProjectResource($project);
}

/**
* Create project
*
* @throws AuthorizationException
*/
public function store(Organization $organization, ProjectStoreRequest $request): JsonResource
Expand All @@ -55,11 +71,13 @@ public function store(Organization $organization, ProjectStoreRequest $request):
}

/**
* Update project
*
* @throws AuthorizationException
*/
public function update(Organization $organization, Project $project, ProjectUpdateRequest $request): JsonResource
{
$this->checkPermission($organization, 'projects:update');
$this->checkPermission($organization, 'projects:update', $project);
$project->name = $request->input('name');
$project->color = $request->input('color');
$project->save();
Expand All @@ -68,13 +86,17 @@ public function update(Organization $organization, Project $project, ProjectUpda
}

/**
* Delete project
*
* @throws AuthorizationException
*/
public function destroy(Organization $organization, Project $project): JsonResource
public function destroy(Organization $organization, Project $project): JsonResponse
{
$this->checkPermission($organization, 'projects:delete');
$this->checkPermission($organization, 'projects:delete', $project);

$project->delete();

return new ProjectResource($project);
return response()
->json(null, 204);
}
}
126 changes: 126 additions & 0 deletions app/Http/Controllers/Api/V1/TimeEntryController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1;

use App\Exceptions\TimeEntryStillRunning;
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest;
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
use App\Http\Resources\V1\TimeEntry\TimeEntryResource;
use App\Models\Organization;
use App\Models\TimeEntry;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;

class TimeEntryController extends Controller
{
protected function checkPermission(Organization $organization, string $permission, ?TimeEntry $timeEntry = null): void
{
parent::checkPermission($organization, $permission);
if ($timeEntry !== null && $timeEntry->organization_id !== $organization->getKey()) {
throw new AuthorizationException('Time entry does not belong to organization');
}
}

/**
* Get time entries
*
* @throws AuthorizationException
*/
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
{
if ($request->has('user_id') && $request->get('user_id') === Auth::id()) {
$this->checkPermission($organization, 'time-entries:view:own');
} else {
$this->checkPermission($organization, 'time-entries:view:all');
}

$timeEntriesQuery = TimeEntry::query()
->whereBelongsTo($organization, 'organization');

if ($request->has('before')) {

}

if ($request->has('after')) {

}

if ($request->has('user_id')) {
$timeEntriesQuery->where('user_id', $request->input('user_id'));
}

$timeEntries = $timeEntriesQuery->get();

return new TimeEntryCollection($timeEntries);
}

/**
* Create time entry
*
* @throws AuthorizationException|TimeEntryStillRunning
*/
public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource
{
if ($request->get('user_id') === Auth::id()) {
$this->checkPermission($organization, 'time-entries:create:own');
} else {
$this->checkPermission($organization, 'time-entries:create:all');
}

if ($request->get('end') === null && TimeEntry::where('user_id', $request->get('user_id'))->where('end', null)->exists()) {
// TODO: documentation
throw new TimeEntryStillRunning('User already has an active time entry');
}

$timeEntry = new TimeEntry();
$timeEntry->fill($request->validated());
$timeEntry->organization()->associate($organization);
$timeEntry->save();

return new TimeEntryResource($timeEntry);
}

/**
* Update time entry
*
* @throws AuthorizationException
*/
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
{
if ($timeEntry->user_id === Auth::id() && $request->get('user_id') === Auth::id()) {
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
} else {
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
}

$timeEntry->fill($request->validated());
$timeEntry->save();

return new TimeEntryResource($timeEntry);
}

/**
* Delete time entry
*
* @throws AuthorizationException
*/
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
{
if ($timeEntry->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
} else {
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
}

$timeEntry->delete();

return response()
->json(null, 204);
}
}
2 changes: 2 additions & 0 deletions app/Http/Middleware/HandleInertiaRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public function version(Request $request): ?string
* Defines the props that are shared by default.
*
* @see https://inertiajs.com/shared-data
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Middleware/TrustProxies.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ class TrustProxies extends Middleware
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
Request::HEADER_X_FORWARDED_AWS_ELB |
Request::HEADER_X_FORWARDED_TRAEFIK;
}
Loading

0 comments on commit 6e23e37

Please sign in to comment.