mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-08 13:53:17 +01:00
Compare commits
1 Commits
fix/api-do
...
47327aa52b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47327aa52b |
1
.env.ci
1
.env.ci
@@ -6,7 +6,6 @@ APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
APP_FORCE_HTTPS=false
|
||||
APP_ENABLE_REGISTRATION=true
|
||||
SUPER_ADMINS=admin@example.com
|
||||
|
||||
# Logging
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
23
.github/workflows/build-onpremise.yml
vendored
23
.github/workflows/build-onpremise.yml
vendored
@@ -103,8 +103,6 @@ jobs:
|
||||
{
|
||||
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
||||
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
||||
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
|
||||
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Checkout invoicing extension"
|
||||
@@ -121,26 +119,9 @@ jobs:
|
||||
- name: "Install npm dependencies in invoicing extension"
|
||||
run: cd extensions/Invoicing && npm ci
|
||||
|
||||
- name: "Checkout auditing extension"
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
|
||||
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
|
||||
path: extensions/Auditing
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
|
||||
|
||||
- name: "Install composer dependencies in auditing extension"
|
||||
run: cd extensions/Auditing && composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
||||
|
||||
- name: "Install npm dependencies in auditing extension"
|
||||
run: cd extensions/Auditing && npm ci
|
||||
|
||||
- name: "Activate invoicing extension"
|
||||
run: php artisan module:enable Invoicing
|
||||
|
||||
- name: "Activate auditing extension"
|
||||
run: php artisan module:enable Auditing
|
||||
|
||||
- name: "Install npm dependencies"
|
||||
run: npm ci
|
||||
|
||||
@@ -160,7 +141,7 @@ jobs:
|
||||
${{ env.DOCKER_REPO }}
|
||||
|
||||
- name: "Login to solidtime OnPremise Registry"
|
||||
uses: docker/login-action@v4.6.0
|
||||
uses: docker/login-action@v4.5.2
|
||||
with:
|
||||
registry: registry.on-premise.solidtime.io
|
||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||
@@ -214,7 +195,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: "Login to solidtime OnPremise Registry"
|
||||
uses: docker/login-action@v4.6.0
|
||||
uses: docker/login-action@v4.5.2
|
||||
with:
|
||||
registry: registry.on-premise.solidtime.io
|
||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||
|
||||
28
.github/workflows/build-private.yml
vendored
28
.github/workflows/build-private.yml
vendored
@@ -84,8 +84,6 @@ jobs:
|
||||
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
|
||||
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
||||
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
||||
echo "auditing_repository=$(jq -r '.Auditing.repository' extensions/manifest.json)"
|
||||
echo "auditing_ref=$(jq -r '.Auditing.ref' extensions/manifest.json)"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Checkout billing extension"
|
||||
@@ -99,7 +97,7 @@ jobs:
|
||||
- name: "Install dependencies in billing extension"
|
||||
uses: php-actions/composer@v6
|
||||
env:
|
||||
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "${{ secrets.LARAVEL_SPARK_USERNAME }}", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
||||
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "gregor@vostrak.at", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
||||
with:
|
||||
working_dir: "extensions/Billing"
|
||||
command: install
|
||||
@@ -147,25 +145,6 @@ jobs:
|
||||
- name: "Install npm dependencies in invoicing extension"
|
||||
run: cd extensions/Invoicing && npm ci
|
||||
|
||||
- name: "Checkout auditing extension"
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ steps.extension-manifest.outputs.auditing_repository }}
|
||||
ref: ${{ steps.extension-manifest.outputs.auditing_ref }}
|
||||
path: extensions/Auditing
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_AUDITING_EXTENSION }}
|
||||
|
||||
- name: "Install composer dependencies in auditing extension"
|
||||
uses: php-actions/composer@v6
|
||||
with:
|
||||
working_dir: "extensions/Auditing"
|
||||
command: install
|
||||
only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative
|
||||
php_version: 8.3
|
||||
|
||||
- name: "Install npm dependencies in auditing extension"
|
||||
run: cd extensions/Auditing && npm ci
|
||||
|
||||
- name: "Setup PHP with PECL extension"
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
@@ -189,9 +168,6 @@ jobs:
|
||||
- name: "Activate invoicing extension"
|
||||
run: php artisan module:enable Invoicing
|
||||
|
||||
- name: "Activate auditing extension"
|
||||
run: php artisan module:enable Auditing
|
||||
|
||||
- name: "Install npm dependencies"
|
||||
run: npm ci
|
||||
|
||||
@@ -201,7 +177,7 @@ jobs:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: "Login to GitHub Container Registry"
|
||||
uses: docker/login-action@v4.6.0
|
||||
uses: docker/login-action@v4.5.2
|
||||
with:
|
||||
registry: rg.fr-par.scw.cloud/solidtime
|
||||
username: nologin
|
||||
|
||||
8
.github/workflows/build-public.yml
vendored
8
.github/workflows/build-public.yml
vendored
@@ -117,13 +117,13 @@ jobs:
|
||||
${{ env.GHCR_REPO }}
|
||||
|
||||
- name: "Login to Docker Hub Container Registry"
|
||||
uses: docker/login-action@v4.6.0
|
||||
uses: docker/login-action@v4.5.2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: "Login to GitHub Container Registry"
|
||||
uses: docker/login-action@v4.6.0
|
||||
uses: docker/login-action@v4.5.2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -177,13 +177,13 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: "Login to Docker Hub"
|
||||
uses: docker/login-action@v4.6.0
|
||||
uses: docker/login-action@v4.5.2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: "Login to GHCR"
|
||||
uses: docker/login-action@v4.6.0
|
||||
uses: docker/login-action@v4.5.2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
31
.github/workflows/extension-manifest-check.yml
vendored
31
.github/workflows/extension-manifest-check.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Extension Manifest Check
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- extensions/manifest.json
|
||||
- .github/workflows/extension-manifest-check.yml
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
check-refs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Extension refs must be pinned to a release tag (vX.Y.Z) or a full commit
|
||||
# SHA. Branch names (main, feature/foo, ...) are only acceptable while
|
||||
# developing and must be replaced before the pull request is merged.
|
||||
- name: "Check extension refs are pinned"
|
||||
run: |
|
||||
failed=0
|
||||
while IFS=$'\t' read -r name ref; do
|
||||
if [[ "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.]+)?$ ]] || [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "✅ $name: $ref"
|
||||
else
|
||||
echo "::error file=extensions/manifest.json::Extension \"$name\" uses ref \"$ref\", which is not a release tag or commit SHA. Pin it before merging."
|
||||
failed=1
|
||||
fi
|
||||
done < <(jq -r 'to_entries[] | [.key, .value.ref] | @tsv' extensions/manifest.json)
|
||||
exit $failed
|
||||
2
.github/workflows/npm-format-check.yml
vendored
2
.github/workflows/npm-format-check.yml
vendored
@@ -1,8 +1,6 @@
|
||||
name: NPM Format Check
|
||||
|
||||
on: [push]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
format-check:
|
||||
|
||||
136
.github/workflows/phpunit-extensions.yml
vendored
136
.github/workflows/phpunit-extensions.yml
vendored
@@ -1,136 +0,0 @@
|
||||
name: PHPUnit Tests - Extensions
|
||||
on: push
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
phpunit-extensions:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
matrix:
|
||||
postgres_version: [ 15, 16, 17 ]
|
||||
|
||||
services:
|
||||
pgsql_test:
|
||||
image: postgres:${{ matrix.postgres_version }}
|
||||
env:
|
||||
PGPASSWORD: 'root'
|
||||
POSTGRES_DB: 'laravel'
|
||||
POSTGRES_USER: 'root'
|
||||
POSTGRES_PASSWORD: 'root'
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
ports:
|
||||
- 3000:3000
|
||||
options: >-
|
||||
--health-cmd "curl --silent --fail http://localhost:3000/health"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: "Setup PHP"
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
||||
coverage: pcov
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: "Read extension manifest"
|
||||
id: extension-manifest
|
||||
run: |
|
||||
{
|
||||
echo "billing_repository=$(jq -r '.Billing.repository' extensions/manifest.json)"
|
||||
echo "billing_ref=$(jq -r '.Billing.ref' extensions/manifest.json)"
|
||||
echo "services_repository=$(jq -r '.Services.repository' extensions/manifest.json)"
|
||||
echo "services_ref=$(jq -r '.Services.ref' extensions/manifest.json)"
|
||||
echo "invoicing_repository=$(jq -r '.Invoicing.repository' extensions/manifest.json)"
|
||||
echo "invoicing_ref=$(jq -r '.Invoicing.ref' extensions/manifest.json)"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Checkout billing extension"
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ steps.extension-manifest.outputs.billing_repository }}
|
||||
ref: ${{ steps.extension-manifest.outputs.billing_ref }}
|
||||
path: extensions/Billing
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
|
||||
|
||||
- name: "Install composer dependencies in billing extension"
|
||||
working-directory: extensions/Billing
|
||||
env:
|
||||
COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "contact@solidtime.io", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}'
|
||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
||||
|
||||
- name: "Install npm dependencies in billing extension"
|
||||
run: cd extensions/Billing && npm ci
|
||||
|
||||
- name: "Checkout services extension"
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ steps.extension-manifest.outputs.services_repository }}
|
||||
ref: ${{ steps.extension-manifest.outputs.services_ref }}
|
||||
path: extensions/Services
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
|
||||
|
||||
- name: "Install composer dependencies in services extension"
|
||||
working-directory: extensions/Services
|
||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
||||
|
||||
- name: "Install npm dependencies in services extension"
|
||||
run: cd extensions/Services && npm ci
|
||||
|
||||
- name: "Checkout invoicing extension"
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
|
||||
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
|
||||
path: extensions/Invoicing
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
||||
|
||||
- name: "Install composer dependencies in invoicing extension"
|
||||
working-directory: extensions/Invoicing
|
||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
||||
|
||||
- name: "Install npm dependencies in invoicing extension"
|
||||
run: cd extensions/Invoicing && npm ci
|
||||
|
||||
- name: "Run composer install"
|
||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
||||
|
||||
- name: "Activate billing extension"
|
||||
run: php artisan module:enable Billing
|
||||
|
||||
- name: "Activate services extension"
|
||||
run: php artisan module:enable Services
|
||||
|
||||
- name: "Activate invoicing extension"
|
||||
run: php artisan module:enable Invoicing
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: npm ci
|
||||
|
||||
- name: "Build Frontend"
|
||||
run: npm run build
|
||||
|
||||
- name: "Prepare Laravel Application"
|
||||
run: |
|
||||
cp .env.ci .env
|
||||
php artisan key:generate
|
||||
php artisan passport:keys
|
||||
|
||||
- name: "Run PHPUnit"
|
||||
run: php artisan test extensions/Billing/tests extensions/Services/tests extensions/Invoicing/tests --stop-on-failure
|
||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
||||
|
||||
- name: "Upload coverage reports to Codecov"
|
||||
uses: codecov/codecov-action@v7.1.1
|
||||
uses: codecov/codecov-action@v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: solidtime-io/solidtime
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,6 @@ dist
|
||||
/public/storage
|
||||
/public/css
|
||||
/public/js
|
||||
/public/fonts/filament
|
||||
/public/vendor
|
||||
/lang/vendor
|
||||
/storage/*.key
|
||||
@@ -44,5 +43,4 @@ yarn-error.log
|
||||
/data
|
||||
/config/caddy
|
||||
/config/composer
|
||||
/config/psysh
|
||||
/AGENTS.md
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Extensions\Scramble;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
|
||||
use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
|
||||
use Dedoc\Scramble\Support\Generator\Response;
|
||||
use Dedoc\Scramble\Support\Generator\Schema;
|
||||
use Dedoc\Scramble\Support\Generator\Types\ArrayType;
|
||||
use Dedoc\Scramble\Support\Generator\Types\BooleanType;
|
||||
use Dedoc\Scramble\Support\Generator\Types\IntegerType;
|
||||
use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType;
|
||||
use Dedoc\Scramble\Support\Generator\Types\StringType;
|
||||
use Dedoc\Scramble\Support\Type\Generic;
|
||||
use Dedoc\Scramble\Support\Type\ObjectType;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
|
||||
{
|
||||
public function shouldHandle(Type $type): bool
|
||||
{
|
||||
return $type instanceof ObjectType
|
||||
&& $type->isInstanceOf(PaginatedResourceCollection::class);
|
||||
}
|
||||
|
||||
public function toSchema(Type $type): ?OpenApiObjectType
|
||||
{
|
||||
/** @var Type|null $collectingClassType */
|
||||
$collectingClassType = $type->templateTypes[0] ?? null;
|
||||
|
||||
if (! $collectingClassType instanceof ObjectType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $collectingClassType->isInstanceOf(JsonResource::class) && ! $collectingClassType->isInstanceOf(Model::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collectingType = $this->openApiTransformer->transform($collectingClassType);
|
||||
|
||||
$newType = new OpenApiObjectType;
|
||||
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));
|
||||
if ($type instanceof ObjectType && $type->isInstanceOf(TimeEntryCollection::class)) {
|
||||
$newType->addProperty(
|
||||
'meta',
|
||||
(new OpenApiObjectType)
|
||||
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
||||
->setRequired(['total'])
|
||||
);
|
||||
$newType->setRequired(['data', 'meta']);
|
||||
} else {
|
||||
$newType->addProperty(
|
||||
'links',
|
||||
(new OpenApiObjectType)
|
||||
->addProperty('first', (new StringType)->nullable(true))
|
||||
->addProperty('last', (new StringType)->nullable(true))
|
||||
->addProperty('prev', (new StringType)->nullable(true))
|
||||
->addProperty('next', (new StringType)->nullable(true))
|
||||
->setRequired(['first', 'last', 'prev', 'next'])
|
||||
);
|
||||
$newType->addProperty(
|
||||
'meta',
|
||||
(new OpenApiObjectType)
|
||||
->addProperty('current_page', new IntegerType)
|
||||
->addProperty('from', (new IntegerType)->nullable(true))
|
||||
->addProperty('last_page', new IntegerType)
|
||||
->addProperty('links', (new ArrayType)->setItems(
|
||||
(new OpenApiObjectType)
|
||||
->addProperty('url', (new StringType)->nullable(true))
|
||||
->addProperty('label', new StringType)
|
||||
->addProperty('active', new BooleanType)
|
||||
->setRequired(['url', 'label', 'active'])
|
||||
)->setDescription('Generated paginator links.'))
|
||||
->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.'))
|
||||
->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.'))
|
||||
->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.'))
|
||||
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
||||
->setRequired(['current_page', 'from', 'last_page', 'links', 'path', 'per_page', 'to', 'total'])
|
||||
);
|
||||
$newType->setRequired(['data', 'links', 'meta']);
|
||||
}
|
||||
|
||||
return $newType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Generic $type
|
||||
*/
|
||||
public function toResponse(Type $type): ?Response
|
||||
{
|
||||
/** @var ObjectType|null $collectingClassType */
|
||||
$collectingClassType = $type->templateTypes[0] ?? null;
|
||||
if (! $collectingClassType instanceof ObjectType) {
|
||||
return null;
|
||||
}
|
||||
$type = $this->toSchema($type);
|
||||
|
||||
return Response::make(200)
|
||||
->description('Paginated set of `'.$this->components->uniqueSchemaName($collectingClassType->name).'`')
|
||||
->setContent('application/json', Schema::fromType($type));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\AuditResource\Pages\CreateAudit;
|
||||
use App\Filament\Resources\AuditResource\Pages\ListAudits;
|
||||
use App\Filament\Resources\AuditResource\Pages\ViewAudit;
|
||||
use App\Models\Audit;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Str;
|
||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||
|
||||
class AuditResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Audit::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-archive-box';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'System';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('user_type')
|
||||
->maxLength(255),
|
||||
TextInput::make('user_id'),
|
||||
TextInput::make('event')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('auditable_type')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('auditable_id')
|
||||
->required(),
|
||||
PrettyJsonField::make('old_values'),
|
||||
PrettyJsonField::make('new_values'),
|
||||
Textarea::make('url'),
|
||||
TextInput::make('ip_address'),
|
||||
TextInput::make('user_agent')
|
||||
->maxLength(1023),
|
||||
TextInput::make('tags')
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('user.name'),
|
||||
TextColumn::make('event'),
|
||||
TextColumn::make('auditable_type'),
|
||||
TextColumn::make('auditable_id'),
|
||||
IconColumn::make('was_command')
|
||||
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
|
||||
->boolean(),
|
||||
TextColumn::make('created_at')
|
||||
->sortable()
|
||||
->dateTime(),
|
||||
TextColumn::make('updated_at')
|
||||
->sortable()
|
||||
->dateTime(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListAudits::route('/'),
|
||||
'create' => CreateAudit::route('/create'),
|
||||
'view' => ViewAudit::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
13
app/Filament/Resources/AuditResource/Pages/CreateAudit.php
Normal file
13
app/Filament/Resources/AuditResource/Pages/CreateAudit.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\AuditResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AuditResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateAudit extends CreateRecord
|
||||
{
|
||||
protected static string $resource = AuditResource::class;
|
||||
}
|
||||
18
app/Filament/Resources/AuditResource/Pages/ListAudits.php
Normal file
18
app/Filament/Resources/AuditResource/Pages/ListAudits.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\AuditResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AuditResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListAudits extends ListRecords
|
||||
{
|
||||
protected static string $resource = AuditResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
13
app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
13
app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources\AuditResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AuditResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewAudit extends ViewRecord
|
||||
{
|
||||
protected static string $resource = AuditResource::class;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ use App\Service\Import\Importers\ImportException;
|
||||
use App\Service\Import\Importers\ReportDto;
|
||||
use App\Service\Import\ImportService;
|
||||
use App\Service\TimezoneService;
|
||||
use Brick\Money\IsoCurrencyProvider;
|
||||
use Brick\Money\ISOCurrencyProvider;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
@@ -40,7 +40,6 @@ use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
|
||||
class OrganizationResource extends Resource
|
||||
{
|
||||
@@ -89,7 +88,7 @@ class OrganizationResource extends Resource
|
||||
Select::make('currency')
|
||||
->label('Currency')
|
||||
->options(function (): array {
|
||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$select = [];
|
||||
foreach ($currencies as $currency) {
|
||||
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
||||
@@ -182,13 +181,9 @@ class OrganizationResource extends Resource
|
||||
->icon('heroicon-o-inbox-arrow-down')
|
||||
->action(function (Organization $record, array $data): void {
|
||||
try {
|
||||
try {
|
||||
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
||||
} catch (UnableToReadFile) {
|
||||
$file = null;
|
||||
}
|
||||
$file = Storage::disk(config('filament.default_filesystem_disk'))->get($data['file']);
|
||||
if ($file === null) {
|
||||
throw new ImportException('File not found');
|
||||
throw new Exception('File not found');
|
||||
}
|
||||
/** @var string $timezone */
|
||||
$timezone = $data['timezone'];
|
||||
|
||||
@@ -98,10 +98,9 @@ class UsersRelationManager extends RelationManager
|
||||
app(BillableRateService::class)->updateTimeEntriesBillableRateForMember($member);
|
||||
}
|
||||
|
||||
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
|
||||
if ($newRole->value !== $member->role) {
|
||||
if ($data['role'] !== $member->role) {
|
||||
try {
|
||||
app(MemberService::class)->changeRole($member, $organization, $newRole, true);
|
||||
app(MemberService::class)->changeRole($member, $organization, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true);
|
||||
} catch (ApiException $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
|
||||
@@ -16,7 +16,7 @@ use App\Models\User;
|
||||
use App\Service\DeletionService;
|
||||
use App\Service\TimezoneService;
|
||||
use App\Service\UserService;
|
||||
use Brick\Money\IsoCurrencyProvider;
|
||||
use Brick\Money\ISOCurrencyProvider;
|
||||
use Exception;
|
||||
use Filament\Actions\BulkAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
@@ -115,7 +115,7 @@ class UserResource extends Resource
|
||||
Select::make('currency')
|
||||
->label('Currency (Personal Organization)')
|
||||
->options(function (): array {
|
||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$select = [];
|
||||
foreach ($currencies as $currency) {
|
||||
$select[$currency->getCurrencyCode()] = $currency->getName().' ('.$currency->getCurrencyCode().')';
|
||||
|
||||
@@ -59,10 +59,9 @@ class OrganizationsRelationManager extends RelationManager
|
||||
/** @var Member $member */
|
||||
$member = $record->getRelation('membership');
|
||||
|
||||
$newRole = $data['role'] instanceof Role ? $data['role'] : Role::from($data['role']);
|
||||
if ($newRole->value !== $member->role) {
|
||||
if ($data['role'] !== $member->role) {
|
||||
try {
|
||||
app(MemberService::class)->changeRole($member, $record, $newRole, true);
|
||||
app(MemberService::class)->changeRole($member, $record, ($data['role'] instanceof Role ? $data['role'] : Role::from($data['role'])), true);
|
||||
} catch (ApiException $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Dedoc\Scramble\Attributes\ExcludeAllRoutesFromDocs;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Fallback for unknown /api/* routes, to prevent a rendered HTML page
|
||||
*/
|
||||
#[ExcludeAllRoutesFromDocs]
|
||||
class FallbackController extends Controller
|
||||
{
|
||||
public function __invoke(): never
|
||||
{
|
||||
throw new NotFoundHttpException('API resource not found');
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ class ClientController extends Controller
|
||||
/**
|
||||
* Get clients
|
||||
*
|
||||
* @return ClientCollection<ClientResource>
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId getClients
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Http\Controllers\Api\V1;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Service\CurrencyService;
|
||||
use Brick\Money\Currency;
|
||||
use Brick\Money\IsoCurrencyProvider;
|
||||
use Brick\Money\ISOCurrencyProvider;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class CurrencyController extends Controller
|
||||
@@ -29,7 +29,7 @@ class CurrencyController extends Controller
|
||||
'name' => $currency->getName(),
|
||||
'symbol' => $currencyService->getCurrencySymbol($currency->getCurrencyCode()),
|
||||
],
|
||||
IsoCurrencyProvider::getInstance()->getAvailableCurrencies()
|
||||
ISOCurrencyProvider::getInstance()->getAvailableCurrencies()
|
||||
));
|
||||
|
||||
return response()->json($currencies);
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
||||
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
|
||||
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
|
||||
use App\Http\Resources\V1\Invitation\InvitationCollection;
|
||||
use App\Http\Resources\V1\Invitation\InvitationResource;
|
||||
use App\Models\Organization;
|
||||
use App\Models\OrganizationInvitation;
|
||||
use App\Service\InvitationService;
|
||||
@@ -29,6 +30,8 @@ class InvitationController extends Controller
|
||||
/**
|
||||
* List all invitations of an organization
|
||||
*
|
||||
* @return InvitationCollection<InvitationResource>
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId getInvitations
|
||||
@@ -85,8 +88,6 @@ class InvitationController extends Controller
|
||||
/**
|
||||
* Remove a pending invitation
|
||||
*
|
||||
* This revokes the invitation: the link in the invitation email stops working. Find the invitation ID with `GET /organizations/{organization}/invitations`.
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId removeInvitation
|
||||
|
||||
@@ -48,6 +48,8 @@ class MemberController extends Controller
|
||||
/**
|
||||
* List all members of an organization
|
||||
*
|
||||
* @return MemberCollection<MemberResource>
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId getMembers
|
||||
@@ -143,9 +145,6 @@ class MemberController extends Controller
|
||||
/**
|
||||
* Merge one member into another
|
||||
*
|
||||
* Only placeholder members (for example people created by an import) can be merged. All time entries and other data of the placeholder
|
||||
* are reassigned to the member given in `member_id`, and the placeholder is removed. Find both member IDs with `GET /organizations/{organization}/members`.
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
* @throws OnlyPlaceholdersCanBeMergedIntoAnotherMember
|
||||
* @throws Throwable
|
||||
|
||||
@@ -35,6 +35,8 @@ class ProjectController extends Controller
|
||||
/**
|
||||
* Get projects visible to the current user
|
||||
*
|
||||
* @return ProjectCollection<ProjectResource>
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId getProjects
|
||||
|
||||
@@ -36,6 +36,8 @@ class ProjectMemberController extends Controller
|
||||
/**
|
||||
* Get project members for project
|
||||
*
|
||||
* @return ProjectMemberCollection<ProjectMemberResource>
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId getProjectMembers
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Http\Requests\V1\Report\ReportStoreRequest;
|
||||
use App\Http\Requests\V1\Report\ReportUpdateRequest;
|
||||
use App\Http\Resources\V1\Report\DetailedReportResource;
|
||||
use App\Http\Resources\V1\Report\ReportCollection;
|
||||
use App\Http\Resources\V1\Report\ReportResource;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Report;
|
||||
use App\Service\Dto\ReportPropertiesDto;
|
||||
@@ -34,6 +35,8 @@ class ReportController extends Controller
|
||||
/**
|
||||
* Get reports
|
||||
*
|
||||
* @return ReportCollection<ReportResource>
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId getReports
|
||||
@@ -68,8 +71,6 @@ class ReportController extends Controller
|
||||
/**
|
||||
* Create report
|
||||
*
|
||||
* A report is a saved set of filters. Set `is_public` to `true` to share it: the response then contains the `shareable_link` that can be opened without logging in.
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId createReport
|
||||
|
||||
@@ -29,6 +29,8 @@ class TagController extends Controller
|
||||
/**
|
||||
* Get tags
|
||||
*
|
||||
* @return TagCollection<TagResource>
|
||||
*
|
||||
* @operationId getTags
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
|
||||
@@ -51,6 +51,8 @@ class TaskController extends Controller
|
||||
/**
|
||||
* Get tasks
|
||||
*
|
||||
* @return TaskCollection<TaskResource>
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
* @operationId getTasks
|
||||
|
||||
@@ -109,13 +109,10 @@ class TimeEntryController extends Controller
|
||||
/**
|
||||
* Get time entries in organization
|
||||
*
|
||||
* Without a member filter this returns the time entries of all members of the organization (for users who may view all time entries, such as owners and admins), not only your own.
|
||||
* To get only your own time entries, pass your member ID as `member_id`. Your member ID is the `id` returned for this organization by `GET /v1/users/me/memberships`; it is not your user ID.
|
||||
* If you only need time entries for a specific user, you can filter by `member_id`.
|
||||
* Users with the permission `time-entries:view:own` can only use this endpoint with their own member ID in the member_id filter.
|
||||
*
|
||||
* The `start` and `end` filters both apply to the start time of an entry, in UTC. Convert the user's local day boundaries to UTC first.
|
||||
* Results are paginated with `limit` (default 100, max 500) and `offset`; check `meta.total` and fetch further pages when needed.
|
||||
* To find the running timer, use `active=true` (or `GET /v1/users/me/time-entries/active`).
|
||||
* @return TimeEntryCollection<TimeEntryResource>
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*
|
||||
@@ -194,9 +191,8 @@ class TimeEntryController extends Controller
|
||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
||||
if ($roundingType !== null && $roundingMinutes !== null) {
|
||||
$select = array_diff($select, ['start', 'end']);
|
||||
// These SQL fragments are generated from a rounding enum and validated minute count.
|
||||
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start'); // @phpstan-ignore argument.type
|
||||
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end'); // @phpstan-ignore argument.type
|
||||
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start');
|
||||
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end');
|
||||
}
|
||||
$timeEntriesQuery = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
@@ -354,11 +350,6 @@ class TimeEntryController extends Controller
|
||||
* The parameters `group` and `sub_group` allow you to group the time entries by different criteria.
|
||||
* If the group parameters are all set to `null` or are all missing, the endpoint will aggregate all filtered time entries.
|
||||
*
|
||||
* Durations are returned in `seconds` (divide by 3600 for hours) and amounts in `cost` as cents in the organization's currency (divide by 100 for money).
|
||||
* Filter by member with `member_id`. Your member ID is the `id` returned for this organization by `GET /v1/users/me/memberships`; it is not your user ID.
|
||||
* Array filters use the query format `client_ids[]=<id>`.
|
||||
* Example: billable hours per client for a period: `group=client&billable=true&start=...&end=...`.
|
||||
*
|
||||
* @operationId getAggregatedTimeEntries
|
||||
*
|
||||
* @return array{
|
||||
@@ -592,12 +583,6 @@ class TimeEntryController extends Controller
|
||||
/**
|
||||
* Create time entry
|
||||
*
|
||||
* `billable` is not taken from the project. To match the web app, set it to the project's `is_billable` value (or `false` without a project).
|
||||
*
|
||||
* A member can only have one running time entry (an entry with `end` set to `null`). Creating a running entry while another one runs fails with `time_entry_still_running`.
|
||||
* To start a new timer, first stop the running entry by updating its `end` to the current time, then create the new entry.
|
||||
* To log past work, send both `start` and `end` (UTC). Create one entry per block of work.
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
* @throws TimeEntryStillRunningApiException
|
||||
*
|
||||
@@ -648,8 +633,6 @@ class TimeEntryController extends Controller
|
||||
/**
|
||||
* Update time entry
|
||||
*
|
||||
* To stop a running timer, set `end` to the stop time (UTC). Times the user gives in their own timezone must be converted to UTC first.
|
||||
*
|
||||
* @throws AuthorizationException|TimeEntryCanNotBeRestartedApiException
|
||||
*
|
||||
* @operationId updateTimeEntry
|
||||
@@ -718,10 +701,6 @@ class TimeEntryController extends Controller
|
||||
/**
|
||||
* Update multiple time entries
|
||||
*
|
||||
* Applies the same `changes` to every entry in `ids`. To find the IDs, list entries with `GET /organizations/{organization}/time-entries`
|
||||
* (filtered by `member_id`, the project and the other criteria), then send their IDs here.
|
||||
* When `changes.project_id` moves entries to another project, also set `changes.task_id` to a task of the new project or to `null`, because tasks belong to a project.
|
||||
*
|
||||
* @operationId updateMultipleTimeEntries
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Models\Organization;
|
||||
use Brick\Money\Currency;
|
||||
use Brick\Money\IsoCurrencyProvider;
|
||||
use Brick\Money\ISOCurrencyProvider;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -53,7 +53,7 @@ class OrganizationController extends Controller
|
||||
],
|
||||
'currencies' => array_map(function (Currency $currency): string {
|
||||
return $currency->getName();
|
||||
}, IsoCurrencyProvider::getInstance()->getAvailableCurrencies()),
|
||||
}, ISOCurrencyProvider::getInstance()->getAvailableCurrencies()),
|
||||
'permissions' => [
|
||||
'canDeleteTeam' => $this->hasPermission($organization, 'organizations:delete'),
|
||||
'canUpdateTeam' => $this->hasPermission($organization, 'organizations:update'),
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Http\Middleware\EnsureEmailIsVerified;
|
||||
use App\Http\Middleware\ForceHttps;
|
||||
use App\Http\Middleware\ForceJsonResponse;
|
||||
use App\Http\Middleware\HandleInertiaRequests;
|
||||
use App\Http\Middleware\PreventRequestForgery;
|
||||
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||
use App\Http\Middleware\ShareInertiaData;
|
||||
@@ -19,6 +18,7 @@ use App\Http\Middleware\TrimStrings;
|
||||
use App\Http\Middleware\TrustHosts;
|
||||
use App\Http\Middleware\TrustProxies;
|
||||
use App\Http\Middleware\ValidateSignature;
|
||||
use App\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
||||
use Illuminate\Auth\Middleware\Authorize;
|
||||
use Illuminate\Auth\Middleware\RequirePassword;
|
||||
@@ -68,7 +68,7 @@ class Kernel extends HttpKernel
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
PreventRequestForgery::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
HandleInertiaRequests::class,
|
||||
ShareInertiaData::class,
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Filament\Http\Middleware\Authenticate as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuthenticateFilamentPanel extends Middleware
|
||||
{
|
||||
/**
|
||||
* The admin panel has no login page of its own (it shares the app's
|
||||
* session-based login), so Filament::getLoginUrl() always returns null.
|
||||
* Fall back to the app's login route so guests are redirected instead
|
||||
* of receiving an empty 401 response.
|
||||
*/
|
||||
protected function redirectTo($request): ?string
|
||||
{
|
||||
return parent::redirectTo($request) ?? ($request->expectsJson() ? null : route('login'));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as Middleware;
|
||||
|
||||
class PreventRequestForgery extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
19
app/Http/Middleware/VerifyCsrfToken.php
Normal file
19
app/Http/Middleware/VerifyCsrfToken.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -27,7 +27,6 @@ class MemberUpdateRequest extends BaseFormRequest
|
||||
'string',
|
||||
Rule::enum(Role::class),
|
||||
],
|
||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
||||
'billable_rate' => array_merge(
|
||||
[
|
||||
'nullable',
|
||||
|
||||
@@ -36,7 +36,6 @@ class OrganizationUpdateRequest extends BaseFormRequest
|
||||
'string',
|
||||
new CurrencyRule,
|
||||
],
|
||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
||||
'billable_rate' => array_merge(
|
||||
[
|
||||
'nullable',
|
||||
|
||||
@@ -55,7 +55,6 @@ class ProjectStoreRequest extends BaseFormRequest
|
||||
'required',
|
||||
'boolean',
|
||||
],
|
||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
||||
'billable_rate' => array_merge(
|
||||
[
|
||||
'nullable',
|
||||
|
||||
@@ -68,7 +68,6 @@ class ProjectUpdateRequest extends BaseFormRequest
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->uuid(),
|
||||
],
|
||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
||||
'billable_rate' => array_merge([
|
||||
'nullable',
|
||||
],
|
||||
|
||||
@@ -31,7 +31,6 @@ class ProjectMemberStoreRequest extends BaseFormRequest
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->uuid(),
|
||||
],
|
||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
||||
'billable_rate' => array_merge(
|
||||
[
|
||||
'nullable',
|
||||
|
||||
@@ -21,7 +21,6 @@ class ProjectMemberUpdateRequest extends BaseFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency)
|
||||
'billable_rate' => array_merge(
|
||||
[
|
||||
'nullable',
|
||||
|
||||
@@ -35,7 +35,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// Filter by member ID. Without it, users who may view all time entries (owners, admins) get the entries of every member; pass your own member ID (from GET /v1/users/me/memberships) to get only yours
|
||||
// Filter by member ID
|
||||
'member_id' => [
|
||||
'string',
|
||||
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder {
|
||||
@@ -155,7 +155,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
||||
'string',
|
||||
Rule::enum(TimeEntryType::class),
|
||||
],
|
||||
// Limit the number of returned time entries (default: 100)
|
||||
// Limit the number of returned time entries (default: 150)
|
||||
'limit' => [
|
||||
'integer',
|
||||
'min:1',
|
||||
|
||||
@@ -32,7 +32,7 @@ class TimeEntryStoreRequest extends BaseFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// ID of the organization member that the time entry should belong to (a member ID from GET /v1/users/me/memberships or the members list, not a user ID)
|
||||
// ID of the organization member that the time entry should belong to
|
||||
'member_id' => [
|
||||
'required',
|
||||
'string',
|
||||
@@ -86,7 +86,7 @@ class TimeEntryStoreRequest extends BaseFormRequest
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
'after_or_equal:start',
|
||||
],
|
||||
// Whether time entry is billable. Not derived from the project: set it to the project's is_billable value to match the web app
|
||||
// Whether time entry is billable
|
||||
'billable' => [
|
||||
'required',
|
||||
'boolean',
|
||||
|
||||
@@ -43,10 +43,9 @@ class UserUpdateRequest extends BaseFormRequest
|
||||
'email' => [
|
||||
'email:rfc,strict',
|
||||
'max:255',
|
||||
UniqueEloquent::make(User::class, 'email')->query(function (Builder $query) {
|
||||
UniqueEloquent::make(User::class, 'email')->ignore($this->user->id)->query(function (Builder $query) {
|
||||
/** @var Builder<User> $query */
|
||||
return $query->where('is_placeholder', '=', false)
|
||||
->whereKeyNot($this->user->getKey());
|
||||
return $query->where('is_placeholder', '=', false);
|
||||
}),
|
||||
],
|
||||
'photo' => [
|
||||
|
||||
7
app/Http/Resources/PaginatedResourceCollection.php
Normal file
7
app/Http/Resources/PaginatedResourceCollection.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
interface PaginatedResourceCollection {}
|
||||
@@ -21,15 +21,15 @@ class ApiTokenResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of the API token, this ID is NOT a UUID */
|
||||
/** @var string $id ID of the API token, this ID is NOT a UUID */
|
||||
'id' => $this->resource->id,
|
||||
/** @var string $name Name of the API token */
|
||||
'name' => $this->resource->name,
|
||||
/** Whether the API token is revoked */
|
||||
/** @var bool $revoked Whether the API token is revoked */
|
||||
'revoked' => $this->resource->revoked,
|
||||
/** List of scopes that the API token has */
|
||||
/** @var array<string> $scopes List of scopes that the API token has */
|
||||
'scopes' => $this->resource->scopes,
|
||||
/** When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
/** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
'expires_at' => $this->formatDateTime($this->resource->expires_at),
|
||||
|
||||
@@ -29,15 +29,15 @@ class ApiTokenWithAccessTokenResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of the API token, this ID is NOT a UUID */
|
||||
/** @var string $id ID of the API token, this ID is NOT a UUID */
|
||||
'id' => $this->resource->id,
|
||||
/** @var string $name Name of the API token */
|
||||
'name' => $this->resource->name,
|
||||
/** Whether the API token is revoked */
|
||||
/** @var bool $revoked Whether the API token is revoked */
|
||||
'revoked' => $this->resource->revoked,
|
||||
/** List of scopes that the API token has */
|
||||
/** @var array<string> $scopes List of scopes that the API token has */
|
||||
'scopes' => $this->resource->scopes,
|
||||
/** When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
/** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */
|
||||
'expires_at' => $this->formatDateTime($this->resource->expires_at),
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\Client;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ClientCollection extends ResourceCollection
|
||||
class ClientCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -21,15 +21,15 @@ class ClientResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID */
|
||||
/** @var string $id ID */
|
||||
'id' => $this->resource->id,
|
||||
/** Name */
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** Whether the client is archived */
|
||||
/** @var bool $is_archived Whether the client is archived */
|
||||
'is_archived' => $this->resource->is_archived,
|
||||
/** When the tag was created */
|
||||
/** @var string $created_at When the tag was created */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** When the tag was last updated */
|
||||
/** @var string $updated_at When the tag was last updated */
|
||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\Invitation;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class InvitationCollection extends ResourceCollection
|
||||
class InvitationCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -21,11 +21,11 @@ class InvitationResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of the invitation */
|
||||
/** @var string $id ID of the invitation */
|
||||
'id' => $this->resource->id,
|
||||
/** Email */
|
||||
/** @var string $email Email */
|
||||
'email' => $this->resource->email,
|
||||
/** Role */
|
||||
/** @var string $role Role */
|
||||
'role' => $this->resource->role,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\Member;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class MemberCollection extends ResourceCollection
|
||||
class MemberCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -22,19 +22,19 @@ class MemberResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of membership */
|
||||
/** @var string $id ID of membership */
|
||||
'id' => $this->resource->id,
|
||||
/** ID of user */
|
||||
/** @var string $id ID of user */
|
||||
'user_id' => $this->resource->user->id,
|
||||
/** Name */
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->user->name,
|
||||
/** Email */
|
||||
/** @var string $email Email */
|
||||
'email' => $this->resource->user->email,
|
||||
/** Role */
|
||||
/** @var string $role Role */
|
||||
'role' => $this->resource->role,
|
||||
/** Placeholder user for imports, user might not really exist and does not know about this placeholder membership */
|
||||
/** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */
|
||||
'is_placeholder' => $this->resource->user->is_placeholder,
|
||||
/** Billable rate in cents per hour */
|
||||
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||
'billable_rate' => $this->resource->billable_rate,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\Member;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class PersonalMembershipCollection extends ResourceCollection
|
||||
class PersonalMembershipCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -21,17 +21,17 @@ class PersonalMembershipResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of membership */
|
||||
/** @var string $id ID of membership */
|
||||
'id' => $this->resource->id,
|
||||
'organization' => [
|
||||
/** ID of organization */
|
||||
/** @var string $id ID of organization */
|
||||
'id' => $this->resource->organization->id,
|
||||
/** Name of organization */
|
||||
/** @var string $name Name of organization */
|
||||
'name' => $this->resource->organization->name,
|
||||
/** Currency code (ISO 4217) of organization */
|
||||
/** @var string $currency Currency code (ISO 4217) of organization */
|
||||
'currency' => $this->resource->organization->currency,
|
||||
],
|
||||
/** Role */
|
||||
/** @var string $role Role */
|
||||
'role' => $this->resource->role,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -43,23 +43,23 @@ class OrganizationResource extends BaseResource
|
||||
$currencyService = app(CurrencyService::class);
|
||||
|
||||
return [
|
||||
/** ID */
|
||||
/** @var string $id ID */
|
||||
'id' => $this->resource->id,
|
||||
/** Name */
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** Personal organizations automatically created after registration */
|
||||
/** @var bool $color Personal organizations automatically created after registration */
|
||||
'is_personal' => $this->resource->personal_team,
|
||||
/** Billable rate in cents per hour */
|
||||
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
|
||||
/** Can members of the organization with role "employee" see the billable rates */
|
||||
/** @var bool $employees_can_see_billable_rates Can members of the organization with role "employee" see the billable rates */
|
||||
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
||||
/** Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */
|
||||
/** @var bool $employees_can_manage_tasks Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */
|
||||
'employees_can_manage_tasks' => $this->resource->employees_can_manage_tasks,
|
||||
/** Prevent creating overlapping time entries (only new entries) */
|
||||
/** @var bool $prevent_overlapping_time_entries Prevent creating overlapping time entries (only new entries) */
|
||||
'prevent_overlapping_time_entries' => $this->resource->prevent_overlapping_time_entries,
|
||||
/** Whether members of the organization can track breaks */
|
||||
/** @var bool $breaks_enabled Whether members of the organization can track breaks */
|
||||
'breaks_enabled' => $this->resource->breaks_enabled,
|
||||
/** Currency code (ISO 4217) */
|
||||
/** @var string $currency Currency code (ISO 4217) */
|
||||
'currency' => $this->resource->currency,
|
||||
/** @var string $currency_symbol Currency symbol */
|
||||
'currency_symbol' => $currencyService->getCurrencySymbol($this->resource->currency),
|
||||
|
||||
@@ -4,11 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\Project;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ProjectCollection extends ResourceCollection
|
||||
class ProjectCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
private bool $showBillableRates;
|
||||
|
||||
|
||||
@@ -30,25 +30,25 @@ class ProjectResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of project */
|
||||
/** @var string $id ID of project */
|
||||
'id' => $this->resource->id,
|
||||
/** Name of project */
|
||||
/** @var string $name Name of project */
|
||||
'name' => $this->resource->name,
|
||||
/** Color of project */
|
||||
/** @var string $color Color of project */
|
||||
'color' => $this->resource->color,
|
||||
/** @var string|null $client_id ID of client */
|
||||
'client_id' => $this->resource->client_id,
|
||||
/** Whether the client is archived */
|
||||
/** @var bool $is_archived Whether the client is archived */
|
||||
'is_archived' => $this->resource->is_archived,
|
||||
/** Billable rate in cents per hour */
|
||||
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
|
||||
/** Project time entries billable default */
|
||||
/** @var bool $is_billable Project time entries billable default */
|
||||
'is_billable' => $this->resource->is_billable,
|
||||
/** Estimated time in seconds */
|
||||
/** @var int|null $estimated_time Estimated time in seconds */
|
||||
'estimated_time' => $this->resource->estimated_time,
|
||||
/** Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
|
||||
/** @var int $spent_time Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
|
||||
'spent_time' => $this->resource->spent_time,
|
||||
/** Whether the project is public */
|
||||
/** @var bool $is_public Whether the project is public */
|
||||
'is_public' => $this->resource->is_public,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\ProjectMember;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ProjectMemberCollection extends ResourceCollection
|
||||
class ProjectMemberCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -21,13 +21,13 @@ class ProjectMemberResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of project member */
|
||||
/** @var string $id ID of project member */
|
||||
'id' => $this->resource->id,
|
||||
/** Billable rate in cents per hour */
|
||||
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||
'billable_rate' => $this->resource->billable_rate,
|
||||
/** ID of the organization member */
|
||||
/** @var string $member_id ID of the organization member */
|
||||
'member_id' => $this->resource->member_id,
|
||||
/** ID of the project */
|
||||
/** @var string $project_id ID of the project */
|
||||
'project_id' => $this->resource->project_id,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,36 +21,36 @@ class DetailedReportResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of the report */
|
||||
/** @var string $id ID of the report */
|
||||
'id' => $this->resource->id,
|
||||
/** Name */
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** Description */
|
||||
/** @var string|null $email Description */
|
||||
'description' => $this->resource->description,
|
||||
/** Whether the report can be accessed via an external link */
|
||||
/** @var bool $is_public Whether the report can be accessed via an external link */
|
||||
'is_public' => $this->resource->is_public,
|
||||
/** @var string|null $public_until Date until the report is public */
|
||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
||||
'shareable_link' => $this->resource->getShareableLink(),
|
||||
'properties' => [
|
||||
/** Type of first grouping */
|
||||
/** @var string $group Type of first grouping */
|
||||
'group' => $this->resource->properties->group->value,
|
||||
/** Type of second grouping */
|
||||
/** @var string $sub_group Type of second grouping */
|
||||
'sub_group' => $this->resource->properties->subGroup->value,
|
||||
/** Type of grouping of the historic aggregation (time chart) */
|
||||
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
||||
'history_group' => $this->resource->properties->historyGroup->value,
|
||||
/** Start date of the report */
|
||||
/** @var string $start Start date of the report */
|
||||
'start' => $this->formatDateTime($this->resource->properties->start),
|
||||
/** End date of the report */
|
||||
/** @var string $end End date of the report */
|
||||
'end' => $this->formatDateTime($this->resource->properties->end),
|
||||
/** Whether the report is active */
|
||||
/** @var bool|null $active Whether the report is active */
|
||||
'active' => $this->resource->properties->active,
|
||||
/** @var array<string>|null $member_ids Filter by multiple member IDs, member IDs are OR combined */
|
||||
'member_ids' => $this->resource->properties->memberIds?->toArray(),
|
||||
/** Filter by billable status */
|
||||
/** @var bool|null $billable Filter by billable status */
|
||||
'billable' => $this->resource->properties->billable,
|
||||
/** Filter by time entry type */
|
||||
/** @var string|null $time_entry_type Filter by time entry type */
|
||||
'time_entry_type' => $this->resource->properties->timeEntryType?->value,
|
||||
/** @var array<string>|null $client_ids Filter by client IDs, client IDs are OR combined */
|
||||
'client_ids' => $this->resource->properties->clientIds?->toArray(),
|
||||
@@ -58,18 +58,18 @@ class DetailedReportResource extends BaseResource
|
||||
'project_ids' => $this->resource->properties->projectIds?->toArray(),
|
||||
/** @var array<string>|null $tags_ids Filter by tag IDs, tag IDs are OR combined */
|
||||
'tag_ids' => $this->resource->properties->tagIds?->toArray(),
|
||||
/** Tag match type */
|
||||
/** @var string|null $tag_match_type Tag match type */
|
||||
'tag_match_type' => $this->resource->properties->tagMatchType?->value,
|
||||
/** @var array<string>|null $task_ids Filter by task IDs, task IDs are OR combined */
|
||||
'task_ids' => $this->resource->properties->taskIds?->toArray(),
|
||||
/** Rounding type for time entries */
|
||||
/** @var string|null $rounding_type Rounding type for time entries */
|
||||
'rounding_type' => $this->resource->properties->roundingType?->value,
|
||||
/** Rounding minutes for time entries */
|
||||
/** @var int|null $rounding_minutes Rounding minutes for time entries */
|
||||
'rounding_minutes' => $this->resource->properties->roundingMinutes,
|
||||
],
|
||||
/** Date when the report was created */
|
||||
/** @var string $created_at Date when the report was created */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** Date when the report was last updated */
|
||||
/** @var string $updated_at Date when the report was last updated */
|
||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -73,13 +73,13 @@ class DetailedWithDataReportResource extends BaseResource
|
||||
$currencyService = app(CurrencyService::class);
|
||||
|
||||
return [
|
||||
/** Name */
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** Description */
|
||||
/** @var string|null $email Description */
|
||||
'description' => $this->resource->description,
|
||||
/** @var string|null $public_until Date until the report is public */
|
||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||
/** Currency code (ISO 4217) */
|
||||
/** @var string $currency Currency code (ISO 4217) */
|
||||
'currency' => $this->resource->organization->currency,
|
||||
/** @var NumberFormat $number_format Number format */
|
||||
'number_format' => $this->resource->organization->number_format->value,
|
||||
@@ -94,15 +94,15 @@ class DetailedWithDataReportResource extends BaseResource
|
||||
/** @var TimeFormat $time_format Time format */
|
||||
'time_format' => $this->resource->organization->time_format->value,
|
||||
'properties' => [
|
||||
/** Type of first grouping */
|
||||
/** @var string $group Type of first grouping */
|
||||
'group' => $this->resource->properties->group->value,
|
||||
/** Type of second grouping */
|
||||
/** @var string $sub_group Type of second grouping */
|
||||
'sub_group' => $this->resource->properties->subGroup->value,
|
||||
/** Type of grouping of the historic aggregation (time chart) */
|
||||
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
||||
'history_group' => $this->resource->properties->historyGroup->value,
|
||||
/** Start date of the report */
|
||||
/** @var string $start Start date of the report */
|
||||
'start' => $this->formatDateTime($this->resource->properties->start),
|
||||
/** End date of the report */
|
||||
/** @var string $end End date of the report */
|
||||
'end' => $this->formatDateTime($this->resource->properties->end),
|
||||
],
|
||||
/** @var array{
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\Report;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ReportCollection extends ResourceCollection
|
||||
class ReportCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -21,21 +21,21 @@ class ReportResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of the report */
|
||||
/** @var string $id ID of the report */
|
||||
'id' => $this->resource->id,
|
||||
/** Name */
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** Description */
|
||||
/** @var string|null $email Description */
|
||||
'description' => $this->resource->description,
|
||||
/** Whether the report can be accessed via an external link */
|
||||
/** @var bool $is_public Whether the report can be accessed via an external link */
|
||||
'is_public' => $this->resource->is_public,
|
||||
/** @var string|null $public_until Date until the report is public */
|
||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
||||
'shareable_link' => $this->resource->getShareableLink(),
|
||||
/** Date when the report was created */
|
||||
/** @var string $created_at Date when the report was created */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** Date when the report was last updated */
|
||||
/** @var string $updated_at Date when the report was last updated */
|
||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\Tag;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class TagCollection extends ResourceCollection
|
||||
class TagCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -21,13 +21,13 @@ class TagResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID */
|
||||
/** @var string $id ID */
|
||||
'id' => $this->resource->id,
|
||||
/** Name */
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** When the tag was created */
|
||||
/** @var string $created_at When the tag was created */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** When the tag was last updated */
|
||||
/** @var string $updated_at When the tag was last updated */
|
||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\Task;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class TaskCollection extends ResourceCollection
|
||||
class TaskCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -22,21 +22,21 @@ class TaskResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID */
|
||||
/** @var string $id ID */
|
||||
'id' => $this->resource->id,
|
||||
/** Name */
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** Whether the task is done */
|
||||
/** @var bool $is_done Whether the task is done */
|
||||
'is_done' => $this->resource->is_done,
|
||||
/** ID of the project */
|
||||
/** @var string $project_id ID of the project */
|
||||
'project_id' => $this->resource->project_id,
|
||||
/** Estimated time in seconds */
|
||||
/** @var int|null $estimated_time Estimated time in seconds */
|
||||
'estimated_time' => $this->resource->estimated_time,
|
||||
/** Spent time on this task in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
|
||||
/** @var int $spent_time Spent time on this task in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
|
||||
'spent_time' => $this->resource->spent_time,
|
||||
/** When the tag was created */
|
||||
/** @var string $created_at When the tag was created */
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** When the tag was last updated */
|
||||
/** @var string $updated_at When the tag was last updated */
|
||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\TimeEntry;
|
||||
|
||||
use App\Http\Resources\PaginatedResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class TimeEntryCollection extends ResourceCollection
|
||||
class TimeEntryCollection extends ResourceCollection implements PaginatedResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
|
||||
@@ -21,10 +21,10 @@ class TimeEntryResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of time entry */
|
||||
/** @var string $id ID of time entry */
|
||||
'id' => $this->resource->id,
|
||||
/**
|
||||
* Start of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z)
|
||||
* @var string $start Start of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z)
|
||||
*/
|
||||
'start' => $this->formatDateTime($this->resource->start),
|
||||
/**
|
||||
@@ -35,19 +35,19 @@ class TimeEntryResource extends BaseResource
|
||||
'duration' => (int) $this->resource->getDuration()?->totalSeconds,
|
||||
/** @var string|null $description Description of time entry */
|
||||
'description' => $this->resource->description,
|
||||
/** ID of task */
|
||||
/** @var string|null $task_id ID of task */
|
||||
'task_id' => $this->resource->task_id,
|
||||
/** ID of project */
|
||||
/** @var string|null $project_id ID of project */
|
||||
'project_id' => $this->resource->project_id,
|
||||
/** ID of organization */
|
||||
/** @var string $organization_id ID of organization */
|
||||
'organization_id' => $this->resource->organization_id,
|
||||
/** ID of user */
|
||||
/** @var string $user_id ID of user */
|
||||
'user_id' => $this->resource->user_id,
|
||||
/** List of tag IDs */
|
||||
/** @var array<string> $tags List of tag IDs */
|
||||
'tags' => $this->resource->tags ?? [],
|
||||
/** Whether time entry is billable */
|
||||
/** @var bool $billable Whether time entry is billable */
|
||||
'billable' => $this->resource->billable,
|
||||
/** Type of the time entry (`work` time or a `break`) */
|
||||
/** @var string $type Type of the time entry (`work` time or a `break`) */
|
||||
'type' => $this->resource->type->value,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -22,21 +22,21 @@ class UserResource extends BaseResource
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** ID of user */
|
||||
/** @var string $id ID of user */
|
||||
'id' => $this->resource->id,
|
||||
/** Name of user */
|
||||
/** @var string $name Name of user */
|
||||
'name' => $this->resource->name,
|
||||
/** Email of user */
|
||||
/** @var string $email Email of user */
|
||||
'email' => $this->resource->email,
|
||||
/** Email address awaiting verification (set when the user has requested an email change but not yet verified the new address) */
|
||||
/** @var string|null $pending_email Email address awaiting verification (set when the user has requested an email change but not yet verified the new address) */
|
||||
'pending_email' => $this->resource->pending_email,
|
||||
/** Profile photo URL */
|
||||
/** @var string $profile_photo_url Profile photo URL */
|
||||
'profile_photo_url' => $this->resource->profile_photo_url,
|
||||
/** Timezone (f.e. Europe/Berlin or America/New_York) */
|
||||
/** @var string $timezone Timezone (f.e. Europe/Berlin or America/New_York) */
|
||||
'timezone' => $this->resource->timezone,
|
||||
/** @var Weekday $week_start Starting day of the week */
|
||||
'week_start' => $this->resource->week_start->value,
|
||||
/** Whether to email the user when a time entry has been running for more than 8 hours */
|
||||
/** @var bool $send_time_entry_still_running_email Whether to email the user when a time entry has been running for more than 8 hours */
|
||||
'send_time_entry_still_running_email' => $this->resource->send_time_entry_still_running_email,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,19 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\AuditableWithoutOwner;
|
||||
use Database\Factories\AuditFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Carbon;
|
||||
use OwenIt\Auditing\Models\Audit as PackageAuditModel;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string|null $actor_type
|
||||
* @property string|null $actor_id
|
||||
* @property string|null $user_type
|
||||
* @property string|null $user_id
|
||||
* @property string $event
|
||||
* @property string $auditable_type
|
||||
* @property string $auditable_id
|
||||
@@ -26,71 +22,13 @@ use OwenIt\Auditing\Models\Audit as PackageAuditModel;
|
||||
* @property string|null $ip_address
|
||||
* @property string|null $user_agent
|
||||
* @property string|null $tags
|
||||
* @property string|null $owner_user_id
|
||||
* @property string|null $owner_organization_id
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read User|null $ownerUser
|
||||
* @property-read Organization|null $ownerOrganization
|
||||
*
|
||||
* @method static AuditFactory factory()
|
||||
* @method static Builder<Audit> whereMissingOwner()
|
||||
*/
|
||||
class Audit extends PackageAuditModel
|
||||
{
|
||||
/** @use HasFactory<AuditFactory> */
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function ownerUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owner_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Organization, $this>
|
||||
*/
|
||||
public function ownerOrganization(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organization::class, 'owner_organization_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether audits of the given auditable type intentionally have no owner (see AuditableWithoutOwner).
|
||||
*/
|
||||
public static function isAuditableTypeWithoutOwner(string $auditableType): bool
|
||||
{
|
||||
$modelClass = Relation::getMorphedModel($auditableType) ?? $auditableType;
|
||||
|
||||
return is_subclass_of($modelClass, AuditableWithoutOwner::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auditable types (morph aliases) whose audits intentionally have no owner (see AuditableWithoutOwner).
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function getAuditableTypesWithoutOwner(): array
|
||||
{
|
||||
return collect(Relation::morphMap())
|
||||
->filter(fn (string $modelClass): bool => is_subclass_of($modelClass, AuditableWithoutOwner::class))
|
||||
->keys()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Audits that have neither an owner organization nor an owner user, although their auditable type should have one.
|
||||
* These are audits whose owner no longer exists or could not be determined (yet).
|
||||
*
|
||||
* @param Builder<Audit> $builder
|
||||
*/
|
||||
public function scopeWhereMissingOwner(Builder $builder): void
|
||||
{
|
||||
$builder->whereNull('owner_organization_id')
|
||||
->whereNull('owner_user_id')
|
||||
->whereNotIn('auditable_type', self::getAuditableTypesWithoutOwner());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Marks an auditable model whose audits are owned by the owner of its parent model,
|
||||
* because the model itself has no organization_id (for example a project member belongs to an organization via its project).
|
||||
* The parent model has to have an organization_id column.
|
||||
* This is used to set the owner of new audits (CustomAuditable) and to backfill the owner of existing audits.
|
||||
*/
|
||||
interface AuditableThroughParent
|
||||
{
|
||||
/**
|
||||
* @return BelongsTo<covariant Model, covariant Model>
|
||||
*/
|
||||
public function getAuditParentRelation(): BelongsTo;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
/**
|
||||
* Marks an auditable model whose audits intentionally have no owner (neither an organization nor a user),
|
||||
* either because the model belongs to neither of them, or because the model and its audits have to be kept
|
||||
* when its organization or user is deleted (for example billing records).
|
||||
* Audits of these models are not deleted together with an organization or user,
|
||||
* and are not considered as missing an owner (for example by the audit backfill command).
|
||||
*/
|
||||
interface AuditableWithoutOwner {}
|
||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use OwenIt\Auditing\Auditable;
|
||||
|
||||
trait CustomAuditable
|
||||
@@ -21,92 +19,4 @@ trait CustomAuditable
|
||||
{
|
||||
$this->auditEvents = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The organization that owns the audited model.
|
||||
* The audits of the model are deleted (via foreign key cascade) when the organization is deleted.
|
||||
*/
|
||||
public function getAuditOwnerOrganizationId(): ?string
|
||||
{
|
||||
if ($this instanceof AuditableThroughParent) {
|
||||
$relation = $this->getAuditParentRelation();
|
||||
$parentId = $this->getAttributes()[$relation->getForeignKeyName()] ?? null;
|
||||
if ($parentId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Note: The loaded parent is only used if it still matches the foreign key and was loaded with the organization
|
||||
$relationName = $relation->getRelationName();
|
||||
if ($this->relationLoaded($relationName)) {
|
||||
$parent = $this->getRelation($relationName);
|
||||
if ($parent instanceof Model
|
||||
&& $parent->getAttribute($relation->getOwnerKeyName()) === $parentId
|
||||
&& array_key_exists('organization_id', $parent->getAttributes())) {
|
||||
/** @var string|null $organizationId */
|
||||
$organizationId = $parent->getAttributes()['organization_id'];
|
||||
|
||||
return $organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var string|null $organizationId */
|
||||
$organizationId = $relation->getRelated()->newQuery()
|
||||
->toBase()
|
||||
->where($relation->getOwnerKeyName(), $parentId)
|
||||
->value('organization_id');
|
||||
|
||||
return $organizationId;
|
||||
}
|
||||
|
||||
return $this->getAttributes()['organization_id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user that owns the audited model.
|
||||
* The audits of the model are deleted (via foreign key cascade) when the user is deleted.
|
||||
*/
|
||||
public function getAuditOwnerUserId(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Models that are the owner of their own audits can not record the deletion audit,
|
||||
* since the audit would reference the already deleted model and therefore violate the foreign key.
|
||||
*/
|
||||
protected function isAuditOwnerOfItself(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, string>
|
||||
*/
|
||||
public function getAuditEvents(): array
|
||||
{
|
||||
$events = $this->auditEvents ?? Config::get('audit.events', [
|
||||
'created',
|
||||
'updated',
|
||||
'deleted',
|
||||
'restored',
|
||||
]);
|
||||
|
||||
if ($this->isAuditOwnerOfItself()) {
|
||||
$events = array_filter($events, fn (string $value, int|string $key): bool => (is_int($key) ? $value : $key) !== 'deleted', ARRAY_FILTER_USE_BOTH);
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function transformAudit(array $data): array
|
||||
{
|
||||
$data['owner_organization_id'] = $this->getAuditOwnerOrganizationId();
|
||||
$data['owner_user_id'] = $this->getAuditOwnerUserId();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
@@ -96,20 +97,10 @@ class Organization extends Model implements AuditableContract
|
||||
protected $attributes = [
|
||||
];
|
||||
|
||||
public function getAuditOwnerOrganizationId(): ?string
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
protected function isAuditOwnerOfItself(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users that belong to the team.
|
||||
*
|
||||
* @return BelongsToMany<User, $this, Member, 'membership'>
|
||||
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
@@ -142,7 +133,7 @@ class Organization extends Model implements AuditableContract
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<User, $this, Member, 'membership'>
|
||||
* @return BelongsToMany<User, $this, Pivot, 'membership'>
|
||||
*/
|
||||
public function realUsers(): BelongsToMany
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\AuditableThroughParent;
|
||||
use App\Models\Concerns\CustomAuditable;
|
||||
use App\Models\Concerns\HasUuids;
|
||||
use Database\Factories\ProjectMemberFactory;
|
||||
@@ -30,7 +29,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @method static Builder<ProjectMember> whereBelongsToOrganization(Organization $organization)
|
||||
* @method static ProjectMemberFactory factory()
|
||||
*/
|
||||
class ProjectMember extends Model implements AuditableContract, AuditableThroughParent
|
||||
class ProjectMember extends Model implements AuditableContract
|
||||
{
|
||||
use CustomAuditable;
|
||||
|
||||
@@ -83,12 +82,4 @@ class ProjectMember extends Model implements AuditableContract, AuditableThrough
|
||||
$query->whereBelongsTo($organization, 'organization');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Project, $this>
|
||||
*/
|
||||
public function getAuditParentRelation(): BelongsTo
|
||||
{
|
||||
return $this->project();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -124,16 +125,6 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
'send_time_entry_still_running_email' => true,
|
||||
];
|
||||
|
||||
public function getAuditOwnerUserId(): ?string
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
protected function isAuditOwnerOfItself(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to the user's profile photo.
|
||||
*
|
||||
@@ -184,7 +175,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Organization, $this, Member, 'membership'>
|
||||
* @return BelongsToMany<Organization, $this, Pivot, 'membership'>
|
||||
*/
|
||||
public function organizations(): BelongsToMany
|
||||
{
|
||||
@@ -199,7 +190,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Organization, $this, Member, 'membership'>
|
||||
* @return BelongsToMany<Organization, $this, Pivot, 'membership'>
|
||||
*/
|
||||
public function ownedOrganizations(): BelongsToMany
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\Filament\Widgets\ServerOverview;
|
||||
use App\Filament\Widgets\TimeEntriesCreated;
|
||||
use App\Filament\Widgets\TimeEntriesImported;
|
||||
use App\Filament\Widgets\UserRegistrations;
|
||||
use App\Http\Middleware\AuthenticateFilamentPanel;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\NavigationGroup;
|
||||
@@ -19,7 +19,7 @@ use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
@@ -71,9 +71,6 @@ class AdminPanelProvider extends PanelProvider
|
||||
NavigationGroup::make()
|
||||
->label('Auth')
|
||||
->collapsed(),
|
||||
NavigationGroup::make()
|
||||
->label('Self Hosting')
|
||||
->collapsed(),
|
||||
])
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
@@ -81,13 +78,13 @@ class AdminPanelProvider extends PanelProvider
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
PreventRequestForgery::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
AuthenticateFilamentPanel::class,
|
||||
Authenticate::class,
|
||||
]);
|
||||
|
||||
$modules = Module::allEnabled();
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Brick\Money\IsoCurrencyProvider;
|
||||
use Brick\Money\ISOCurrencyProvider;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Translation\PotentiallyTranslatedString;
|
||||
@@ -24,7 +24,7 @@ class CurrencyRule implements ValidationRule
|
||||
return;
|
||||
}
|
||||
|
||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
if (array_key_exists($value, $currencies)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,18 +15,12 @@ use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Log;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use Throwable;
|
||||
|
||||
class ApiService
|
||||
{
|
||||
private const string API_URL = 'https://app.solidtime.io/api/v1';
|
||||
|
||||
private function isPremium(): bool
|
||||
{
|
||||
return Module::has('Invoicing');
|
||||
}
|
||||
|
||||
public function checkForUpdate(): ?string
|
||||
{
|
||||
try {
|
||||
@@ -37,7 +31,6 @@ class ApiService
|
||||
'version' => config('app.version'),
|
||||
'build' => config('app.build'),
|
||||
'url' => config('app.url'),
|
||||
'is_premium' => $this->isPremium(),
|
||||
]);
|
||||
|
||||
if ($response->status() === 200 && isset($response->json()['version']) && is_string($response->json()['version'])) {
|
||||
@@ -69,7 +62,6 @@ class ApiService
|
||||
'version' => config('app.version'),
|
||||
'build' => config('app.build'),
|
||||
'url' => config('app.url'),
|
||||
'is_premium' => $this->isPremium(),
|
||||
// telemetry data
|
||||
'user_count' => User::count(),
|
||||
'organization_count' => Organization::count(),
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Brick\Money\IsoCurrencyProvider;
|
||||
use Brick\Money\ISOCurrencyProvider;
|
||||
use Brick\Money\Money;
|
||||
|
||||
class CurrencyService
|
||||
@@ -378,7 +378,7 @@ class CurrencyService
|
||||
|
||||
public function getRandomCurrencyCode(): string
|
||||
{
|
||||
$currencies = IsoCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$currencies = ISOCurrencyProvider::getInstance()->getAvailableCurrencies();
|
||||
$currencyCodes = array_keys($currencies);
|
||||
|
||||
return $currencyCodes[array_rand($currencyCodes)];
|
||||
|
||||
@@ -152,13 +152,12 @@ class DashboardService
|
||||
|
||||
$possibleDays = $this->lastDays($days, $timezone);
|
||||
|
||||
// The SQL fragment contains only a timezone offset calculated as an integer.
|
||||
$query = TimeEntry::query()
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) // @phpstan-ignore argument.type
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
|
||||
->where('user_id', '=', $user->getKey())
|
||||
->where('organization_id', '=', $organization->getKey())
|
||||
->workTime()
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) // @phpstan-ignore argument.type
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
|
||||
->orderBy('date');
|
||||
|
||||
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
|
||||
@@ -195,13 +194,12 @@ class DashboardService
|
||||
}
|
||||
$possibleDays = $this->daysOfThisWeek($timezone, $user->week_start);
|
||||
|
||||
// The SQL fragment contains only a timezone offset calculated as an integer.
|
||||
$query = TimeEntry::query()
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate')) // @phpstan-ignore argument.type
|
||||
->select(DB::raw('DATE('.$dateWithTimeZone.') as date, round(sum(extract(epoch from (coalesce("end", now()) - start)))) as aggregate'))
|
||||
->where('user_id', '=', $user->getKey())
|
||||
->where('organization_id', '=', $organization->getKey())
|
||||
->workTime()
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')')) // @phpstan-ignore argument.type
|
||||
->groupBy(DB::raw('DATE('.$dateWithTimeZone.')'))
|
||||
->orderBy('date');
|
||||
|
||||
$query = $this->constrainDateByPossibleDates($query, $possibleDays, $timezone);
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\OrganizationInvitation;
|
||||
use App\Models\Passport\RefreshToken;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectMember;
|
||||
use App\Models\Report;
|
||||
@@ -170,10 +169,6 @@ class DeletionService
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh tokens are not linked to the user directly, so they need to be deleted via their access tokens.
|
||||
// Otherwise a still-valid refresh token could be used to issue a new access token for a deleted user,
|
||||
// which fails with a foreign key violation on oauth_access_tokens.user_id.
|
||||
RefreshToken::query()->whereIn('access_token_id', $user->accessTokens()->pluck('id'))->delete();
|
||||
$user->accessTokens()->delete();
|
||||
$user->authCodes()->delete();
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class ClockifyProjectsImporter extends DefaultImporter
|
||||
'organization_id' => $this->organization->id,
|
||||
], [
|
||||
'color' => $this->colorService->getRandomColor(),
|
||||
'is_billable' => ($record['Billability'] ?? '') === 'Yes',
|
||||
'is_billable' => $record['Billability'] === 'Yes',
|
||||
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
|
||||
'estimated_time' => isset($record['Estimated (h)']) && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null,
|
||||
'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null,
|
||||
@@ -55,7 +55,6 @@ class ClockifyProjectsImporter extends DefaultImporter
|
||||
if ($tasksKey !== null && $record[$tasksKey] !== '') {
|
||||
$tasks = explode(', ', $record[$tasksKey]);
|
||||
foreach ($tasks as $task) {
|
||||
$this->checkTaskNameLength($task);
|
||||
$this->taskImportHelper->getKey([
|
||||
'name' => $task,
|
||||
'project_id' => $projectId,
|
||||
@@ -85,6 +84,7 @@ class ClockifyProjectsImporter extends DefaultImporter
|
||||
'Project',
|
||||
'Status',
|
||||
'Visibility',
|
||||
'Billability',
|
||||
];
|
||||
foreach ($requiredFields as $requiredField) {
|
||||
if (! in_array($requiredField, $header, true)) {
|
||||
|
||||
@@ -104,7 +104,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
}
|
||||
$taskId = null;
|
||||
if (! $isBreak && $taskKey !== null && $record[$taskKey] !== '') {
|
||||
$this->checkTaskNameLength($record[$taskKey]);
|
||||
$taskId = $this->taskImportHelper->getKey([
|
||||
'name' => $record[$taskKey],
|
||||
'project_id' => $projectId,
|
||||
@@ -229,6 +228,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
'Project',
|
||||
'Description',
|
||||
'User',
|
||||
'Group',
|
||||
'Email',
|
||||
'Tags',
|
||||
'Start Date',
|
||||
|
||||
@@ -22,8 +22,6 @@ use Override;
|
||||
|
||||
abstract class DefaultImporter implements ImporterContract
|
||||
{
|
||||
protected const TASK_NAME_MAX_LENGTH = 500;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
/**
|
||||
@@ -184,16 +182,6 @@ abstract class DefaultImporter implements ImporterContract
|
||||
$this->billableRateService = app(BillableRateService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ImportException
|
||||
*/
|
||||
protected function checkTaskNameLength(string $taskName): void
|
||||
{
|
||||
if (strlen($taskName) > self::TASK_NAME_MAX_LENGTH) {
|
||||
throw new ImportException('Task name ("'.$taskName.'") is too long, maximum length is '.self::TASK_NAME_MAX_LENGTH.' characters');
|
||||
}
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getReport(): ReportDto
|
||||
{
|
||||
|
||||
@@ -114,7 +114,6 @@ class GenericTimeEntriesImporter extends DefaultImporter
|
||||
}
|
||||
$taskId = null;
|
||||
if ($record['task'] !== '') {
|
||||
$this->checkTaskNameLength($record['task']);
|
||||
$taskId = $this->taskImportHelper->getKey([
|
||||
'name' => $record['task'],
|
||||
'project_id' => $projectId,
|
||||
|
||||
@@ -92,7 +92,6 @@ class HarvestTimeEntriesImporter extends DefaultImporter
|
||||
}
|
||||
$taskId = null;
|
||||
if ($record['Task'] !== '') {
|
||||
$this->checkTaskNameLength($record['Task']);
|
||||
$taskId = $this->taskImportHelper->getKey([
|
||||
'name' => $record['Task'],
|
||||
'project_id' => $projectId,
|
||||
|
||||
@@ -199,7 +199,6 @@ class SolidtimeImporter extends DefaultImporter
|
||||
if ($projectId === null) {
|
||||
throw new Exception('Project does not exist');
|
||||
}
|
||||
$this->checkTaskNameLength($task['name']);
|
||||
$this->taskImportHelper->getKey([
|
||||
'name' => $task['name'],
|
||||
'project_id' => $projectId,
|
||||
|
||||
@@ -153,16 +153,9 @@ class TogglDataImporter extends DefaultImporter
|
||||
}
|
||||
foreach ($projectMembers as $projectMember) {
|
||||
$userId = $this->userImportHelper->getKeyByExternalIdentifier((string) $projectMember->user_id);
|
||||
if ($userId === null) {
|
||||
throw new Exception('User does not exist');
|
||||
}
|
||||
$memberId = $this->memberImportHelper->getKeyByExternalIdentifier($userId);
|
||||
if ($memberId === null) {
|
||||
throw new Exception('Member does not exist');
|
||||
}
|
||||
$this->projectMemberImportHelper->getKey([
|
||||
'project_id' => $projectId,
|
||||
'member_id' => $memberId,
|
||||
'member_id' => $this->memberImportHelper->getKeyByExternalIdentifier($userId),
|
||||
], [
|
||||
'user_id' => $userId,
|
||||
'billable_rate' => $projectMember->rate !== null ? (int) ($projectMember->rate * 100) : null,
|
||||
@@ -189,7 +182,6 @@ class TogglDataImporter extends DefaultImporter
|
||||
if ($projectId === null) {
|
||||
throw new Exception('Project does not exist');
|
||||
}
|
||||
$this->checkTaskNameLength($task->name);
|
||||
$this->taskImportHelper->getKey([
|
||||
'name' => $task->name,
|
||||
'project_id' => $projectId,
|
||||
|
||||
@@ -98,7 +98,6 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
||||
}
|
||||
$taskId = null;
|
||||
if ($record['Task'] !== '') {
|
||||
$this->checkTaskNameLength($record['Task']);
|
||||
$taskId = $this->taskImportHelper->getKey([
|
||||
'name' => $record['Task'],
|
||||
'project_id' => $projectId,
|
||||
|
||||
@@ -75,9 +75,8 @@ class TimeEntryAggregationService
|
||||
$startRawSelect = app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes);
|
||||
$endRawSelect = app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes);
|
||||
|
||||
// SQL fragments come from enum cases, integer offsets, and formatted dates.
|
||||
$timeEntriesQuery->selectRaw(
|
||||
($group1Select !== null ? $group1Select.' as group_1,' : ''). // @phpstan-ignore argument.type
|
||||
($group1Select !== null ? $group1Select.' as group_1,' : '').
|
||||
($group2Select !== null ? $group2Select.' as group_2,' : '').
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||
@@ -106,7 +105,7 @@ class TimeEntryAggregationService
|
||||
$baseTotalsPerGroup1Query = $baseTotalsQuery->clone();
|
||||
$baseTotalsPerGroup1 = $baseTotalsPerGroup1Query
|
||||
->selectRaw(
|
||||
$group1Select.' as group_1,'. // @phpstan-ignore argument.type
|
||||
$group1Select.' as group_1,'.
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||
)
|
||||
@@ -171,7 +170,7 @@ class TimeEntryAggregationService
|
||||
// Reset selects and ordering on the cloned base query
|
||||
$baseTotals = $baseTotalsQuery
|
||||
->selectRaw(
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'. // @phpstan-ignore argument.type
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')))) as aggregate,'.
|
||||
' round(sum(extract(epoch from ('.$endRawSelect.' - '.$startRawSelect.')) * (coalesce(billable_rate, 0)::float/60/60))) as cost'
|
||||
)
|
||||
->first();
|
||||
|
||||
@@ -7,22 +7,22 @@
|
||||
"require": {
|
||||
"php": "8.3.*",
|
||||
"ext-zip": "*",
|
||||
"brick/money": "^0.15.1",
|
||||
"brick/money": "^0.11.2",
|
||||
"datomatic/laravel-enum-helper": "^2.0.0",
|
||||
"dedoc/scramble": "^0.13.43",
|
||||
"filament/filament": "^5.0",
|
||||
"flowframe/laravel-trend": "^0.5.0",
|
||||
"gotenberg/gotenberg-php": "^2.8",
|
||||
"guzzlehttp/guzzle": "^8.2",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"inertiajs/inertia-laravel": "^3.3.4",
|
||||
"korridor/laravel-computed-attributes": "^3.1",
|
||||
"korridor/laravel-has-many-sync": "^3.1",
|
||||
"korridor/laravel-model-validation-rules": "^3.0",
|
||||
"laravel/fortify": "^1.37",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/framework": "^12.19.3",
|
||||
"laravel/octane": "^2.3",
|
||||
"laravel/passport": "^13.0.5",
|
||||
"laravel/tinker": "^3.0",
|
||||
"laravel/tinker": "^2.8",
|
||||
"league/csv": "^9.16.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"league/iso3166": "^4.3",
|
||||
|
||||
749
composer.lock
generated
749
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,9 @@ return [
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| This value is the name of your application. This value is used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
| any other location as required by the application or its packages.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -110,8 +110,8 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
| will be used by the PHP date and date-time functions. We have gone
|
||||
| ahead and set this to a sensible default for you out of the box.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -123,8 +123,8 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
| by the translation service provider. You are free to set this value
|
||||
| to any of the locales which will be supported by the application.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -178,21 +178,15 @@ return [
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
| This key is used by the Illuminate encrypter service and should be set
|
||||
| to a random, 32 character string, otherwise these encrypted strings
|
||||
| will not be safe. Please do this before deploying an application!
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'localization' => [
|
||||
'default_currency' => env('LOCALIZATION_DEFAULT_CURRENCY', 'EUR'),
|
||||
@@ -212,13 +206,13 @@ return [
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache", "array"
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
'driver' => 'file',
|
||||
// 'store' => 'redis',
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -32,7 +32,7 @@ return [
|
||||
*/
|
||||
|
||||
'user' => [
|
||||
'morph_prefix' => 'actor',
|
||||
'morph_prefix' => 'user',
|
||||
'guards' => [
|
||||
'web',
|
||||
'api',
|
||||
|
||||
@@ -10,8 +10,8 @@ return [
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| This option controls the default authentication "guard" and password
|
||||
| reset options for your application. You may change these defaults
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
@@ -28,11 +28,11 @@ return [
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
| here which uses session storage and the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| All authentication drivers have a user provider. This defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
| mechanisms used by this application to persist your user's data.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
@@ -55,12 +55,12 @@ return [
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| All authentication drivers have a user provider. This defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
| mechanisms used by this application to persist your user's data.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| sources which represent each model / table. These sources may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
@@ -80,9 +80,9 @@ return [
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
| You may specify multiple password reset configurations if you have more
|
||||
| than one user table or model in the application and you want to have
|
||||
| separate password reset settings based on the specific user types.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
@@ -108,13 +108,13 @@ return [
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| Here you may define the amount of seconds before a password confirmation
|
||||
| times out and the user is prompted to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
'password_timeout' => 10800,
|
||||
|
||||
'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')),
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ return [
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
| This option controls the default cache connection that gets used while
|
||||
| using this caching library. This connection is used when another is
|
||||
| not explicitly specified when executing a given caching function.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -28,9 +28,8 @@ return [
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "storage", "octane",
|
||||
| "session", "failover", "null"
|
||||
| Supported drivers: "apc", "array", "database", "file",
|
||||
| "memcached", "redis", "dynamodb", "octane", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -47,10 +46,9 @@ return [
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
'table' => 'cache',
|
||||
'connection' => null,
|
||||
'lock_connection' => null,
|
||||
],
|
||||
|
||||
'file' => [
|
||||
@@ -59,12 +57,6 @@ return [
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'storage' => [
|
||||
'driver' => 'storage',
|
||||
'disk' => env('CACHE_STORAGE_DISK'),
|
||||
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
@@ -86,8 +78,8 @@ return [
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
'connection' => 'cache',
|
||||
'lock_connection' => 'default',
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
@@ -103,14 +95,6 @@ return [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -118,24 +102,12 @@ return [
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| When utilizing the APC, database, memcached, Redis, or DynamoDB cache
|
||||
| stores there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Serializable Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the classes that can be unserialized from cache
|
||||
| storage. By default, no PHP classes will be unserialized from your
|
||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||
|
|
||||
*/
|
||||
|
||||
'serializable_classes' => false,
|
||||
];
|
||||
|
||||
@@ -12,22 +12,26 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
| to use as your default connection for all database work. Of course
|
||||
| you may use many connections at once using the Database library.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'pgsql'),
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
| Here are each of the database connections setup for your application.
|
||||
| Of course, examples of configuring each database platform that is
|
||||
| supported by Laravel is shown below to make development simple.
|
||||
|
|
||||
|
|
||||
| All database work in Laravel is done through the PHP PDO facilities
|
||||
| so make sure you have the driver for your particular database of
|
||||
| choice installed on your machine before you begin development.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -35,34 +39,50 @@ return [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL', env('DATABASE_URL')),
|
||||
'url' => env('DATABASE_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL', env('DATABASE_URL')),
|
||||
'url' => env('DATABASE_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSL_MODE', 'prefer'),
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'pgsql_test' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL', env('DATABASE_URL')),
|
||||
'url' => env('DATABASE_URL'),
|
||||
'host' => env('DB_TEST_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_TEST_PORT', '5432'),
|
||||
'database' => env('DB_TEST_DATABASE', 'forge'),
|
||||
@@ -72,18 +92,18 @@ return [
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSL_MODE', 'prefer'),
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL', env('DATABASE_URL')),
|
||||
'url' => env('DATABASE_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
@@ -99,14 +119,11 @@ return [
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
| the migrations on disk haven't actually been run in the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => false,
|
||||
],
|
||||
'migrations' => 'migrations',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -115,7 +132,7 @@ return [
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -126,7 +143,6 @@ return [
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
@@ -136,10 +152,6 @@ return [
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
@@ -149,10 +161,6 @@ return [
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
@@ -11,7 +11,7 @@ return [
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
| based disks are available to your application. Just store away!
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -26,11 +26,11 @@ return [
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
| Here you may configure as many filesystem "disks" as you wish, and you
|
||||
| may even configure multiple disks of the same driver. Defaults have
|
||||
| been set up for each driver as an example of the required values.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
| Supported Drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
@@ -41,16 +41,14 @@ return [
|
||||
'root' => storage_path('app'),
|
||||
'serve' => true,
|
||||
'throw' => true,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => true,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
@@ -64,7 +62,6 @@ return [
|
||||
'endpoint' => env('S3_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('S3_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => true,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'testfiles' => [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user