first commit

This commit is contained in:
Noor E Ilahi
2026-01-09 12:54:53 +05:30
commit 7ccf44f7da
1070 changed files with 113036 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
final class ActuatorController
{
public function __invoke(): JsonResponse
{
return responder()->success()->respond(204);
}
}

View File

@@ -0,0 +1,301 @@
<?php
namespace App\Http\Controllers\Api;
use App\Console\Commands\RotateScreenshots;
use App\Helpers\ModuleHelper;
use App\Helpers\ReportHelper;
use App\Helpers\StorageCleaner;
use App\Helpers\Version;
use Artisan;
use Cache;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller;
use JsonException;
use Settings;
class AboutController extends Controller
{
private Client $client;
public function __construct()
{
$this->client = new Client([
'base_uri' => config('app.stats_collector_url') . '/v2/',
'headers' => ['x-cattr-instance' => Settings::scope('core')->get('instance')],
]);
}
/**
* @api {get} /about/reports Get Available Report Formats
* @apiDescription Retrieves the list of available report formats and their corresponding MIME types.
*
* @apiVersion 4.0.0
* @apiName GetReportFormats
* @apiGroup About
*
* @apiSuccess {Object} types List of available report formats and their MIME types.
* @apiSuccess {String} types.csv MIME type for CSV format.
* @apiSuccess {String} types.xlsx MIME type for XLSX format.
* @apiSuccess {String} types.pdf MIME type for PDF format.
* @apiSuccess {String} types.xls MIME type for XLS format.
* @apiSuccess {String} types.ods MIME type for ODS format.
* @apiSuccess {String} types.html MIME type for HTML format.
*
* @apiSuccessExample {json} Success Response:
* HTTP/1.1 200 OK
* {
* "types": {
* "csv": "text/csv",
* "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
* "pdf": "application/pdf",
* "xls": "application/vnd.ms-excel",
* "ods": "application/vnd.oasis.opendocument.spreadsheet",
* "html": "text/html"
* }
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function reports(): JsonResponse
{
return responder()->success([
'types' => ReportHelper::getAvailableReportFormats(),
])->respond();
}
/**
* Returns information about this instance.
* @throws JsonException
*/
/**
* @api {get} /about Get Application and Module Information
* @apiDescription Retrieves information about the application instance, modules, and image version details.
*
* @apiVersion 4.0.0
* @apiName GetAppInfo
* @apiGroup About
*
* @apiSuccess {Object} app Application information.
* @apiSuccess {String} app.version The current version of the application.
* @apiSuccess {String} app.instance_id The unique identifier of the application instance.
* @apiSuccess {Boolean} app.vulnerable Indicates if the application is vulnerable.
* @apiSuccess {String} app.last_version The latest version available for the application.
* @apiSuccess {String} app.message Any important message related to the application.
*
* @apiSuccess {Object[]} modules List of modules integrated into the application.
* @apiSuccess {String} modules.name Name of the module.
* @apiSuccess {String} modules.version Current version of the module.
* @apiSuccess {Boolean} modules.enabled Indicates if the module is enabled.
*
* @apiSuccess {Object} image Information about the image version.
* @apiSuccess {String} image.version The version of the image (if available).
* @apiSuccess {Boolean} image.vulnerable Indicates if the image is vulnerable.
* @apiSuccess {String} image.last_version The latest available version for the image.
* @apiSuccess {String} image.message Any important message related to the image.
*
* @apiSuccessExample {json} Success Response:
* HTTP/1.1 200 OK
* {
* "app": {
* "version": "dev",
* "instance_id": null,
* "vulnerable": null,
* "last_version": null,
* "message": null
* },
* "modules": [
* {
* "name": "JiraIntegration",
* "version": "3.0.0",
* "enabled": false
* }
* ],
* "image": {
* "version": null,
* "vulnerable": null,
* "last_version": null,
* "message": null
* }
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function __invoke(): JsonResponse
{
$imageVersion = getenv('IMAGE_VERSION', true) ?: null;
$releaseInfo = $this->requestReleaseInfo();
$modulesInfo = $this->requestModulesInfo();
$imageInfo = ($imageVersion) ? $this->requestImageInfo($imageVersion) : false;
return responder()->success([
'app' => [
'version' => config('app.version'),
'instance_id' => Settings::scope('core')->get('instance'),
'vulnerable' => optional($releaseInfo)->vulnerable,
'last_version' => optional($releaseInfo)->lastVersion,
'message' => optional($releaseInfo)->flashMessage,
],
'modules' => $modulesInfo,
'image' => [
'version' => $imageVersion,
'vulnerable' => optional($imageInfo)->vulnerable,
'last_version' => optional($imageInfo)->lastVersion,
'message' => optional($imageInfo)->flashMessage,
]
])->respond();
}
private function requestReleaseInfo(): ?object
{
try {
return json_decode(
$this->client->get('release/' . config('app.version'))->getBody()->getContents(),
false,
512,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
);
} catch (Exception) {
return null;
}
}
/**
* @throws JsonException
*/
private function requestModulesInfo(): array
{
$options = [
'json' => ModuleHelper::getModulesInfo(),
];
try {
return array_map(static function ($el) {
$el['version'] = (string)(new Version($el['name']));
return $el;
}, json_decode(
$this->client->post('modules', $options)->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
)['modules']);
} catch (Exception) {
return ModuleHelper::getModulesInfo();
}
}
private function requestImageInfo(string $imageVersion): ?object
{
try {
return json_decode(
$this->client->get("image/$imageVersion")->getBody()->getContents(),
false,
512,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
);
} catch (Exception) {
return null;
}
}
/**
* @throws BindingResolutionException
*/
/**
* @api {get} /storage Get Storage Information
* @apiDescription Retrieves information about the storage space, thinning status, and available screenshots.
*
* @apiVersion 4.0.0
* @apiName GetStorageInfo
* @apiGroup Storage
*
* @apiSuccess {Object} space Information about the storage space.
* @apiSuccess {Number} space.left The amount of free space left (in bytes).
* @apiSuccess {Number} space.used The amount of space currently used (in bytes).
* @apiSuccess {Number} space.total The total amount of storage space available (in bytes).
*
* @apiSuccess {Number} threshold The storage usage threshold percentage before thinning is needed.
* @apiSuccess {Boolean} need_thinning Indicates if the storage requires thinning.
* @apiSuccess {Number} screenshots_available The number of available screenshots in the storage.
*
* @apiSuccess {Object} thinning Information about the thinning process.
* @apiSuccess {Boolean} thinning.now Indicates if the thinning process is currently ongoing.
* @apiSuccess {String} thinning.last Timestamp of the last thinning process.
*
* @apiSuccessExample {json} Success Response:
* HTTP/1.1 200 OK
* {
* "space": {
* "left": 26579533824,
* "used": 178705711104,
* "total": 205285244928
* },
* "threshold": 75,
* "need_thinning": true,
* "screenshots_available": 0,
* "thinning": {
* "now": null,
* "last": null
* }
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function storage(): JsonResponse
{
return responder()->success([
'space' => [
'left' => StorageCleaner::getFreeSpace(),
'used' => StorageCleaner::getUsedSpace(),
'total' => config('cleaner.total_space'),
],
'threshold' => config('cleaner.threshold'),
'need_thinning' => StorageCleaner::needThinning(),
'screenshots_available' => StorageCleaner::countAvailableScreenshots(),
'thinning' => [
'now' => Cache::store('octane')->get('thinning_now'),
'last' => Cache::store('octane')->get('last_thin'),
]
])->respond();
}
/**
* @api {get} /storage Get Storage Information
* @apiDescription Retrieves information about the storage space, thinning status, and available screenshots.
*
* @apiVersion 4.0.0
* @apiName GetStorageInfo
* @apiGroup Storage
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
* {
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function startStorageClean(): JsonResponse
{
Artisan::queue(RotateScreenshots::class);
return responder()->success()->respond(204);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Api;
use App\Contracts\AttachmentService;
use App\Enums\AttachmentStatus;
use App\Http\Requests\Attachment\CreateAttachmentRequest;
use App\Http\Requests\Attachment\DownloadAttachmentRequest;
use App\Models\Attachment;
use Filter;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Throwable;
use URL;
class AttachmentController extends ItemController
{
protected const MODEL = Attachment::class;
public function __construct(protected AttachmentService $attachmentService)
{
}
/**
* @param CreateAttachmentRequest $request
* @return JsonResponse
*
* @throws Throwable
*/
public function create(CreateAttachmentRequest $request): JsonResponse
{
Filter::listen(Filter::getActionFilterName(), static function (Attachment $attachment) {
$attachment->load('user');
return $attachment;
});
return $this->_create($request);
}
public function tmpDownload(Request $request, Attachment $attachment): ?StreamedResponse
{
if (! $request->hasValidSignature()) {
abort(401);
}
return $this->streamDownloadLogic($attachment);
}
public function createTemporaryUrl(DownloadAttachmentRequest $request, Attachment $attachment): string
{
// we do this because signature breaks if we use built in method for creating relative path
// also cannot determine request scheme when ran inside docker
$url = URL::temporarySignedRoute(
'attachment.temporary-download',
now()->addSeconds($request->validated('seconds') ?? 3600),
$attachment
);
$parsedUrl = parse_url($url);
// Combine the path and query string
return $parsedUrl['path'] . (isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '');
}
/**
* @param Attachment $attachment
* @return StreamedResponse|void
*/
protected function streamDownloadLogic(Attachment $attachment)
{
if ($attachment->status === AttachmentStatus::GOOD && $this->attachmentService->fileExists($attachment)) {
$headers = [];
if ($attachment->mime_type !== '') {
$headers['Content-Type'] = $attachment->mime_type;
}
return response()->streamDownload(
function () use ($attachment) {
$stream = $this->attachmentService->readStream($attachment);
while (!feof($stream)) {
echo fread($stream, 2048);
}
fclose($stream);
},
$attachment->original_name,
$headers
);
}
abort(404);
}
}

View File

@@ -0,0 +1,229 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\CompanySettings\UpdateCompanySettingsRequest;
use App\Http\Transformers\CompanySettingsTransformer;
use App\Models\Priority;
use Illuminate\Http\JsonResponse;
use Settings;
class CompanySettingsController extends Controller
{
/**
* @api {get} /company-settings/ List
* @apiDescription Returns all company settings.
*
* @apiVersion 4.0.0
* @apiName ListCompanySettings
* @apiGroup Company Settings
*
* @apiUse AuthHeader
*
* @apiVersion 4.0.0
* @apiName ListCompanySettings
* @apiGroup Company Settings
*
* @apiUse AuthHeader
*
* @apiSuccess {String} timezone The timezone setting for the company.
* @apiSuccess {String} language The language setting for the company.
* @apiSuccess {Integer} work_time The configured work time in hours.
* @apiSuccess {Array} color Array of colors configured for the company settings.
* @apiSuccess {Array} internal_priorities Array of internal priorities.
* @apiSuccess {Integer} internal_priorities.id The unique ID of the priority.
* @apiSuccess {String} internal_priorities.name The name of the priority.
* @apiSuccess {String} internal_priorities.created_at The creation timestamp of the priority.
* @apiSuccess {String} internal_priorities.updated_at The last update timestamp of the priority.
* @apiSuccess {String} internal_priorities.color The color associated with the priority.
* @apiSuccess {Integer} heartbeat_period The period for heartbeat checks in seconds.
* @apiSuccess {Boolean} auto_thinning Indicates if automatic thinning of old data is enabled.
* @apiSuccess {Integer} screenshots_state The current state of screenshot monitoring.
* @apiSuccess {Integer} env_screenshots_state The environmental screenshot state.
* @apiSuccess {Integer} default_priority_id The default priority ID.
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* {
* "timezone": "UTC",
* "language": "ru",
* "work_time": 0,
* "color": [],
* "internal_priorities": [
* {
* "id": 1,
* "name": "Normal",
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-07-12T17:57:40.000000Z",
* "color": null
* },
* {
* "id": 2,
* "name": "Normal",
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-06-21T10:06:50.000000Z",
* "color": "#49E637"
* },
* {
* "id": 3,
* "name": "High",
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-06-21T10:07:00.000000Z",
* "color": "#D40C0C"
* },
* {
* "id": 5,
* "name": "Normal",
* "created_at": "2024-07-12T17:10:54.000000Z",
* "updated_at": "2024-07-12T17:10:54.000000Z",
* "color": null
* }
*],
* "heartbeat_period": 60,
* "auto_thinning": true,
* "screenshots_state": 1,
* "env_screenshots_state": -1,
* "default_priority_id": 2
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function index(): JsonResponse
{
return responder()->success(
array_merge(
Settings::scope('core')->all(),
['internal_priorities' => Priority::all()]
),
new CompanySettingsTransformer
)->respond();
}
/**
* @param UpdateCompanySettingsRequest $request
*
* @return JsonResponse
*
* @api {patch} /company-settings/ Update
* @apiDescription Updates the specified company settings.
*
* @apiVersion 4.0.0
* @apiName UpdateCompanySettings
* @apiGroup Company Settings
*
* @apiUse AuthHeader
*
* @apiParam {String} timezone The timezone setting for the company.
* @apiParam {String} language The language setting for the company.
* @apiParam {Integer} work_time The configured work time in hours.
* @apiParam {Array} color Array of colors configured for the company settings.
* @apiParam {Array} internal_priorities Array of internal priorities.
* @apiParam {Integer} internal_priorities.id The unique ID of the priority.
* @apiParam {String} internal_priorities.name The name of the priority.
* @apiParam {String} internal_priorities.created_at The creation timestamp of the priority.
* @apiParam {String} internal_priorities.updated_at The last update timestamp of the priority.
* @apiParam {String} internal_priorities.color The color associated with the priority.
* @apiParam {Integer} heartbeat_period The period for heartbeat checks in seconds.
* @apiParam {Boolean} auto_thinning Indicates if automatic thinning of old data is enabled.
* @apiParam {Integer} screenshots_state The current state of screenshot monitoring.
* @apiParam {Integer} env_screenshots_state The environmental screenshot state.
* @apiParam {Integer} default_priority_id The default priority ID.
*
* @apiParamExample {json} Request Example
* { "timezone" : "Europe/Moscow",
* "language" : "en",
* "work_time" : 0,
* "color" : [],
* "internal_priorities" : [
* {
* "id" : 1,
* "name" : "Normal",
* "created_at" : "2023-10-26T10:26:17.000000Z",
* "updated_at" : "2024-07-12T17:57:40.000000Z",
* "color" : null
* },
* {
* "id" : 2,
* "name" : "Normal",
* "created_at" : "2023-10-26T10:26:17.000000Z",
* "updated_at" : "2024-06-21T10:06:50.000000Z",
* "color" : "#49E637"},
* {
* "id" : 3,
* "name" : "High",
* "created_at" : "2023-10-26T10:26:17.000000Z",
* "updated_at" : "2024-06-21T10:07:00.000000Z",
* "color" : "#D40C0C"
* },{
* "id" : 5,
* "name" : "Normal",
* "created_at" : "2024-07-12T17:10:54.000000Z",
* "updated_at" : "2024-07-12T17:10:54.000000Z",
* "color" : null
* }
* ],
* "heartbeat_period" : 60,
* "auto_thinning" : true,
* "screenshots_state" : 1,
* "env_screenshots_state" : -1,
* "default_priority_id" : 2
* }
*
* @apiSuccess {Array} data Contains an array of settings that changes were applied to
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
* {
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function update(UpdateCompanySettingsRequest $request): JsonResponse
{
Settings::scope('core')->set($request->validated());
return responder()->success()->respond(204);
}
/**
* @api {get} /offline-sync/public-key Get Offline Sync Public Key
* @apiDescription Retrieves the public key for offline synchronization.
*
* @apiVersion 4.0.0
* @apiName GetOfflineSyncPublicKey
* @apiGroup OfflineSync
*
* @apiUse AuthHeader
*
* @apiPermission offline_sync_view
* @apiPermission offline_sync_full_access
*
* @apiSuccess {Boolean} success Indicates if the operation was successful.
* @apiSuccess {Object} data The response data.
* @apiSuccess {String} data.key The public key for offline synchronization.
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "key": null
* }
* @apiError (Error 401) Unauthorized The user is not authorized to access this resource.
* @apiError (Error 403) Forbidden The user does not have the necessary permissions.
* @apiError (Error 404) NotFound The requested resource was not found.
*
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
* @apiUse ForbiddenError
*/
public function getOfflineSyncPublicKey(): JsonResponse
{
return responder()->success(
[ 'key' => Settings::scope('core.offline-sync')->get('public_key')],
)->respond();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,306 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Invitation\CreateInvitationRequest;
use App\Http\Requests\Invitation\ListInvitationRequest;
use App\Http\Requests\Invitation\DestroyInvitationRequest;
use App\Http\Requests\Invitation\ShowInvitationRequest;
use App\Http\Requests\Invitation\UpdateInvitationRequest;
use App\Models\Invitation;
use App\Services\InvitationService;
use Exception;
use Filter;
use Illuminate\Http\JsonResponse;
use Throwable;
class InvitationController extends ItemController
{
protected const MODEL = Invitation::class;
/**
* @throws Throwable
* @api {post} /invitations/show Show
* @apiDescription Show invitation.
*
* @apiVersion 1.0.0
* @apiName Show Invitation
* @apiGroup Invitation
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id Invitation ID
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {Array} res Array of records containing the id, email, key, expiration date and role id
*
* @apiUse InvitationObject
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "res": [
* {
* "id": 1
* "email": "test@example.com",
* "key": "06d4a090-9675-11ea-bf39-5f84c549e29c",
* "expires_at": "2020-01-01T00:00:00.000000Z",
* "role_id": 1
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function show(ShowInvitationRequest $request): JsonResponse
{
return $this->_show($request);
}
/**
* @throws Exception
* @api {get} /invitations/list List
* @apiDescription Get list of invitations.
*
* @apiVersion 1.0.0
* @apiName Invitation List
* @apiGroup Invitation
*
* @apiUse AuthHeader
*
* @apiSuccess {Array} res Array of records containing the id, email, key, expiration date and role id
*
* @apiUse InvitationObject
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "res": [
* {
* "id": 1
* "email": "test@example.com",
* "key": "06d4a090-9675-11ea-bf39-5f84c549e29c",
* "expires_at": "2020-01-01T00:00:00.000000Z",
* "role_id": 1
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function index(ListInvitationRequest $request): JsonResponse
{
return $this->_index($request);
}
/**
* @param CreateInvitationRequest $request
* @return JsonResponse
* @throws Exception
* @api {post} /invitations/create Create
* @apiDescription Creates a unique invitation token and sends an email to the users
*
* @apiVersion 1.0.0
* @apiName Create Invitation
* @apiGroup Invitation
*
* @apiUse AuthHeader
*
* @apiParam {Array} users List of users to send an invitation to
* @apiParam {String} users.email User email
* @apiParam {Integer} users.role_id ID of the role that will be assigned to the created user
*
* @apiParamExample {json} Request Example
* {
* "users": [
* {
* "email": "test@example.com",
* "role_id": 1
* }
* ]
* }
*
* @apiSuccess {String} res Array of records containing the id, email, key, expiration date and role id
*
* @apiUse InvitationObject
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "res": [
* {
* "id": 1
* "email": "test@example.com",
* "key": "06d4a090-9675-11ea-bf39-5f84c549e29c",
* "expires_at": "2020-01-01T00:00:00.000000Z",
* "role_id": 1
* }
* ]
* }
*
* @apiErrorExample {json} Email is not specified
* HTTP/1.1 400 Bad Request
* {
* "error_type": "validation",
* "message": "Validation error",
* "info": {
* "users.0.email": [
* "The email field is required."
* ]
* }
* }
*
* @apiErrorExample {json} Email already exists
* HTTP/1.1 400 Bad Request
* {
* "error_type": "validation",
* "message": "Validation error",
* "info": {
* "users.0.email": [
* "The email test@example.com has already been taken."
* ]
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function create(CreateInvitationRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
$invitations = [];
foreach ($requestData['users'] as $user) {
$invitations[] = InvitationService::create($user);
}
return responder()->success($invitations)->respond();
}
/**
* @param UpdateInvitationRequest $request
* @return JsonResponse
* @throws Exception
*
* @api {post} /invitations/resend Resend
* @apiDescription Updates the token expiration date and sends an email to the user's email address.
*
* @apiVersion 1.0.0
* @apiName Update Invitation
* @apiGroup Invitation
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id Invitation ID
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {Array} res Invitation data
*
* @apiUse InvitationObject
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "res": {
* "id": 1
* "email": "test@example.com",
* "key": "06d4a090-9675-11ea-bf39-5f84c549e29c",
* "expires_at": "2020-01-01T00:00:00.000000Z",
* "role_id": 1
* }
* }
*
* @apiErrorExample {json} The id does not exist
* HTTP/1.1 400 Bad Request
* {
* "error_type": "validation",
* "message": "Validation error",
* "info": {
* "id": [
* "The selected id is invalid."
* ]
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function resend(UpdateInvitationRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
$invitation = InvitationService::update($requestData['id']);
return responder()->success($invitation)->respond();
}
/**
* @throws Throwable
* @api {post} /invitations/remove Destroy
* @apiDescription Destroy User
*
* @apiVersion 1.0.0
* @apiName Destroy Invitation
* @apiGroup Invitation
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id ID of the target invitation
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {String} message Destroy status
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "message": "Item has been removed"
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function destroy(DestroyInvitationRequest $request): JsonResponse
{
return $this->_destroy($request);
}
/**
* @param ListInvitationRequest $request
* @return JsonResponse
* @throws Exception
*/
/**
* @api {get} /api/invitations/count Count Invitations
* @apiDescription Returns the total count of invitations
*
* @apiVersion 4.0.0
* @apiName CountInvitations
* @apiGroup Invitations
*
* @apiUse TotalSuccess
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function count(ListInvitationRequest $request): JsonResponse
{
return $this->_count($request);
}
}

View File

@@ -0,0 +1,648 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\CattrFormRequest;
use Filter;
use App\Helpers\QueryHelper;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use CatEvent;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
abstract class ItemController extends Controller
{
protected const MODEL = Model::class;
/**
* @apiDefine ItemNotFoundError
* @apiErrorExample {json} No such item
* HTTP/1.1 404 Not Found
* {
* "message": "Item not found",
* "error_type": "query.item_not_found"
* }
*
* @apiVersion 1.0.0
*/
/**
* @apiDefine ValidationError
* @apiErrorExample {json} Validation error
* HTTP/1.1 400 Bad Request
* {
* "message": "Validation error",
* "error_type": "validation",
* "info": "Invalid id"
* }
*
* @apiError (Error 400) {String} info Validation errors
*
* @apiVersion 1.0.0
*/
/**
* @apiDefine ProjectIDParam
* @apiParam {Integer} id Project ID
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*/
/**
* @apiDefine ProjectObject
*
* @apiSuccess {Integer} id Project ID
* @apiSuccess {Integer} company_id Company ID
* @apiSuccess {String} name Project name
* @apiSuccess {String} description Project description
* @apiSuccess {String} deleted_at Project deletion date or null
* @apiSuccess {String} created_at Project creation date
* @apiSuccess {String} updated_at Project update date
* @apiSuccess {Boolean} important Project importance
* @apiSuccess {String} source Project source
* @apiSuccess {Integer} default_priority_id Default priority ID or null
* @apiSuccess {Object[]} phases List of project phases
* @apiSuccess {Integer} phases.id Phase ID
* @apiSuccess {String} phases.name Phase name
* @apiSuccess {String} phases.description Phase description
* @apiSuccess {Integer} phases.tasks_count Number of tasks in the phase
* @apiSuccess {String} phases.created_at Phase creation date
* @apiSuccess {String} phases.updated_at Phase update date
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "id": 1,
* "company_id": 1,
* "name": "Dolores voluptates.",
* "description": "Deleniti maxime fugit nesciunt. Ut maiores deleniti tempora vel. Nisi aut doloremque accusantium tempore aut.",
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2023-10-26T10:26:17.000000Z",
* "important": 1,
* "source": "internal",
* "default_priority_id": null,
* "phases": []
* }
*/
/**
* @apiDefine UserObject
* @apiSuccess {Integer} id User ID.
* @apiSuccess {String} full_name Full name of the user.
* @apiSuccess {String} email Email address of the user.
* @apiSuccess {String} url URL associated with the user (if any).
* @apiSuccess {Integer} company_id ID of the company the user belongs to.
* @apiSuccess {String} avatar URL of the user's avatar (if any).
* @apiSuccess {Boolean} screenshots_state State of screenshot capturing.
* @apiSuccess {Boolean} manual_time Indicates whether manual time tracking is enabled.
* @apiSuccess {Integer} computer_time_popup Time in seconds for the computer time popup.
* @apiSuccess {Boolean} blur_screenshots Indicates if screenshots are blurred.
* @apiSuccess {Boolean} web_and_app_monitoring Indicates if web and app monitoring is enabled.
* @apiSuccess {Integer} screenshots_interval Interval for capturing screenshots in minutes.
* @apiSuccess {Boolean} active Indicates if the user account is active.
* @apiSuccess {String} created_at Timestamp of when the user was created.
* @apiSuccess {String} updated_at Timestamp of when the user was last updated.
* @apiSuccess {String} deleted_at Timestamp of when the user was deleted (if applicable).
* @apiSuccess {String} timezone User's timezone.
* @apiSuccess {Boolean} important Indicates if the user is marked as important.
* @apiSuccess {Boolean} change_password Indicates if the user is required to change their password.
* @apiSuccess {Integer} role_id Role ID associated with the user.
* @apiSuccess {String} user_language Language preference of the user.
* @apiSuccess {String} type User type (e.g., employee, admin).
* @apiSuccess {Boolean} invitation_sent Indicates if an invitation has been sent to the user.
* @apiSuccess {Boolean} client_installed Indicates if the tracking client is installed.
* @apiSuccess {Boolean} permanent_screenshots Indicates if permanent screenshots are enabled.
* @apiSuccess {String} last_activity Timestamp of the user's last activity.
* @apiSuccess {Boolean} screenshots_state_locked Indicates if the screenshot state is locked.
* @apiSuccess {Boolean} online Indicates if the user is currently online.
* @apiSuccess {Boolean} can_view_team_tab Indicates if the user can view the team tab.
* @apiSuccess {Boolean} can_create_task Indicates if the user can create tasks.
*
* @apiSuccessExample {json} Response Example:
* HTTP/1.1 200 OK
* {
* "id": 1,
* "full_name": "Admin",
* "email": "admin@cattr.app",
* "url": "",
* "company_id": 1,
* "avatar": "",
* "screenshots_state": 1,
* "manual_time": 0,
* "computer_time_popup": 300,
* "blur_screenshots": false,
* "web_and_app_monitoring": true,
* "screenshots_interval": 5,
* "active": 1,
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-08-20T09:22:02.000000Z",
* "timezone": null,
* "important": 0,
* "change_password": 0,
* "role_id": 0,
* "user_language": "en",
* "type": "employee",
* "invitation_sent": false,
* "nonce": 0,
* "client_installed": 0,
* "permanent_screenshots": 0,
* "last_activity": "2024-08-20 09:22:02",
* "screenshots_state_locked": false,
* "online": false,
* "can_view_team_tab": true,
* "can_create_task": true
* }
*/
/**
* @apiDefine UserParams
*/
/**
* @apiDefine User
* @apiSuccess {String} access_token Token
* @apiSuccess {String} token_type Token Type
* @apiSuccess {String} expires_in Token TTL (ISO 8601 Date)
* @apiSuccess {Object} user User Entity
* @apiSuccess {Integer} user.id ID of the user
* @apiSuccess {String} user.full_name Full name of the user
* @apiSuccess {String} user.email Email of the user
* @apiSuccess {String} [user.url] URL of the user (optional)
* @apiSuccess {Integer} user.company_id Company ID of the user
* @apiSuccess {String} [user.avatar] Avatar URL of the user (optional)
* @apiSuccess {Boolean} user.screenshots_active Indicates if screenshots are active
* @apiSuccess {Boolean} user.manual_time Indicates if manual time tracking is allowed
* @apiSuccess {Integer} user.computer_time_popup Time interval for computer time popup
* @apiSuccess {Boolean} user.blur_screenshots Indicates if screenshots are blurred
* @apiSuccess {Boolean} user.web_and_app_monitoring Indicates if web and app monitoring is enabled
* @apiSuccess {Integer} user.screenshots_interval Interval for taking screenshots
* @apiSuccess {Boolean} user.active Indicates if the user is active
* @apiSuccess {String} [user.deleted_at] Deletion timestamp (if applicable, otherwise null)
* @apiSuccess {String} user.created_at Creation timestamp
* @apiSuccess {String} user.updated_at Last update timestamp
* @apiSuccess {String} [user.timezone] Timezone of the user (optional)
* @apiSuccess {Boolean} user.important Indicates if the user is marked as important
* @apiSuccess {Boolean} user.change_password Indicates if the user needs to change password
* @apiSuccess {Integer} user.role_id Role ID of the user
* @apiSuccess {String} user.user_language Language of the user
* @apiSuccess {String} user.type Type of the user (e.g., "employee")
* @apiSuccess {Boolean} user.invitation_sent Indicates if invitation is sent to the user
* @apiSuccess {Integer} user.nonce Nonce value of the user
* @apiSuccess {Boolean} user.client_installed Indicates if client is installed
* @apiSuccess {Boolean} user.permanent_screenshots Indicates if screenshots are permanent
* @apiSuccess {String} user.last_activity Last activity timestamp of the user
* @apiSuccess {Boolean} user.online Indicates if the user is online
* @apiSuccess {Boolean} user.can_view_team_tab Indicates if the user can view team tab
* @apiSuccess {Boolean} user.can_create_task Indicates if the user can create tasks
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "access_token": "51|d6HvWGk6zY1aqqRms5pkp6Pb6leBs7zaW4IAWGvQ5d00b8be",
* "token_type": "bearer",
* "expires_in": "2024-07-12T11:59:31+00:00",
* "user": {
* "id": 1,
* "full_name": "Admin",
* "email": "johndoe@example.com",
* "url": "",
* "company_id": 1,
* "avatar": "",
* "screenshots_active": 1,
* "manual_time": 0,
* "computer_time_popup": 300,
* "blur_screenshots": false,
* "web_and_app_monitoring": true,
* "screenshots_interval": 5,
* "active": 1,
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-02-15T19:06:42.000000Z",
* "timezone": null,
* "important": 0,
* "change_password": 0,
* "role_id": 0,
* "user_language": "en",
* "type": "employee",
* "invitation_sent": false,
* "nonce": 0,
* "client_installed": 0,
* "permanent_screenshots": 0,
* "last_activity": "2023-10-26 10:26:17",
* "online": false,
* "can_view_team_tab": true,
* "can_create_task": true
* }
* }
*/
/**
* @apiDefine ScreenshotObject
* @apiSuccess {Integer} id ID of the screenshot
* @apiSuccess {Integer} time_interval_id Time interval ID to which the screenshot belongs
* @apiSuccess {String} path Path to the screenshot
* @apiSuccess {String} created_at Timestamp when the screenshot was created
* @apiSuccess {String} updated_at Timestamp when the screenshot was last updated
* @apiSuccess {String} [deleted_at] Timestamp when the screenshot was deleted (if applicable)
* @apiSuccess {String} [thumbnail_path] Path to the thumbnail of the screenshot (if applicable)
* @apiSuccess {Boolean} important Indicates if the screenshot is marked as important
* @apiSuccess {Boolean} is_removed Indicates if the screenshot is removed
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "data": {
* "id": 1,
* "time_interval_id": 1,
* "path": "uploads\/screenshots\/1_1_1.png",
* "created_at": "2020-01-23T09:42:26+00:00",
* "updated_at": "2020-01-23T09:42:26+00:00",
* "deleted_at": null,
* "thumbnail_path": null,
* "important": false,
* "is_removed": false
* }
* }
*/
/**
* @apiDefine ScreenshotParams
*/
/**
* @apiDefine TimeIntervalObject
* @apiSuccess {Integer} id ID of the time interval
* @apiSuccess {Integer} task_id ID of the task
* @apiSuccess {String} start_at Start timestamp of the time interval
* @apiSuccess {String} end_at End timestamp of the time interval
* @apiSuccess {String} created_at Creation timestamp
* @apiSuccess {String} updated_at Last update timestamp
* @apiSuccess {String} [deleted_at] Deletion timestamp (if applicable)
* @apiSuccess {String} user_id The ID of the user
* @apiSuccess {Boolean} is_manual Indicates whether the time was logged manually (true) or automatically
* @apiSuccess {Integer} activity_fill Activity fill percentage
* @apiSuccess {Integer} mouse_fill Mouse activity fill percentage
* @apiSuccess {Integer} keyboard_fill Keyboard activity fill percentage
* @apiSuccess {Integer} location Additional location information, if available
* @apiSuccess {Integer} screenshot_id The ID of the screenshot associated with this interval
* @apiSuccess {Integer} has_screenshot Indicates if there is a screenshot for this interval
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* {
* "id": 1,
* "task_id": 1,
* "start_at": "2023-10-26 10:21:17",
* "end_at": "2023-10-26 10:26:17",
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2023-10-26T10:26:17.000000Z",
* "deleted_at": null,
* "user_id": 2,
* "is_manual": false,
* "activity_fill": 60,
* "mouse_fill": 47,
* "keyboard_fill": 13,
* "location": null,
* "screenshot_id": null,
* "has_screenshot": true
* },...
* }
*/
/**
* @apiDefine TimeIntervalParams
*/
/**
* @apiDefine InvitationObject
*/
/**
* @apiDefine PriorityObject
* @apiSuccess {Boolean} status Indicates if the request was successful
* @apiSuccess {Boolean} success Indicates if the request was successful
* @apiSuccess {Object} data Response object
* @apiSuccess {Integer} data.id Priority ID
* @apiSuccess {String} data.name Priority name
* @apiSuccess {String} data.color Priority color (can be null)
* @apiSuccess {String} data.created_at Creation timestamp
* @apiSuccess {String} data.updated_at Update timestamp
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "status": 200,
* "success": true,
* "data": {
* "id": 1,
* "name": "Normal",
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-07-12T17:57:40.000000Z",
* "color": null
* }
* }
*/
/**
* @apiDefine ProjectParams
* @apiParam {Object} [filters] Filters to apply to the project list.
* @apiParam {Object} [filters.id] Filter by project ID.
* @apiParam {String} [filters.name] Filter by project name.
* @apiParam {String} [filters.description] Filter by project description.
* @apiParam {String} [filters.created_at] Filter by project creation date.
* @apiParam {String} [filters.updated_at] Filter by project update date.
* @apiParam {Integer} [page=1] Page number for pagination.
* @apiParam {Integer} [perPage=15] Number of items per page.
*/
/**
* @apiDefine StatusObject
* @apiSuccess {Number} id The ID of the status.
* @apiSuccess {String} name The name of the status.
* @apiSuccess {Boolean} active Indicates if the status is active.
* @apiSuccess {String} color The color of the status (in HEX).
* @apiSuccess {Number} order The sort order of the status.
* @apiSuccess {String} created_at The creation timestamp.
* @apiSuccess {String} updated_at The last update timestamp.
*
* @apiSuccessExample {json} Success Response Example:
* HTTP/1.1 200 OK
* {
* "id": 1,
* "name": "Normal",
* "active": true,
* "color": "#363334",
* "order": 1,
* "created_at": "2024-08-26T10:47:30.000000Z",
* "updated_at": "2024-08-26T10:48:35.000000Z"
* }
*/
/**
* @apiDefine ParamTimeInterval
*
* @apiParam {String} start_at The start datetime for the interval
* @apiParam {String} end_at The end datetime for the interval
* @apiParam {Integer} user_id The ID of the user
* @apiParamExample {json} Request Example
* {
* "start_at": "2024-08-16T12:32:11.000000Z",
* "end_at": "2024-08-17T12:32:11.000000Z",
* "user_id": 1
* }
*/
/**
* @apiDefine AuthHeader
* @apiHeader {String} Authorization Token for user auth
* @apiHeaderExample {json} Authorization Header Example
* {
* "Authorization": "bearer 16184cf3b2510464a53c0e573c75740540fe..."
* }
*/
/**
* @apiDefine 400Error
* @apiError (Error 4xx) {String} message Message from server
* @apiError (Error 4xx) {Boolean} success Indicates erroneous response when `FALSE`
* @apiError (Error 4xx) {String} error_type Error type
*
* @apiVersion 1.0.0
*/
/**
* @apiDefine UnauthorizedError
* @apiErrorExample {json} Unauthorized
* HTTP/1.1 401 Unauthorized
* {
* "message": "Not authorized",
* "error_type": "authorization.unauthorized"
* }
*
* @apiVersion 1.0.0
*/
/**
* @apiDefine ForbiddenError
* @apiErrorExample {json} Forbidden
* HTTP/1.1 403 Forbidden
* {
* "message": "Access denied to this item",
* "error_type": "authorization.forbidden"
* }
*
* @apiVersion 1.0.0
*/
/**
* @apiDefine TotalSuccess
* @apiSuccess {Boolean} success Indicates if the request was successful
* @apiSuccess {Object} data The response data
* @apiSuccess {Integer} data.total The total count of items
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "status": 200,
* "success": true,
* "data": {
* "total": 0
* }
* }
*/
/**
* @throws Exception
*/
public function _index(CattrFormRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
$itemsQuery = $this->getQuery($requestData);
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestData);
$items = $request->header('X-Paginate', true) !== 'false' ? $itemsQuery->paginate() : $itemsQuery->get();
Filter::process(
Filter::getActionFilterName(),
$items,
);
CatEvent::dispatch(Filter::getAfterActionEventName(), [$items, $requestData]);
return responder()->success($items)->respond();
}
/**
* @throws Exception
*/
protected function getQuery(array $filter = []): Builder
{
$model = static::MODEL;
$model = new $model;
$query = new Builder($model::getQuery());
$query->setModel($model);
$modelScopes = $model->getGlobalScopes();
foreach ($modelScopes as $key => $value) {
$query->withGlobalScope($key, $value);
}
foreach (Filter::process(Filter::getQueryAdditionalRelationsFilterName(), []) as $with) {
$query->with($with);
}
foreach (Filter::process(Filter::getQueryAdditionalRelationsSumFilterName(), []) as $withSum) {
$query->withSum(...$withSum);
}
QueryHelper::apply($query, $model, $filter);
return Filter::process(
Filter::getQueryFilterName(),
$query
);
}
/**
* @throws Throwable
*/
public function _create(CattrFormRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
CatEvent::dispatch(Filter::getBeforeActionEventName(), [$requestData]);
/** @var Model $cls */
$cls = static::MODEL;
$item = Filter::process(
Filter::getActionFilterName(),
$cls::create($requestData),
);
CatEvent::dispatch(Filter::getAfterActionEventName(), [$item, $requestData]);
return responder()->success($item)->respond();
}
/**
* @throws Throwable
*/
public function _edit(CattrFormRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
throw_unless(is_int($request->get('id')), ValidationException::withMessages(['Invalid id']));
$itemsQuery = $this->getQuery();
/** @var Model $item */
$item = $itemsQuery->get()->collect()->firstWhere('id', $request->get('id'));
if (!$item) {
/** @var Model $cls */
$cls = static::MODEL;
throw_if($cls::find($request->get('id'))?->count(), new AccessDeniedHttpException);
throw new NotFoundHttpException;
}
CatEvent::dispatch(Filter::getBeforeActionEventName(), [$item, $requestData]);
$item = Filter::process(Filter::getActionFilterName(), $item->fill($requestData));
$item->save();
CatEvent::dispatch(Filter::getAfterActionEventName(), [$item, $requestData]);
return responder()->success($item)->respond();
}
/**
* @throws Throwable
*/
public function _destroy(CattrFormRequest $request): JsonResponse
{
$requestId = Filter::process(Filter::getRequestFilterName(), $request->validated('id'));
throw_unless(is_int($requestId), ValidationException::withMessages(['Invalid id']));
$itemsQuery = $this->getQuery(['where' => ['id' => $requestId]]);
/** @var Model $item */
$item = $itemsQuery->first();
if (!$item) {
/** @var Model $cls */
$cls = static::MODEL;
throw_if($cls::find($requestId)?->count(), new AccessDeniedHttpException);
throw new NotFoundHttpException;
}
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestId);
CatEvent::dispatch(
Filter::getAfterActionEventName(),
tap(
Filter::process(Filter::getActionFilterName(), $item),
static fn ($item) => $item->delete(),
)
);
return responder()->success()->respond(204);
}
/**
* @throws Exception
*/
protected function _count(CattrFormRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestData);
$itemsQuery = $this->getQuery($requestData);
$count = Filter::process(Filter::getActionFilterName(), $itemsQuery->count());
CatEvent::dispatch(Filter::getAfterActionEventName(), [$count, $requestData]);
return responder()->success(['total' => $count])->respond();
}
/**
* @throws Throwable
*/
protected function _show(CattrFormRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
$itemId = (int)$requestData['id'];
throw_unless($itemId, ValidationException::withMessages(['Invalid id']));
$filters = [
'where' => ['id' => $itemId]
];
if (!empty($requestData['with'])) {
$filters['with'] = $requestData['with'];
}
if (!empty($requestData['withSum'])) {
$filters['withSum'] = $requestData['withSum'];
}
CatEvent::dispatch(Filter::getBeforeActionEventName(), $filters);
$itemsQuery = $this->getQuery($filters ?: []);
$item = Filter::process(Filter::getActionFilterName(), $itemsQuery->first());
throw_unless($item, new NotFoundHttpException);
CatEvent::dispatch(Filter::getAfterActionEventName(), [$item, $filters]);
return responder()->success($item)->respond();
}
}

View File

@@ -0,0 +1,220 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Priority\CreatePriorityRequest;
use App\Http\Requests\Priority\DestroyPriorityRequest;
use App\Http\Requests\Priority\ListPriorityRequest;
use App\Http\Requests\Priority\ShowPriorityRequest;
use App\Http\Requests\Priority\UpdatePriorityRequest;
use App\Models\Priority;
use Exception;
use Illuminate\Http\JsonResponse;
use Throwable;
class PriorityController extends ItemController
{
protected const MODEL = Priority::class;
/**
* @throws Throwable
* @api {post} /priorities/show Show
* @apiDescription Show priority.
*
* @apiVersion 4.0.0
* @apiName Show Priority
* @apiGroup Priority
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id Priority ID
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {Object} res Priority
*
* @apiUse PriorityObject
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "res": {
* "id": 1
* "name": "Normal",
* "color": null
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function show(ShowPriorityRequest $request): JsonResponse
{
return $this->_show($request);
}
/**
* @throws Exception
* @api {get} /priorities/list List
* @apiDescription Get list of priorities.
*
* @apiVersion 4.0.0
* @apiName Priority List
* @apiGroup Priority
*
* @apiUse AuthHeader
*
* @apiSuccess {Object} res Priority
*
* @apiUse PriorityObject
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "res": [{
* "id": 1
* "name": "Normal",
* "color": null
* }]
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function index(ListPriorityRequest $request): JsonResponse
{
return $this->_index($request);
}
/**
* @param CreatePriorityRequest $request
* @return JsonResponse
* @throws Throwable
* @api {post} /priorities/create Create
* @apiDescription Creates priority
*
* @apiVersion 4.0.0
* @apiName Create Priority
* @apiGroup Priority
*
* @apiUse AuthHeader
*
* @apiParam {String} name Priority name
* @apiParam {String} color Priority color
*
* @apiParamExample {json} Request Example
* {
* "name": "Normal",
* "color": null
* }
*
* @apiSuccess {Object} res Priority
*
* @apiUse PriorityObject
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function create(CreatePriorityRequest $request): JsonResponse
{
return $this->_create($request);
}
/**
* @throws Throwable
* @api {post} /priorities/edit Edit
* @apiDescription Edit Priority
*
* @apiVersion 4.0.0
* @apiName Edit
* @apiGroup Priority
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id ID
* @apiParam {String} name Priority name
* @apiParam {String} color Priority color
*
* @apiParamExample {json} Simple Request Example
* {
* "id": 1,
* "name": "Normal",
* "color": null
* }
*
* @apiSuccess {Object} res Priority
*
* @apiUse PriorityObject
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
*/
public function edit(UpdatePriorityRequest $request): JsonResponse
{
return $this->_edit($request);
}
/**
* @throws Throwable
* @api {post} /priorities/remove Destroy
* @apiDescription Destroy User
*
* @apiVersion 4.0.0
* @apiName Destroy Priority
* @apiGroup Priority
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id ID of the target priority
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {String} message Destroy status
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "message": "Item has been removed"
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function destroy(DestroyPriorityRequest $request): JsonResponse
{
return $this->_destroy($request);
}
/**
* @api {get} /api/priorities/count Count Priorities
* @apiDescription Returns the total count of priorities
*
* @apiVersion 4.0.0
* @apiName CountPriorities
* @apiGroup Priorities
*
* @apiUse TotalSuccess
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
/**
* @param ListPriorityRequest $request
* @return JsonResponse
* @throws Exception
*/
public function count(ListPriorityRequest $request): JsonResponse
{
return $this->_count($request);
}
}

View File

@@ -0,0 +1,521 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Project\CreateProjectRequest;
use App\Http\Requests\Project\EditProjectRequest;
use App\Http\Requests\Project\DestroyProjectRequest;
use App\Http\Requests\Project\GanttDataRequest;
use App\Http\Requests\Project\ListProjectRequest;
use App\Http\Requests\Project\PhasesRequest;
use App\Http\Requests\Project\ShowProjectRequest;
use CatEvent;
use Filter;
use App\Models\Project;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\JsonResponse;
use DB;
use Staudenmeir\LaravelAdjacencyList\Eloquent\Builder as AdjacencyListBuilder;
use Throwable;
class ProjectController extends ItemController
{
protected const MODEL = Project::class;
/**
* @api {get, post} /projects/list List
* @apiDescription Get list of Projects
*
* @apiVersion 4.0.0
* @apiName GetProjectList
* @apiGroup Project
*
* @apiUse AuthHeader
*
* @apiPermission projects_list
* @apiPermission projects_full_access
*
* @apiUse ProjectParams
*
* @apiParamExample {json} Simple Request Example
* {
* "id": [">", 1],
* "user_id": ["=", [1,2,3]],
* "name": ["like", "%lorem%"],
* "description": ["like", "%lorem%"],
* "created_at": [">", "2019-01-01 00:00:00"],
* "updated_at": ["<", "2019-01-01 00:00:00"]
* }
*
* @apiUse ProjectObject
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* [
* {
* "id": 1,
* "company_id": 1,
* "name": "Dolores voluptates.",
* "description": "Deleniti maxime fugit nesciunt. Ut maiores deleniti tempora vel. Nisi aut doloremque accusantium tempore aut.",
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2023-10-26T10:26:17.000000Z",
* "important": 1,
* "source": "internal",
* "default_priority_id": null
* },
* {
* "id": 2,
* "company_id": 5,
* "name": "Et veniam velit tempore.",
* "description": "Consequatur nulla distinctio reprehenderit rerum omnis debitis. Fugit illum ratione quia harum. Optio porro consequatur enim esse.",
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:42.000000Z",
* "updated_at": "2023-10-26T10:26:42.000000Z",
* "important": 1,
* "source": "internal",
* "default_priority_id": null
* }
* ]
*
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
/**
* @param ListProjectRequest $request
* @return JsonResponse
* @throws Exception
*/
public function index(ListProjectRequest $request): JsonResponse
{
return $this->_index($request);
}
/**
* @api {get} /projects/gantt-data Gantt Data
* @apiDescription Получение данных для диаграммы Ганта по проекту
*
* @apiVersion 4.0.0
* @apiName GetGanttData
* @apiGroup Project
*
* @apiUse AuthHeader
* @apiUse ProjectIDParam
*
* @apiSuccess {Integer} id Project ID
* @apiSuccess {Integer} company_id Company ID
* @apiSuccess {String} name Project name
* @apiSuccess {String} description Project description
* @apiSuccess {String} deleted_at Deletion date (null if not deleted)
* @apiSuccess {String} created_at Creation date
* @apiSuccess {String} updated_at Update date
* @apiSuccess {Integer} important Project importance (1 - important, 0 - not important)
* @apiSuccess {String} source Project source (internal/external)
* @apiSuccess {Integer} default_priority_id Default priority ID (null if not set)
* @apiSuccess {Object[]} tasks_relations Task relations
* @apiSuccess {Integer} tasks_relations.parent_id Parent task ID
* @apiSuccess {Integer} tasks_relations.child_id Child task ID
* @apiSuccess {Object[]} tasks List of tasks
* @apiSuccess {Object[]} phases List of project phases
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "id": 1,
* "company_id": 1,
* "name": "Dolores voluptates.",
* "description": "Deleniti maxime fugit nesciunt. Ut maiores deleniti tempora vel. Nisi aut doloremque accusantium tempore aut.",
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2023-10-26T10:26:17.000000Z",
* "important": 1,
* "source": "internal",
* "default_priority_id": null,
* "tasks_relations": [
* {
* "parent_id": 5,
* "child_id": 1
* }
* ],
* "tasks": [],
* "phases": []
*
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
/**
* @param GanttDataRequest $request
* @return JsonResponse
* @throws Exception
* @throws Throwable
*/
public function ganttData(GanttDataRequest $request): JsonResponse
{
Filter::listen(Filter::getQueryFilterName(), static fn(Builder $query) => $query->with([
'tasks' => fn(HasMany $queue) => $queue
->orderBy('start_date')
->select([
'id',
'task_name',
'priority_id',
'status_id',
'estimate',
'start_date',
'due_date',
'project_phase_id',
'project_id'
])->with(['status', 'priority'])
->withSum(['workers as total_spent_time'], 'duration')
->withSum(['workers as total_offset'], 'offset')
->withCasts(['start_date' => 'string', 'due_date' => 'string'])
->whereNotNull('start_date')->whereNotNull('due_date'),
'phases' => fn(HasMany $queue) => $queue
->select(['id', 'name', 'project_id'])
->withMin([
'tasks as start_date' => fn(AdjacencyListBuilder $q) => $q
->whereNotNull('start_date')
->whereNotNull('due_date')
], 'start_date')
->withMax([
'tasks as due_date' => fn(AdjacencyListBuilder $q) => $q
->whereNotNull('start_date')
->whereNotNull('due_date')
], 'due_date'),
]));
Filter::listen(Filter::getActionFilterName(), static function (Project $item) {
$item->append('tasks_relations');
return $item;
});
return $this->_show($request);
}
/**
* @api {get} /projects/phases Project Phases
* @apiDescription Retrieve project phases along with the number of tasks in each phase.
*
* @apiVersion 4.0.0
* @apiName GetProjectPhases
* @apiGroup Project
*
* @apiUse AuthHeader
* @apiUse ProjectIDParam
* @apiUse ProjectObject
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
/**
* @param PhasesRequest $request
* @return JsonResponse
* @throws Exception
* @throws Throwable
*/
public function phases(PhasesRequest $request): JsonResponse
{
Filter::listen(
Filter::getQueryFilterName(),
static fn(Builder $query) => $query
->with([
'phases'=> fn(HasMany $q) => $q->withCount('tasks')
])
);
return $this->_show($request);
}
/**
* @throws Throwable
* @api {get} /projects/show Project Show
* @apiDescription Retrieve project show along with the number of tasks in each phase.
*
* @apiVersion 4.0.0
* @apiName GetProjectShow
* @apiGroup Project
*
* @apiUse AuthHeader
* @apiUse ProjectIDParam
* @apiUse ProjectObject
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function show(ShowProjectRequest $request): JsonResponse
{
Filter::listen(
Filter::getQueryFilterName(),
static fn(Builder $query) => $query
->with([
'phases'=> fn(HasMany $q) => $q->withCount('tasks')
])
);
return $this->_show($request);
}
/**
* @throws Throwable
* @api {post} /projects/create Create Project
* @apiDescription Creates a new project
*
* @apiVersion 4.0.0
* @apiName CreateProject
* @apiGroup Project
*
* @apiUse AuthHeader
*
* @apiParam {Boolean} important Project importance
* @apiParam {Integer} screenshots_state State of the screenshots
* @apiParam {String} name Project name
* @apiParam {String} description Project description
* @apiParam {Integer} default_priority_id Default priority ID
* @apiParam {Object[]} statuses Project statuses
* @apiParam {Integer} statuses.id Status ID
* @apiParam {String} statuses.color Status color
*
* @apiParamExample {json} Request Example
* {
* "important": true,
* "screenshots_state": 1,
* "name": "test",
* "description": "test",
* "default_priority_id": 2,
* "statuses": [
* {
* "id": 2,
* "color": null
* }
* ]
* }
*
* @apiSuccess {String} name Project name
* @apiSuccess {String} description Project description
* @apiSuccess {Boolean} important Project importance
* @apiSuccess {Integer} default_priority_id Default priority ID
* @apiSuccess {Integer} screenshots_state State of the screenshots
* @apiSuccess {String} created_at Creation timestamp
* @apiSuccess {String} updated_at Update timestamp
* @apiSuccess {Object[]} statuses Project statuses
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "name": "test",
* "description": "test",
* "important": 1,
* "default_priority_id": 2,
* "screenshots_state": 1,
* "updated_at": "2024-08-06T12:28:07.000000Z",
* "created_at": "2024-08-06T12:28:07.000000Z",
* "id": 161,
* "statuses": []
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function create(CreateProjectRequest $request): JsonResponse
{
Filter::listen(Filter::getRequestFilterName(), static function ($requestData) {
if (isset($requestData['group']) && is_array($requestData['group'])) {
$requestData['group'] = $requestData['group']['id'];
}
return $requestData;
});
CatEvent::listen(Filter::getAfterActionEventName(), static function (Project $project, $requestData) use ($request) {
if ($request->has('statuses')) {
$statuses = [];
foreach ($request->get('statuses') as $status) {
$statuses[$status['id']] = ['color' => $status['color']];
}
$project->statuses()->sync($statuses);
}
if (isset($requestData['phases'])) {
$project->phases()->createMany($requestData['phases']);
}
});
Filter::listen(Filter::getActionFilterName(), static fn($data) => $data->load('statuses'));
return $this->_create($request);
}
/**
* @throws Throwable
* @api {post} /projects/edit Edit
* @apiDescription Edit Project
*
* @apiVersion 4.0.0
* @apiName EditProject
* @apiGroup Project
*
* @apiUse AuthHeader
*
* @apiPermission projects_edit
* @apiPermission projects_full_access
*
* @apiParamExample {json} Request Example
* {
* "id": 1,
* "name": "test",
* "description": "test"
* }
*
* @apiParam {String} id Project id
* @apiParam {String} name Project name
* @apiParam {String} description Project description
*
* @apiSuccess {Integer} id Project ID
* @apiSuccess {Integer} company_id Company ID
* @apiSuccess {String} name Project name
* @apiSuccess {String} description Project description
* @apiSuccess {String} deleted_at Deletion timestamp
* @apiSuccess {String} created_at Creation timestamp
* @apiSuccess {String} updated_at Update timestamp
* @apiSuccess {Boolean} important Project importance
* @apiSuccess {String} source Project source
* @apiSuccess {Integer} default_priority_id Default priority ID
* @apiSuccess {Integer} screenshots_state State of the screenshots
* @apiSuccess {Object[]} statuses Project statuses
*
* @apiSuccessExample {json} Response Example
* {
* "id": 1,
* "company_id": 1,
* "name": "test",
* "description": "test",
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-08-07T16:47:01.000000Z",
* "important": 1,
* "source": "internal",
* "default_priority_id": null,
* "screenshots_state": 1,
* "statuses": []
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
*/
public function edit(EditProjectRequest $request): JsonResponse
{
Filter::listen(Filter::getRequestFilterName(), static function ($requestData) {
if (isset($requestData['group']) && is_array($requestData['group'])) {
$requestData['group'] = $requestData['group']['id'];
}
return $requestData;
});
CatEvent::listen(Filter::getAfterActionEventName(), static function (Project $project, $requestData) use ($request) {
if ($request->has('statuses')) {
$statuses = [];
foreach ($request->get('statuses') as $status) {
$statuses[$status['id']] = ['color' => $status['color']];
}
$project->statuses()->sync($statuses);
}
if (isset($requestData['phases'])) {
$phases = collect($requestData['phases']);
$project->phases()
->whereNotIn('id', $phases->pluck('id')->filter())
->delete();
$project->phases()->upsert(
$phases->filter(fn (array $val) => isset($val['id']))->toArray(),
['id'],
['name']
);
$project->phases()->createMany($phases->filter(fn (array $val) => !isset($val['id'])));
}
});
Filter::listen(Filter::getActionFilterName(), static fn($data) => $data->load('statuses'));
return $this->_edit($request);
}
/**
* @throws Throwable
* @api {post} /projects/remove Destroy
* @apiDescription Destroy Project
*
* @apiVersion 4.0.0
* @apiName DestroyProject
* @apiGroup Project
*
* @apiUse AuthHeader
*
* @apiPermission projects_remove
* @apiPermission projects_full_access
*
* @apiParam {String} id Project id
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {String} message Destroy status
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
* {
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
*/
public function destroy(DestroyProjectRequest $request): JsonResponse
{
return $this->_destroy($request);
}
/**
* @throws Exception
* @api {get,post} /projects/count Count
* @apiDescription Count Projects
*
* @apiVersion 4.0.0
* @apiName Count
* @apiGroup Project
*
* @apiUse AuthHeader
*
* @apiPermission projects_count
* @apiPermission projects_full_access
*
* @apiSuccess {Integer} total Amount of projects that we have
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "total": 159
* }
*
* @apiUse 400Error
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function count(ListProjectRequest $request): JsonResponse
{
return $this->_count($request);
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\QueryHelper;
use App\Http\Requests\ProjectGroup\CreateProjectGroupRequest;
use App\Http\Requests\ProjectGroup\DestroyProjectGroupRequest;
use App\Http\Requests\ProjectGroup\EditProjectGroupRequest;
use App\Http\Requests\ProjectGroup\ListProjectGroupRequest;
use App\Http\Requests\ProjectGroup\ShowProjectGroupRequest;
use App\Models\ProjectGroup;
use CatEvent;
use Exception;
use Filter;
use Illuminate\Http\JsonResponse;
use Kalnoy\Nestedset\QueryBuilder;
use Throwable;
class ProjectGroupController extends ItemController
{
protected const MODEL = ProjectGroup::class;
/**
* Display a listing of the resource.
*
* @param ListProjectGroupRequest $request
* @return JsonResponse
* @throws Exception
*/
public function index(ListProjectGroupRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
$itemsQuery = $this->getQuery($requestData);
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestData);
$itemsQuery->withDepth()->withCount('projects')->defaultOrder();
$items = $request->header('X-Paginate', true) !== 'false' ? $itemsQuery->paginate($request->input('limit', null)) : $itemsQuery->get();
Filter::process(
Filter::getActionFilterName(),
$items,
);
CatEvent::dispatch(Filter::getAfterActionEventName(), [$items, $requestData]);
return responder()->success($items)->respond();
}
/**
* @throws Exception
*/
protected function getQuery(array $filter = []): QueryBuilder
{
$model = static::MODEL;
$model = new $model;
$query = new QueryBuilder($model::getQuery());
$query->setModel($model);
$modelScopes = $model->getGlobalScopes();
foreach ($modelScopes as $key => $value) {
$query->withGlobalScope($key, $value);
}
foreach (Filter::process(Filter::getQueryAdditionalRelationsFilterName(), []) as $with) {
$query->with($with);
}
QueryHelper::apply($query, $model, $filter);
return Filter::process(
Filter::getQueryFilterName(),
$query
);
}
/**
* Show the form for creating a new resource.
*
* @param CreateProjectGroupRequest $request
* @return JsonResponse
* @throws Throwable
*/
public function create(CreateProjectGroupRequest $request): JsonResponse
{
if ($parent_id = $request->safe(['parent_id'])['parent_id'] ?? null) {
CatEvent::listen(
Filter::getAfterActionEventName(),
static fn(ProjectGroup $group) => $group->parent()->associate($parent_id)->save(),
);
}
return $this->_create($request);
}
/**
* Display the specified resource.
*
* @param ShowProjectGroupRequest $request
* @return JsonResponse
* @throws Throwable
*/
public function show(ShowProjectGroupRequest $request): JsonResponse
{
return $this->_show($request);
}
/**
* Show the form for editing the specified resource.
*
* @param EditProjectGroupRequest $request
* @return JsonResponse
* @throws Throwable
*/
public function edit(EditProjectGroupRequest $request): JsonResponse
{
CatEvent::listen(
Filter::getAfterActionEventName(),
static function (ProjectGroup $group) use ($request) {
if ($parent_id = $request->input('parent_id', null)) {
$group->parent()->associate($parent_id)->save();
} else {
$group->saveAsRoot();
}
},
);
return $this->_edit($request);
}
/**
* Remove the specified resource from storage.
*
* @param DestroyProjectGroupRequest $request
* @return JsonResponse
* @throws Throwable
*/
public function destroy(DestroyProjectGroupRequest $request): JsonResponse
{
return $this->_destroy($request);
}
/**
* @throws Exception
*/
public function count(ListProjectGroupRequest $request): JsonResponse
{
return $this->_count($request);
}
}

View File

@@ -0,0 +1,237 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\ProjectMember\BulkEditProjectMemberRequest;
use App\Http\Requests\ProjectMember\ShowProjectMemberRequest;
use App\Services\ProjectMemberService;
use CatEvent;
use Filter;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class ProjectMemberController extends Controller
{
/**
* @api {post} /project-members/list List Project Members
* @apiDescription Get list of project members
*
* @apiVersion 4.0.0
* @apiName ListProjectMembers
* @apiGroup ProjectMember
*
* @apiUse AuthHeader
*
* @apiParam {Integer} project_id ID of the project
*
* @apiParamExample {json} Request Example
* {
* "project_id": 1
* }
*
* @apiSuccess {Integer} id Project ID
* @apiSuccess {Object[]} users List of users
* @apiSuccess {Integer} users.id User ID
* @apiSuccess {String} users.full_name User full name
* @apiSuccess {String} users.email User email
* @apiSuccess {String} users.url User URL
* @apiSuccess {Integer} users.company_id Company ID
* @apiSuccess {String} users.avatar User avatar
* @apiSuccess {Integer} users.screenshots_active Screenshots active status
* @apiSuccess {Integer} users.manual_time Manual time status
* @apiSuccess {Integer} users.computer_time_popup Computer time popup interval
* @apiSuccess {Boolean} users.blur_screenshots Blur screenshots status
* @apiSuccess {Boolean} users.web_and_app_monitoring Web and app monitoring status
* @apiSuccess {Integer} users.screenshots_interval Screenshots interval
* @apiSuccess {Boolean} users.active User active status
* @apiSuccess {String} users.deleted_at Deletion timestamp
* @apiSuccess {String} users.created_at Creation timestamp
* @apiSuccess {String} users.updated_at Last update timestamp
* @apiSuccess {String} users.timezone User timezone
* @apiSuccess {Boolean} users.important User importance status
* @apiSuccess {Boolean} users.change_password Change password status
* @apiSuccess {Integer} users.role_id User role ID
* @apiSuccess {String} users.user_language User language
* @apiSuccess {String} users.type User type
* @apiSuccess {Boolean} users.invitation_sent Invitation sent status
* @apiSuccess {Integer} users.nonce User nonce
* @apiSuccess {Boolean} users.client_installed Client installed status
* @apiSuccess {Boolean} users.permanent_screenshots Permanent screenshots status
* @apiSuccess {String} users.last_activity Last activity timestamp
* @apiSuccess {Boolean} users.online Online status
* @apiSuccess {Boolean} users.can_view_team_tab Can view team tab status
* @apiSuccess {Boolean} users.can_create_task Can create task status
* @apiSuccess {Object} users.pivot Pivot data
* @apiSuccess {Integer} users.pivot.project_id Project ID in pivot
* @apiSuccess {Integer} users.pivot.user_id User ID in pivot
* @apiSuccess {Integer} users.pivot.role_id Role ID in pivot
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "id": 1,
* "users": [
* {
* "id": 1,
* "full_name": "Admin",
* "email": "admin@cattr.app",
* "url": "",
* "company_id": 1,
* "avatar": "",
* "screenshots_active": 1,
* "manual_time": 0,
* "computer_time_popup": 300,
* "blur_screenshots": false,
* "web_and_app_monitoring": true,
* "screenshots_interval": 5,
* "active": 1,
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-02-15T19:06:42.000000Z",
* "timezone": null,
* "important": 0,
* "change_password": 0,
* "role_id": 0,
* "user_language": "en",
* "type": "employee",
* "invitation_sent": false,
* "nonce": 0,
* "client_installed": 0,
* "permanent_screenshots": 0,
* "last_activity": "2023-10-26 10:26:17",
* "online": false,
* "can_view_team_tab": true,
* "can_create_task": true,
* "pivot": {
* "project_id": 1,
* "user_id": 1,
* "role_id": 2
* }
* },
* {
* "id": 2,
* "full_name": "Fabiola Mertz",
* "email": "projectManager@example.com",
* "url": "",
* "company_id": 1,
* "avatar": "",
* "screenshots_active": 1,
* "manual_time": 0,
* "computer_time_popup": 300,
* "blur_screenshots": false,
* "web_and_app_monitoring": true,
* "screenshots_interval": 5,
* "active": 1,
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2023-10-26T10:26:17.000000Z",
* "timezone": null,
* "important": 0,
* "change_password": 0,
* "role_id": 2,
* "user_language": "en",
* "type": "employee",
* "invitation_sent": false,
* "nonce": 0,
* "client_installed": 0,
* "permanent_screenshots": 0,
* "last_activity": "2023-10-26 09:44:17",
* "online": false,
* "can_view_team_tab": false,
* "can_create_task": false,
* "pivot": {
* "project_id": 1,
* "user_id": 2,
* "role_id": 2
* }
* },
* ...
* ]
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
/**
*
* @param ShowProjectMemberRequest $request
* @return JsonResponse
* @throws Throwable
*/
public function list(ShowProjectMemberRequest $request): JsonResponse
{
$data = $request->validated();
throw_unless($data, ValidationException::withMessages([]));
$projectMembers = ProjectMemberService::getMembers($data['project_id']);
$projectMembers['users'] = $projectMembers['users'] ?? [];
return responder()->success($projectMembers)->respond();
}
/**
* @api {post} /api/project-members/bulk-edit Bulk Edit Project Members
* @apiDescription Edit roles of multiple project members
*
* @apiVersion 4.0.0
* @apiName BulkEditProjectMembers
* @apiGroup ProjectMember
*
* @apiUse AuthHeader
*
* @apiParam {Integer} project_id Project ID
* @apiParam {Object[]} user_roles Array of user roles
* @apiParam {Integer} user_roles.user_id User ID
* @apiParam {Integer} user_roles.role_id Role ID
*
* @apiParamExample {json} Request Example
* {
* "project_id": 1,
* "user_roles": [
* {
* "user_id": 1,
* "role_id": 2
* },
* {
* "user_id": 2,
* "role_id": 3
* }
* ]
* }
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
*/
/**
* @param BulkEditProjectMemberRequest $request
* @return JsonResponse
*/
public function bulkEdit(BulkEditProjectMemberRequest $request): JsonResponse
{
$data = Filter::process(Filter::getRequestFilterName(), $request->validated());
$userRoles = [];
foreach ($data['user_roles'] as $value) {
$userRoles[$value['user_id']] = ['role_id' => $value['role_id']];
}
CatEvent::dispatch(Filter::getBeforeActionEventName(), [$data['project_id'], $userRoles]);
ProjectMemberService::syncMembers($data['project_id'], $userRoles);
CatEvent::dispatch(Filter::getAfterActionEventName(), [$data['project_id'], $userRoles]);
return responder()->success()->respond(204);
}
}

View File

@@ -0,0 +1,193 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Enums\DashboardSortBy;
use App\Enums\SortDirection;
use App\Helpers\ReportHelper;
use App\Http\Requests\Reports\DashboardRequest;
use App\Jobs\GenerateAndSendReport;
use App\Models\Project;
use App\Models\User;
use App\Reports\DashboardExport;
use Carbon\Carbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Http\JsonResponse;
use Settings;
use Throwable;
class DashboardController
{
/**
* @api {post} /report/dashboard Dashboard Data
* @apiDescription Retrieve dashboard data based on provided parameters.
*
* @apiVersion 4.0.0
* @apiName DashboardData
* @apiGroup Dashboard
*
* @apiUse AuthHeader
*
* @apiParam {String} start_at Start date-time in "Y-m-d H:i:s" format.
* @apiParam {String} end_at End date-time in "Y-m-d H:i:s" format.
* @apiParam {String} user_timezone User's timezone.
* @apiParam {Array} [users] Array of user IDs. If not provided, all users are considered.
* @apiParam {Array} [projects] Array of project IDs. If not provided, all projects are considered.
*
* @apiParamExample {json} Request Example
* {
* "start_at": "2006-05-31 16:15:09",
* "end_at": "2006-05-31 16:20:07",
* "user_timezone": "Asia/Omsk"
* }
*
* @apiSuccess {Object} data Dashboard data keyed by user ID.
* @apiSuccess {Array} data.7 Array of records for user with ID 7.
* @apiSuccess {String} data.7.start_at Start date-time of the record.
* @apiSuccess {Integer} data.7.activity_fill Activity fill percentage.
* @apiSuccess {Integer} data.7.mouse_fill Mouse activity fill percentage.
* @apiSuccess {Integer} data.7.keyboard_fill Keyboard activity fill percentage.
* @apiSuccess {String} data.7.end_at End date-time of the record.
* @apiSuccess {Boolean} data.7.is_manual Indicates if the record is manual.
* @apiSuccess {String} data.7.user_email User's email address.
* @apiSuccess {Integer} data.7.id Record ID.
* @apiSuccess {Integer} data.7.project_id Project ID.
* @apiSuccess {String} data.7.project_name Project name.
* @apiSuccess {Integer} data.7.task_id Task ID.
* @apiSuccess {String} data.7.task_name Task name.
* @apiSuccess {Integer} data.7.user_id User ID.
* @apiSuccess {String} data.7.full_name User's full name.
* @apiSuccess {Integer} data.7.duration Duration in seconds.
* @apiSuccess {Integer} data.7.from_midnight Time from midnight in seconds.
* @apiSuccess {Object} data.7.durationByDay Duration grouped by day.
* @apiSuccess {Integer} data.7.durationByDay.2018-05-31 Duration for the day 2018-05-31.
* @apiSuccess {Integer} data.7.durationByDay.2018-06-04 Duration for the day 2018-06-04.
* @apiSuccess {Integer} data.7.durationAtSelectedPeriod Duration for the selected period.
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "status": 200,
* "success": true,
* "data": {
* "7": [
* {
* "start_at": "2018-05-31 10:43:45",
* "activity_fill": 111,
* "mouse_fill": 64,
* "keyboard_fill": 47,
* "end_at": "2018-06-03 22:03:45",
* "is_manual": 0,
* "user_email": "projectManager1231@example.com",
* "id": 2109,
* "project_id": 159,
* "project_name": "Voluptas ab et ea.",
* "task_id": 54,
* "task_name": "Quo consequatur mollitia nam.",
* "user_id": 7,
* "full_name": "Dr. Adaline Toy",
* "duration": 300000,
* "from_midnight": 38625,
* "durationByDay": {
* "2018-05-31": 285375,
* "2018-06-04": 14625
* },
* "durationAtSelectedPeriod": 14625
* }
* ]
* }
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
*/
public function __invoke(DashboardRequest $request): JsonResponse
{
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
return responder()->success(
DashboardExport::init(
$request->input('users') ?? User::all()->pluck('id')->toArray(),
$request->input('projects') ?? Project::all()->pluck('id')->toArray(),
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
$companyTimezone,
$request->input('user_timezone'),
)->collection()->all(),
)->respond();
}
/**
* @throws Throwable
* @api {post} /report/dashboard/download Download Dashboard Report
* @apiDescription Generate and download a dashboard report
*
* @apiVersion 4.0.0
* @apiName DownloadDashboardReport
* @apiGroup Report
*
* @apiUse AuthHeader
*
* @apiHeader {String} Accept Specifies the content type of the response. (Example: `text/csv`)
* @apiHeader {String} Authorization Bearer token for API access. (Example: `82|LosbyrFljFDJqUcqMNG6UveCgrclt6OzTrCWdnJBEZ1fee08e6`)
* @apiPermission report_generate
* @apiPermission report_full_access
*
* @apiParam {String} start_at Start date and time (ISO 8601 format)
* @apiParam {String} end_at End date and time (ISO 8601 format)
* @apiParam {String} user_timezone User's timezone
* @apiParam {String} sort_column Column to sort by
* @apiParam {String} sort_direction Direction to sort (asc/desc)
*
* @apiParamExample {json} Request Example
* {
* "start_at": "2024-08-06T18:00:00.000Z",
* "end_at": "2024-08-07T17:59:59.999Z",
* "user_timezone": "Asia/Omsk",
* "sort_column": "user",
* "sort_direction": "asc",
* }
*
* @apiSuccess {String} url URL to download the generated report
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "status": 200,
* "success": true,
* "data": {
* "url": "/storage/reports/f7ac500e-a741-47ee-9e61-1b62a341fb8d/Dashboard_Report.csv"
* }
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
* @apiUse ItemNotFoundError
*/
public function download(DashboardRequest $request): JsonResponse
{
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
$job = new GenerateAndSendReport(
DashboardExport::init(
$request->input('users') ?? User::all()->pluck('id')->toArray(),
$request->input('projects') ?? Project::all()->pluck('id')->toArray(),
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
$companyTimezone,
$request->input('user_timezone'),
DashboardSortBy::tryFrom($request->input('sort_column')),
SortDirection::tryFrom($request->input('sort_direction')),
),
$request->user(),
ReportHelper::getReportFormat($request),
);
app(Dispatcher::class)->dispatchSync($job);
return responder()->success(['url' => $job->getPublicPath()])->respond();
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Helpers\ReportHelper;
use App\Http\Requests\Reports\PlannedTimeReportRequest;
use App\Jobs\GenerateAndSendReport;
use App\Models\Project;
use App\Reports\PlannedTimeReportExport;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Http\JsonResponse;
use Throwable;
class PlannedTimeReportController
{
/**
* @api {post} /report/planned-time Planned Time Report
* @apiDescription Generate a report on planned tasks and associated time for a given project
*
* @apiVersion 4.0.0
* @apiName PlannedTimeReport
* @apiGroup Report
*
* @apiUse AuthHeader
*
* @apiPermission report_generate
* @apiPermission report_full_access
*
* @apiParam {Integer} id Project ID
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {Object} reportData Report data object
* @apiSuccess {Integer} reportData.id Project ID
* @apiSuccess {Integer} reportData.company_id Company ID
* @apiSuccess {String} reportData.name Project name
* @apiSuccess {String} reportData.description Project description
* @apiSuccess {String} reportData.deleted_at Deleted timestamp (null if not deleted)
* @apiSuccess {String} reportData.created_at Creation timestamp
* @apiSuccess {String} reportData.updated_at Update timestamp
* @apiSuccess {Boolean} reportData.important Whether the project is marked as important
* @apiSuccess {String} reportData.source Source of the project (e.g., "internal")
* @apiSuccess {Integer} reportData.screenshots_state Screenshots state (1 if active)
* @apiSuccess {Integer} reportData.default_priority_id Default priority ID (null if not set)
* @apiSuccess {Integer} reportData.total_spent_time Total time spent on the project
* @apiSuccess {Object[]} reportData.tasks List of tasks under the project
* @apiSuccess {Integer} reportData.tasks.id Task ID
* @apiSuccess {String} reportData.tasks.task_name Task name
* @apiSuccess {String} reportData.tasks.due_date Task due date (null if not set)
* @apiSuccess {String} reportData.tasks.estimate Estimated time for the task (null if not set)
* @apiSuccess {Integer} reportData.tasks.project_id Project ID to which the task belongs
* @apiSuccess {Integer} reportData.tasks.total_spent_time Total time spent on the task
* @apiSuccess {Object[]} reportData.tasks.workers List of workers assigned to the task
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "reportData": [
* {
* "id": 2,
* "company_id": 5,
* "name": "Et veniam velit tempore.",
* "description": "Consequatur nulla distinctio reprehenderit rerum omnis debitis. Fugit illum ratione quia harum. Optio porro consequatur enim esse.",
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:42.000000Z",
* "updated_at": "2023-10-26T10:26:42.000000Z",
* "important": 1,
* "source": "internal",
* "default_priority_id": null,
* "screenshots_state": 1,
* "total_spent_time": null,
* "tasks": [
* {
* "id": 11,
* "task_name": "Qui velit fugiat magni accusantium.",
* "due_date": null,
* "estimate": null,
* "project_id": 2,
* "total_spent_time": null,
* "workers": []
* },
* ...
* ]
* }
* ]
*
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
* @apiUse ItemNotFoundError
*/
public function __invoke(PlannedTimeReportRequest $request): JsonResponse
{
return responder()->success(
PlannedTimeReportExport::init(
$request->input('projects', Project::all()->pluck('id')->toArray()),
)->collection()->all(),
)->respond();
}
/**
* @api {post} /report/planned-time/download Download Planned Time Report
* @apiDescription Generate and download a report on planned time for specific projects.
*
* @apiVersion 4.0.0
* @apiName DownloadPlannedTimeReport
* @apiGroup Report
*
* @apiUse AuthHeader
*
* @apiHeader {String} Accept Specifies the content type of the response. (Example: `text/csv`)
* @apiHeader {String} Authorization Bearer token for API access. (Example: `82|LosbyrFljFDJqUcqMNG6UveCgrclt6OzTrCWdnJBEZ1fee08e6`)
* @apiPermission report_generate
* @apiPermission report_full_access
*
* @apiParam {Array} projects Array of project IDs to include in the report. If not provided, all projects will be included.
*
* @apiParamExample {json} Request Example
* {
* "projects": [2]
* }
*
* @apiSuccess {String} url URL to the generated report file.
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "url": "/storage/reports/0611766a-2807-4524-9add-2e8be33c3e58/PlannedTime_Report.csv"
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
/**
* @throws Throwable
*/
public function download(PlannedTimeReportRequest $request): JsonResponse
{
$job = new GenerateAndSendReport(
PlannedTimeReportExport::init(
$request->input('projects', Project::all()->pluck('id')->toArray()),
),
$request->user(),
ReportHelper::getReportFormat($request),
);
app(Dispatcher::class)->dispatchSync($job);
return responder()->success(['url' => $job->getPublicPath()])->respond();
}
}

View File

@@ -0,0 +1,228 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Helpers\ReportHelper;
use App\Http\Requests\Reports\ProjectReportRequest;
use App\Jobs\GenerateAndSendReport;
use App\Models\User;
use App\Models\Project;
use App\Reports\ProjectReportExport;
use Carbon\Carbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Http\JsonResponse;
use Settings;
use Throwable;
class ProjectReportController
{
/**
* @api {post} /report/project Project Report
* @apiDescription Retrieve detailed project report data including user tasks and time intervals.
*
* @apiVersion 4.0.0
* @apiName ProjectReport
* @apiGroup Report
* @apiUse AuthHeader
* @apiPermission report_view
* @apiPermission report_full_access
*
* @apiParam {String} start_at The start date and time for the report period (ISO 8601 format).
* @apiParam {String} end_at The end date and time for the report period (ISO 8601 format).
* @apiParam {String} user_timezone The timezone of the user making the request.
*
* @apiParamExample {json} Request Example
* {
* "start_at": "2023-05-31 16:15:09",
* "end_at": "2023-11-30 16:20:07",
* "user_timezone": "Asia/Omsk"
* }
*
* @apiSuccess {Object[]} data List of projects.
* @apiSuccess {Integer} data.id Project ID.
* @apiSuccess {String} data.name Project name.
* @apiSuccess {Integer} data.time Total time spent on the project.
* @apiSuccess {Object[]} data.users List of users associated with the project.
* @apiSuccess {Integer} data.users.id User ID.
* @apiSuccess {String} data.users.full_name User's full name.
* @apiSuccess {String} data.users.email User's email address.
* @apiSuccess {Integer} data.users.time Total time spent by the user on the project.
* @apiSuccess {Object[]} data.users.tasks List of tasks associated with the user.
* @apiSuccess {Integer} data.users.tasks.id Task ID.
* @apiSuccess {String} data.users.tasks.task_name Task name.
* @apiSuccess {Integer} data.users.tasks.time Total time spent on the task.
* @apiSuccess {Object[]} data.users.tasks.intervals List of time intervals for the task.
* @apiSuccess {String} data.users.tasks.intervals.date Date of the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.time Time spent in the interval.
* @apiSuccess {Object[]} data.users.tasks.intervals.items Detailed breakdown of intervals.
* @apiSuccess {String} data.users.tasks.intervals.items.start_at Start time of the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.items.activity_fill Activity fill percentage during the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.items.mouse_fill Mouse activity fill percentage.
* @apiSuccess {Integer} data.users.tasks.intervals.items.keyboard_fill Keyboard activity fill percentage.
* @apiSuccess {String} data.users.tasks.intervals.items.end_at End time of the interval.
* @apiSuccess {String} data.users.tasks.intervals.items.user_email User's email associated with the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.items.id Interval ID.
* @apiSuccess {Integer} data.users.tasks.intervals.items.project_id Project ID associated with the interval.
* @apiSuccess {String} data.users.tasks.intervals.items.project_name Project name associated with the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.items.task_id Task ID associated with the interval.
* @apiSuccess {String} data.users.tasks.intervals.items.task_name Task name associated with the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.items.user_id User ID associated with the interval.
* @apiSuccess {String} data.users.tasks.intervals.items.full_name User's full name associated with the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.items.hour Hour of the day for the interval.
* @apiSuccess {String} data.users.tasks.intervals.items.day Day of the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.items.minute Minute of the hour for the interval.
* @apiSuccess {Integer} data.users.tasks.intervals.items.duration Duration of the interval.
* @apiSuccess {Object} data.users.tasks.intervals.items.durationByDay Duration of the interval by day.
* @apiSuccess {Integer} data.users.tasks.intervals.items.durationAtSelectedPeriod Duration at the selected period.
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "status": 200,
* "success": true,
* "data": [
* {
* "id": 159,
* "name": "Voluptas ab et ea.",
* "time": 3851703975,
* "users": [
* {
* "id": 7,
* "full_name": "Dr. Adaline Toy",
* "email": "projectManager1231@example.com",
* "time": 3851703975,
* "tasks": [
* {
* "id": 54,
* "task_name": "Quo consequatur mollitia nam.",
* "time": 550243425,
* "intervals": [
* {
* "date": "2006-05-29",
* "time": 43425,
* "items": [
* {
* "start_at": "2006-05-29 00:43:45",
* "activity_fill": 87,
* "mouse_fill": 81,
* "keyboard_fill": 6,
* "end_at": "2006-06-01 12:03:45",
* "user_email": "projectManager1231@example.com",
* "id": 3372,
* "project_id": 159,
* "project_name": "Voluptas ab et ea.",
* "task_id": 54,
* "task_name": "Quo consequatur mollitia nam.",
* "user_id": 7,
* "full_name": "Dr. Adaline Toy",
* "hour": 0,
* "day": "2006-05-29",
* "minute": 40,
* "duration": 300000,
* "durationByDay": {
* "2006-05-29": 256575,
* "2006-06-01": 43425
* },
* "durationAtSelectedPeriod": 43425
* }
* ]
* },
* // More intervals...
* ]
* }
* // More tasks...
* ]
* }
* // More users...
* ]
* }
* // More projects...
* ]
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function __invoke(ProjectReportRequest $request): JsonResponse
{
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
return responder()->success(
ProjectReportExport::init(
$request->input('users', User::all()->pluck('id')->toArray()),
$request->input('projects', Project::all()->pluck('id')->toArray()),
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
$companyTimezone
)->collection()->all(),
)->respond();
}
/**
* @api {post} /api/report/dashboard/download Download Dashboard Report
* @apiDescription Downloads a dashboard report in the specified file format.
*
* @apiVersion 4.0.0
* @apiName DownloadDashboardReport
* @apiGroup Reports
* @apiUse AuthHeader
* @apiHeader {String} Accept Accept mime type. Example: `text/csv`.
*
* @apiParam {String} start_at The start date and time for the report in ISO 8601 format.
* @apiParam {String} end_at The end date and time for the report in ISO 8601 format.
* @apiParam {String} user_timezone The timezone of the user. Example: `Asia/Omsk`.
* @apiParam {Array} users List of user IDs to include in the report.
* @apiParam {Array} projects List of project IDs to include in the report.
*
* @apiParamExample {json} Request Example:
* {
* "start_at": "2023-11-01T16:15:09Z",
* "end_at": "2023-11-30T23:59:07Z",
* "user_timezone": "Asia/Omsk",
* "users": [7],
* "projects": [159]
* }
*
* @apiSuccess {String} url The URL where the generated report can be downloaded.
*
* @apiSuccessExample {json} Success Response:
* HTTP/1.1 200 OK
* {
* "status": 200,
* "success": true,
* "data": {
* "url": "/storage/reports/1b11d8f9-c5a3-4fe5-86bd-ae6a3031352c/Dashboard_Report.csv"
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
/**
* @throws Throwable
*/
public function download(ProjectReportRequest $request): JsonResponse
{
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
$job = new GenerateAndSendReport(
ProjectReportExport::init(
$request->input('users', User::all()->pluck('id')->toArray()),
$request->input('projects', Project::all()->pluck('id')->toArray()),
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
$companyTimezone
),
$request->user(),
ReportHelper::getReportFormat($request),
);
app(Dispatcher::class)->dispatchSync($job);
return responder()->success(['url' => $job->getPublicPath()])->respond();
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Http\Requests\Reports\TimeUseReportRequest;
use App\Models\User;
use App\Reports\TimeUseReportExport;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Settings;
/**
* Class TimeUseReportController
*
*/
class TimeUseReportController
{
/**
* @api {post} /api/report/time Get User Time Report
* @apiDescription Retrieves the time report for specified users within a given time range.
*
* @apiVersion 4.0.0
* @apiName GetUserTimeReport
* @apiGroup Reports
*
* @apiParam {String} start_at The start date and time for the report in ISO 8601 format.
* @apiParam {String} end_at The end date and time for the report in ISO 8601 format.
* @apiParam {String} user_timezone The timezone of the user. Example: `Asia/Omsk`.
* @apiParam {Array} users List of user IDs to include in the report.
*
* @apiParamExample {json} Request Example:
* {
* "start_at": "2023-11-01T16:15:09Z",
* "end_at": "2023-11-30T23:59:07Z",
* "user_timezone": "Asia/Omsk",
* "users": [7]
* }
*
* @apiSuccess {Object[]} data List of users and their respective time logs.
* @apiSuccess {Number} data.time Total time logged by the user within the specified period (in seconds).
* @apiSuccess {Object} data.user User information.
* @apiSuccess {Number} data.user.id User ID.
* @apiSuccess {String} data.user.email User's email address.
* @apiSuccess {String} data.user.full_name User's full name.
* @apiSuccess {Object[]} data.tasks List of tasks the user has logged time for.
* @apiSuccess {Number} data.tasks.time Time logged for the task (in seconds).
* @apiSuccess {Number} data.tasks.task_id Task ID.
* @apiSuccess {String} data.tasks.task_name Task name.
* @apiSuccess {Number} data.tasks.project_id Project ID associated with the task.
* @apiSuccess {String} data.tasks.project_name Project name associated with the task.
*
* @apiSuccessExample {json} Success Response:
* HTTP/1.1 200 OK
* {
* "status": 200,
* "success": true,
* "data": [
* {
* "time": 2151975,
* "user": {
* "id": 7,
* "email": "projectManager1231@example.com",
* "full_name": "Dr. Adaline Toy"
* },
* "tasks": [
* {
* "time": 307425,
* "task_id": 56,
* "task_name": "Similique enim aspernatur.",
* "project_id": 159,
* "project_name": "Voluptas ab et ea."
* },
* ...
* ]
* }
* ]
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function __invoke(TimeUseReportRequest $request): JsonResponse
{
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
return responder()->success(
TimeUseReportExport::init(
$request->input('users') ?? User::all()->pluck('id')->toArray(),
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
$companyTimezone
)->collection()->all(),
)->respond();
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Enums\UniversalReportType;
use App\Enums\UniversalReportBase;
use App\Exceptions\Entities\NotEnoughRightsException;
use App\Helpers\ReportHelper;
use App\Http\Requests\Reports\UniversalReport\UniversalReportEditRequest;
use App\Http\Requests\Reports\UniversalReport\UniversalReportRequest;
use App\Http\Requests\Reports\UniversalReport\UniversalReportShowRequest;
use App\Http\Requests\Reports\UniversalReport\UniversalReportStoreRequest;
use App\Http\Requests\Reports\UniversalReport\UniversalReportDestroyRequest;
use App\Jobs\GenerateAndSendReport;
use App\Models\Project;
use App\Models\UniversalReport;
use App\Reports\PlannedTimeReportExport;
use App\Reports\UniversalReportExport;
use Carbon\Carbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Settings;
use Throwable;
class UniversalReportController
{
public function index()
{
$items = [
UniversalReportType::COMPANY->value => [],
UniversalReportType::PERSONAL->value => [],
];
$user = request()->user();
if (request()->user()->isAdmin()) {
UniversalReport::select('id', 'name', 'type')
->where([
['type', '=', UniversalReportType::COMPANY->value, 'or'],
['user_id', '=', request()->user()->id, 'or']
])
->get()
->each(function ($item) use (&$items) {
$items[$item->type->value][] = $item->toArray();
});
return responder()->success($items)->respond();
}
UniversalReport::select('id', 'name', 'data_objects', 'base', 'type')->get()->each(function ($item) use (&$items) {
if ($item->base->checkAccess($item->data_objects)) {
unset($item->data_objects, $item->base);
$items[$item->type->value][] = $item->toArray();
}
});
return responder()->success($items)->respond();
}
public function getBases()
{
return responder()->success(UniversalReportBase::bases())->respond();
}
public function getDataObjectsAndFields(Request $request)
{
$base = UniversalReportBase::tryFrom($request->input('base', null));
// dd($base->dataObjects());
return responder()->success([
'fields' => $base->fields(),
'dataObjects' => $base->dataObjects(),
'charts' => $base->charts(),
])->respond();
}
public function store(UniversalReportStoreRequest $request)
{
$user = $request->user();
if ($request->input('type') === UniversalReportType::COMPANY->value) {
if ($request->user()->isAdmin()) {
$report = $user->universalReports()->create([
'name' => $request->name,
'type' => $request->type,
'base' => $request->base,
'data_objects' => $request->dataObjects,
'fields' => $request->fields,
'charts' => $request->charts,
]);
return responder()->success(['message' => "The report was saved successfully", 'id' => $report->id])->respond(200);
} else {
return throw new NotEnoughRightsException('User rights do not allow saving the report for the company');
}
}
$report = $user->universalReports()->create([
'name' => $request->name,
'type' => $request->type,
'base' => $request->base,
'data_objects' => $request->dataObjects,
'fields' => $request->fields,
'charts' => $request->charts,
]);
return responder()->success(['message' => "The report was saved successfully", 'id' => $report->id])->respond(200);
}
public function show(UniversalReportShowRequest $request)
{
return responder()->success(UniversalReport::find($request->id))->respond();
}
public function edit(UniversalReportEditRequest $request)
{
if ($request->input('type') === UniversalReportType::COMPANY->value) {
if ($request->user()->isAdmin()) {
UniversalReport::where('id', $request->id)->update([
'name' => $request->name,
'type' => $request->type,
'base' => $request->base,
'data_objects' => $request->dataObjects,
'fields' => $request->fields,
'charts' => $request->charts,
]);
} else {
return throw new NotEnoughRightsException('User rights do not allow saving the report for the company');
}
}
UniversalReport::where('id', $request->id)->update([
'name' => $request->name,
'type' => $request->type,
'base' => $request->base,
'data_objects' => $request->dataObjects,
'fields' => $request->fields,
'charts' => $request->charts,
]);
}
public function __invoke(UniversalReportRequest $request): JsonResponse
{
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
return responder()->success(
UniversalReportExport::init(
$request->input('id'),
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
Settings::scope('core')->get('timezone', 'UTC'),
)->collection()->all(),
)->respond();
}
public function destroy(UniversalReportDestroyRequest $request)
{
UniversalReport::find($request->input('id', null))->delete();
return responder()->success()->respond(204);
}
public function download(UniversalReportRequest $request): JsonResponse
{
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
$job = new GenerateAndSendReport(
UniversalReportExport::init(
$request->id,
Carbon::parse($request->start_at) ?? Carbon::parse(),
Carbon::parse($request->end_at) ?? Carbon::parse(),
Settings::scope('core')->get('timezone', 'UTC'),
$request->user()?->timezone ?? 'UTC',
),
$request->user(),
ReportHelper::getReportFormat($request),
);
$job->handle();
// app(Dispatcher::class)->dispatchSync($job);
return responder()->success(['url' => $job->getPublicPath()])->respond();
}
// /**
// * @throws Throwable
// */
// public function download(UniversalReportRequest $request): JsonResponse
// {
// $job = new GenerateAndSendReport(
// PlannedTimeReportExport::init(
// $request->input('projects', Project::all()->pluck('id')->toArray()),
// ),
// $request->user(),
// ReportHelper::getReportFormat($request),
// );
// app(Dispatcher::class)->dispatchSync($job);
// return responder()->success(['url' => $job->getPublicPath()])->respond();
// }
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enums\Role;
use CatEvent;
use Filter;
use Illuminate\Http\JsonResponse;
class RoleController extends ItemController
{
/**
* @api {post} /api/roles/list Get Roles List
* @apiDescription Retrieves the list of roles available in the system.
*
* @apiVersion 4.0.0
* @apiName GetRolesList
* @apiGroup Roles
*
* @apiSuccess {Number} status HTTP status code.
* @apiSuccess {Boolean} success Request success status.
* @apiSuccess {Object[]} data List of roles.
* @apiSuccess {String} data.name Role name.
* @apiSuccess {Number} data.id Role ID.
*
* @apiSuccessExample {json} Success Response:
* HTTP/1.1 200 OK
* {
* "status": 200,
* "success": true,
* "data": [
* {
* "name": "ANY",
* "id": -1
* },
* {
* "name": "ADMIN",
* "id": 0
* },
* {
* "name": "MANAGER",
* "id": 1
* },
* {
* "name": "USER",
* "id": 2
* },
* {
* "name": "AUDITOR",
* "id": 3
* }
* ]
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function index(): JsonResponse
{
CatEvent::dispatch(Filter::getBeforeActionEventName());
$items = Filter::process(
Filter::getActionFilterName(),
//For compatibility reasons generate serialized model-like array
array_map(fn ($role) => ['name' => $role->name, 'id' => $role->value], Role::cases()),
);
CatEvent::dispatch(Filter::getAfterActionEventName(), [$items]);
return responder()->success($items)->respond();
}
}

View File

@@ -0,0 +1,253 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\Status;
use App\Http\Requests\Status\CreateStatusRequest;
use App\Http\Requests\Status\DestroyStatusRequest;
use App\Http\Requests\Status\ListStatusRequest;
use App\Http\Requests\Status\ShowStatusRequestStatus;
use App\Http\Requests\Status\UpdateStatusRequest;
use CatEvent;
use Exception;
use Filter;
use Illuminate\Http\JsonResponse;
use Throwable;
class StatusController extends ItemController
{
protected const MODEL = Status::class;
/**
* @throws Throwable
* @api {post} /statuses/show Show
* @apiDescription Show status.
*
* @apiVersion 4.0.0
* @apiName Show Status
* @apiGroup Status
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id Status ID
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiUse StatusObject
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function show(ShowStatusRequestStatus $request): JsonResponse
{
return $this->_show($request);
}
/**
* @throws Throwable
* @api {get} /statuses/list List
* @apiDescription Get list of statuses.
*
* @apiVersion 4.0.0
* @apiName Status List
* @apiGroup Status
*
* @apiUse AuthHeader
*
* @apiSuccess {Object} res Status
*
* @apiUse StatusObject
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*
*/
public function index(ListStatusRequest $request): JsonResponse
{
return $this->_index($request);
}
/**
* @param CreateStatusRequest $request
* @return JsonResponse
* @throws Throwable
* @api {post} /statuses/create Create
* @apiDescription Creates status
*
* @apiVersion 4.0.0
* @apiName Create Status
* @apiGroup Status
*
* @apiUse AuthHeader
*
* @apiParam {String} name Status name
* @apiParam {String} active Status active
*
* @apiParamExample {json} Request Example
* {
* "name": "Normal",
* "active": false
* }
*
* @apiSuccess {Object} res Status
*
* @apiSuccess {Number} id The ID of the status.
* @apiSuccess {String} name The name of the status.
* @apiSuccess {Boolean} active Indicates if the status is active.\
* @apiSuccess {String} created_at The creation timestamp.
* @apiSuccess {String} updated_at The last update timestamp.
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "id": 10,
* "name": "Normal",
* "active": false,
* "created_at": "2024-08-15T14:04:03.000000Z",
* "updated_at": "2024-08-15T14:04:03.000000Z"
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function create(CreateStatusRequest $request): JsonResponse
{
Filter::listen(Filter::getRequestFilterName(), static function ($item) {
$maxOrder = Status::max('order');
$item['order'] = $maxOrder + 1;
return $item;
});
return $this->_create($request);
}
/**
* @throws Throwable
* @api {post} /statuses/edit Edit
* @apiDescription Edit Status
*
* @apiVersion 4.0.0
* @apiName Edit
* @apiGroup Status
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id ID
* @apiParam {String} name Status name
* @apiParam {String} active Status active
*
* @apiParamExample {json} Simple Request Example
* {
* "id": 1,
* "name": "Normal",
* "active": false
* }
*
* @apiSuccess {Object} res Status
*
* @apiUse StatusObject
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
*/
public function edit(UpdateStatusRequest $request): JsonResponse
{
CatEvent::listen(Filter::getBeforeActionEventName(), static function ($item, $requestData) {
if (isset($requestData['order'])) {
$newOrder = $requestData['order'];
$oldOrder = $item->order;
if ($newOrder < 1) {
$newOrder = 1;
}
$maxOrder = Status::max('order');
if ($newOrder > $maxOrder) {
$newOrder = $maxOrder + 1;
}
$swapItem = Status::where('order', '=', $newOrder)->first();
if (isset($swapItem)) {
$swapItemOrder = $swapItem->order;
$item->order = 0;
$item->save();
$swapItem->order = $oldOrder;
$swapItem->save();
$item->order = $swapItemOrder;
$item->save();
} else {
$item->order = $newOrder;
}
}
});
return $this->_edit($request);
}
/**
* @throws Throwable
* @api {post} /statuses/remove Destroy
* @apiDescription Destroy User
*
* @apiVersion 4.0.0
* @apiName Destroy Status
* @apiGroup Status
*
* @apiUse AuthHeader
*
* @apiParam {Integer} id ID of the target status
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess (204) No Content Indicates that the status was successfully removed or deactivated.
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function destroy(DestroyStatusRequest $request): JsonResponse
{
return $this->_destroy($request);
}
/**
* @param ListStatusRequest $request
* @return JsonResponse
* @throws Exception
*/
/**
* @api {get} /invitations/count Count Invitations
* @apiDescription Get the count of invitations
*
* @apiVersion 4.0.0
* @apiName CountInvitations
* @apiGroup Invitations
*
* @apiSuccess {Integer} total The total count of pending invitations.
*
* @apiSuccessExample {json} Success Response:
* {
* "total": 0
* }
*
* @apiUse TotalSuccess
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function count(ListStatusRequest $request): JsonResponse
{
return $this->_count($request);
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enums\ActivityType;
use App\Enums\SortDirection;
use App\Http\Requests\TaskActivity\ShowTaskActivityRequest;
use App\Models\TaskComment;
use App\Models\TaskHistory;
use CatEvent;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Filter;
use App\Helpers\QueryHelper;
class TaskActivityController extends ItemController
{
private function getQueryBuilder(array $filter, string $model): Builder
{
$model = new $model;
$query = new Builder($model::getQuery());
$query->setModel($model);
$modelScopes = $model->getGlobalScopes();
foreach ($modelScopes as $key => $value) {
$query->withGlobalScope($key, $value);
}
foreach (Filter::process(Filter::getQueryAdditionalRelationsFilterName(), []) as $with) {
$query->with($with);
}
QueryHelper::apply($query, $model, $filter);
$sortDirection = SortDirection::tryFrom($filter['orderBy'][1])?->value ?? 'asc';
$query->orderBy('id', $sortDirection);
return Filter::process(
Filter::getQueryFilterName(),
$query
);
}
private function getCollectionFromModel(array $requestData, string $model): LengthAwarePaginator
{
if ($model === TaskComment::class) {
$requestData['with'][] = 'attachmentsRelation';
$requestData['with'][] = 'attachmentsRelation.user:id,full_name';
}
$itemsQuery = $this->getQueryBuilder($requestData, $model);
$items = $itemsQuery->paginate(30);
Filter::process(
Filter::getActionFilterName(),
$items,
);
return $items;
}
/**
* @param ShowTaskActivityRequest $request
* @return JsonResponse
* @throws Exception
*/
public function index(ShowTaskActivityRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
$requestedActivity = ActivityType::from($requestData['type']);
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestData);
$items = [];
$total = 0;
$perPage = 0;
if ($requestedActivity === ActivityType::ALL) {
$taskComments = $this->getCollectionFromModel($requestData, TaskComment::class);
$taskHistory = $this->getCollectionFromModel($requestData, TaskHistory::class);
$total = $taskComments->total() + $taskHistory->total();
$perPage = $taskComments->perPage() + $taskHistory->perPage();
$sortDirection = SortDirection::tryFrom($requestData['orderBy'][1])?->value ?? 'asc';
$items = collect(array_merge($taskComments->items(), $taskHistory->items()))->sortBy([
fn ($a, $b) => $sortDirection === 'asc'
? strtotime($a['created_at']) <=> strtotime($b['created_at'])
: strtotime($b['created_at']) <=> strtotime($a['created_at']),
], SORT_REGULAR, $sortDirection === 'desc');
} elseif ($requestedActivity === ActivityType::HISTORY) {
$taskHistory = $this->getCollectionFromModel($requestData, TaskHistory::class);
$total = $taskHistory->total();
$perPage = $taskHistory->perPage();
$items = $taskHistory->items();
} elseif ($requestedActivity === ActivityType::COMMENTS) {
$taskComments = $this->getCollectionFromModel($requestData, TaskComment::class);
$total = $taskComments->total();
$perPage = $taskComments->perPage();
$items = $taskComments->items();
}
CatEvent::dispatch(Filter::getAfterActionEventName(), [$items, $requestData]);
return responder()->success(new LengthAwarePaginator($items, $total, $perPage))->respond();
}
}

View File

@@ -0,0 +1,281 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enums\Role;
use App\Http\Requests\Status\DestroyStatusRequest;
use App\Http\Requests\TaskComment\CreateTaskCommentRequest;
use App\Http\Requests\TaskComment\DestroyTaskCommentRequest;
use App\Http\Requests\TaskComment\ListTaskCommentRequest;
use App\Http\Requests\TaskComment\ShowTaskCommentRequestStatus;
use App\Http\Requests\TaskComment\UpdateTaskCommentRequest;
use Filter;
use App\Models\TaskComment;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Throwable;
class TaskCommentController extends ItemController
{
protected const MODEL = TaskComment::class;
/**
* @api {post} /task-comment/create Create Task Comment
* @apiDescription Create a new task comment
*
* @apiVersion 4.0.0
* @apiName CreateTaskComment
* @apiGroup TaskComments
*
*
* @apiPermission task_comment_create
* @apiPermission task_comment_full_access
* @apiParam {Integer} task_id ID of the task
* @apiParam {String} comment The content of the comment
*
* @apiParamExample {json} Request Example:
* {
* "task_id": 1,
* "comment": "This is a new comment"
* }
*
* @apiSuccess {Integer} id ID of the created comment
* @apiSuccess {Integer} task_id ID of the task
* @apiSuccess {Integer} user_id ID of the user who created the comment
* @apiSuccess {String} comment The content of the comment
* @apiSuccess {String} created_at Creation timestamp
* @apiSuccess {String} updated_at Last update timestamp
*
* @apiSuccessExample {json} Response Example:
* HTTP/1.1 201 Created
* {
* "id": 1,
* "task_id": 1,
* "user_id": 1,
* "comment": "This is a new comment",
* "created_at": "2024-07-09T10:00:00.000000Z",
* "updated_at": "2024-07-09T10:00:00.000000Z"
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function create(CreateTaskCommentRequest $request): JsonResponse
{
Filter::listen(
Filter::getRequestFilterName(),
static function (array $data) use ($request) {
$data['user_id'] = $request->user()->id;
return $data;
}
);
return $this->_create($request);
}
/**
* @api {post} /task-comment/edit Edit Task Comment
* @apiDescription Edit an existing task comment
*
* @apiVersion 4.0.0
* @apiName EditTaskComment
* @apiGroup TaskComments
*
*
* @apiParam {Integer} id ID of the comment to edit
* @apiParam {String} comment The updated content of the comment
*
* @apiParamExample {json} Request Example:
* {
* "id": 1,
* "comment": "This is the updated comment"
* }
*
* @apiSuccess {Integer} id ID of the edited comment
* @apiSuccess {Integer} task_id ID of the task
* @apiSuccess {Integer} user_id ID of the user who edited the comment
* @apiSuccess {String} comment The updated content of the comment
* @apiSuccess {String} created_at Creation timestamp
* @apiSuccess {String} updated_at Last update timestamp
* @apiSuccess {String} deleted_at Deletion timestamp (if applicable, otherwise null)
*
* @apiSuccessExample {json} Response Example:
* HTTP/1.1 200 OK
* {
* "id": 1,
* "task_id": 1,
* "user_id": 1,
* "content": "2344",
* "created_at": "2024-05-03T10:45:36.000000Z",
* "updated_at": "2024-05-03T10:45:36.000000Z",
* "deleted_at": null
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function edit(UpdateTaskCommentRequest $request): JsonResponse
{
return $this->_edit($request);
}
/**
* @api {any} /task-comment/list List Task Comments
* @apiDescription Get list of Task Comments
*
* @apiVersion 4.0.0
* @apiName GetTaskCommentList
* @apiGroup Task Comments
*
* @apiPermission task_comment_list
* @apiPermission task_comment_full_access
*
* @apiParam {Integer} [task_id] Optional task ID to filter comments
*
* @apiParamExample {json} Request Example:
* {
* "task_id": 1
* }
*
* @apiSuccess {Integer} id ID of the comment
* @apiSuccess {Integer} task_id ID of the task
* @apiSuccess {Integer} user_id ID of the user who created the comment
* @apiSuccess {String} content Content of the comment
* @apiSuccess {String} created_at Creation timestamp
* @apiSuccess {String} updated_at Last update timestamp
* @apiSuccess {String} deleted_at Deletion timestamp (if applicable, otherwise null)
* @apiSuccess {Object} user User who created the comment
*
* @apiSuccessExample {json} Response Example:
* HTTP/1.1 200 OK
* {
* "id": 1,
* "task_id": 1,
* "user_id": 1,
* "content": "2344",
* "created_at": "2024-05-03T10:45:36.000000Z",
* "updated_at": "2024-05-03T10:45:36.000000Z",
* "deleted_at": null,
* "user": {
* "id": 1,
* "full_name": "Admin",
* "email": "admin@cattr.app",
* "url": "",
* "company_id": 1,
* "avatar": "",
* "screenshots_active": 1,
* "manual_time": 0,
* "computer_time_popup": 300,
* "blur_screenshots": false,
* "web_and_app_monitoring": true,
* "screenshots_interval": 5,
* "active": 1,
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-02-15T19:06:42.000000Z",
* "timezone": null,
* "important": 0,
* "change_password": 0,
* "role_id": 0,
* "user_language": "en",
* "type": "employee",
* "invitation_sent": false,
* "nonce": 0,
* "client_installed": 0,
* "permanent_screenshots": 0,
* "last_activity": "2023-10-26 10:26:17",
* "online": false,
* "can_view_team_tab": true,
* "can_create_task": true
* }
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function index(ListTaskCommentRequest $request): JsonResponse
{
Filter::listen(
Filter::getQueryFilterName(),
static function ($query) use ($request) {
if (!$request->user()->can('edit', TaskComment::class)) {
$query = $query->whereHas(
'task',
static fn (Builder $taskQuery) => $taskQuery->where(
'user_id',
'=',
$request->user()->id
)
);
}
return $query->with('user');
}
);
return $this->_index($request);
}
/**
* @apiDeprecated since 1.0.0
* @throws Throwable
* @api {post} /task-comment/show Show
* @apiDescription Show Task Comment
*
* @apiVersion 1.0.0
* @apiName ShowTaskComment
* @apiGroup Task Comment
*
* @apiPermission task_comment_show
* @apiPermission task_comment_full_access
*/
public function show(ShowTaskCommentRequestStatus $request): JsonResponse
{
return $this->_show($request);
}
/**
* @apiDeprecated since 4.0.0
* @api {post} /task-comment/remove Destroy Task Comment
* @apiDescription Destroy a Task Comment
*
* @apiVersion 4.0.0
* @apiName DestroyTaskComment
* @apiGroup Task Comment
*
* @apiPermission task_comment_remove
* @apiPermission task_comment_full_access
*
* @apiParam {Integer} id ID of the task comment to destroy
*
* @apiParamExample {json} Request Example:
* {
* "id": 1
* }
*
* @apiSuccess {Integer} status Response status code
* @apiSuccess {Boolean} success Response success status
* @apiSuccess {String} message Success message
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function destroy(DestroyTaskCommentRequest $request): JsonResponse
{
$user = $request->user();
Filter::listen(
Filter::getQueryFilterName(),
static fn($query) => $user->hasRole([Role::ADMIN, Role::MANAGER]) ? $query :
$query->where(['user_id' => $user->id])
->whereHas('task', static fn($taskQuery) => $taskQuery->where(['user_id' => $user->id]))
);
return $this->_destroy($request);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,279 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Filter;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Validator;
/**
* @deprecated
*/
class TimeController extends Controller
{
/**
* @deprecated
*/
public static function getControllerRules(): array
{
return [
'total' => 'time.total',
'project' => 'time.project',
'tasks' => 'time.tasks',
'task' => 'time.task',
'taskUser' => 'time.task-user',
];
}
/**
* @api {get,post} /time/total Total
* @apiDescription Get total of Time
*
* @apiVersion 4.0.0
* @apiName Total
* @apiGroup Time
*
* @apiUse AuthHeader
*
* @apiPermission time_total
* @apiPermission time_full_access
*
* @apiParam {String} start_at Start DataTime
* @apiParam {String} end_at End DataTime
* @apiParam {Integer} user_id User ID
*
* @apiParamExample {json} Request Example
* {
* "user_id": 1,
* "start_at": "2005-01-01 00:00:00",
* "end_at": "2019-01-01 00:00:00"
* }
*
* @apiSuccess {Integer} time Total time in seconds
* @apiSuccess {String} start Datetime of first Time Interval start_at
* @apiSuccess {String} end Datetime of last Time Interval end_at
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "time": 338230,
* "start": "2020-01-23T19:42:27+00:00",
* "end": "2020-04-30T21:58:31+00:00"
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
* @apiUse ValidationError
*/
/**
* Display a total of time
* @param Request $request
* @return JsonResponse
* @throws Exception
* @deprecated
*/
public function total(Request $request): JsonResponse
{
$validationRules = [
'start_at' => 'required|date',
'end_at' => 'required|date',
'user_id' => 'required|integer|exists:users,id'
];
$validator = Validator::make($request->all(), $validationRules);
if ($validator->fails()) {
return new JsonResponse(
Filter::process($this->getEventUniqueName('answer.error.time.total'), [
'error_type' => 'validation',
'message' => 'Validation error',
'info' => $validator->errors()
]),
400
);
}
$filters = [
'start_at' => ['>=', $request->get('start_at')],
'end_at' => ['<=', $request->get('end_at')],
'user_id' => ['=', $request->get('user_id')]
];
/** @var Builder $itemsQuery */
$itemsQuery = Filter::process(
$this->getEventUniqueName('answer.success.item.query.prepare'),
$this->applyQueryFilter($this->getQuery(), $filters)
);
$timeIntervals = $itemsQuery->get();
$totalTime = $timeIntervals->sum(static fn($el) => Carbon::parse($el->end_at)->diffInSeconds($el->start_at));
return responder()->success([
'time' => $totalTime,
'start' => $timeIntervals->min('start_at'),
'end' => $timeIntervals->max('end_at')
])->respond();
}
/**
* @api {get,post} /time/tasks Tasks
* @apiDescription Get tasks and its total time
*
* @apiVersion 4.0.0
* @apiName Tasks
* @apiGroup Time
*
* @apiUse TimeIntervalParams
*
* @apiParamExample {json} Request Example:
* {
* "user_id": 1,
* "task_id": 1,
* "project_id": 2,
* "start_at": "2005-01-01 00:00:00",
* "end_at": "2019-01-01 00:00:00",
* "activity_fill": 42,
* "mouse_fill": 43,
* "keyboard_fill": 43,
* "id": [">", 1]
* }
*
* @apiSuccess {String} current_datetime Current datetime of server
* @apiSuccess {Object[]} tasks Array of objects Task
* @apiSuccess {Integer} tasks.id Tasks id
* @apiSuccess {Integer} tasks.user_id Tasks User id
* @apiSuccess {Integer} tasks.project_id Tasks Project id
* @apiSuccess {Integer} tasks.time Tasks total time in seconds
* @apiSuccess {String} tasks.start Datetime of first Tasks Time Interval start_at
* @apiSuccess {String} tasks.end Datetime of last Tasks Time Interval end_at
* @apiSuccess {Object[]} total Array of total tasks time
* @apiSuccess {Integer} total.time Total time of tasks in seconds
* @apiSuccess {String} total.start Datetime of first Time Interval start_at
* @apiSuccess {String} total.end DateTime of last Time Interval end_at
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "current_datetime": "2020-01-28T10:57:40+00:00",
* "tasks": [
* {
* "id": 1,
* "user_id": 1,
* "project_id": 1,
* "time": 1490,
* "start": "2020-01-23T19:42:27+00:00",
* "end": "2020-01-23T20:07:21+00:00"
* },
* ],
* "total": {
* "time": 971480,
* "start": "2020-01-23T19:42:27+00:00",
* "end": "2020-11-01T08:28:06+00:00"
* }
* }
*
* @apiUse 400Error
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
* @apiUse ValidationError
*/
/**
* Display the Tasks and theirs total time
*
* @param Request $request
* @return JsonResponse
* @throws Exception
* @deprecated
*/
public function tasks(Request $request): JsonResponse
{
$validationRules = [
'start_at' => 'date',
'end_at' => 'date',
'project_id' => 'exists:projects,id',
'task_id' => 'exists:tasks,id'
];
$validator = Validator::make($request->all(), $validationRules);
if ($validator->fails()) {
return new JsonResponse(
Filter::process($this->getEventUniqueName('answer.error.time.total'), [
'error_type' => 'validation',
'message' => 'Validation error',
'info' => $validator->errors()
]),
400
);
}
$filters = $request->all();
$request->get('start_at') ? $filters['start_at'] = ['>=', (string)$request->get('start_at')] : false;
$request->get('end_at') ? $filters['end_at'] = ['<=', (string)$request->get('end_at')] : false;
$request->get('project_id') ? $filters['task.project_id'] = $request->get('project_id') : false;
$request->get('task_id') ? $filters['task_id'] = ['in', $request->get('task_id')] : false;
$baseQuery = $this->applyQueryFilter(
$this->getQuery(),
$filters ?: []
);
$itemsQuery = Filter::process(
$this->getEventUniqueName('answer.success.item.list.query.prepare'),
$baseQuery
);
$totalTime = 0;
$tasks = $itemsQuery
->with('task')
->get()
->groupBy(['task_id', 'user_id'])
->map(static function ($taskIntervals, $taskId) use (&$totalTime) {
$task = [];
foreach ($taskIntervals as $userId => $userIntervals) {
$taskTime = 0;
foreach ($userIntervals as $interval) {
$taskTime += Carbon::parse($interval->end_at)->diffInSeconds($interval->start_at);
}
$firstUserInterval = $userIntervals->first();
$lastUserInterval = $userIntervals->last();
$task = [
'id' => $taskId,
'user_id' => $userId,
'project_id' => $userIntervals[0]['task']['project_id'],
'time' => $taskTime,
'start' => Carbon::parse($firstUserInterval->start_at)->toISOString(),
'end' => Carbon::parse($lastUserInterval->end_at)->toISOString()
];
$totalTime += $taskTime;
}
return $task;
})
->values();
$first = $itemsQuery->get()->first();
$last = $itemsQuery->get()->last();
return responder()->success([
'tasks' => $tasks,
'total' => [
'time' => $totalTime,
'start' => $first ? Carbon::parse($first->start_at)->toISOString() : null,
'end' => $last ? Carbon::parse($last->end_at)->toISOString() : null,
]
])->respond();
}
}

View File

@@ -0,0 +1,570 @@
<?php
namespace App\Http\Controllers\Api;
use App;
use App\Enums\Role;
use App\Enums\ScreenshotsState;
use App\Http\Requests\User\ListUsersRequest;
use App\Scopes\UserAccessScope;
use Settings;
use Carbon\Carbon;
use Exception;
use Filter;
use App\Mail\UserCreated;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use CatEvent;
use Mail;
use App\Http\Requests\User\CreateUserRequest;
use App\Http\Requests\User\EditUserRequest;
use App\Http\Requests\User\SendInviteUserRequest;
use App\Http\Requests\User\ShowUserRequest;
use App\Http\Requests\User\DestroyUserRequest;
use App\Models\Setting;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class UserController extends ItemController
{
protected const MODEL = User::class;
/**
* @throws Exception
* @api {get, post} /users/list List
* @apiDescription Get list of Users with any params
*
* @apiVersion 4.0.0
* @apiName GetUserList
* @apiGroup User
*
* @apiUse AuthHeader
*
* @apiPermission users_list
* @apiPermission users_full_access
*
* @apiSuccess {Object[]} users List of users.
* @apiSuccess {Integer} users.id The unique ID of the user.
* @apiSuccess {String} users.full_name Full name of the user.
* @apiSuccess {String} users.email Email address of the user.
* @apiSuccess {String} users.url URL associated with the user.
* @apiSuccess {Integer} users.company_id ID of the company the user belongs to.
* @apiSuccess {String} users.avatar URL of the user's avatar image.
* @apiSuccess {Integer} users.screenshots_state The current state of screenshot monitoring.
* @apiSuccess {Boolean} users.manual_time Indicates if manual time tracking is allowed.
* @apiSuccess {Integer} users.computer_time_popup Time in seconds before showing a time popup.
* @apiSuccess {Boolean} users.blur_screenshots Indicates if screenshots are blurred.
* @apiSuccess {Boolean} users.web_and_app_monitoring Indicates if web and app monitoring is enabled.
* @apiSuccess {Integer} users.screenshots_interval Interval in minutes for taking screenshots.
* @apiSuccess {Boolean} users.active Indicates if the user is active.
* @apiSuccess {String} users.deleted_at Deletion timestamp, or `null` if the user is not deleted.
* @apiSuccess {String} users.created_at Creation timestamp of the user.
* @apiSuccess {String} users.updated_at Last update timestamp of the user.
* @apiSuccess {String} users.timezone The timezone of the user, or `null`.
* @apiSuccess {Boolean} users.important Indicates if the user is marked as important.
* @apiSuccess {Boolean} users.change_password Indicates if the user must change their password.
* @apiSuccess {Integer} users.role_id ID of the user's role.
* @apiSuccess {String} users.user_language Language preference of the user.
* @apiSuccess {String} users.type The user type, e.g., "employee".
* @apiSuccess {Boolean} users.invitation_sent Indicates if an invitation has been sent.
* @apiSuccess {Integer} users.nonce Nonce value for secure actions.
* @apiSuccess {Boolean} users.client_installed Indicates if the client software is installed.
* @apiSuccess {Boolean} users.permanent_screenshots Indicates if permanent screenshots are enabled.
* @apiSuccess {String} users.last_activity The last recorded activity timestamp.
* @apiSuccess {Boolean} users.screenshots_state_locked Indicates if screenshot state is locked.
* @apiSuccess {Boolean} users.online Indicates if the user is currently online.
* @apiSuccess {Boolean} users.can_view_team_tab Indicates if the user can view the team tab.
* @apiSuccess {Boolean} users.can_create_task Indicates if the user can create tasks.
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* [
* {
* "id": 1,
* "full_name": "Admin",
* "email": "admin@cattr.app",
* "url": "",
* "company_id": 1,
* "avatar": "",
* "screenshots_state": 1,
* "manual_time": 0,
* "computer_time_popup": 300,
* "blur_screenshots": false,
* "web_and_app_monitoring": true,
* "screenshots_interval": 5,
* "active": 1,
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-08-19T10:42:18.000000Z",
* "timezone": null,
* "important": 0,
* "change_password": 0,
* "role_id": 0,
* "user_language": "en",
* "type": "employee",
* "invitation_sent": false,
* "nonce": 0,
* "client_installed": 0,
* "permanent_screenshots": 0,
* "last_activity": "2024-08-19 10:42:18",
* "screenshots_state_locked": false,
* "online": false,
* "can_view_team_tab": true,
* "can_create_task": true
* },
* {
* "id": 2,
* "full_name": "Fabiola Mertz",
* "email": "projectManager@example.com",
* "url": "",
* "company_id": 1,
* "avatar": "",
* "screenshots_state": 2,
* "manual_time": 0,
* "computer_time_popup": 300,
* "blur_screenshots": false,
* "web_and_app_monitoring": true,
* "screenshots_interval": 5,
* "active": 1,
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2023-10-26T10:26:17.000000Z",
* "timezone": null,
* "important": 0,
* "change_password": 0,
* "role_id": 2,
* "user_language": "en",
* "type": "employee",
* "invitation_sent": false,
* "nonce": 0,
* "client_installed": 0,
* "permanent_screenshots": 0,
* "last_activity": "2023-10-26 09:44:17",
* "screenshots_state_locked": false,
* "online": false,
* "can_view_team_tab": false,
* "can_create_task": false
* },...
* ]
* }
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function index(ListUsersRequest $request): JsonResponse
{
return $this->_index($request);
}
/**
* @api {post} /users/create Create
* @apiDescription Create User Entity
*
* @apiVersion 4.0.0
* @apiName CreateUser
* @apiGroup User
*
* @apiUse AuthHeader
*
* @apiPermission users_create
* @apiPermission users_full_access
*
* @apiParam {String} user_language The language of the new user (e.g., "en")
* @apiParam {String} timezone The timezone of the new user (e.g., "Europe/Moscow")
* @apiParam {Integer} role_id ID of the role of the new user
* @apiParam {Integer} active Will new user be active or not `(1 - active, 0 - not)`
* @apiParam {Integer} screenshots_state State of screenshots monitoring (e.g., 1 for enabled)
* @apiParam {Boolean} send_invite Whether to send an invitation to the new user (true - send, false - do not send)
* @apiParam {Boolean} manual_time Whether manual time tracking is enabled for the new user
* @apiParam {Integer} screenshots_interval Interval in minutes for taking screenshots
* @apiParam {Integer} computer_time_popup Time in minutes before showing a time popup
* @apiParam {String} type The type of user (e.g., "employee")
* @apiParam {Boolean} web_and_app_monitoring Whether web and app monitoring is enabled
* @apiParam {String} email New user email
* @apiParam {String} password New user password
* @apiParam {String} full_name New user name
* @apiParamExample {json} Request Example
* {
* "user_language" : "en",
* "timezone" : "Europe/Moscow",
* "role_id" : 2,
* "active" : true,
* "screenshots_state" : 1,
* "send_invite" : 1,
* "manual_time" : 1,
* "screenshots_interval" : 10,
* "computer_time_popup" : 3,
* "type" : "employee",
* "web_and_app_monitoring" : 1,
* "email" : "123@cattr.app",
* "password" : "password",
* "full_name" : "name"
* }
* @apiSuccess {String} full_name Full name of the user.
* @apiSuccess {String} email Email address of the user.
* @apiSuccess {String} user_language Language of the user.
* @apiSuccess {Boolean} active Whether the user is active.
* @apiSuccess {Integer} screenshots_state State of screenshots monitoring.
* @apiSuccess {Boolean} manual_time Whether manual time tracking is enabled.
* @apiSuccess {Integer} screenshots_interval Interval in minutes for taking screenshots.
* @apiSuccess {Integer} computer_time_popup Time in minutes before showing a time popup.
* @apiSuccess {String} timezone Timezone of the user.
* @apiSuccess {Integer} role_id ID of the role assigned to the user.
* @apiSuccess {String} type Type of the user (e.g., "employee").
* @apiSuccess {Boolean} web_and_app_monitoring Whether web and app monitoring is enabled.
* @apiSuccess {Boolean} screenshots_state_locked Whether the screenshot state is locked.
* @apiSuccess {Boolean} invitation_sent Whether an invitation has been sent.
* @apiSuccess {String} updated_at Timestamp of the last update.
* @apiSuccess {String} created_at Timestamp of when the user was created.
* @apiSuccess {Integer} id ID of the created user.
* @apiSuccess {Boolean} online Whether the user is currently online.
* @apiSuccess {Boolean} can_view_team_tab Whether the user can view the team tab.
* @apiSuccess {Boolean} can_create_task Whether the user can create tasks.
*
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "full_name": "name",
* "email": "123@cattr.app",
* "user_language": "en",
* "active": 1,
* "screenshots_state": 1,
* "manual_time": 1,
* "screenshots_interval": 10,
* "computer_time_popup": 3,
* "timezone": "Europe/Moscow",
* "role_id": 2,
* "type": "employee",
* "web_and_app_monitoring": true,
* "screenshots_state_locked": true,
* "invitation_sent": true,
* "updated_at": "2024-08-21T14:29:06.000000Z",
* "created_at": "2024-08-21T14:29:06.000000Z",
* "id": 10,
* "online": false,
* "can_view_team_tab": false,
* "can_create_task": false
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
/**
* @param CreateUserRequest $request
* @return JsonResponse
* @throws Throwable
*/
public function create(CreateUserRequest $request): JsonResponse
{
Filter::listen(Filter::getRequestFilterName(), static function ($requestData) use ($request) {
$requestData['screenshots_state_locked'] = $request->user()->isAdmin() && ScreenshotsState::tryFrom($requestData['screenshots_state'])->mustBeInherited();
return $requestData;
});
return $this->_create($request);
}
/**
* @api {post} /users/edit Edit
* @apiDescription Edit User
*
* @apiVersion 4.0.0
* @apiName EditUser
* @apiGroup User
*
* @apiUse AuthHeader
*
* @apiPermission users_edit
* @apiPermission users_full_access
* @apiParam {String} user_language The language of the new user (e.g., "en")
* @apiParam {String} timezone The timezone of the new user (e.g., "Europe/Moscow")
* @apiParam {Integer} role_id ID of the role of the new user
* @apiParam {Integer} id The ID of the user being edited.
* @apiParam {String} full_name New user name
* @apiParam {String} email New user email
* @apiParam {String} url URL associated with the user
* @apiParam {Integer} company_id The ID of the company to which the user belongs
* @apiParam {String} avatar The URL of the users avatar
* @apiParam {Integer} screenshots_state State of screenshots monitoring (e.g., 1 for enabled)
* @apiParam {Boolean} manual_time Whether manual time tracking is enabled for the new user
* @apiParam {Integer} computer_time_popup Time in minutes before showing a time popup
* @apiParam {Boolean} blur_screenshots Indicates if screenshots are blurred
* @apiParam {Boolean} web_and_app_monitoring Whether web and app monitoring is enabled
* @apiParam {Integer} screenshots_interval Interval in minutes for taking screenshots
* @apiParam {Integer} active Will new user be active or not `(1 - active, 0 - not)`
* @apiParam {String} deleted_at Deletion timestamp, or `null` if the user is not deleted.
* @apiParam {Boolean} send_invite Whether to send an invitation to the new user (true - send, false - do not send)
*
*
*
* @apiParam {String} type The type of user (e.g., "employee")
*
* @apiParam {String} password New user password
*
*
* @apiParamExample {json} Request Example
* {
* "user_language" : "en",
* "timezone" : "Europe/Moscow",
* "role_id" : 2,
* "id" : 3,
* "full_name" : "Rachael Reichert",
* "email": "projectAuditor@example.com",
* "url" : null,
* "company_id" : 1,
* "avatar" : null,
* "screenshots_state" : 1,
* "manual_time" : 0,
* "computer_time_popup" : 300,
* "blur_screenshots" : false,
* "web_and_app_monitoring" : true,
* "screenshots_interval" : 5,
* "active" : true,
* "deleted_at" : null,
* "created_at" : "2023-10-26T10:26:42.000000Z",
* "updated_at" : "2023-10-26T10:26:42.000000Z",
* "important" : 0,
* "change_password" : 0,
* "type" : "employee",
* "invitation_sent" : false,
* "nonce" : 0,
* "client_installed" : 0,
* "permanent_screenshots" : 0,
* "last_activity" : "2023-10-26 10:05:42",
* "screenshots_state_locked" : false,
* "online" : false,
* "can_view_team_tab" : false,
* "can_create_task" : false
* }
* @apiUse UserObject
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
*/
/**
* @param EditUserRequest $request
* @return JsonResponse
* @throws Throwable
*/
public function edit(EditUserRequest $request): JsonResponse
{
Filter::listen(Filter::getActionFilterName(), static function (User $user) use ($request) {
if ($user->screenshots_state_locked && !$request->user()->isAdmin()) {
$user->screenshots_state = $user->getOriginal('screenshots_state');
return $user;
}
$user->screenshots_state_locked = $request->user()->isAdmin() && ScreenshotsState::tryFrom($user->screenshots_state)->mustBeInherited();
return $user;
});
return $this->_edit($request);
}
/**
* @api {get, post} /users/show Show User
* @apiDescription Retrieves detailed information about a specific user.
*
* @apiVersion 4.0.0
* @apiName ShowUser
* @apiGroup User
*
* @apiUse AuthHeader
*
* @apiPermission users_show
* @apiPermission users_full_access
*
* @apiParam {Integer} id User id
*
* @apiParamExample {json} Request Example:
* {
* "id": 1
* }
* @apiUse UserObject
*
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
* @apiUse ForbiddenError
* @apiUse ValidationError
*/
/**
* @param ShowUserRequest $request
* @return JsonResponse
* @throws Exception
* @throws Throwable
*/
public function show(ShowUserRequest $request): JsonResponse
{
return $this->_show($request);
}
/**
* @throws Throwable
* @api {post} /users/remove Destroy
* @apiDescription Destroy User
*
* @apiVersion 4.0.0
* @apiName DestroyUser
* @apiGroup User
*
* @apiUse AuthHeader
*
* @apiPermission users_remove
* @apiPermission users_full_access
*
* @apiParam {Integer} id ID of the target user
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {String} message Destroy status
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
* {
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function destroy(DestroyUserRequest $request): JsonResponse
{
return $this->_destroy($request);
}
/**
* @throws Exception
* @api {get,post} /users/count Count
* @apiDescription Count Users
*
* @apiVersion 4.0.0
* @apiName Count
* @apiGroup User
*
* @apiUse AuthHeader
*
* @apiPermission users_count
* @apiPermission users_full_access
*
* @apiSuccess {String} total Amount of users that we have
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "total": 2
* }
*
* @apiUse 400Error
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function count(ListUsersRequest $request): JsonResponse
{
return $this->_count($request);
}
/**
* @param SendInviteUserRequest $request
* @return JsonResponse
* @throws Throwable
*/
/**
* @api {post} /api/users/send-invite Send User Invitation
* @apiDescription Sends an invitation to a user by generating a password, marking the invitation as sent, and dispatching relevant events.
*
* @apiVersion 4.0.0
* @apiName SendUserInvite
* @apiGroup User
*
* @apiUse AuthHeader
*
* @apiPermission users_invite
*
* @apiParam {Integer} id The ID of the user to whom the invitation will be sent.
*
* @apiParamExample {json} Request Example:
* {
* "id": 1
* }
*
* @apiSuccess {String} message A confirmation that the invite was sent successfully.
*
* @apiSuccessExample {json} Success Response:
* HTTP/1.1 204 No Content
*
* @apiUse 400Error
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function sendInvite(SendInviteUserRequest $request): JsonResponse
{
$requestId = Filter::process(Filter::getRequestFilterName(), $request->validated('id'));
$itemsQuery = $this->getQuery(['id' => $requestId]);
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestId);
$item = Filter::process(Filter::getActionFilterName(), $itemsQuery->first());
$password = Str::random();
$item->password = $password;
$item->invitation_sent = true;
$item->save();
throw_unless($item, new NotFoundHttpException);
CatEvent::dispatch(Filter::getAfterActionEventName(), [$requestId, $item]);
$language = Settings::scope('core')->get('language', 'en');
Mail::to($item->email)->locale($language)->send(new UserCreated($item->email, $password));
return responder()->success()->respond(204);
}
/**
* @api {patch} /users/activity Activity
* @apiDescription Updates the time of the user's last activity
*
* @apiVersion 4.0.0
* @apiName Activity
* @apiGroup User
*
* @apiUse AuthHeader
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
* {
* }
*
* @apiUse UnauthorizedError
*/
public function updateActivity(): JsonResponse
{
$user = request()->user();
CatEvent::dispatch(Filter::getBeforeActionEventName(), $user);
Filter::process(Filter::getActionFilterName(), $user)->update(['last_activity' => Carbon::now()]);
CatEvent::dispatch(Filter::getAfterActionEventName(), $user);
return responder()->success()->respond(204);
}
}

View File

@@ -0,0 +1,334 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\Entities\AuthorizationException;
use App\Exceptions\Entities\DeprecatedApiException;
use App\Helpers\Recaptcha;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Transformers\AuthTokenTransformer;
use Cache;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Str;
class AuthController extends BaseController
{
/**
* @apiDefine AuthHeader
* @apiHeader {String} Authorization Token for user auth
* @apiHeaderExample {json} Authorization Header Example
* {
* "Authorization": "bearer 16184cf3b2510464a53c0e573c75740540fe..."
* }
*/
public function __construct(protected Recaptcha $recaptcha)
{
}
/**
* @api {post} /auth/login Login
* @apiDescription Get user Token
*
* @apiVersion 4.0.0
* @apiName Login
* @apiGroup Auth
*
* @apiParam {String} email User email
* @apiParam {String} password User password
* @apiParam {String} [recaptcha] Recaptcha token
*
* @apiParamExample {json} Request Example
* {
* "email": "johndoe@example.com",
* "password": "amazingpassword",
* "recaptcha": "03AOLTBLR5UtIoenazYWjaZ4AFZiv1OWegWV..."
* }
* @apiUse User
* @apiUse 400Error
* @apiUse ParamsValidationError
* @apiUse UnauthorizedError
* @apiUse UserDeactivatedError
* @apiUse CaptchaError
* @apiUse LimiterError
*/
public function login(LoginRequest $request): JsonResponse
{
$credentials = $request->only(['email', 'password', 'recaptcha']);
$this->recaptcha->check($credentials);
if (!auth()->attempt([
'email' => $credentials['email'],
'password' => $credentials['password'],
])) {
$this->recaptcha->incrementCaptchaAmounts();
$this->recaptcha->check($credentials);
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_UNAUTHORIZED);
}
$user = auth()->user();
if (!$user || !$user->active) {
$this->recaptcha->incrementCaptchaAmounts();
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_USER_DISABLED);
}
if ($user->invitation_sent) {
$user->invitation_sent = false;
$user->save();
}
$this->recaptcha->clearCaptchaAmounts();
if (preg_match('/' . config('auth.cattr-client-agent') . '/', $request->header('User_agent'))) {
$user->client_installed = 1;
$user->save();
}
return responder()->success([
'token' => $user->createToken(Str::uuid())->plainTextToken,
], new AuthTokenTransformer)->respond();
}
/**
* @api {post} /auth/logout Logout
* @apiDescription Invalidate current token
*
* @apiVersion 4.0.0
* @apiName Logout
* @apiGroup Auth
*
* @apiUse AuthHeader
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 OK
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function logout(Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
return responder()->success()->respond(204);
}
/**
* @api {post} /auth/logout-from-all Logout from all
* @apiDescription Invalidate all user tokens
*
* @apiVersion 4.0.0
* @apiName Logout all
* @apiGroup Auth
*
* @apiUse AuthHeader
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 OK
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function logoutFromAll(Request $request): JsonResponse
{
$request->user()->tokens()->delete();
return responder()->success()->respond(204);
}
/**
* @api {get} /auth/me Me
* @apiDescription Get authenticated User Entity
*
* @apiVersion 4.0.0
* @apiName Me
* @apiGroup Auth
*
* @apiUse AuthHeader
*
* @apiSuccess {Integer} id ID of the user
* @apiSuccess {String} full_name Full name of the user
* @apiSuccess {String} email Email of the user
* @apiSuccess {String} [url] URL of the user (optional)
* @apiSuccess {Integer} company_id Company ID of the user
* @apiSuccess {String} [avatar] Avatar URL of the user (optional)
* @apiSuccess {Boolean} screenshots_active Indicates if screenshots are active
* @apiSuccess {Boolean} manual_time Indicates if manual time tracking is allowed
* @apiSuccess {Integer} computer_time_popup Time interval for computer time popup
* @apiSuccess {Boolean} blur_screenshots Indicates if screenshots are blurred
* @apiSuccess {Boolean} web_and_app_monitoring Indicates if web and app monitoring is enabled
* @apiSuccess {Integer} screenshots_interval Interval for taking screenshots
* @apiSuccess {Boolean} active Indicates if the user is active
* @apiSuccess {String} [deleted_at] Deletion timestamp (if applicable, otherwise null)
* @apiSuccess {String} created_at Creation timestamp
* @apiSuccess {String} updated_at Last update timestamp
* @apiSuccess {String} [timezone] Timezone of the user (optional)
* @apiSuccess {Boolean} important Indicates if the user is marked as important
* @apiSuccess {Boolean} change_password Indicates if the user needs to change password
* @apiSuccess {Integer} role_id Role ID of the user
* @apiSuccess {String} user_language Language of the user
* @apiSuccess {String} type Type of the user (e.g., "employee")
* @apiSuccess {Boolean} invitation_sent Indicates if invitation is sent to the user
* @apiSuccess {Integer} nonce Nonce value of the user
* @apiSuccess {Boolean} client_installed Indicates if client is installed
* @apiSuccess {Boolean} permanent_screenshots Indicates if screenshots are permanent
* @apiSuccess {String} last_activity Last activity timestamp of the user
* @apiSuccess {Boolean} online Indicates if the user is online
* @apiSuccess {Boolean} can_view_team_tab Indicates if the user can view team tab
* @apiSuccess {Boolean} can_create_task Indicates if the user can create tasks
*
* @apiSuccessExample {json} Response Example:
* HTTP/1.1 200 OK
* {
* "id": 1,
* "full_name": "Admin",
* "email": "admin@cattr.app",
* "url": "",
* "company_id": 1,
* "avatar": "",
* "screenshots_active": 1,
* "manual_time": 0,
* "computer_time_popup": 300,
* "blur_screenshots": false,
* "web_and_app_monitoring": true,
* "screenshots_interval": 5,
* "active": 1,
* "deleted_at": null,
* "created_at": "2023-10-26T10:26:17.000000Z",
* "updated_at": "2024-02-15T19:06:42.000000Z",
* "timezone": null,
* "important": 0,
* "change_password": 0,
* "role_id": 0,
* "user_language": "en",
* "type": "employee",
* "invitation_sent": false,
* "nonce": 0,
* "client_installed": 0,
* "permanent_screenshots": 0,
* "last_activity": "2023-10-26 10:26:17",
* "online": false,
* "can_view_team_tab": true,
* "can_create_task": true
* }
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
public function me(Request $request): JsonResponse
{
return responder()->success($request->user())->respond();
}
/**
* @api {get} /auth/desktop-key Issue key
* @apiDescription Issues key for desktop auth
*
* @apiVersion 4.0.0
* @apiName Issue key
* @apiGroup Auth
*
* @apiUse AuthHeader
* @apiUse User
* @apiUse UnauthorizedError
*/
/**
* @param Request $request
*
* @return JsonResponse
* @throws Exception
*/
public function issueDesktopKey(Request $request): JsonResponse
{
$token = Str::random(40);
$lifetime = now()->addMinutes(config('auth.lifetime_minutes.desktop_token'));
Cache::store('octane')->put(
sha1($request->ip()) . ":$token",
$request->user()->id,
$lifetime,
);
return responder()->success([
'token' => $token,
'type' => 'desktop',
'expires' => now()->addMinutes(config('auth.lifetime_minutes.desktop_token'))->toIso8601String(),
], new AuthTokenTransformer)->meta(['frontend_uri' => config('app.frontend_url')])->respond();
}
/**
* @api {put} /auth/desktop-key Key auth
* @apiDescription Exchange desktop key to JWT
*
* @apiVersion 4.0.0
* @apiName Key auth
* @apiGroup Auth
*
* @apiUse AuthHeader
* @apiUse User
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
/**
* @param Request $request
*
* @return JsonResponse
* @throws Exception
*/
public function authDesktopKey(Request $request): JsonResponse
{
$token = $request->header('Authorization');
if (!$token) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_UNAUTHORIZED);
}
$token = explode(' ', $token);
if (count($token) !== 2 || $token[0] !== 'desktop' || !Cache::store('octane')->has(sha1($request->ip()) . ":$token[1]")) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_UNAUTHORIZED);
}
$user = auth()->loginUsingId(Cache::store('octane')->get(sha1($request->ip()) . ":$token[1]"));
if (!optional($user)->active) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_USER_DISABLED);
}
return responder()->success([
'token' => $user->createToken(Str::uuid())->plainTextToken,
], new AuthTokenTransformer)->respond();
}
/**
* @apiDeprecated Exists only for compatibility with old Cattr client
* @api {post} /auth/refresh Refresh
* @apiDescription Refreshes JWT
*
* @apiVersion 4.0.0
* @apiName Refresh
* @apiGroup Auth
*
* @apiUse AuthHeader
*
* @apiSuccess {String} access_token Token
* @apiSuccess {String} token_type Token Type
* @apiSuccess {String} expires_in Token TTL 8601String Date
*
* @apiUse 400Error
* @apiUse UnauthorizedError
*/
/**
* @return JsonResponse
* @deprecated Exists only for compatibility with old Cattr client
*/
public function refresh(): JsonResponse
{
throw new DeprecatedApiException();
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Routing\Route as RouteModel;
use Illuminate\Routing\RouteCollection;
use Illuminate\Routing\Router;
use Illuminate\Support\Collection;
use Route;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
public static function getControllerRules(): array
{
return [];
}
public function frontendRoute(Request $request) {
return view('app');
}
/**
* Laravel router pass to fallback not non-exist urls only but wrong-method requests too.
* So required to check if route have alternative request methods
* throw not-found or wrong-method exceptions manually
* @param Request $request
*/
public function universalRoute(Request $request): void
{
/** @var Router $router */
$router = app('router');
/** @var RouteCollection $routes */
$routeCollection = $router->getRoutes();
/** @var string[] $methods */
$methods = array_diff(Router::$verbs, [$request->getMethod(), 'OPTIONS']);
foreach ($methods as $method) {
// Get all routes for method without fallback routes
/** @var Route[]|Collection $routes */
$routes = collect($routeCollection->get($method))->filter(static function ($route) {
/** @var RouteModel $route */
return !$route->isFallback && $route->uri !== '{fallbackPlaceholder}';
});
// Look if any route have match with current request
$mismatch = $routes->first(static function ($value) use ($request) {
/** @var RouteModel $value */
return $value->matches($request, false);
});
// Throw wrong-method exception if matches found
if ($mismatch !== null) {
throw new MethodNotAllowedHttpException([]);
}
}
// No matches, throw not-found exception
throw new NotFoundHttpException();
}
}

View File

@@ -0,0 +1,237 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\Entities\AuthorizationException;
use App\Helpers\Recaptcha;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset as PasswordResetEvent;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use DB;
use Mail;
use Password;
use Validator;
class PasswordResetController extends BaseController
{
public function __construct(private Recaptcha $recaptcha)
{
}
/**
* @api {get} /v1/auth/password/reset/validate Validate
* @apiDescription Validates password reset token
*
* @apiVersion 1.0.0
* @apiName Validate token
* @apiGroup Password Reset
*
* @apiParam {String} email User email
* @apiParam {String} token Password reset token
*
* @apiParamExample {json} Request Example
* {
* "email": "johndoe@example.com",
* "token": "03AOLTBLR5UtIoenazYWjaZ4AFZiv1OWegWV..."
* }
*
* @apiSuccess {String} message Message from server
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "message": "Password reset data is valid"
* }
*
* @apiUse 400Error
* @apiUse ParamsValidationError
* @apiUse InvalidPasswordResetDataError
*/
/**
* @param Request $request
* @return JsonResponse
* @throws AuthorizationException
*/
public function validate(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'token' => 'required|string'
]);
if ($validator->fails()) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_VALIDATION_FAILED);
}
$user = Password::broker()->getUser($request->all());
if (!$user) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_INVALID_PASSWORD_RESET_DATA);
}
$isValidToken = Password::broker()->getRepository()->exists($user, $request->input('token'));
if (!$isValidToken) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_INVALID_PASSWORD_RESET_DATA);
}
return new JsonResponse(['message' => 'Password reset data is valid']);
}
/**
* @api {post} /v1/auth/password/reset/request Request
* @apiDescription Sends email to user with reset link
*
* @apiVersion 1.0.0
* @apiName Request
* @apiGroup Password Reset
*
* @apiParam {String} login User login
* @apiParam {String} [recaptcha] Recaptcha token
*
* @apiParamExample {json} Request Example
* {
* "email": "johndoe@example.com",
* "recaptcha": "03AOLTBLR5UtIoenazYWjaZ4AFZiv1OWegWV..."
* }
*
* @apiSuccess {String} message Message from server
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "message": "Link for restore password has been sent to specified email"
* }
*
* @apiUse 400Error
* @apiUse ParamsValidationError
* @apiUse NoSuchUserError
* @apiUse CaptchaError
* @apiUse LimiterError
*/
/**
* @param Request $request
* @return JsonResponse
* @throws AuthorizationException
*/
public function request(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), ['email' => 'required|email']);
if ($validator->fails()) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_VALIDATION_FAILED);
}
$credentials = $request->only(['email', 'recaptcha']);
$this->recaptcha->check($credentials);
$user = User::where('email', $credentials['email'])->first();
if (!$user) {
$this->recaptcha->incrementCaptchaAmounts();
$this->recaptcha->check($credentials);
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_USER_NOT_FOUND);
}
$this->recaptcha->clearCaptchaAmounts();
Password::broker()->sendResetLink(['email' => $credentials['email']]);
return new JsonResponse([
'message' => 'Link for restore password has been sent to specified email',
]);
}
/**
* @api {post} /v1/auth/password/reset/process Process
* @apiDescription Resets user password
*
* @apiVersion 1.0.0
* @apiName Process
* @apiGroup Password Reset
*
* @apiParam {String} email User email
* @apiParam {String} token Password reset token
* @apiParam {String} password New password
* @apiParam {String} password_confirmation Password confirmation
*
* @apiParamExample {json} Request Example
* {
* "email": "johndoe@example.com",
* "token": "16184cf3b2510464a53c0e573c75740540fe...",
* "password_confirmation": "amazingpassword",
* "password": "amazingpassword"
* }
*
* @apiSuccess {String} access_token Token
* @apiSuccess {String} token_type Token Type
* @apiSuccess {String} expires_in Token TTL in seconds
* @apiSuccess {Object} user User Entity
*
* @apiUse UserObject
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "access_token": "16184cf3b2510464a53c0e573c75740540fe...",
* "token_type": "bearer",
* "password": "amazingpassword",
* "expires_in": "3600",
* "user": {}
* }
*
* @apiUse 400Error
* @apiUse ParamsValidationError
* @apiUse InvalidPasswordResetDataError
* @apiUse UnauthorizedError
*/
/**
* @param Request $request
* @return JsonResponse
* @throws AuthorizationException
*/
public function process(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'token' => 'required|string',
'password' => 'required',
'password_confirmation' => 'required'
]);
if ($validator->fails()) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_VALIDATION_FAILED);
}
$resetRequest = DB::table('password_resets')
->where('email', $request->input('email'))
->first();
if (!$resetRequest) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_INVALID_PASSWORD_RESET_DATA);
}
$response = Password::broker()->reset(
$request->all(),
static function (User $user, string $password) {
$user->password = $password;
$user->save();
event(new PasswordResetEvent($user));
auth()->login($user);
}
);
if ($response !== Password::PASSWORD_RESET) {
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_UNAUTHORIZED);
}
$token = auth()->setTTL(config('auth.lifetime_minutes.jwt'))->refresh();
return new JsonResponse([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => now()->addMinutes(config('auth.lifetime_minutes.jwt'))->toIso8601String(),
'user' => auth()->user(),
]);
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace App\Http\Controllers;
use App\Enums\ScreenshotsState;
use App\Models\Invitation;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Settings;
/**
* Class RegistrationController
* @codeCoverageIgnore until it is implemented on frontend
*/
class RegistrationController extends Controller
{
/**
* @param $key
* @return JsonResponse
* @api {get} /auth/register/{key} Get Form
* @apiDescription Returns invitation form data by a invitation token
*
* @apiVersion 1.0.0
* @apiName GetRegistration
* @apiGroup Invitation
*
* @apiParam (Parameters from url) {String} key User invitation key
*
* @apiSuccess {String} email UserInvited email
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "email": "test@example.com"
* }
*
* @apiErrorExample {json} Email not found
* HTTP/1.1 404 Not found
* {
* "error": "Not found"
* }
*
* @apiUse 400Error
*
*/
public function getForm($key): JsonResponse
{
$invitation = Invitation::where('key', $key)
->where('expires_at', '>=', time())
->first();
if (!isset($invitation)) {
return new JsonResponse([
'message' => __('The specified key has expired or does not exist')
], 404);
}
return responder()->success(['email' => $invitation->email])->respond();
}
/**
* Creates a new user.
*
* @param Request $request
* @param string $key
* @return JsonResponse
* @api {post} /auth/register/{key} Post Form
* @apiDescription Registers user by key
*
* @apiVersion 1.0.0
* @apiName PostRegistration
* @apiGroup Invitation
*
* @apiParam (Parameters from url) {String} key User invitation key
*
* @apiParam {String} email New user email
* @apiParam {String} password New user password
* @apiParam {String} fullName New user name
*
* @apiParamExample {json} Request Example
* {
* "email": "johndoe@example.com",
* "password": "amazingpassword",
* "fullName": "John Doe"
* }
*
* @apiSuccess {Number} user_id New user ID
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "user_id": 2
* }
*
* @apiErrorExample {json} Email not found
* HTTP/1.1 404 Not found
* {
* "message": "The specified key has expired or does not exist"
* }
*
* @apiErrorExample {json} Email mismatch
* HTTP/1.1 400 Bad request
* {
* "message": "The email address does not match the key"
* }
*
* @apiUse 400Error
*
*/
public function postForm(Request $request, string $key): JsonResponse
{
$invitation = Invitation::where('key', $key)
->where('expires_at', '>=', time())
->first();
if (!isset($invitation)) {
return new JsonResponse([
'message' => __('The specified key has expired or does not exist'),
], 404);
}
if ($request->input('email') !== $invitation->email) {
return new JsonResponse([
'message' => __('The email address does not match the key'),
], 400);
}
$language = Settings::scope('core')->get('language', 'en');
/** @var User $user */
$user = User::create([
'full_name' => $request->input('full_name'),
'email' => $request->input('email'),
'password' => $request->input('password'),
'active' => true,
'manual_time' => false,
'screenshots_state' => ScreenshotsState::REQUIRED,
'screenshots_state_locked' => true,
'computer_time_popup' => 3,
'screenshots_interval' => 10,
'role_id' => $invitation->role_id,
'user_language' => $language,
]);
$invitation->delete();
return responder()->success(['user_id' => $user->id])->respond();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\CatHelper;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller;
class StatusController extends Controller
{
/**
* @api {get} /status Status
* @apiDescription Check API status
*
* @apiVersion 4.0.0
* @apiName Status
* @apiGroup Status
*
* @apiSuccess {String} cat A cat for you
* @apiSuccess {Array} modules Information about installed modules
* @apiSuccess {Array} version Information about version modules
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "cattr": true,
* "cat": "(=ㅇ༝ㅇ=)"
* "version": "dev"
* }
*/
/**
* @return JsonResponse
* @throws Exception
*/
public function __invoke(): JsonResponse
{
return responder()->success([
'cattr' => true,
'cat' => CatHelper::getCat(),
'version' => config('app.version'),
])->respond();
}
}

85
app/Http/Kernel.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
namespace App\Http;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\RegisterModulesEvents;
use App\Http\Middleware\SentryContext;
use App\Http\Middleware\TrimStrings;
use Illuminate\Http\Middleware\HandleCors;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Http\Middleware\TrustProxies;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Routing\Middleware\ValidateSignature;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
CheckForMaintenanceMode::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
HandleCors::class,
SentryContext::class,
TrustProxies::class,
RegisterModulesEvents::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
ShareErrorsFromSession::class,
SubstituteBindings::class,
],
'api' => [
SubstituteBindings::class,
EnsureFrontendRequestsAreStateful::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class,
'bindings' => SubstituteBindings::class,
'can' => Authorize::class,
'throttle' => ThrottleRequests::class,
'abilities' => CheckAbilities::class,
'ability' => CheckForAnyAbility::class,
'signed' => ValidateSignature::class,
];
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use App\Exceptions\Entities\AuthorizationException;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as BaseAuthenticate;
use Lang;
class Authenticate extends BaseAuthenticate
{
public const DEFAULT_USER_LANGUAGE = 'en';
public function handle($request, Closure $next, ...$guards): mixed
{
$this->authenticate($request, $guards);
if (!$request->user()->active) {
$request->user()->tokens()->whereId($request->user()->currentAccessToken()->id)->delete();
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_USER_DISABLED);
}
Lang::setLocale($request->user()->user_language ?: self::DEFAULT_USER_LANGUAGE);
return $next($request);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Http\Middleware;
use App;
use App\Events\ChangeEvent;
use App\Filters\AttachmentFilter;
use App\Models\Project;
use App\Models\ProjectGroup;
use App\Models\Task;
use App\Models\TaskHistory;
use App\Models\TimeInterval;
use App\Observers\AttachmentObserver;
use CatEvent;
use Filter;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Nwidart\Modules\Facades\Module;
use Symfony\Component\HttpFoundation\Response;
class RegisterModulesEvents
{
/**
* @param Task|Project|TimeInterval|TaskHistory $model
*/
public static function broadcastEvent(string $entityType, string $action, $model): void
{
foreach (ChangeEvent::getRelatedUserIds($model) as $userId) {
broadcast(new ChangeEvent($entityType, $action, $model, $userId));
}
}
/**
* Add subscribers from modules for Event and Filter
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// TODO:
// [ ] move to Observers folder
// [ ] rewrite with laravel Event so updates that come from modules will trigger update
CatEvent::listen('event.after.action.*', static function (string $eventName, array $data) {
$eventNameParts = explode('.', $eventName);
[$entityType, $action] = array_slice($eventNameParts, 3, 2); // Strip "event.after.action" and get the next two parts
if (!in_array($entityType, ['tasks', 'projects', 'projects_members', 'intervals', 'task_comments', 'project_groups'])) {
return;
}
if (!in_array($action, ['create', 'edit', 'destroy'])) {
return;
}
if ($entityType === 'projects_members') {
$entityType = 'projects';
$projectId = $data[0];
$model = Project::query()->find($projectId);
} elseif ($entityType === 'task_comments') {
$entityType = 'tasks_activities';
$model = $data[0];
} elseif ($entityType === 'project_groups') {
$entityType = 'projects';
$action = 'edit';
/** @var ProjectGroup $group */
$group = $data[0];
$model = $group->projects()->get();
} else {
$model = $data[0];
}
App::terminating(static function () use ($entityType, $action, $model) {
$items = is_array($model) || $model instanceof Collection ? $model : [$model];
foreach ($items as $item) {
static::broadcastEvent($entityType, $action, $item);
if (in_array($entityType, ['tasks', 'projects'])) {
$project = match (true) {
$item instanceof Task => $item->project,
$item instanceof Project => $item,
};
static::broadcastEvent('gantt', 'updateAll', $project);
}
}
});
});
// CatEvent and Filter are scoped to request, subscribe on every request for it to work with laravel octane
Filter::subscribe(AttachmentFilter::class);
CatEvent::subscribe(AttachmentObserver::class);
collect(Module::allEnabled())->each(static function (\Nwidart\Modules\Module $module) {
App::call([preg_grep("/ModuleServiceProvider$/i", $module->get('providers'))[0], 'registerEvents']);
});
return $next($request);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Middleware;
use App\Enums\Role;
use Closure;
use Illuminate\Http\Request;
use Route;
use Sentry\State\Scope;
use Throwable;
use function Sentry\configureScope;
class SentryContext
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
if (env('IMAGE_VERSION')) {
configureScope(static function (Scope $scope): void {
$scope->setTag('docker', env('IMAGE_VERSION'));
});
}
if ($user = $request->user()) {
configureScope(static function (Scope $scope) use ($user): void {
$scope->setUser([
'id' => $user->id,
'email' => config('sentry.send_default_pii') ? $user->email : sha1($user->email),
'role' => is_int($user->role_id) ? Role::tryFrom($user->role_id)?->name :$user->role_id->name,
]);
});
}
configureScope(static function (Scope $scope) use ($request): void {
$scope->setTag('request.host', $request->host());
$scope->setTag('request.method', $request->method());
try {
$scope->setTag('request.route', Route::getRoutes()->match($request)->getName());
} catch (Throwable) {
}
});
return $next($request);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as BaseTrimmer;
class TrimStrings extends BaseTrimmer
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
];
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'/api/*'
];
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Attachment;
use App\Helpers\AttachmentHelper;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
class CreateAttachmentRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return true;
}
public function _rules(): array
{
$maxFileSize = AttachmentHelper::getMaxAllowedFileSize();
return [
'attachment' => "file|required|max:$maxFileSize",
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Attachment;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
class DownloadAttachmentRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('view', request('attachment')->project);
}
public function _rules(): array
{
return [
'seconds' => 'sometimes|int'
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\Auth;
use App\Http\Requests\CattrFormRequest;
class LoginRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return true;
}
public function _rules(): array
{
return [
'email' => 'required',
'password' => 'required',
'recaptcha' => 'sometimes|nullable|string'
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use App\Helpers\FilterDispatcher;
use Filter;
trait AuthorizesAfterValidation
{
/**
* @return bool
*/
public function _authorize(): bool
{
return true;
}
/**
* @param $validator
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
if (! $validator->failed() && ! Filter::process(Filter::getAuthValidationFilterName(), $this->authorizeValidated())) {
$this->failedAuthorization();
}
});
}
/**
* @return mixed
*/
abstract public function authorizeValidated(): mixed;
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Requests;
use App\Exceptions\Entities\AuthorizationException;
use Filter;
use Illuminate\Foundation\Http\FormRequest;
abstract class CattrFormRequest extends FormRequest
{
/**
* Handle a failed validation attempt.
*
* @throws AuthorizationException
*/
protected function failedAuthorization(): void
{
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_FORBIDDEN);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return Filter::process(Filter::getValidationFilterName(), $this->_rules());
}
/**
* Determine if user authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return Filter::process(Filter::getAuthFilterName(), $this->_authorize());
}
/**
* Determine if user authorized to make this request.
*
* @return bool
*/
abstract protected function _authorize(): bool;
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
abstract protected function _rules(): array;
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\CompanySettings;
use App\Enums\Role;
use App\Enums\ScreenshotsState;
use App\Http\Requests\CattrFormRequest;
use Illuminate\Validation\Rules\Enum;
class UpdateCompanySettingsRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->hasRole(Role::ADMIN);
}
public function _rules(): array
{
return [
'timezone' => 'sometimes|required|timezone',
'work_time' => 'sometimes|int',
'auto_thinning' => 'sometimes|boolean',
'screenshots_state' => ['sometimes', 'required', new Enum(ScreenshotsState::class)],
'language' => 'sometimes|string',
'default_priority_id' => 'sometimes|int',
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Installation;
use App\Http\Requests\CattrFormRequest;
class CheckDatabaseInfoRequest extends CattrFormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function _rules(): array
{
return [
'db_host' => 'required|string',
'database' => 'required|string',
'db_user' => 'required|string',
'db_password' => 'required|string',
];
}
/**
* @inheritDoc
*/
protected function _authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\Installation;
use App\Http\Requests\CattrFormRequest;
class SaveSetupRequest extends CattrFormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function _rules(): array
{
return [
'db_host' => 'sometimes|required|string',
'database' => 'sometimes|required|string',
'db_user' => 'sometimes|required|string',
'db_password' => 'sometimes|required|string',
'captcha_enabled' => 'required|boolean',
'email' => 'required|email',
'password' => 'required|string',
'timezone' => 'required|string',
'language' => 'required|string',
'secret_key' => 'nullable|string',
'site_key' => 'nullable|string',
'origin' => 'required|string'
];
}
/**
* @inheritDoc
*/
protected function _authorize(): bool
{
return true;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
class BulkDestroyTimeIntervalRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('bulkDestroy', [TimeInterval::class, request('intervals')]);
}
public function _rules(): array
{
return [
'intervals' => 'required|array',
'intervals.*' => 'int|exists:time_intervals,id'
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
class BulkEditTimeIntervalRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
$timeIntervalIds = [];
foreach (request('intervals') as $interval) {
$timeIntervalIds[] = $interval['id'];
}
return $this->user()->can('bulkUpdate', [TimeInterval::class, $timeIntervalIds]);
}
public function _rules(): array
{
return [
'intervals' => 'required|array',
'intervals.*.id' => 'required|int|exists:time_intervals,id',
'intervals.*.task_id' => 'required|int|exists:tasks,id'
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Requests\Interval;
use AllowDynamicProperties;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
use App\Models\User;
use App\Rules\TimeIntervalDoesNotExist;
use Carbon\Carbon;
use Settings;
#[AllowDynamicProperties] class CreateTimeIntervalRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can(
'create',
[
TimeInterval::class,
$this->get('user_id'),
$this->get('task_id'),
$this->get('is_manual', false),
],
);
}
public function _rules(): array
{
$timezone = Settings::scope('core')->get('timezone', 'UTC');
return [
'task_id' => 'required|exists:tasks,id',
'user_id' => 'required|exists:users,id',
'start_at' => 'required|date|bail|before:end_at',
'end_at' => [
'required',
'date',
'bail',
'after:start_at',
new TimeIntervalDoesNotExist(
User::find($this->user_id),
Carbon::parse($this->start_at)->setTimezone($timezone),
Carbon::parse($this->end_at)->setTimezone($timezone),
),
],
'activity_fill' => 'nullable|int|between:0,100',
'mouse_fill' => 'nullable|int|between:0,100',
'keyboard_fill' => 'nullable|int|between:0,100',
'is_manual' => 'sometimes|bool',
'location' => 'sometimes|array',
'screenshot' => 'sometimes|required|image',
];
}
public function getRules($user_id, $start_at, $end_at): array
{
$this->user_id = $user_id;
$this->start_at = $start_at;
$this->end_at = $end_at;
return $this->_rules();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
use App\Exceptions\Entities\IntervalAlreadyDeletedException;
class DestroyTimeIntervalRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('destroy', TimeInterval::find(request('id')));
}
protected function failedAuthorization(): void
{
throw new IntervalAlreadyDeletedException;
}
public function _rules(): array
{
return [
'id' => 'required|int|exists:time_intervals,id',
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
class EditTimeIntervalRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('update', TimeInterval::find(request('id')));
}
public function _rules(): array
{
return [
'id' => 'required|int|exists:time_intervals,id',
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
class IntervalTasksRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('viewAny', TimeInterval::class);
}
public function _rules(): array
{
return [
'start_at' => 'date',
'end_at' => 'date',
'project_id' => 'exists:projects,id',
'task_id' => 'exists:tasks,id',
'user_id' => 'required|integer|exists:users,id',
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
class IntervalTotalRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('viewAny', TimeInterval::class);
}
public function _rules(): array
{
return [
'start_at' => 'required|date',
'end_at' => 'required|date',
'user_id' => 'required|integer|exists:users,id',
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Interval;
use App\Helpers\QueryHelper;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\TimeInterval;
use App\Http\Requests\CattrFormRequest;
class ListIntervalRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('viewAny', TimeInterval::class);
}
public function _rules(): array
{
return QueryHelper::getValidationRules();
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
class PutScreenshotRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return true;
}
public function _rules(): array
{
return [
'screenshot' => 'required|image',
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
class ScreenshotRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('view', request('interval'));
}
public function _rules(): array
{
return [];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\TimeInterval;
class ShowIntervalRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('view', TimeInterval::find(request('id')));
}
public function _rules(): array
{
return [
'id' => 'required|int|exists:time_intervals,id',
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
class TrackAppRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'title' => 'nullable|string',
'executable' => 'required|string',
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\Rules\File;
use Str;
class UploadOfflineIntervalsRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'file' => [
'required',
File::types('application/zip')->max(12 * 1024),
function ($_, UploadedFile $file, $fail) {
$fileName = $file->getClientOriginalName();
if (Str::endsWith($fileName, '.cattr') === false) {
$fail('validation.offline-sync.wrong_extension')->translate();
}
}
],
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Interval;
use App\Http\Requests\CattrFormRequest;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\Rules\File;
use Str;
class UploadOfflineScreenshotsRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'file' => [
'required',
File::types('application/zip'),
function ($_, UploadedFile $file, $fail) {
$fileName = $file->getClientOriginalName();
if (Str::endsWith($fileName, '.cattr') === false) {
$fail('validation.offline-sync.wrong_extension')->translate();
}
}
],
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\Invitation;
use App\Enums\Role;
use App\Http\Requests\CattrFormRequest;
use App\Models\Invitation;
use Illuminate\Validation\Rules\Enum;
class CreateInvitationRequest extends CattrFormRequest
{
/**
* Determine if user authorized to make this request.
*
* @return bool
*/
public function _authorize(): bool
{
return $this->user()->can('create', Invitation::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function _rules(): array
{
return [
'users' => 'required|array',
'users.*.email' => 'required|email|unique:users,email|unique:invitations,email',
'users.*.role_id' => ['required', new Enum(Role::class)],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'users.*.email' => 'Email'
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Invitation;
use App\Http\Requests\CattrFormRequest;
use App\Models\Invitation;
class DestroyInvitationRequest extends CattrFormRequest
{
/**
* Determine if user authorized to make this request.
*
* @return bool
*/
public function _authorize(): bool
{
return $this->user()->can('destroy', Invitation::find(request('id')));
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function _rules(): array
{
return ['id' => 'required|int|exists:invitations,id'];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Invitation;
use App\Helpers\QueryHelper;
use App\Http\Requests\CattrFormRequest;
use App\Models\Invitation;
class ListInvitationRequest extends CattrFormRequest
{
/**
* Determine if user authorized to make this request.
*
* @return bool
*/
public function _authorize(): bool
{
return $this->user()->can('viewAny', Invitation::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function _rules(): array
{
return QueryHelper::getValidationRules();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Invitation;
use App\Http\Requests\CattrFormRequest;
use App\Models\Invitation;
class ShowInvitationRequest extends CattrFormRequest
{
/**
* Determine if user authorized to make this request.
*
* @return bool
*/
public function _authorize(): bool
{
return $this->user()->can('view', Invitation::find(request('id')));
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function _rules(): array
{
return [
'id' => 'required|integer|exists:invitations,id'
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Invitation;
use App\Http\Requests\CattrFormRequest;
use App\Models\Invitation;
class UpdateInvitationRequest extends CattrFormRequest
{
/**
* Determine if user authorized to make this request.
*
* @return bool
*/
public function _authorize(): bool
{
return $this->user()->can('update', Invitation::find(request('id')));
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function _rules(): array
{
return [
'id' => 'required|integer|exists:invitations,id'
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Priority;
use App\Http\Requests\CattrFormRequest;
use App\Models\Priority;
class CreatePriorityRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('create', Priority::class);
}
public function _rules(): array
{
return [
'name' => 'required|string',
'color' => 'sometimes|nullable|string|regex:/^#[a-f0-9]{6}$/i',
];
}
public function attributes(): array
{
return [
'users.*.email' => 'Email'
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Priority;
use App\Http\Requests\CattrFormRequest;
use App\Models\Priority;
class DestroyPriorityRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('destroy', Priority::find(request('id')));
}
public function _rules(): array
{
return [
'id' => 'required|integer|exists:priorities,id',
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Priority;
use App\Helpers\QueryHelper;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Priority;
use App\Http\Requests\CattrFormRequest;
class ListPriorityRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('viewAny', Priority::class);
}
public function _rules(): array
{
return QueryHelper::getValidationRules();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Priority;
use App\Http\Requests\CattrFormRequest;
use App\Models\Priority;
class ShowPriorityRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('view', Priority::find(request('id')));
}
public function _rules(): array
{
return [
'id' => 'required|integer|exists:priorities,id',
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Priority;
use App\Http\Requests\CattrFormRequest;
use App\Models\Priority;
use App\Models\User;
class UpdatePriorityRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('update', Priority::find(request('id')));
}
public function _rules(): array
{
return [
'id' => 'required|integer|exists:priorities,id',
'name' => 'required|string',
'color' => 'sometimes|nullable|string|regex:/^#[a-f0-9]{6}$/i',
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Project;
use App\Enums\ScreenshotsState;
use App\Models\Project;
use App\Http\Requests\CattrFormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
class CreateProjectRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('create', Project::class);
}
public function _rules(): array
{
return [
'name' => 'required|string',
'description' => 'required|string',
'important' => 'sometimes|required|bool',
'default_priority_id' => 'sometimes|integer|exists:priorities,id',
'screenshots_state' => ['required', new Enum(ScreenshotsState::class)],
'statuses' => 'sometimes|array',
'statuses.*.id' => 'required|exists:statuses,id',
'statuses.*.color' => 'sometimes|nullable|string|regex:/^#[a-f0-9]{6}$/i',
'phases' => 'sometimes|array',
'phases.*.name' => 'required|string|min:1|max:255',
'group' => Rule::when(!is_array($this->input('group')), 'sometimes|nullable|integer|exists:project_groups,id'),
'group.id' => Rule::when(is_array($this->input('group')), 'sometimes|nullable|integer|exists:project_groups,id'),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\Project;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Project;
use App\Http\Requests\CattrFormRequest;
class DestroyProjectRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('destroy', Project::find(request('id')));
}
public function _rules(): array
{
return ['id' => 'required|int|exists:projects,id'];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\Project;
use App\Enums\ScreenshotsState;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\Project;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
class EditProjectRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('update', Project::find(request('id')));
}
public function _rules(): array
{
return [
'id' => 'required|int|exists:projects,id',
'name' => 'sometimes|required|string',
'description' => 'sometimes|required|string',
'default_priority_id' => 'sometimes|integer|exists:priorities,id',
'screenshots_state' => ['sometimes', 'required', new Enum(ScreenshotsState::class)],
'statuses' => 'sometimes|array',
'statuses.*.id' => 'required|exists:statuses,id',
'statuses.*.color' => 'sometimes|nullable|string|regex:/^#[a-f0-9]{6}$/i',
'phases' => 'sometimes|array',
'phases.*.id' => 'sometimes|required|exists:project_phases,id',
'phases.*.name' => 'required|string|min:1|max:255',
'group' => Rule::when(!is_array($this->input('group')), 'sometimes|nullable|integer|exists:project_groups,id'),
'group.id' => Rule::when(is_array($this->input('group')), 'sometimes|nullable|integer|exists:project_groups,id'),
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Project;
use App\Helpers\QueryHelper;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Project;
use App\Http\Requests\CattrFormRequest;
class GanttDataRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('viewAny', Project::class);
}
public function _rules(): array
{
return [
'id' => 'required|int|exists:projects,id'
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Project;
use App\Helpers\QueryHelper;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Project;
use App\Http\Requests\CattrFormRequest;
class ListProjectRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('viewAny', Project::class);
}
public function _rules(): array
{
return QueryHelper::getValidationRules();
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Project;
use App\Helpers\QueryHelper;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Project;
use App\Http\Requests\CattrFormRequest;
class PhasesRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('viewAny', Project::class);
}
public function _rules(): array
{
return [
'id' => 'required|int|exists:projects,id'
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Project;
use App\Helpers\QueryHelper;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Project;
use App\Http\Requests\CattrFormRequest;
class ShowProjectRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('view', Project::find(request('id')));
}
public function _rules(): array
{
return array_merge(QueryHelper::getValidationRules(), [
'id' => 'required|int|exists:projects,id'
]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\ProjectGroup;
use App\Http\Requests\CattrFormRequest;
use App\Models\ProjectGroup;
class CreateProjectGroupRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('create', ProjectGroup::class);
}
public function _rules(): array
{
return [
'name' => 'required|string',
'parent_id' => 'nullable|sometimes|integer|exists:project_groups,id',
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Requests\ProjectGroup;
use App\Http\Requests\CattrFormRequest;
use App\Models\ProjectGroup;
class DestroyProjectGroupRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('destroy', ProjectGroup::class);
}
public function _rules(): array
{
return ['id' => 'required|int|exists:project_groups,id'];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\ProjectGroup;
use App\Http\Requests\CattrFormRequest;
use App\Models\ProjectGroup;
class EditProjectGroupRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('update', ProjectGroup::class);
}
public function _rules(): array
{
return [
'id' => 'required|int|exists:project_groups,id',
'name' => 'sometimes|required|string',
'parent_id' => 'nullable|sometimes|integer|exists:project_groups,id',
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\ProjectGroup;
use App\Helpers\QueryHelper;
use App\Http\Requests\CattrFormRequest;
use App\Models\ProjectGroup;
class ListProjectGroupRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('viewAny', ProjectGroup::class);
}
public function _rules(): array
{
return QueryHelper::getValidationRules();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\ProjectGroup;
use App\Helpers\QueryHelper;
use App\Http\Requests\CattrFormRequest;
use App\Models\ProjectGroup;
class ShowProjectGroupRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('view', ProjectGroup::class);
}
public function _rules(): array
{
return array_merge(
QueryHelper::getValidationRules(),
['id' => 'required|int|exists:project_groups,id'],
);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\ProjectMember;
use App\Enums\Role;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\Project;
use Illuminate\Validation\Rules\Enum;
class BulkEditProjectMemberRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('updateMembers', Project::find(request('project_id')));
}
public function _rules(): array
{
return [
'project_id' => 'required|int|exists:projects,id',
'user_roles' => 'present|array',
'user_roles.*.user_id' => 'required|distinct|int|exists:users,id',
'user_roles.*.role_id' => ['required', new Enum(Role::class)],
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\ProjectMember;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use App\Models\Project;
class ShowProjectMemberRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('updateMembers', Project::find(request('project_id')));
}
public function _rules(): array
{
return ['project_id' => 'required|int|exists:projects,id'];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Reports;
use App\Enums\DashboardSortBy;
use App\Enums\SortDirection;
use App\Http\Requests\CattrFormRequest;
use Filter;
use Illuminate\Validation\Rules\Enum;
class DashboardRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'users' => 'nullable|exists:users,id|array',
'projects' => 'nullable|exists:projects,id|array',
'start_at' => 'required|date',
'end_at' => 'required|date',
'user_timezone' => 'required|timezone',
'sort_column' => ['nullable', new Enum(DashboardSortBy::class)],
'sort_direction' => ['nullable', new Enum(SortDirection::class)],
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Reports;
use App\Http\Requests\CattrFormRequest;
class PlannedTimeReportRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'projects' => 'nullable|exists:projects,id|array',
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Reports;
use App\Http\Requests\CattrFormRequest;
use Filter;
class ProjectReportRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'users' => 'nullable|exists:users,id|array',
'projects' => 'nullable|exists:projects,id|array',
'start_at' => 'required|date',
'end_at' => 'required|date',
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Reports;
use App\Http\Requests\CattrFormRequest;
use Filter;
class TimeUseReportRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'users' => 'nullable|exists:users,id|array',
'start_at' => 'required|date',
'end_at' => 'required|date',
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Reports\UniversalReport;
use App\Http\Requests\CattrFormRequest;
class UniversalReportDestroyRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'id' => 'required|int|exists:universal_reports,id',
];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Requests\Reports\UniversalReport;
use App\Enums\UniversalReportType;
use App\Enums\UniversalReportBase;
use App\Exceptions\Entities\InvalidMainException;
use App\Http\Requests\CattrFormRequest;
use Exception;
use Illuminate\Validation\Rules\Enum;
class UniversalReportEditRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
$enumCase = UniversalReportBase::tryFrom(request('base'));
switch ($enumCase) {
case UniversalReportBase::PROJECT:
$table = 'projects';
break;
case UniversalReportBase::USER:
$table = 'users';
break;
case UniversalReportBase::TASK:
$table = 'tasks';
break;
default:
return throw new InvalidMainException();
}
return [
'id' => 'required|int|exists:universal_reports,id',
'name' => 'required|string',
'base' => ['required', new Enum(UniversalReportBase::class)],
'fields' => 'required|array',
'fields.*' => 'array',
'fields.*.*' => 'required|string|in:'.implode(',', array_map(fn($item) => implode(',', $item), $enumCase->fields())),
'dataObjects' => 'required|array',
'dataObjects.*' => "required|int|exists:$table,id",
'charts' => 'nullable|array',
'charts.*' => 'required|string|in:'.implode(',', array_map(fn($item) => $item, $enumCase->charts())),
'type' => ['required', 'string', new Enum(UniversalReportType::class)],
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Reports\UniversalReport;
use App\Enums\UniversalReportBase;
use App\Http\Requests\CattrFormRequest;
use Exception;
use Illuminate\Validation\Rules\Enum;
class UniversalReportRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'id' => 'required|exists:universal_reports,id|int',
'start_at' => 'nullable|date',
'end_at' => 'nullable|date',
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Reports\UniversalReport;
use App\Enums\UniversalReportBase;
use App\Http\Requests\CattrFormRequest;
use Exception;
use Illuminate\Validation\Rules\Enum;
class UniversalReportShowRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
return [
'id' => 'required|exists:universal_reports,id|int'
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests\Reports\UniversalReport;
use App\Enums\UniversalReportType;
use App\Enums\UniversalReportBase;
use App\Exceptions\Entities\InvalidMainException;
use App\Http\Requests\CattrFormRequest;
use Exception;
use Illuminate\Validation\Rules\Enum;
class UniversalReportStoreRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return auth()->check();
}
public function _rules(): array
{
$enumCase = UniversalReportBase::tryFrom(request('base'));
switch ($enumCase) {
case UniversalReportBase::PROJECT:
$table = 'projects';
break;
case UniversalReportBase::USER:
$table = 'users';
break;
case UniversalReportBase::TASK:
$table = 'tasks';
break;
default:
return throw new InvalidMainException();
}
return [
'name' => 'required|string',
'base' => ['required', new Enum(UniversalReportBase::class)],
'fields' => 'required|array',
'fields.*' => 'array',
'fields.*.*' => 'required|string|in:'.implode(',', array_map(fn($item) => implode(',', $item), $enumCase->fields())),
'dataObjects' => 'required|array',
'dataObjects.*' => "required|int|exists:$table,id",
'charts' => 'nullable|array',
'charts.*' => 'required|string|in:'.implode(',', array_map(fn($item) => $item, $enumCase->charts())),
'type' => ['required', 'string', new Enum(UniversalReportType::class)]
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Status;
use App\Http\Requests\CattrFormRequest;
use App\Models\Status;
use App\Models\User;
use Illuminate\Validation\Rule;
class CreateStatusRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('create', Status::class);
}
public function _rules(): array
{
return [
'name' => 'required|string',
'order' => [
'sometimes',
'integer',
],
'active' => 'sometimes|boolean',
'color' => 'sometimes|nullable|string|regex:/^#[a-f0-9]{6}$/i',
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\Status;
use App\Http\Requests\CattrFormRequest;
use App\Models\Status;
use App\Models\User;
class DestroyStatusRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('destroy', Status::class);
}
public function _rules(): array
{
return [
'id' => 'required|integer|exists:statuses,id',
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Status;
use App\Helpers\QueryHelper;
use App\Http\Requests\CattrFormRequest;
use App\Models\Status;
class ListStatusRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('viewAny', Status::class);
}
public function _rules(): array
{
return QueryHelper::getValidationRules();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Status;
use App\Http\Requests\CattrFormRequest;
use App\Models\Status;
class ShowStatusRequestStatus extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('view', Status::class);
}
public function _rules(): array
{
return [
'id' => 'required|integer|exists:statuses,id',
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Status;
use App\Http\Requests\CattrFormRequest;
use App\Models\Status;
use App\Models\User;
use Illuminate\Validation\Rule;
class UpdateStatusRequest extends CattrFormRequest
{
public function _authorize(): bool
{
return $this->user()->can('update', Status::class);
}
public function _rules(): array
{
return [
'id' => 'required|integer|exists:statuses,id',
'name' => 'required|string',
'order' => ['sometimes', 'integer', Rule::exists('statuses', 'order')],
'active' => 'sometimes|boolean',
'color' => 'sometimes|nullable|string|regex:/^#[a-f0-9]{6}$/i',
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Task;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Http\Requests\CattrFormRequest;
use Illuminate\Validation\Rule;
class CalendarRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return true;
}
public function _rules(): array
{
return [
'project_id' => ['sometimes', Rule::when(!is_array($this->input('project_id')), 'nullable|integer|exists:projects,id')],
'project_id.*' => [Rule::when(is_array($this->input('project_id')), 'integer|exists:projects,id')],
'start_at' => 'required|date|before_or_equal:end_at',
'end_at' => 'required|date|after_or_equal:start_at',
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\Task;
use App\Enums\TaskRelationType;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Task;
use App\Http\Requests\CattrFormRequest;
use Illuminate\Validation\Rule;
class CreateRelationRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('update', Task::find(request('task_id')))
&& $this->user()->can('update', Task::find(request('related_task_id')));
}
public function _rules(): array
{
return [
'task_id' => [
'required',
'int',
'different:related_task_id',
Rule::exists('tasks', 'id'),
],
'related_task_id' => [
'required',
'int',
'different:task_id',
Rule::exists('tasks', 'id'),
],
'relation_type' => ['required', Rule::enum(TaskRelationType::class)],
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Requests\Task;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Task;
use App\Http\Requests\CattrFormRequest;
use Illuminate\Validation\Rule;
class CreateTaskRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('create', [Task::class, $this->get('project_id')]);
}
public function _rules(): array
{
return [
'project_id' => 'required|exists:projects,id',
'project_phase_id' => [
'sometimes',
'nullable',
Rule::exists('project_phases', 'id')
->where('project_id', $this->input('project_id')),
],
'task_name' => 'required|string',
'description' => 'string',
'users' => 'sometimes|array',
'users.*' => 'exists:users,id',
'active' => 'bool',
'important' => 'bool',
'priority_id' => 'sometimes|nullable|exists:priorities,id',
'status_id' => 'sometimes|required|exists:statuses,id',
'relative_position' => 'sometimes|required|integer',
'start_date' => [
'sometimes',
'nullable',
'date',
Rule::when($this->input('due_date'), 'before_or_equal:due_date')
],
'due_date' => [
'sometimes',
'nullable',
'date',
Rule::when($this->input('start_date'), 'after_or_equal:start_date')
],
'estimate' => 'sometimes|nullable|integer|gte:0',
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Task;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Task;
use App\Http\Requests\CattrFormRequest;
use Illuminate\Validation\Rule;
class DestroyRelationRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('update', Task::find(request('parent_id')))
&& $this->user()->can('update', Task::find(request('child_id')));
}
public function _rules(): array
{
return [
'parent_id' => [
'required',
'int',
Rule::exists('tasks', 'id'),
],
'child_id' => [
'required',
'int',
Rule::exists('tasks', 'id'),
],
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Task;
use App\Http\Requests\AuthorizesAfterValidation;
use App\Models\Task;
use App\Http\Requests\CattrFormRequest;
class DestroyTaskRequest extends CattrFormRequest
{
use AuthorizesAfterValidation;
public function authorizeValidated(): bool
{
return $this->user()->can('destroy', Task::find(request('id')));
}
public function _rules(): array
{
return [
'id' => 'required|int'
];
}
}

Some files were not shown because too many files have changed in this diff Show More