mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-09 22:33:18 +01:00
Compare commits
1 Commits
main
...
feature/op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5e5e915ab |
@@ -1,54 +0,0 @@
|
|||||||
.git
|
|
||||||
**/.git
|
|
||||||
.gitmodules
|
|
||||||
**/.gitmodules
|
|
||||||
.github
|
|
||||||
.DS_Store
|
|
||||||
.fleet
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.log
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
k8s
|
|
||||||
docs
|
|
||||||
e2e
|
|
||||||
tests
|
|
||||||
|
|
||||||
docker-compose.yml
|
|
||||||
docker/local
|
|
||||||
|
|
||||||
.phpunit.cache
|
|
||||||
.phpunit.result.cache
|
|
||||||
coverage
|
|
||||||
test-results
|
|
||||||
playwright-report
|
|
||||||
blob-report
|
|
||||||
playwright/.cache
|
|
||||||
openapi.json
|
|
||||||
playwright
|
|
||||||
playwright.config.ts
|
|
||||||
vitest.config.ts
|
|
||||||
phpunit.xml
|
|
||||||
phpstan.neon
|
|
||||||
pint.json
|
|
||||||
eslint.config.mjs
|
|
||||||
tsconfig.json
|
|
||||||
jsconfig.json
|
|
||||||
postcss.config.js
|
|
||||||
tailwind.config.js
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
extensions/*/node_modules
|
|
||||||
|
|
||||||
Homestead.json
|
|
||||||
Homestead.yaml
|
|
||||||
auth.json
|
|
||||||
.env.backup
|
|
||||||
.rnd
|
|
||||||
|
|
||||||
_ide_helper.php
|
|
||||||
.phpstorm.meta.php
|
|
||||||
|
|
||||||
storage/logs/*
|
|
||||||
storage/*.key
|
|
||||||
4
.env.ci
4
.env.ci
@@ -6,7 +6,6 @@ APP_DEBUG=true
|
|||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
APP_FORCE_HTTPS=false
|
APP_FORCE_HTTPS=false
|
||||||
APP_ENABLE_REGISTRATION=true
|
APP_ENABLE_REGISTRATION=true
|
||||||
SUPER_ADMINS=admin@example.com
|
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
@@ -65,6 +64,3 @@ GOTENBERG_URL=http://localhost:3000
|
|||||||
|
|
||||||
# Octane
|
# Octane
|
||||||
OCTANE_SERVER=frankenphp
|
OCTANE_SERVER=frankenphp
|
||||||
|
|
||||||
# Inertia
|
|
||||||
INERTIA_SSR_ENABLED=false
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
|
|||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=https://solidtime.test
|
APP_URL=https://solidtime.test
|
||||||
APP_FORCE_HTTPS=false
|
APP_FORCE_HTTPS=false
|
||||||
# Supported values: on, off, invite-only (true/false are supported for backwards compatibility)
|
APP_ENABLE_REGISTRATION=true
|
||||||
APP_ENABLE_REGISTRATION=on
|
|
||||||
SUPER_ADMINS=admin@example.com
|
SUPER_ADMINS=admin@example.com
|
||||||
PAGINATION_PER_PAGE_DEFAULT=500
|
PAGINATION_PER_PAGE_DEFAULT=500
|
||||||
|
|
||||||
@@ -88,6 +87,3 @@ FORWARD_DB_PORT=54329
|
|||||||
VITE_HOST_NAME=vite.solidtime.test
|
VITE_HOST_NAME=vite.solidtime.test
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
#SAIL_XDEBUG_MODE=develop,debug,coverage
|
#SAIL_XDEBUG_MODE=develop,debug,coverage
|
||||||
|
|
||||||
# This app does not ship a Node SSR bundle, so Inertia SSR must stay disabled.
|
|
||||||
INERTIA_SSR_ENABLED=false
|
|
||||||
|
|||||||
@@ -13,5 +13,3 @@ LOG_DEPRECATIONS_CHANNEL=null
|
|||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=pgsql
|
DB_CONNECTION=pgsql
|
||||||
|
|
||||||
INERTIA_SSR_ENABLED=false
|
|
||||||
|
|||||||
27
.github/VOUCHED.td
vendored
27
.github/VOUCHED.td
vendored
@@ -1,27 +0,0 @@
|
|||||||
# Vouched contributors for solidtime.
|
|
||||||
#
|
|
||||||
# One handle per line, without the leading @, sorted alphabetically.
|
|
||||||
# Prefix a handle with - to denounce them, optionally followed by a reason.
|
|
||||||
# Format reference: https://github.com/mitchellh/vouch
|
|
||||||
#
|
|
||||||
# Maintainers do not need to edit this file by hand. Comment "vouch @user",
|
|
||||||
# "unvouch @user" or "denounce @user <reason>" on any issue, pull request or
|
|
||||||
# discussion and the vouch workflows will update this file.
|
|
||||||
#
|
|
||||||
# Collaborators with write access and bots are always allowed and do not need
|
|
||||||
# an entry here.
|
|
||||||
#
|
|
||||||
# Seeded 2026-07-25 from the authors of every merged pull request.
|
|
||||||
|
|
||||||
agross
|
|
||||||
akolenda
|
|
||||||
bufferhead-code
|
|
||||||
candideu
|
|
||||||
kasparrosin
|
|
||||||
korridor
|
|
||||||
nikbucher tasks table sorting
|
|
||||||
onatcer
|
|
||||||
shrootbuck
|
|
||||||
smilebeda
|
|
||||||
thespyder
|
|
||||||
utlark
|
|
||||||
62
.github/workflows/build-onpremise.yml
vendored
62
.github/workflows/build-onpremise.yml
vendored
@@ -8,8 +8,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-onpremise.yml'
|
- '.github/workflows/build-onpremise.yml'
|
||||||
- '.dockerignore'
|
|
||||||
- 'extensions/manifest.json'
|
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -37,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Check out code"
|
- name: "Check out code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
||||||
|
|
||||||
@@ -48,9 +46,9 @@ jobs:
|
|||||||
- name: "Get Previous tag (normal push)"
|
- name: "Get Previous tag (normal push)"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v2"
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
with:
|
with:
|
||||||
pattern: "v*[0-9].*[0-9].*[0-9]"
|
prefix: "v"
|
||||||
|
|
||||||
- name: "Get version"
|
- name: "Get version"
|
||||||
id: release-version
|
id: release-version
|
||||||
@@ -93,25 +91,14 @@ jobs:
|
|||||||
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
|
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
- name: "Read extension manifest"
|
|
||||||
id: extension-manifest
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
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"
|
- name: "Checkout invoicing extension"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
|
repository: solidtime-io/extension-invoicing
|
||||||
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
|
|
||||||
path: extensions/Invoicing
|
path: extensions/Invoicing
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
||||||
|
|
||||||
@@ -121,26 +108,9 @@ jobs:
|
|||||||
- name: "Install npm dependencies in invoicing extension"
|
- name: "Install npm dependencies in invoicing extension"
|
||||||
run: cd extensions/Invoicing && npm ci
|
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"
|
- name: "Activate invoicing extension"
|
||||||
run: php artisan module:enable Invoicing
|
run: php artisan module:enable Invoicing
|
||||||
|
|
||||||
- name: "Activate auditing extension"
|
|
||||||
run: php artisan module:enable Auditing
|
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
- name: "Install npm dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -154,27 +124,27 @@ jobs:
|
|||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: "meta"
|
id: "meta"
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_REPO }}
|
${{ env.DOCKER_REPO }}
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
password: ${{ secrets.ONPREMISE_TOKEN }}
|
password: ${{ secrets.ONPREMISE_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up QEMU"
|
- name: "Set up QEMU"
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: "Build and push by digest"
|
- name: "Build and push by digest"
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/prod/Dockerfile
|
file: docker/prod/Dockerfile
|
||||||
@@ -193,7 +163,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: "Upload digest"
|
- name: "Upload digest"
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -207,25 +177,25 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: "Download digests"
|
- name: "Download digests"
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
password: ${{ secrets.ONPREMISE_TOKEN }}
|
password: ${{ secrets.ONPREMISE_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_REPO }}
|
${{ env.DOCKER_REPO }}
|
||||||
|
|||||||
73
.github/workflows/build-private.yml
vendored
73
.github/workflows/build-private.yml
vendored
@@ -8,8 +8,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-private.yml'
|
- '.github/workflows/build-private.yml'
|
||||||
- '.dockerignore'
|
|
||||||
- 'extensions/manifest.json'
|
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
permissions:
|
permissions:
|
||||||
@@ -24,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Check out code"
|
- name: "Check out code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
||||||
|
|
||||||
@@ -35,9 +33,9 @@ jobs:
|
|||||||
- name: "Get Previous tag (normal push)"
|
- name: "Get Previous tag (normal push)"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v2"
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
with:
|
with:
|
||||||
pattern: "v*[0-9].*[0-9].*[0-9]"
|
prefix: "v"
|
||||||
|
|
||||||
- name: "Get version"
|
- name: "Get version"
|
||||||
id: version
|
id: version
|
||||||
@@ -70,36 +68,21 @@ jobs:
|
|||||||
run: cat .env
|
run: cat .env
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
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)"
|
|
||||||
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"
|
- name: "Checkout billing extension"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: ${{ steps.extension-manifest.outputs.billing_repository }}
|
repository: solidtime-io/extension-billing
|
||||||
ref: ${{ steps.extension-manifest.outputs.billing_ref }}
|
|
||||||
path: extensions/Billing
|
path: extensions/Billing
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
|
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }}
|
||||||
|
|
||||||
- name: "Install dependencies in billing extension"
|
- name: "Install dependencies in billing extension"
|
||||||
uses: php-actions/composer@v6
|
uses: php-actions/composer@v6
|
||||||
env:
|
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:
|
with:
|
||||||
working_dir: "extensions/Billing"
|
working_dir: "extensions/Billing"
|
||||||
command: install
|
command: install
|
||||||
@@ -110,10 +93,9 @@ jobs:
|
|||||||
run: cd extensions/Billing && npm ci
|
run: cd extensions/Billing && npm ci
|
||||||
|
|
||||||
- name: "Checkout services extension"
|
- name: "Checkout services extension"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: ${{ steps.extension-manifest.outputs.services_repository }}
|
repository: solidtime-io/extension-services
|
||||||
ref: ${{ steps.extension-manifest.outputs.services_ref }}
|
|
||||||
path: extensions/Services
|
path: extensions/Services
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
|
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }}
|
||||||
|
|
||||||
@@ -129,10 +111,9 @@ jobs:
|
|||||||
run: cd extensions/Services && npm ci
|
run: cd extensions/Services && npm ci
|
||||||
|
|
||||||
- name: "Checkout invoicing extension"
|
- name: "Checkout invoicing extension"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: ${{ steps.extension-manifest.outputs.invoicing_repository }}
|
repository: solidtime-io/extension-invoicing
|
||||||
ref: ${{ steps.extension-manifest.outputs.invoicing_ref }}
|
|
||||||
path: extensions/Invoicing
|
path: extensions/Invoicing
|
||||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }}
|
||||||
|
|
||||||
@@ -147,25 +128,6 @@ jobs:
|
|||||||
- name: "Install npm dependencies in invoicing extension"
|
- name: "Install npm dependencies in invoicing extension"
|
||||||
run: cd extensions/Invoicing && npm ci
|
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"
|
- name: "Setup PHP with PECL extension"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -189,9 +151,6 @@ jobs:
|
|||||||
- name: "Activate invoicing extension"
|
- name: "Activate invoicing extension"
|
||||||
run: php artisan module:enable Invoicing
|
run: php artisan module:enable Invoicing
|
||||||
|
|
||||||
- name: "Activate auditing extension"
|
|
||||||
run: php artisan module:enable Auditing
|
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
- name: "Install npm dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -201,7 +160,7 @@ jobs:
|
|||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: rg.fr-par.scw.cloud/solidtime
|
registry: rg.fr-par.scw.cloud/solidtime
|
||||||
username: nologin
|
username: nologin
|
||||||
@@ -209,7 +168,7 @@ jobs:
|
|||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: "meta"
|
id: "meta"
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: rg.fr-par.scw.cloud/solidtime/solidtime
|
images: rg.fr-par.scw.cloud/solidtime/solidtime
|
||||||
tags: |
|
tags: |
|
||||||
@@ -220,13 +179,13 @@ jobs:
|
|||||||
type=sha,format=long
|
type=sha,format=long
|
||||||
|
|
||||||
- name: "Set up QEMU"
|
- name: "Set up QEMU"
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: "Build and push"
|
- name: "Build and push"
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
build-args: |
|
build-args: |
|
||||||
|
|||||||
33
.github/workflows/build-public.yml
vendored
33
.github/workflows/build-public.yml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-public.yml'
|
- '.github/workflows/build-public.yml'
|
||||||
- '.dockerignore'
|
|
||||||
- 'docker/prod/**'
|
- 'docker/prod/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Check out code"
|
- name: "Check out code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
|
||||||
|
|
||||||
@@ -48,9 +47,9 @@ jobs:
|
|||||||
- name: "Get Previous tag (normal push)"
|
- name: "Get Previous tag (normal push)"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v2"
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
with:
|
with:
|
||||||
pattern: "v*[0-9].*[0-9].*[0-9]"
|
prefix: "v"
|
||||||
|
|
||||||
- name: "Get version"
|
- name: "Get version"
|
||||||
id: release-version
|
id: release-version
|
||||||
@@ -93,7 +92,7 @@ jobs:
|
|||||||
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
|
if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
@@ -110,34 +109,34 @@ jobs:
|
|||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: "meta"
|
id: "meta"
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKERHUB_REPO }}
|
${{ env.DOCKERHUB_REPO }}
|
||||||
${{ env.GHCR_REPO }}
|
${{ env.GHCR_REPO }}
|
||||||
|
|
||||||
- name: "Login to Docker Hub Container Registry"
|
- name: "Login to Docker Hub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up QEMU"
|
- name: "Set up QEMU"
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: "Build and push by digest"
|
- name: "Build and push by digest"
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/prod/Dockerfile
|
file: docker/prod/Dockerfile
|
||||||
@@ -156,7 +155,7 @@ jobs:
|
|||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
- name: "Upload digest"
|
- name: "Upload digest"
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
path: ${{ runner.temp }}/digests/*
|
path: ${{ runner.temp }}/digests/*
|
||||||
@@ -170,31 +169,31 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: "Download digests"
|
- name: "Download digests"
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to Docker Hub"
|
- name: "Login to Docker Hub"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GHCR"
|
- name: "Login to GHCR"
|
||||||
uses: docker/login-action@v4.6.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up Docker Buildx"
|
- name: "Set up Docker Buildx"
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKERHUB_REPO }}
|
${{ env.DOCKERHUB_REPO }}
|
||||||
|
|||||||
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
|
|
||||||
4
.github/workflows/generate-api-docs.yml
vendored
4
.github/workflows/generate-api-docs.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Setup PHP"
|
- name: "Setup PHP"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
run: php artisan scramble:export --path=build/api-docs.json
|
run: php artisan scramble:export --path=build/api-docs.json
|
||||||
|
|
||||||
- name: "Upload API docs to GitHub"
|
- name: "Upload API docs to GitHub"
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: api-docs.json
|
name: api-docs.json
|
||||||
path: build/api-docs.json
|
path: build/api-docs.json
|
||||||
|
|||||||
4
.github/workflows/npm-build.yml
vendored
4
.github/workflows/npm-build.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Setup PHP (for Ziggy)"
|
- name: "Setup PHP (for Ziggy)"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
run: composer install -n --prefer-dist
|
run: composer install -n --prefer-dist
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/npm-format-check.yml
vendored
6
.github/workflows/npm-format-check.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: NPM Format Check
|
name: NPM Format Check
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
format-check:
|
format-check:
|
||||||
@@ -11,10 +9,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/npm-lint.yml
vendored
4
.github/workflows/npm-lint.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/npm-publish-api.yml
vendored
4
.github/workflows/npm-publish-api.yml
vendored
@@ -11,11 +11,11 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- name: Install root project dependencies
|
- name: Install root project dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|||||||
4
.github/workflows/npm-publish-ui.yml
vendored
4
.github/workflows/npm-publish-ui.yml
vendored
@@ -11,9 +11,9 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|||||||
27
.github/workflows/npm-test-unit.yml
vendored
27
.github/workflows/npm-test-unit.yml
vendored
@@ -1,27 +0,0 @@
|
|||||||
name: NPM Test Unit
|
|
||||||
|
|
||||||
on: [push]
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
env:
|
|
||||||
TZ: UTC
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Checkout code"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: "Use Node.js"
|
|
||||||
uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '20.x'
|
|
||||||
|
|
||||||
- name: "Install npm dependencies"
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: "Run vitest"
|
|
||||||
run: npm run test:unit
|
|
||||||
4
.github/workflows/npm-typecheck.yml
vendored
4
.github/workflows/npm-typecheck.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Setup PHP (for Ziggy)"
|
- name: "Setup PHP (for Ziggy)"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
run: composer install -n --prefer-dist
|
run: composer install -n --prefer-dist
|
||||||
|
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/phpstan.yml
vendored
2
.github/workflows/phpstan.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Setup PHP"
|
- name: "Setup PHP"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
|
|||||||
139
.github/workflows/phpunit-extensions.yml
vendored
139
.github/workflows/phpunit-extensions.yml
vendored
@@ -1,139 +0,0 @@
|
|||||||
name: PHPUnit Tests - Extensions
|
|
||||||
on: push
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
|
||||||
phpunit-extensions:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 25
|
|
||||||
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 (extensions)"
|
|
||||||
run: php artisan test extensions/Billing/tests extensions/Services/tests extensions/Invoicing/tests --stop-on-failure
|
|
||||||
|
|
||||||
- name: "Run PHPUnit (core)"
|
|
||||||
run: php artisan test --testsuite=Unit,Feature --stop-on-failure
|
|
||||||
6
.github/workflows/phpunit.yml
vendored
6
.github/workflows/phpunit.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
--health-retries 5
|
--health-retries 5
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Setup PHP"
|
- name: "Setup PHP"
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
- name: "Run composer install"
|
- name: "Run composer install"
|
||||||
run: composer install -n --prefer-dist
|
run: composer install -n --prefer-dist
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
||||||
|
|
||||||
- name: "Upload coverage reports to Codecov"
|
- name: "Upload coverage reports to Codecov"
|
||||||
uses: codecov/codecov-action@v7.1.1
|
uses: codecov/codecov-action@v5.5.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
slug: solidtime-io/solidtime
|
slug: solidtime-io/solidtime
|
||||||
|
|||||||
2
.github/workflows/pint.yml
vendored
2
.github/workflows/pint.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Check code style"
|
- name: "Check code style"
|
||||||
uses: aglipanci/laravel-pint-action@2.6
|
uses: aglipanci/laravel-pint-action@2.6
|
||||||
|
|||||||
185
.github/workflows/playwright-extensions.yml
vendored
185
.github/workflows/playwright-extensions.yml
vendored
@@ -1,185 +0,0 @@
|
|||||||
name: Playwright Tests - Extensions
|
|
||||||
on: push
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: test (${{ matrix.variant }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 60
|
|
||||||
|
|
||||||
# Same extension sets as build-private.yml and build-onpremise.yml, so the extension specs run
|
|
||||||
# with and without Billing.
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- variant: private
|
|
||||||
billing: true
|
|
||||||
extensions: Billing Services Invoicing
|
|
||||||
- variant: onpremise
|
|
||||||
billing: false
|
|
||||||
extensions: Invoicing
|
|
||||||
|
|
||||||
services:
|
|
||||||
mailpit:
|
|
||||||
image: 'axllent/mailpit:latest'
|
|
||||||
ports:
|
|
||||||
- 1025:1025
|
|
||||||
- 8025:8025
|
|
||||||
pgsql_test:
|
|
||||||
image: postgres:15
|
|
||||||
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 node"
|
|
||||||
uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: '20.x'
|
|
||||||
|
|
||||||
- name: "Setup PHP"
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- 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"
|
|
||||||
if: matrix.billing
|
|
||||||
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"
|
|
||||||
if: matrix.billing
|
|
||||||
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"
|
|
||||||
if: matrix.billing
|
|
||||||
run: cd extensions/Billing && npm ci
|
|
||||||
|
|
||||||
- name: "Checkout services extension"
|
|
||||||
if: matrix.billing
|
|
||||||
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"
|
|
||||||
if: matrix.billing
|
|
||||||
working-directory: extensions/Services
|
|
||||||
run: composer install -n --prefer-dist --ignore-platform-reqs
|
|
||||||
|
|
||||||
- name: "Install npm dependencies in services extension"
|
|
||||||
if: matrix.billing
|
|
||||||
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
|
|
||||||
|
|
||||||
# Must run before `migrate` so the extensions' migrations are applied, and before the
|
|
||||||
# frontend build so vite collects the extensions' assets. It also writes
|
|
||||||
# modules_statuses.json, which playwright.config.ts reads to collect the extension specs.
|
|
||||||
- name: "Activate extensions"
|
|
||||||
run: |
|
|
||||||
for extension in ${{ matrix.extensions }}; do
|
|
||||||
php artisan module:enable "$extension"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: "Prepare Laravel Application"
|
|
||||||
run: |
|
|
||||||
cp .env.ci .env
|
|
||||||
php artisan key:generate
|
|
||||||
php artisan passport:keys
|
|
||||||
php artisan migrate --seed
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: "Build Frontend"
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: "Install FrankenPHP"
|
|
||||||
run: |
|
|
||||||
ARCH="$(uname -m)"
|
|
||||||
curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-${ARCH}" -o /usr/local/bin/frankenphp
|
|
||||||
chmod +x /usr/local/bin/frankenphp
|
|
||||||
|
|
||||||
- name: "Run Laravel Octane Server"
|
|
||||||
run: php artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000 --workers=4 --max-requests=500 > /dev/null 2>&1 &
|
|
||||||
env:
|
|
||||||
OCTANE_SERVER: frankenphp
|
|
||||||
|
|
||||||
- name: "Install Playwright Browsers"
|
|
||||||
run: npx playwright install --with-deps
|
|
||||||
|
|
||||||
# Only the extension specs, the core specs run without extensions in playwright.yml
|
|
||||||
- name: "Run Playwright tests"
|
|
||||||
run: npx playwright test extensions/
|
|
||||||
env:
|
|
||||||
PLAYWRIGHT_BASE_URL: 'http://127.0.0.1:8000'
|
|
||||||
MAILPIT_BASE_URL: 'http://localhost:8025'
|
|
||||||
|
|
||||||
# No artifacts are uploaded on purpose. This repository is public, so every artifact is
|
|
||||||
# downloadable by anyone, and Playwright's reports carry the source of the specs that
|
|
||||||
# produced them (traces embed whole source files, error-context.md embeds a window of the
|
|
||||||
# spec). Those specs live in the private extension repositories. Failures are diagnosed from
|
|
||||||
# the job log; to inspect a trace, reproduce the failure locally with the extension checked out.
|
|
||||||
14
.github/workflows/playwright.yml
vendored
14
.github/workflows/playwright.yml
vendored
@@ -35,10 +35,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Setup node"
|
- name: "Setup node"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
MAILPIT_BASE_URL: 'http://localhost:8025'
|
MAILPIT_BASE_URL: 'http://localhost:8025'
|
||||||
|
|
||||||
- name: "Upload blob report"
|
- name: "Upload blob report"
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
name: blob-report-${{ matrix.shardIndex }}
|
||||||
@@ -99,10 +99,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: "Setup node"
|
- name: "Setup node"
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: "Download blob reports"
|
- name: "Download blob reports"
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: all-blob-reports
|
path: all-blob-reports
|
||||||
pattern: blob-report-*
|
pattern: blob-report-*
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||||
|
|
||||||
- name: "Upload merged HTML report"
|
- name: "Upload merged HTML report"
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
|
|||||||
75
.github/workflows/vouch-check-pr.yml
vendored
75
.github/workflows/vouch-check-pr.yml
vendored
@@ -1,75 +0,0 @@
|
|||||||
name: Vouch (check PR)
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'pull_request_target' ||
|
|
||||||
(github.event_name == 'issue_comment' &&
|
|
||||||
github.event.issue.pull_request &&
|
|
||||||
contains(github.event.comment.body, '/recheck'))
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# Pull requests of 50 changed lines or fewer skip the vouch requirement.
|
|
||||||
- name: "Measure diff size"
|
|
||||||
id: size
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
PR: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
||||||
|
|
||||||
# Changes to these files do not count towards the 50-line limit.
|
|
||||||
# One extended regex per line, matched against the whole repo-relative
|
|
||||||
# path, so use a leading .* to match a file in any directory.
|
|
||||||
IGNORED: |
|
|
||||||
package-lock\.json
|
|
||||||
composer\.lock
|
|
||||||
tests/.*
|
|
||||||
e2e/.*
|
|
||||||
.*\.(test|spec)\.(ts|js|vue)
|
|
||||||
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
# An empty list yields "^()$", which matches no filename. grep exits
|
|
||||||
# 1 on an empty list, so swallow that rather than fail the step.
|
|
||||||
join() { { grep -vE '^[[:space:]]*$' || true; } | paste -sd'|' -; }
|
|
||||||
ignored="^($(join <<<"$IGNORED"))$"
|
|
||||||
|
|
||||||
total=$(gh api --paginate "repos/$REPO/pulls/$PR/files" \
|
|
||||||
--jq '.[] | [.filename, .additions + .deletions] | @tsv' |
|
|
||||||
awk -F'\t' -v ignored="$ignored" '
|
|
||||||
$1 ~ ignored { next }
|
|
||||||
{ n += $2 }
|
|
||||||
END { print n+0 }')
|
|
||||||
echo "total=$total" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Countable diff size: $total line(s)"
|
|
||||||
|
|
||||||
- name: "Small patch (denounced users still blocked)"
|
|
||||||
if: fromJSON(steps.size.outputs.total) <= 50
|
|
||||||
uses: mitchellh/vouch/action/check-pr@v1.5.0
|
|
||||||
with:
|
|
||||||
pr-number: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
||||||
auto-close: true
|
|
||||||
require-vouch: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: "Full vouch required"
|
|
||||||
if: fromJSON(steps.size.outputs.total) > 50
|
|
||||||
uses: mitchellh/vouch/action/check-pr@v1.5.0
|
|
||||||
with:
|
|
||||||
pr-number: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
||||||
auto-close: true
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
33
.github/workflows/vouch-manage-by-discussion.yml
vendored
33
.github/workflows/vouch-manage-by-discussion.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
name: Vouch (manage by discussion)
|
|
||||||
|
|
||||||
# Same commands as vouch-manage-by-issue.yml, but for discussion comments.
|
|
||||||
|
|
||||||
on:
|
|
||||||
discussion_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: vouch-manage
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
discussions: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
manage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Checkout code"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: "Apply vouch command"
|
|
||||||
uses: mitchellh/vouch/action/manage-by-discussion@v1.5.0
|
|
||||||
with:
|
|
||||||
discussion-number: ${{ github.event.discussion.number }}
|
|
||||||
comment-node-id: ${{ github.event.comment.node_id }}
|
|
||||||
roles: admin,maintain,write
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
35
.github/workflows/vouch-manage-by-issue.yml
vendored
35
.github/workflows/vouch-manage-by-issue.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: Vouch (manage by issue)
|
|
||||||
|
|
||||||
# Maintainers comment "vouch @user", "unvouch @user" or "denounce @user <reason>"
|
|
||||||
# on any issue or pull request, and this workflow updates .github/VOUCHED.td.
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: vouch-manage
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
manage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Checkout code"
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: "Apply vouch command"
|
|
||||||
uses: mitchellh/vouch/action/manage-by-issue@v1.5.0
|
|
||||||
with:
|
|
||||||
issue-id: ${{ github.event.issue.number }}
|
|
||||||
comment-id: ${{ github.event.comment.id }}
|
|
||||||
roles: admin,maintain,write
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,7 +6,6 @@ dist
|
|||||||
/public/storage
|
/public/storage
|
||||||
/public/css
|
/public/css
|
||||||
/public/js
|
/public/js
|
||||||
/public/fonts/filament
|
|
||||||
/public/vendor
|
/public/vendor
|
||||||
/lang/vendor
|
/lang/vendor
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
@@ -27,10 +26,9 @@ yarn-error.log
|
|||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/coverage
|
/coverage
|
||||||
/extensions/*
|
/extensions
|
||||||
!/extensions/.gitkeep
|
!/extensions/.gitkeep
|
||||||
!/extensions/extensions_autoload.php
|
!/extensions/extensions_autoload.php
|
||||||
!/extensions/manifest.json
|
|
||||||
/auth.json
|
/auth.json
|
||||||
/modules_statuses.json
|
/modules_statuses.json
|
||||||
/k8s
|
/k8s
|
||||||
@@ -44,5 +42,3 @@ yarn-error.log
|
|||||||
/data
|
/data
|
||||||
/config/caddy
|
/config/caddy
|
||||||
/config/composer
|
/config/composer
|
||||||
/config/psysh
|
|
||||||
/AGENTS.md
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# Build environment for Anthropic's OSS Scanner (https://github.com/anthropics/oss-scanner).
|
|
||||||
# The scanner builds this image with the repository root as the build context and then audits it without network
|
|
||||||
# access, so everything needed to run the app and its test suite (PHP + Composer deps, Node deps + built frontend,
|
|
||||||
# a local PostgreSQL and a local Gotenberg for PDF rendering) is installed here.
|
|
||||||
#
|
|
||||||
# The base image is the FrankenPHP image the production image (docker/prod/Dockerfile) is built on: in production the
|
|
||||||
# app runs as long-lived Laravel Octane workers in FrankenPHP worker mode, not as one PHP process per request.
|
|
||||||
#
|
|
||||||
# Inside the finished image:
|
|
||||||
# .oss-scanner/start-postgres.sh start the local PostgreSQL server (required for tests and the app)
|
|
||||||
# .oss-scanner/start-gotenberg.sh start the local Gotenberg server (required for PDF exports)
|
|
||||||
# php artisan test run the PHPUnit suite (needs PostgreSQL and Gotenberg running)
|
|
||||||
# .oss-scanner/start-octane.sh run the app like production on http://127.0.0.1:8000 (starts everything above)
|
|
||||||
|
|
||||||
ARG FRANKENPHP_VERSION=1.11
|
|
||||||
ARG PHP_VERSION=8.3
|
|
||||||
|
|
||||||
FROM node:20-trixie-slim AS node
|
|
||||||
|
|
||||||
FROM gotenberg/gotenberg:8 AS gotenberg
|
|
||||||
|
|
||||||
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION}
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
|
||||||
COMPOSER_ALLOW_SUPERUSER=1 \
|
|
||||||
COMPOSER_NO_INTERACTION=1 \
|
|
||||||
OCTANE_SERVER=frankenphp \
|
|
||||||
TZ=UTC
|
|
||||||
|
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
|
||||||
COPY --from=node /usr/local/bin/node /usr/local/bin/node
|
|
||||||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
|
||||||
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
|
||||||
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
|
||||||
|
|
||||||
# PostgreSQL 17 (Debian trixie default) is one of the versions solidtime is tested against in CI.
|
|
||||||
# chromium, qpdf and exiftool are what Gotenberg needs for HTML to PDF rendering.
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends git unzip curl ca-certificates postgresql postgresql-client \
|
|
||||||
chromium fonts-liberation qpdf libimage-exiftool-perl \
|
|
||||||
&& install-php-extensions pdo_pgsql pgsql intl gd zip bcmath exif pcntl sockets soap apcu \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& echo "memory_limit=2G" > "$PHP_INI_DIR/conf.d/99-oss-scanner.ini"
|
|
||||||
|
|
||||||
# Gotenberg 8 (the PDF renderer, same image as in CI). solidtime only uses its Chromium HTML to PDF route, so LibreOffice
|
|
||||||
# and pdftk are not installed: Gotenberg only checks at startup that their binaries exist, start-gotenberg.sh disables
|
|
||||||
# the LibreOffice routes and the Chromium route does not use pdftk.
|
|
||||||
COPY --from=gotenberg /usr/bin/gotenberg /usr/local/bin/gotenberg
|
|
||||||
COPY --from=gotenberg /usr/bin/pdfcpu /usr/local/bin/pdfcpu
|
|
||||||
COPY --from=gotenberg /opt/gotenberg /opt/gotenberg
|
|
||||||
ENV CHROMIUM_BIN_PATH=/usr/bin/chromium \
|
|
||||||
CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/chromium-hyphen-data \
|
|
||||||
GOTENBERG_VERSIONS_DIR_PATH=/opt/gotenberg/versions \
|
|
||||||
QPDF_BIN_PATH=/usr/bin/qpdf \
|
|
||||||
EXIFTOOL_BIN_PATH=/usr/bin/exiftool \
|
|
||||||
PDFCPU_BIN_PATH=/usr/local/bin/pdfcpu \
|
|
||||||
LIBREOFFICE_BIN_PATH=/bin/false \
|
|
||||||
UNOCONVERTER_BIN_PATH=/bin/false \
|
|
||||||
PDFTK_BIN_PATH=/bin/false
|
|
||||||
|
|
||||||
# Database matching .env.ci: user root / password root, database laravel.
|
|
||||||
RUN pg_ctlcluster 17 main start \
|
|
||||||
&& runuser -u postgres -- psql -c "CREATE ROLE root WITH LOGIN SUPERUSER PASSWORD 'root';" \
|
|
||||||
&& runuser -u postgres -- createdb -O root laravel \
|
|
||||||
&& pg_ctlcluster 17 main stop
|
|
||||||
|
|
||||||
# scanner contract: the checkout lives inside the image, at /src
|
|
||||||
COPY . /src
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
RUN composer install --prefer-dist \
|
|
||||||
&& npm ci \
|
|
||||||
&& npm run build
|
|
||||||
|
|
||||||
RUN cp .env.ci .env \
|
|
||||||
&& php artisan key:generate \
|
|
||||||
&& php artisan passport:keys --force \
|
|
||||||
&& php artisan octane:install --server=frankenphp --no-interaction \
|
|
||||||
&& chmod +x .oss-scanner/*.sh
|
|
||||||
|
|
||||||
# Run the test suite so the image is known to work, but do not fail the build on test failures.
|
|
||||||
RUN .oss-scanner/start-postgres.sh \
|
|
||||||
&& .oss-scanner/start-gotenberg.sh \
|
|
||||||
&& (php artisan test || echo "WARNING: PHPUnit reported failures") \
|
|
||||||
&& kill "$(cat /tmp/gotenberg.pid)" \
|
|
||||||
&& pg_ctlcluster 17 main stop
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Used instead of the root .dockerignore when building .oss-scanner/Dockerfile. The root one is tailored to the
|
|
||||||
# production image and excludes tests, phpunit.xml etc., which the scanner needs.
|
|
||||||
node_modules
|
|
||||||
extensions/*/node_modules
|
|
||||||
vendor
|
|
||||||
.env
|
|
||||||
public/build
|
|
||||||
public/hot
|
|
||||||
storage/*.key
|
|
||||||
storage/logs/*
|
|
||||||
coverage
|
|
||||||
test-results
|
|
||||||
playwright-report
|
|
||||||
.phpunit.cache
|
|
||||||
.phpunit.result.cache
|
|
||||||
auth.json
|
|
||||||
.DS_Store
|
|
||||||
.idea
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Start the local Gotenberg server (PDF rendering) on 127.0.0.1:3000, matching GOTENBERG_URL in .env.ci.
|
|
||||||
# Only the Chromium routes are needed by solidtime, see .oss-scanner/Dockerfile. Stop: kill "$(cat /tmp/gotenberg.pid)"
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if ! curl -fs http://127.0.0.1:3000/health >/dev/null; then
|
|
||||||
nohup gotenberg \
|
|
||||||
--api-port=3000 \
|
|
||||||
--libreoffice-disable-routes \
|
|
||||||
--libreoffice-auto-start=false \
|
|
||||||
--log-level=warn \
|
|
||||||
>/tmp/gotenberg.log 2>&1 &
|
|
||||||
echo $! >/tmp/gotenberg.pid
|
|
||||||
until curl -fs http://127.0.0.1:3000/health >/dev/null; do sleep 0.5; done
|
|
||||||
fi
|
|
||||||
echo "Gotenberg is running on http://127.0.0.1:3000 (log: /tmp/gotenberg.log)"
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Run the app the way production does: Laravel Octane on FrankenPHP in worker mode, with the production Caddyfile.
|
|
||||||
# A worker boots the app once and then serves many requests, so state kept in memory (static properties, singletons,
|
|
||||||
# changed config, ...) survives from one request to the next. With a single worker, consecutive requests always hit the
|
|
||||||
# same worker, so such leaks between requests (e.g. between users of different organizations) reproduce reliably.
|
|
||||||
#
|
|
||||||
# Usage: .oss-scanner/start-octane.sh (OCTANE_WORKERS and OCTANE_MAX_REQUESTS can be overridden)
|
|
||||||
# Stop: php artisan octane:stop
|
|
||||||
# After changing PHP code, run `php artisan octane:reload`: workers keep the code they booted with.
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
.oss-scanner/start-postgres.sh
|
|
||||||
.oss-scanner/start-gotenberg.sh
|
|
||||||
|
|
||||||
php artisan migrate --force
|
|
||||||
if [ "$(PGPASSWORD=root psql -h 127.0.0.1 -U root -d laravel -tAc 'SELECT count(*) FROM users')" = "0" ]; then
|
|
||||||
php artisan db:seed --force
|
|
||||||
fi
|
|
||||||
php artisan optimize:clear >/dev/null
|
|
||||||
|
|
||||||
nohup php artisan octane:frankenphp \
|
|
||||||
--host=127.0.0.1 \
|
|
||||||
--port=8000 \
|
|
||||||
--workers="${OCTANE_WORKERS:-1}" \
|
|
||||||
--max-requests="${OCTANE_MAX_REQUESTS:-10000}" \
|
|
||||||
--caddyfile=docker/prod/deployment/octane/FrankenPHP/Caddyfile \
|
|
||||||
>/tmp/octane.log 2>&1 &
|
|
||||||
|
|
||||||
until curl -fs -o /dev/null http://127.0.0.1:8000/login; do
|
|
||||||
if ! kill -0 $! 2>/dev/null; then
|
|
||||||
cat /tmp/octane.log
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 0.5
|
|
||||||
done
|
|
||||||
echo "solidtime is running on http://127.0.0.1:8000 with Octane/FrankenPHP (log: /tmp/octane.log)"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Start the local PostgreSQL server used by the test suite and the app (see .oss-scanner/Dockerfile).
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
pg_ctlcluster 17 main start 2>/dev/null || true
|
|
||||||
until pg_isready -h 127.0.0.1 -p 5432 -q; do sleep 0.5; done
|
|
||||||
echo "PostgreSQL is running on 127.0.0.1:5432 (user root, password root, database laravel)"
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# Threat model
|
|
||||||
|
|
||||||
## What this project does
|
|
||||||
|
|
||||||
solidtime is an open-source, multi-tenant time tracking web application (Laravel backend, Vue 3 + Inertia frontend,
|
|
||||||
PostgreSQL). It runs as a hosted SaaS (solidtime.io) and is self-hosted by many organisations. Users belong to one or
|
|
||||||
more **organizations**; inside an organization each member has a role: `owner`, `admin`, `manager`, `employee` or
|
|
||||||
`placeholder` (an imported, non-login member). What each role may do is defined in `app/Service/PermissionStore.php`.
|
|
||||||
|
|
||||||
The most important security property is **isolation**: a user must never read or modify data of an organization they
|
|
||||||
are not a member of, and within an organization a member must not exceed the permissions of their role (e.g. an
|
|
||||||
employee must not see other members' time entries, billable rates, or manage members, unless the organization settings
|
|
||||||
explicitly allow it).
|
|
||||||
|
|
||||||
## Trust boundaries
|
|
||||||
|
|
||||||
- **Super admins are fully trusted.** They are the instance operators, configured via the `SUPER_ADMINS` env
|
|
||||||
variable, and have access to the Filament admin panel (`app/Filament`), which can view and change data of every
|
|
||||||
organization and impersonate users. Anything a super admin can do through the panel (including XSS, SQL injection,
|
|
||||||
SSRF or file access that is only reachable from the panel) is not a vulnerability.
|
|
||||||
- What **is** in scope: a user who is not a super admin reaching the admin panel, or any of its actions, at all.
|
|
||||||
- Operators of a self-hosted instance (shell, database, environment, filesystem access) are trusted.
|
|
||||||
- Everyone else, including organization owners and admins when acting outside their own organization, is untrusted.
|
|
||||||
|
|
||||||
## Runtime: long-lived Octane workers
|
|
||||||
|
|
||||||
In production (the hosted SaaS and the official Docker image, `docker/prod/`) the app does **not** run as one PHP
|
|
||||||
process per request. It runs on Laravel Octane with FrankenPHP in worker mode: each worker boots the application once
|
|
||||||
and then serves many requests from different users and organizations. Anything kept in memory survives from one request
|
|
||||||
to the next unless Octane resets it (`config/octane.php` lists what is reset). This includes static properties and
|
|
||||||
static caches, container bindings registered with `singleton()` instead of `scoped()` (see
|
|
||||||
`app/Providers/AppServiceProvider.php`), objects captured by those singletons, runtime `config()` / locale / timezone
|
|
||||||
changes, macros and event listeners registered during a request, and state in third-party packages.
|
|
||||||
|
|
||||||
Request A leaving state behind that request B (another user, possibly of another organization) then sees or is
|
|
||||||
affected by is in scope, rated by its impact like any other issue (see severity below). The PHPUnit suite cannot
|
|
||||||
show this class of bug, because it boots a fresh application for every test. It has to be reproduced over HTTP against
|
|
||||||
the Octane server, see "How to exercise it".
|
|
||||||
|
|
||||||
## Where untrusted input enters
|
|
||||||
|
|
||||||
All authenticated users, including employees of any organization and anyone who self-registers (registration is open
|
|
||||||
by default), are untrusted.
|
|
||||||
|
|
||||||
- **JSON API** `routes/api.php` (`/api/v1/...`), authenticated via Passport (session cookie or personal access token).
|
|
||||||
Most routes are scoped by `{organization}` and authorised in the controllers / form requests.
|
|
||||||
- **Public, unauthenticated** endpoints: `GET /api/v1/public/reports` (shared reports, accessed by a secret), login,
|
|
||||||
registration, password reset, email verification, organization invitation acceptance (`routes/web.php`).
|
|
||||||
- **Web / Inertia routes** `routes/web.php` and Fortify/Jetstream actions in `app/Actions`.
|
|
||||||
- **Imports** (`app/Service/Import/Importers`): user-uploaded CSV and ZIP files from Toggl, Clockify, Harvest,
|
|
||||||
generic CSV and solidtime's own export format. ZIP handling is in `ZipImportHelper.php`.
|
|
||||||
- **Exports / reports** (`app/Service/Export`, `app/Service/ReportExport`): CSV/XLSX/ODS and PDF. PDFs are rendered by
|
|
||||||
sending HTML to a Gotenberg (headless Chromium) service, so user-controlled content in that HTML matters.
|
|
||||||
- **OAuth** (Passport) authorization and token endpoints.
|
|
||||||
- **Filament admin panel** (`app/Filament`): only its access control is in scope (see Trust boundaries).
|
|
||||||
|
|
||||||
## Components that matter most / least
|
|
||||||
|
|
||||||
Most important: organization scoping and role checks in the API controllers, form requests (`app/Http/Requests`),
|
|
||||||
`PermissionStore`, public report sharing, invitations and member management (role changes, ownership transfer, member
|
|
||||||
merge), authentication flows (Fortify, 2FA, email change, API tokens), import parsing.
|
|
||||||
|
|
||||||
Less important / out of scope:
|
|
||||||
- `extensions/` is empty in this repository (proprietary modules are not part of the open-source code).
|
|
||||||
- `docker/`, `k8s/`, `e2e/`, `playwright/`, `docs/` and developer tooling.
|
|
||||||
- Third-party dependencies in `vendor/` and `node_modules/`, unless solidtime uses them in an unsafe way.
|
|
||||||
|
|
||||||
## How to exercise it
|
|
||||||
|
|
||||||
- `.oss-scanner/start-postgres.sh` starts the local PostgreSQL server (user `root`, password `root`, db `laravel`).
|
|
||||||
- `.oss-scanner/start-gotenberg.sh` starts the local Gotenberg server (PDF rendering via headless Chromium) on
|
|
||||||
http://127.0.0.1:3000, so the PDF export code path, including what Chromium does with the rendered HTML, can be
|
|
||||||
exercised.
|
|
||||||
- `php artisan test` runs the PHPUnit suite (start PostgreSQL and Gotenberg first); all tests are expected to pass. Endpoint tests in `tests/Unit/Endpoint/Api/V1/` show how to create users,
|
|
||||||
organizations and members with factories and call the API with a given role; they are the quickest way to write a
|
|
||||||
reproducer. Example: `php artisan test --filter=TimeEntryEndpointTest`.
|
|
||||||
- To run the app like production: `.oss-scanner/start-octane.sh` (http://127.0.0.1:8000). It starts PostgreSQL and
|
|
||||||
Gotenberg, migrates (and seeds an empty database, see `database/seeders/DatabaseSeeder.php` for the users) and runs
|
|
||||||
Octane/FrankenPHP with the production Caddyfile and a **single worker**, so consecutive requests always hit the same
|
|
||||||
worker and leaks between requests reproduce reliably. Workers keep the code they booted with: after changing PHP
|
|
||||||
code run `php artisan octane:reload`. Stop it with `php artisan octane:stop`. Note that the test suite and the app
|
|
||||||
share the same database, so `php artisan test` wipes the app's data.
|
|
||||||
- A reproducer for a leak between requests is a script that sends request A (e.g. as a member of organization X) and
|
|
||||||
then request B (as a user of organization Y) to the running Octane server and shows that B observes A's state. API
|
|
||||||
requests can be authenticated with a personal access token, e.g. created with
|
|
||||||
`php artisan tinker --execute="echo App\Models\User::where('email', '...')->first()->createToken('t')->accessToken;"`.
|
|
||||||
- There is no network: mail delivery is not available (mail uses the `array` driver in tests), and remote resources
|
|
||||||
referenced by PDF templates (e.g. fonts from fonts.bunny.net) fail to load, so PDFs fall back to local fonts.
|
|
||||||
|
|
||||||
## How we rate severity
|
|
||||||
|
|
||||||
- **Critical**: unauthenticated access to other users' data or accounts; authentication bypass; remote code execution;
|
|
||||||
SQL injection reachable by any registered user; reading or writing data of an organization the attacker is not a
|
|
||||||
member of, including through state leaking between requests in an Octane worker.
|
|
||||||
- **High**: privilege escalation within an organization (e.g. employee to admin/owner, or performing admin-only
|
|
||||||
actions); access to data the role must not see (other members' time entries, billable rates, member emails) when
|
|
||||||
the organization settings do not allow it; stored XSS that executes in another user's session; SSRF via PDF
|
|
||||||
rendering or imports; account takeover requiring user interaction.
|
|
||||||
- **Medium**: information disclosure with limited impact, CSRF on state-changing endpoints, issues requiring an
|
|
||||||
unusual but realistic configuration, denial of service by a single authenticated request (e.g. pathological
|
|
||||||
import file).
|
|
||||||
- **Low**: everything else with real security impact.
|
|
||||||
|
|
||||||
## Anything to leave alone
|
|
||||||
|
|
||||||
Please do not report (see also `SECURITY.md`):
|
|
||||||
- Theoretical findings without a working reproducer.
|
|
||||||
- Missing or weak security headers in isolation; TLS / mail DNS configuration.
|
|
||||||
- Self-XSS; CSRF on non-state-changing endpoints (logout, theme).
|
|
||||||
- CSV / spreadsheet formula injection in exports.
|
|
||||||
- Owners or admins acting destructively within their own organization.
|
|
||||||
- Anything requiring direct DB, shell or filesystem access on a self-hosted instance.
|
|
||||||
- Anything that requires being a super admin, including issues inside the Filament admin panel.
|
|
||||||
- Missing OAuth scope enforcement (not implemented yet).
|
|
||||||
- Rate-limit tuning and generic DoS through volume of requests.
|
|
||||||
|
|
||||||
## Reports and patches
|
|
||||||
|
|
||||||
Please include the affected endpoint or code path, the attacker's role and the victim, a PHPUnit test (in the style of
|
|
||||||
`tests/Unit/Endpoint/Api/V1/`) that reproduces the issue (for leaks between requests: a script against
|
|
||||||
`.oss-scanner/start-octane.sh` instead), and a minimal patch that follows the existing patterns
|
|
||||||
(authorisation in form requests/controllers via `PermissionStore`).
|
|
||||||
@@ -12,22 +12,6 @@ In order to keep the issues of the repository clean we decided to only use them
|
|||||||
|
|
||||||
To respect your time and help us manage contributions effectively, please open an issue or start a discussion and wait for approval before submitting a pull request (PR). This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons.
|
To respect your time and help us manage contributions effectively, please open an issue or start a discussion and wait for approval before submitting a pull request (PR). This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons.
|
||||||
|
|
||||||
### Vouched contributors
|
|
||||||
|
|
||||||
Pull requests from authors who are not vouched are closed automatically. This lets us keep up with the volume of AI slop pull requests without a maintainer having to triage every one of them by hand.
|
|
||||||
|
|
||||||
Your pull request is not affected if any of the following applies:
|
|
||||||
|
|
||||||
- You have write access to this repository.
|
|
||||||
- Someone with write access has vouched for you. The list lives in [.github/VOUCHED.td](.github/VOUCHED.td).
|
|
||||||
- Your pull request changes 50 lines or fewer. Test files and lockfiles do not count towards that number, so a small fix that comes with tests still qualifies.
|
|
||||||
|
|
||||||
To get vouched, open an issue or discussion before you start and explain how you intend to implement the change. We will discuss the approach with you, and only once we have agreed on the implementation does a maintainer comment `vouch @your-handle`, which puts you on the list from then on.
|
|
||||||
|
|
||||||
Being vouched only stops your pull requests from being closed automatically. [Only work on approved issues](#only-work-on-approved-issues) still applies to every pull request you send.
|
|
||||||
|
|
||||||
Contributors who abuse this are denounced, and their pull requests are closed regardless of size.
|
|
||||||
|
|
||||||
### Contributor License Agreement
|
### Contributor License Agreement
|
||||||
|
|
||||||
You'll also notice that we’ve set up a [Contributor License Agreement (CLA)](https://cla-assistant.io/solidtime-io/solidtime), which must be signed before any PR can be merged. Don’t worry - the process is quick and only takes a few clicks.
|
You'll also notice that we’ve set up a [Contributor License Agreement (CLA)](https://cla-assistant.io/solidtime-io/solidtime), which must be signed before any PR can be merged. Don’t worry - the process is quick and only takes a few clicks.
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ Please open an issue or start a discussion and wait for approval before submitti
|
|||||||
|
|
||||||
**If you submit an AI slop pull request (especially without following the proper procedure), you will be banned from future contributions to solidtime.**
|
**If you submit an AI slop pull request (especially without following the proper procedure), you will be banned from future contributions to solidtime.**
|
||||||
|
|
||||||
To keep that manageable, pull requests from authors who are not vouched are closed automatically, unless they change 50 lines or fewer. To get vouched, open an issue or discussion first and explain how you intend to implement the change. Once we have agreed on the approach, we vouch for you. See [Vouched contributors](./CONTRIBUTING.md#vouched-contributors).
|
|
||||||
|
|
||||||
Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) before sumbitting a Pull Request.
|
Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) before sumbitting a Pull Request.
|
||||||
|
|
||||||
We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides.
|
We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides.
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Actions\Fortify;
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
use App\Enums\RegistrationMode;
|
|
||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Events\NewsletterRegistered;
|
use App\Events\NewsletterRegistered;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Service\InvitationService;
|
|
||||||
use App\Service\IpLookup\IpLookupServiceContract;
|
use App\Service\IpLookup\IpLookupServiceContract;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use App\Service\UserService;
|
use App\Service\UserService;
|
||||||
@@ -18,6 +16,7 @@ use Illuminate\Support\Facades\Validator;
|
|||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||||
|
use Laravel\Jetstream\Jetstream;
|
||||||
use Log;
|
use Log;
|
||||||
|
|
||||||
class CreateNewUser implements CreatesNewUsers
|
class CreateNewUser implements CreatesNewUsers
|
||||||
@@ -33,14 +32,13 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
*/
|
*/
|
||||||
public function create(array $input): User
|
public function create(array $input): User
|
||||||
{
|
{
|
||||||
$registrationMode = RegistrationMode::fromConfig(config('app.enable_registration'));
|
if (! config('app.enable_registration')) {
|
||||||
if ($registrationMode === RegistrationMode::Off) {
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'email' => [__('Registration is disabled.')],
|
'email' => [__('Registration is disabled.')],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$validated = Validator::make($input, [
|
Validator::make($input, [
|
||||||
'name' => [
|
'name' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
@@ -57,26 +55,12 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
'password' => $this->passwordRules(),
|
'password' => $this->passwordRules(),
|
||||||
'terms' => ['accepted', 'required'],
|
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
|
||||||
'newsletter_consent' => [
|
'newsletter_consent' => [
|
||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
if ($registrationMode === RegistrationMode::InviteOnly) {
|
|
||||||
$invitationService = app(InvitationService::class);
|
|
||||||
$email = (string) $validated['email'];
|
|
||||||
if (! $invitationService->hasAcceptedInvitationForEmail($email)) {
|
|
||||||
$message = $invitationService->hasPendingInvitationForEmail($email)
|
|
||||||
? __('Please accept the organization invitation sent to your email address before registering.')
|
|
||||||
: __('Registration is only available to invited users.');
|
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'email' => [$message],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$timezone = null;
|
$timezone = null;
|
||||||
if (array_key_exists('timezone', $input) && is_string($input['timezone'])) {
|
if (array_key_exists('timezone', $input) && is_string($input['timezone'])) {
|
||||||
if (app(TimezoneService::class)->isValid($input['timezone'])) {
|
if (app(TimezoneService::class)->isValid($input['timezone'])) {
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Actions\Fortify;
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
use App\Exceptions\MovedToApiException;
|
use App\Enums\Weekday;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||||
|
|
||||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||||
@@ -20,6 +24,56 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
|||||||
*/
|
*/
|
||||||
public function update(User $user, array $input): void
|
public function update(User $user, array $input): void
|
||||||
{
|
{
|
||||||
throw new MovedToApiException;
|
Validator::make($input, [
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:255',
|
||||||
|
],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
UniqueEloquent::make(User::class, 'email')->ignore($user->id)->query(function (Builder $query) {
|
||||||
|
/** @var Builder<User> $query */
|
||||||
|
return $query->where('is_placeholder', '=', false);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
'photo' => [
|
||||||
|
'nullable',
|
||||||
|
'mimes:jpg,jpeg,png',
|
||||||
|
'max:1024',
|
||||||
|
],
|
||||||
|
'timezone' => [
|
||||||
|
'required',
|
||||||
|
'timezone:all',
|
||||||
|
],
|
||||||
|
'week_start' => [
|
||||||
|
'required',
|
||||||
|
Rule::enum(Weekday::class),
|
||||||
|
],
|
||||||
|
])->validateWithBag('updateProfileInformation');
|
||||||
|
|
||||||
|
if (isset($input['photo'])) {
|
||||||
|
$user->updateProfilePhoto($input['photo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input['email'] !== $user->email) {
|
||||||
|
$user->forceFill([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'email' => $input['email'],
|
||||||
|
'email_verified_at' => null,
|
||||||
|
'timezone' => $input['timezone'],
|
||||||
|
'week_start' => $input['week_start'],
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$user->sendEmailVerificationNotification();
|
||||||
|
} else {
|
||||||
|
$user->forceFill([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'timezone' => $input['timezone'],
|
||||||
|
'week_start' => $input['week_start'],
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
app/Actions/Jetstream/AddOrganizationMember.php
Normal file
94
app/Actions/Jetstream/AddOrganizationMember.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Service\MemberService;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Rules\In;
|
||||||
|
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||||
|
use Laravel\Jetstream\Contracts\AddsTeamMembers;
|
||||||
|
|
||||||
|
class AddOrganizationMember implements AddsTeamMembers
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Add a new team member to the given team.
|
||||||
|
*/
|
||||||
|
public function add(User $owner, Organization $organization, string $email, ?string $role = null): void
|
||||||
|
{
|
||||||
|
Gate::forUser($owner)->authorize('addTeamMember', $organization); // TODO: refactor after owner refactoring
|
||||||
|
|
||||||
|
$this->validate($organization, $email, $role);
|
||||||
|
|
||||||
|
$newOrganizationMember = User::query()
|
||||||
|
->where('email', $email)
|
||||||
|
->where('is_placeholder', '=', false)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
app(MemberService::class)->addMember($newOrganizationMember, $organization, Role::from($role));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the add member operation.
|
||||||
|
*/
|
||||||
|
protected function validate(Organization $organization, string $email, ?string $role): void
|
||||||
|
{
|
||||||
|
Validator::make([
|
||||||
|
'email' => $email,
|
||||||
|
'role' => $role,
|
||||||
|
], $this->rules())->after(
|
||||||
|
$this->ensureUserIsNotAlreadyOnTeam($organization, $email)
|
||||||
|
)->validateWithBag('addTeamMember');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules for adding a team member.
|
||||||
|
*
|
||||||
|
* @return array<string, array<ValidationRule|Rule|string|In>>
|
||||||
|
*/
|
||||||
|
protected function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'email',
|
||||||
|
ExistsEloquent::make(User::class, 'email', function (Builder $builder) {
|
||||||
|
/** @var Builder<User> $builder */
|
||||||
|
return $builder->where('is_placeholder', '=', false);
|
||||||
|
})->withMessage(__('We were unable to find a registered user with this email address.')),
|
||||||
|
],
|
||||||
|
'role' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
Rule::in([
|
||||||
|
Role::Admin->value,
|
||||||
|
Role::Manager->value,
|
||||||
|
Role::Employee->value,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the user is not already on the team.
|
||||||
|
*/
|
||||||
|
protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $email): Closure
|
||||||
|
{
|
||||||
|
return function ($validator) use ($team, $email): void {
|
||||||
|
$validator->errors()->addIf(
|
||||||
|
$team->hasRealUserWithEmail($email),
|
||||||
|
'email',
|
||||||
|
__('This user already belongs to the team.')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Actions/Jetstream/CreateOrganization.php
Normal file
59
app/Actions/Jetstream/CreateOrganization.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Events\AfterCreateOrganization;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Service\IpLookup\IpLookupServiceContract;
|
||||||
|
use App\Service\OrganizationService;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Laravel\Jetstream\Contracts\CreatesTeams;
|
||||||
|
use Laravel\Jetstream\Jetstream;
|
||||||
|
|
||||||
|
class CreateOrganization implements CreatesTeams
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate and create a new team for the given user.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*
|
||||||
|
* @throws AuthorizationException
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function create(User $user, array $input): Organization
|
||||||
|
{
|
||||||
|
Gate::forUser($user)->authorize('create', Jetstream::newTeamModel());
|
||||||
|
|
||||||
|
Validator::make($input, [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
])->validateWithBag('createTeam');
|
||||||
|
|
||||||
|
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
||||||
|
|
||||||
|
$currency = null;
|
||||||
|
if ($ipLookupResponse !== null) {
|
||||||
|
$currency = $ipLookupResponse->currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
$organization = app(OrganizationService::class)->createOrganization(
|
||||||
|
$input['name'],
|
||||||
|
$user,
|
||||||
|
false,
|
||||||
|
$currency
|
||||||
|
);
|
||||||
|
|
||||||
|
$user->switchTeam($organization);
|
||||||
|
|
||||||
|
// Note: The refresh is necessary for currently unknown reasons. Do not remove it.
|
||||||
|
$organization = $organization->refresh();
|
||||||
|
AfterCreateOrganization::dispatch($organization);
|
||||||
|
|
||||||
|
return $organization;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Actions/Jetstream/DeleteOrganization.php
Normal file
21
app/Actions/Jetstream/DeleteOrganization.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Service\DeletionService;
|
||||||
|
use Laravel\Jetstream\Contracts\DeletesTeams;
|
||||||
|
|
||||||
|
class DeleteOrganization implements DeletesTeams
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Delete the given team.
|
||||||
|
*/
|
||||||
|
public function delete(Organization $organization): void
|
||||||
|
{
|
||||||
|
/** @see ValidateOrganizationDeletion */
|
||||||
|
app(DeletionService::class)->deleteOrganization($organization);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Actions/Jetstream/DeleteUser.php
Normal file
30
app/Actions/Jetstream/DeleteUser.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Exceptions\Api\ApiException;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Service\DeletionService;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Laravel\Jetstream\Contracts\DeletesUsers;
|
||||||
|
|
||||||
|
class DeleteUser implements DeletesUsers
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Delete the given user.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function delete(User $user): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
app(DeletionService::class)->deleteUser($user);
|
||||||
|
} catch (ApiException $exception) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => $exception->getTranslatedMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Actions/Jetstream/InviteOrganizationMember.php
Normal file
24
app/Actions/Jetstream/InviteOrganizationMember.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Exceptions\MovedToApiException;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\User;
|
||||||
|
use Exception;
|
||||||
|
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
|
||||||
|
|
||||||
|
class InviteOrganizationMember implements InvitesTeamMembers
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Invite a new team member to the given team.
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function invite(User $user, Organization $organization, string $email, ?string $role = null): void
|
||||||
|
{
|
||||||
|
throw new MovedToApiException;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Actions/Jetstream/RemoveOrganizationMember.php
Normal file
24
app/Actions/Jetstream/RemoveOrganizationMember.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Exceptions\MovedToApiException;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\User;
|
||||||
|
use Exception;
|
||||||
|
use Laravel\Jetstream\Contracts\RemovesTeamMembers;
|
||||||
|
|
||||||
|
class RemoveOrganizationMember implements RemovesTeamMembers
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Remove the team member from the given team.
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function remove(User $user, Organization $organization, User $teamMember): void
|
||||||
|
{
|
||||||
|
throw new MovedToApiException;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Actions/Jetstream/UpdateMemberRole.php
Normal file
25
app/Actions/Jetstream/UpdateMemberRole.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
|
use App\Exceptions\MovedToApiException;
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\User;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class UpdateMemberRole
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the role for the given team member.
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function update(User $actingUser, Organization $organization, string $userId, string $role): void
|
||||||
|
{
|
||||||
|
throw new MovedToApiException;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Actions/Jetstream/UpdateOrganization.php
Normal file
48
app/Actions/Jetstream/UpdateOrganization.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Rules\CurrencyRule;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Laravel\Jetstream\Contracts\UpdatesTeamNames;
|
||||||
|
|
||||||
|
class UpdateOrganization implements UpdatesTeamNames
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate and update the given team's name.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*
|
||||||
|
* @throws AuthorizationException
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function update(User $user, Organization $organization, array $input): void
|
||||||
|
{
|
||||||
|
Gate::forUser($user)->authorize('update', $organization);
|
||||||
|
|
||||||
|
Validator::make($input, [
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:255',
|
||||||
|
],
|
||||||
|
'currency' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
new CurrencyRule,
|
||||||
|
],
|
||||||
|
])->validateWithBag('updateTeamName');
|
||||||
|
|
||||||
|
$organization->forceFill([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'currency' => $input['currency'],
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Actions/Jetstream/ValidateOrganizationDeletion.php
Normal file
28
app/Actions/Jetstream/ValidateOrganizationDeletion.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions\Jetstream;
|
||||||
|
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Service\PermissionStore;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
|
||||||
|
class ValidateOrganizationDeletion
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate that the team can be deleted by the given user.
|
||||||
|
*
|
||||||
|
* @param User $user Authenticated user
|
||||||
|
* @param Organization $organization Organization to be deleted
|
||||||
|
*
|
||||||
|
* @throws AuthorizationException
|
||||||
|
*/
|
||||||
|
public function validate(User $user, Organization $organization): void
|
||||||
|
{
|
||||||
|
if (! app(PermissionStore::class)->userHas($organization, $user, 'organizations:delete')) {
|
||||||
|
throw new AuthorizationException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Auth;
|
|
||||||
|
|
||||||
use Illuminate\Auth\EloquentUserProvider;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User provider that only resolves non-placeholder users.
|
|
||||||
*
|
|
||||||
* Placeholder users are created by imports and when members are removed from an
|
|
||||||
* organization. They can share an email address with a real user, so resolving a user by
|
|
||||||
* email can return a placeholder instead of the real account. The login flow filters them
|
|
||||||
* out explicitly, but the password broker and the guard credential checks (for example the
|
|
||||||
* password confirmation) resolve users through the configured user provider.
|
|
||||||
*
|
|
||||||
* Registered as the "eloquent" provider driver in the AuthServiceProvider, so it replaces the
|
|
||||||
* built-in one for every provider in config/auth.php.
|
|
||||||
*/
|
|
||||||
class ActiveUserProvider extends EloquentUserProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param Model|null $model
|
|
||||||
* @return Builder<Model>
|
|
||||||
*/
|
|
||||||
#[Override]
|
|
||||||
protected function newModelQuery($model = null): Builder
|
|
||||||
{
|
|
||||||
$query = parent::newModelQuery($model);
|
|
||||||
$query->getQuery()->where('is_placeholder', '=', false);
|
|
||||||
|
|
||||||
return $query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -69,7 +69,7 @@ class UserCreateCommand extends Command
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
/** @var Organization|null $organization */
|
/** @var Organization|null $organization */
|
||||||
$organization = $user->ownedOrganizations->first();
|
$organization = $user->ownedTeams->first();
|
||||||
if ($organization === null) {
|
if ($organization === null) {
|
||||||
throw new LogicException('User does not have an organization');
|
throw new LogicException('User does not have an organization');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use Illuminate\Database\Query\JoinClause;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class SelfHostDatabaseConsistency extends Command
|
class SelfHostDatabaseConsistency extends Command
|
||||||
{
|
{
|
||||||
@@ -102,7 +101,7 @@ class SelfHostDatabaseConsistency extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection<int, stdClass> $problems
|
* @param Collection<int, \stdClass> $problems
|
||||||
*/
|
*/
|
||||||
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
|
private function logProblems(Collection $problems, string $message, bool &$hadAProblem): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace App\Console\Commands\SelfHost;
|
|||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Encryption\Encrypter;
|
use Illuminate\Encryption\Encrypter;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use phpseclib4\Crypt\RSA;
|
use phpseclib3\Crypt\RSA;
|
||||||
|
|
||||||
class SelfHostGenerateKeysCommand extends Command
|
class SelfHostGenerateKeysCommand extends Command
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ class TimeEntrySendStillRunningMailsCommand extends Command
|
|||||||
])
|
])
|
||||||
->whereHas('user', function (Builder $query): void {
|
->whereHas('user', function (Builder $query): void {
|
||||||
/** @var Builder<User> $query */
|
/** @var Builder<User> $query */
|
||||||
$query->where('is_placeholder', '=', false)
|
$query->where('is_placeholder', '=', false);
|
||||||
->where('send_time_entry_still_running_email', '=', true);
|
|
||||||
})
|
})
|
||||||
->orderBy('created_at', 'asc')
|
->orderBy('created_at', 'asc')
|
||||||
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void {
|
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum GoalComparison: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The goal is reached when at least the target amount of time was tracked in the period.
|
|
||||||
*/
|
|
||||||
case AtLeast = 'at_least';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The goal is reached when less than the target amount of time was tracked in the period.
|
|
||||||
*/
|
|
||||||
case LessThan = 'less_than';
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum GoalPeriod: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Day = 'day';
|
|
||||||
case Week = 'week';
|
|
||||||
case Month = 'month';
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum GoalStatus: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "At least" goal, the target has not been reached yet in the current period.
|
|
||||||
*/
|
|
||||||
case InProgress = 'in_progress';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "At least" goal, the target has been reached in the current period.
|
|
||||||
*/
|
|
||||||
case Achieved = 'achieved';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Less than" goal, the tracked time is still below the limit.
|
|
||||||
*/
|
|
||||||
case OnTrack = 'on_track';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Less than" goal, the tracked time reached or exceeded the limit.
|
|
||||||
*/
|
|
||||||
case Exceeded = 'exceeded';
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum GoalType: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A goal a member set for themselves. Only that member can see and manage it, no permission reaches it.
|
|
||||||
*/
|
|
||||||
case Personal = 'personal';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A goal the organization set for a member or for the whole organization.
|
|
||||||
* Managed by the members that may manage goals, visible to them and to the member the goal is for.
|
|
||||||
* Requires the team goals extension.
|
|
||||||
*/
|
|
||||||
case Organization = 'organization';
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
enum RegistrationMode: string
|
|
||||||
{
|
|
||||||
case On = 'on';
|
|
||||||
case InviteOnly = 'invite-only';
|
|
||||||
case Off = 'off';
|
|
||||||
|
|
||||||
public static function fromConfig(mixed $value): self
|
|
||||||
{
|
|
||||||
if ($value === true) {
|
|
||||||
return self::On;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value === false || $value === null) {
|
|
||||||
return self::Off;
|
|
||||||
}
|
|
||||||
|
|
||||||
return match (strtolower(trim((string) $value))) {
|
|
||||||
'1', 'on', 'true' => self::On,
|
|
||||||
'invite-only' => self::InviteOnly,
|
|
||||||
default => self::Off,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Enums;
|
namespace App\Enums;
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum Role: string
|
enum Role: string
|
||||||
{
|
{
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Owner = 'owner';
|
case Owner = 'owner';
|
||||||
case Admin = 'admin';
|
case Admin = 'admin';
|
||||||
case Manager = 'manager';
|
case Manager = 'manager';
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum TagMatchType: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Contains = 'contains';
|
|
||||||
|
|
||||||
case NotContains = 'not_contains';
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ enum TimeEntryAggregationType: string
|
|||||||
case Billable = 'billable';
|
case Billable = 'billable';
|
||||||
case Description = 'description';
|
case Description = 'description';
|
||||||
case Tag = 'tag';
|
case Tag = 'tag';
|
||||||
case Type = 'type';
|
|
||||||
|
|
||||||
public static function fromInterval(TimeEntryAggregationTypeInterval $timeEntryAggregationTypeInterval): TimeEntryAggregationType
|
public static function fromInterval(TimeEntryAggregationTypeInterval $timeEntryAggregationTypeInterval): TimeEntryAggregationType
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
|
||||||
|
|
||||||
enum TimeEntryType: string
|
|
||||||
{
|
|
||||||
use LaravelEnumHelper;
|
|
||||||
|
|
||||||
case Work = 'work';
|
|
||||||
case Break = 'break';
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Events;
|
|
||||||
|
|
||||||
use App\Models\Member;
|
|
||||||
use App\Models\Organization;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces legacy TeamMemberAdded event.
|
|
||||||
*/
|
|
||||||
class MemberAdded
|
|
||||||
{
|
|
||||||
use Dispatchable;
|
|
||||||
|
|
||||||
public Member $member;
|
|
||||||
|
|
||||||
public Organization $organization;
|
|
||||||
|
|
||||||
public User $user;
|
|
||||||
|
|
||||||
public function __construct(Member $member, Organization $organization, User $user)
|
|
||||||
{
|
|
||||||
$this->member = $member;
|
|
||||||
$this->organization = $organization;
|
|
||||||
$this->user = $user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Events;
|
|
||||||
|
|
||||||
use App\Enums\Role;
|
|
||||||
use App\Models\Organization;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces legacy AddingTeamMember event.
|
|
||||||
*/
|
|
||||||
class MemberAdding
|
|
||||||
{
|
|
||||||
use Dispatchable;
|
|
||||||
|
|
||||||
public User $user;
|
|
||||||
|
|
||||||
public Organization $organization;
|
|
||||||
|
|
||||||
public Role $role;
|
|
||||||
|
|
||||||
public function __construct(User $user, Organization $organization, Role $role)
|
|
||||||
{
|
|
||||||
$this->user = $user;
|
|
||||||
$this->organization = $organization;
|
|
||||||
$this->role = $role;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Events;
|
|
||||||
|
|
||||||
use App\Enums\Role;
|
|
||||||
use App\Models\Organization;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces legacy InvitingTeamMember event.
|
|
||||||
*/
|
|
||||||
class OrganizationInvitationAdding
|
|
||||||
{
|
|
||||||
use Dispatchable;
|
|
||||||
|
|
||||||
public Organization $organization;
|
|
||||||
|
|
||||||
public string $email;
|
|
||||||
|
|
||||||
public Role $role;
|
|
||||||
|
|
||||||
public User $inviter;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
Organization $organization,
|
|
||||||
string $email,
|
|
||||||
Role $role,
|
|
||||||
User $inviter
|
|
||||||
) {
|
|
||||||
$this->role = $role;
|
|
||||||
$this->email = $email;
|
|
||||||
$this->organization = $organization;
|
|
||||||
$this->inviter = $inviter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -60,6 +60,7 @@ abstract class ApiException extends Exception
|
|||||||
*/
|
*/
|
||||||
public function report(): bool
|
public function report(): bool
|
||||||
{
|
{
|
||||||
|
// TODO: temporary activated
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers extends ApiException
|
class CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'can_not_delete_user_who_is_owner_of_organization_with_multiple_members';
|
public const string KEY = 'can_not_delete_user_who_is_owner_of_organization_with_multiple_members';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class CanNotRemoveOwnerFromOrganization extends ApiException
|
class CanNotRemoveOwnerFromOrganization extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'can_not_remove_owner_from_organization';
|
public const string KEY = 'can_not_remove_owner_from_organization';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException
|
class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'changing_role_of_placeholder_is_not_allowed';
|
public const string KEY = 'changing_role_of_placeholder_is_not_allowed';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class ChangingRoleToPlaceholderIsNotAllowed extends ApiException
|
class ChangingRoleToPlaceholderIsNotAllowed extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'changing_role_to_placeholder_is_not_allowed';
|
public const string KEY = 'changing_role_to_placeholder_is_not_allowed';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Exceptions\Api;
|
namespace App\Exceptions\Api;
|
||||||
|
|
||||||
use Override;
|
|
||||||
|
|
||||||
class EntityStillInUseApiException extends ApiException
|
class EntityStillInUseApiException extends ApiException
|
||||||
{
|
{
|
||||||
private string $modelToDelete;
|
private string $modelToDelete;
|
||||||
@@ -21,20 +19,10 @@ class EntityStillInUseApiException extends ApiException
|
|||||||
|
|
||||||
public const string KEY = 'entity_still_in_use';
|
public const string KEY = 'entity_still_in_use';
|
||||||
|
|
||||||
/**
|
|
||||||
* Report the exception.
|
|
||||||
*
|
|
||||||
* @return bool true means the exception handler will not report it again
|
|
||||||
*/
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the translated message for the exception.
|
* Get the translated message for the exception.
|
||||||
*/
|
*/
|
||||||
#[Override]
|
#[\Override]
|
||||||
public function getTranslatedMessage(): string
|
public function getTranslatedMessage(): string
|
||||||
{
|
{
|
||||||
return __('exceptions.api.'.$this->getKey(), [
|
return __('exceptions.api.'.$this->getKey(), [
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class FeatureIsNotAvailableInFreePlanApiException extends ApiException
|
class FeatureIsNotAvailableInFreePlanApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'feature_is_not_available_in_free_plan';
|
public const string KEY = 'feature_is_not_available_in_free_plan';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class InactiveUserCanNotBeUsedApiException extends ApiException
|
class InactiveUserCanNotBeUsedApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'inactive_user_can_not_be_used';
|
public const string KEY = 'inactive_user_can_not_be_used';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
|
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'invitation_for_the_email_already_exists';
|
public const string KEY = 'invitation_for_the_email_already_exists';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OnlyOwnerCanChangeOwnership extends ApiException
|
class OnlyOwnerCanChangeOwnership extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'only_owner_can_change_ownership';
|
public const string KEY = 'only_owner_can_change_ownership';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException
|
class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'only_placeholders_can_be_merged_into_another_member';
|
public const string KEY = 'only_placeholders_can_be_merged_into_another_member';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OrganizationHasNoSubscriptionButMultipleMembersException extends ApiException
|
class OrganizationHasNoSubscriptionButMultipleMembersException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'organization_has_no_subscription_but_multiple_members';
|
public const string KEY = 'organization_has_no_subscription_but_multiple_members';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OrganizationNeedsAtLeastOneOwner extends ApiException
|
class OrganizationNeedsAtLeastOneOwner extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'organization_needs_at_least_one_owner';
|
public const string KEY = 'organization_needs_at_least_one_owner';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class OverlappingTimeEntryApiException extends ApiException
|
class OverlappingTimeEntryApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'overlapping_time_entry';
|
public const string KEY = 'overlapping_time_entry';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException
|
class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception';
|
public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class TimeEntryCanNotBeRestartedApiException extends ApiException
|
class TimeEntryCanNotBeRestartedApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'time_entry_can_not_be_restarted';
|
public const string KEY = 'time_entry_can_not_be_restarted';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class TimeEntryStillRunningApiException extends ApiException
|
class TimeEntryStillRunningApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'time_entry_still_running';
|
public const string KEY = 'time_entry_still_running';
|
||||||
|
|
||||||
/**
|
|
||||||
* Report the exception.
|
|
||||||
*
|
|
||||||
* @return bool true means the exception handler will not report it again
|
|
||||||
*/
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class UserIsAlreadyMemberOfOrganizationApiException extends ApiException
|
class UserIsAlreadyMemberOfOrganizationApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'user_is_already_member_of_organization';
|
public const string KEY = 'user_is_already_member_of_organization';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class UserIsAlreadyMemberOfProjectApiException extends ApiException
|
class UserIsAlreadyMemberOfProjectApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'user_is_already_member_of_project';
|
public const string KEY = 'user_is_already_member_of_project';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
|||||||
class UserNotPlaceholderApiException extends ApiException
|
class UserNotPlaceholderApiException extends ApiException
|
||||||
{
|
{
|
||||||
public const string KEY = 'user_not_placeholder';
|
public const string KEY = 'user_not_placeholder';
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Api;
|
|
||||||
|
|
||||||
class UserResendEmailVerificationNoPendingEmailApiException extends ApiException
|
|
||||||
{
|
|
||||||
public const string KEY = 'user_resend_email_verification_no_pending_email';
|
|
||||||
|
|
||||||
public function report(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,11 +6,7 @@ namespace App\Exceptions;
|
|||||||
|
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
|
||||||
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
class Handler extends ExceptionHandler
|
||||||
@@ -34,33 +30,6 @@ class Handler extends ExceptionHandler
|
|||||||
$this->reportable(function (Throwable $e): void {
|
$this->reportable(function (Throwable $e): void {
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->dontReportWhen(fn (Throwable $e): bool => $e instanceof OAuthServerException
|
|
||||||
&& $e->getErrorType() === 'access_denied'
|
|
||||||
&& $e->getHttpStatusCode() === 401);
|
|
||||||
|
|
||||||
// A request on an untrusted host (see App\Http\Middleware\TrustHosts)
|
|
||||||
// otherwise renders as a bare "Bad request." 400. Show a message that
|
|
||||||
// says how to fix it instead. The framework has already converted the
|
|
||||||
// SuspiciousOperationException into a BadRequestHttpException by the time
|
|
||||||
// renderables run, so we match that and inspect the original.
|
|
||||||
$this->renderable(function (BadRequestHttpException $e, Request $request): ?Response {
|
|
||||||
$previous = $e->getPrevious();
|
|
||||||
|
|
||||||
if (! $previous instanceof SuspiciousOperationException
|
|
||||||
|| ! str_starts_with($previous->getMessage(), 'Untrusted Host')) {
|
|
||||||
return null; // any other bad request keeps the default response
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = 'This hostname is not configured for this instance. '
|
|
||||||
.'Set APP_URL, or add the host to TRUSTED_HOSTS.';
|
|
||||||
|
|
||||||
if ($request->expectsJson()) {
|
|
||||||
return response()->json(['message' => $message], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->view('errors.untrusted-host', ['message' => $message], 400);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render($request, Throwable $e): Response|RedirectResponse
|
public function render($request, Throwable $e): Response|RedirectResponse
|
||||||
|
|||||||
@@ -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,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditResource\Pages;
|
||||||
|
use App\Models\Audit;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
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 $navigationIcon = 'heroicon-o-archive-box';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'System';
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('user_type')
|
||||||
|
->maxLength(255),
|
||||||
|
Forms\Components\TextInput::make('user_id'),
|
||||||
|
Forms\Components\TextInput::make('event')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Forms\Components\TextInput::make('auditable_type')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Forms\Components\TextInput::make('auditable_id')
|
||||||
|
->required(),
|
||||||
|
PrettyJsonField::make('old_values'),
|
||||||
|
PrettyJsonField::make('new_values'),
|
||||||
|
Forms\Components\Textarea::make('url'),
|
||||||
|
Forms\Components\TextInput::make('ip_address'),
|
||||||
|
Forms\Components\TextInput::make('user_agent')
|
||||||
|
->maxLength(1023),
|
||||||
|
Forms\Components\TextInput::make('tags')
|
||||||
|
->maxLength(255),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('user.name'),
|
||||||
|
Tables\Columns\TextColumn::make('event'),
|
||||||
|
Tables\Columns\TextColumn::make('auditable_type'),
|
||||||
|
Tables\Columns\TextColumn::make('auditable_id'),
|
||||||
|
IconColumn::make('was_command')
|
||||||
|
->getStateUsing(fn (Audit $record) => Str::startsWith($record->url, 'artisan '))
|
||||||
|
->boolean(),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->sortable()
|
||||||
|
->dateTime(),
|
||||||
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
|
->sortable()
|
||||||
|
->dateTime(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\ViewAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListAudits::route('/'),
|
||||||
|
'create' => Pages\CreateAudit::route('/create'),
|
||||||
|
'view' => Pages\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;
|
||||||
|
}
|
||||||
@@ -4,18 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Filament\Resources\ClientResource\Pages\CreateClient;
|
use App\Filament\Resources\ClientResource\Pages;
|
||||||
use App\Filament\Resources\ClientResource\Pages\EditClient;
|
|
||||||
use App\Filament\Resources\ClientResource\Pages\ListClients;
|
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
@@ -23,16 +18,16 @@ class ClientResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Client::class;
|
protected static ?string $model = Client::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-briefcase';
|
protected static ?string $navigationIcon = 'heroicon-o-briefcase';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
protected static ?string $navigationGroup = 'Timetracking';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 4;
|
protected static ?int $navigationSort = 4;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('name')
|
TextInput::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->required(),
|
->required(),
|
||||||
@@ -48,17 +43,17 @@ class ClientResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->label('Name')
|
->label('Name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('organization.name')
|
Tables\Columns\TextColumn::make('organization.name')
|
||||||
->sortable()
|
->sortable()
|
||||||
->label('Organization'),
|
->label('Organization'),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->label('Created at')
|
->label('Created at')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->label('Updated at')
|
->label('Updated at')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
])
|
])
|
||||||
@@ -73,12 +68,12 @@ class ClientResource extends Resource
|
|||||||
->relationship('organization', 'id')
|
->relationship('organization', 'id')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -93,9 +88,9 @@ class ClientResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListClients::route('/'),
|
'index' => Pages\ListClients::route('/'),
|
||||||
'create' => CreateClient::route('/create'),
|
'create' => Pages\CreateClient::route('/create'),
|
||||||
'edit' => EditClient::route('/{record}/edit'),
|
'edit' => Pages\EditClient::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ClientResource\Pages;
|
namespace App\Filament\Resources\ClientResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ClientResource;
|
use App\Filament\Resources\ClientResource;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditClient extends EditRecord
|
class EditClient extends EditRecord
|
||||||
@@ -15,7 +15,7 @@ class EditClient extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->icon('heroicon-m-trash'),
|
->icon('heroicon-m-trash'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\ClientResource\Pages;
|
namespace App\Filament\Resources\ClientResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ClientResource;
|
use App\Filament\Resources\ClientResource;
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListClients extends ListRecords
|
class ListClients extends ListRecords
|
||||||
@@ -15,7 +15,7 @@ class ListClients extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Actions\CreateAction::make()
|
||||||
->icon('heroicon-s-plus'),
|
->icon('heroicon-s-plus'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ namespace App\Filament\Resources;
|
|||||||
use App\Filament\Resources\FailedJobResource\Pages\ListFailedJobs;
|
use App\Filament\Resources\FailedJobResource\Pages\ListFailedJobs;
|
||||||
use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs;
|
use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs;
|
||||||
use App\Models\FailedJob;
|
use App\Models\FailedJob;
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Actions\BulkAction;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Actions\ViewAction;
|
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables\Actions\Action;
|
||||||
|
use Filament\Tables\Actions\BulkAction;
|
||||||
|
use Filament\Tables\Actions\DeleteAction;
|
||||||
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Tables\Actions\ViewAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -30,19 +30,19 @@ class FailedJobResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = FailedJob::class;
|
protected static ?string $model = FailedJob::class;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-circle';
|
protected static ?string $navigationIcon = 'heroicon-o-exclamation-circle';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'System';
|
protected static ?string $navigationGroup = 'System';
|
||||||
|
|
||||||
public static function getNavigationBadge(): ?string
|
public static function getNavigationBadge(): ?string
|
||||||
{
|
{
|
||||||
return (string) FailedJob::query()->count();
|
return (string) FailedJob::query()->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('uuid')->disabled()->columnSpan(4),
|
TextInput::make('uuid')->disabled()->columnSpan(4),
|
||||||
TextInput::make('failed_at')->disabled(),
|
TextInput::make('failed_at')->disabled(),
|
||||||
TextInput::make('id')->disabled(),
|
TextInput::make('id')->disabled(),
|
||||||
@@ -50,7 +50,7 @@ class FailedJobResource extends Resource
|
|||||||
TextInput::make('queue')->disabled(),
|
TextInput::make('queue')->disabled(),
|
||||||
|
|
||||||
// make text a little bit smaller because often a complete Stack Trace is shown:
|
// make text a little bit smaller because often a complete Stack Trace is shown:
|
||||||
Textarea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
|
TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
|
||||||
PrettyJsonField::make('payload')->disabled()->columnSpan(4),
|
PrettyJsonField::make('payload')->disabled()->columnSpan(4),
|
||||||
])->columns(4);
|
])->columns(4);
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ class FailedJobResource extends Resource
|
|||||||
TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true),
|
TextColumn::make('queue')->sortable()->searchable()->toggleable(isToggledHiddenByDefault: true),
|
||||||
])
|
])
|
||||||
->filters([])
|
->filters([])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkAction::make('retry')
|
BulkAction::make('retry')
|
||||||
->icon('heroicon-o-arrow-path')
|
->icon('heroicon-o-arrow-path')
|
||||||
->label('Retry selected')
|
->label('Retry selected')
|
||||||
@@ -91,7 +91,7 @@ class FailedJobResource extends Resource
|
|||||||
}),
|
}),
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
DeleteAction::make(),
|
DeleteAction::make(),
|
||||||
ViewAction::make(),
|
ViewAction::make(),
|
||||||
Action::make('retry')
|
Action::make('retry')
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
|
||||||
|
|
||||||
use App\Enums\GoalComparison;
|
|
||||||
use App\Enums\GoalPeriod;
|
|
||||||
use App\Enums\GoalType;
|
|
||||||
use App\Enums\Weekday;
|
|
||||||
use App\Filament\Resources\GoalResource\Pages;
|
|
||||||
use App\Models\Goal;
|
|
||||||
use App\Service\Dto\GoalFiltersDto;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Actions\ViewAction;
|
|
||||||
use Filament\Forms;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
|
||||||
|
|
||||||
class GoalResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = Goal::class;
|
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-viewfinder-circle';
|
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Timetracking';
|
|
||||||
|
|
||||||
protected static ?int $navigationSort = 8;
|
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return $schema
|
|
||||||
->columns(1)
|
|
||||||
->components([
|
|
||||||
Forms\Components\TextInput::make('name')
|
|
||||||
->label('Name')
|
|
||||||
->required()
|
|
||||||
->maxLength(255),
|
|
||||||
Forms\Components\Select::make('type')
|
|
||||||
->label('Type')
|
|
||||||
->options(collect(GoalType::cases())->mapWithKeys(fn (GoalType $type): array => [$type->value => $type->name])->all())
|
|
||||||
->disabled()
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('comparison')
|
|
||||||
->label('Comparison')
|
|
||||||
->options(collect(GoalComparison::cases())->mapWithKeys(fn (GoalComparison $comparison): array => [$comparison->value => $comparison->name])->all())
|
|
||||||
->required(),
|
|
||||||
Forms\Components\TextInput::make('target_seconds')
|
|
||||||
->label('Target (seconds)')
|
|
||||||
->numeric()
|
|
||||||
->minValue(1)
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('period')
|
|
||||||
->label('Period')
|
|
||||||
->options(collect(GoalPeriod::cases())->mapWithKeys(fn (GoalPeriod $period): array => [$period->value => $period->name])->all())
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('timezone')
|
|
||||||
->label('Timezone')
|
|
||||||
->options(fn (): array => collect(\DateTimeZone::listIdentifiers())->mapWithKeys(fn (string $timezone): array => [$timezone => $timezone])->all())
|
|
||||||
->searchable()
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('week_start')
|
|
||||||
->label('Week start')
|
|
||||||
->options(collect(Weekday::cases())->mapWithKeys(fn (Weekday $weekday): array => [$weekday->value => $weekday->name])->all())
|
|
||||||
->required(),
|
|
||||||
DateTimePicker::make('archived_at')
|
|
||||||
->label('Archived At'),
|
|
||||||
Forms\Components\Select::make('organization_id')
|
|
||||||
->label('Organization')
|
|
||||||
->relationship(name: 'organization', titleAttribute: 'name')
|
|
||||||
->searchable(['name'])
|
|
||||||
->disabled()
|
|
||||||
->required(),
|
|
||||||
Forms\Components\Select::make('member_id')
|
|
||||||
->label('Member (empty = every member)')
|
|
||||||
->relationship(name: 'member', titleAttribute: 'id')
|
|
||||||
->disabled(),
|
|
||||||
PrettyJsonField::make('filters')
|
|
||||||
->formatStateUsing(function (GoalFiltersDto $state, Goal $record): string {
|
|
||||||
return $record->getRawOriginal('filters');
|
|
||||||
})
|
|
||||||
->disabled(),
|
|
||||||
DateTimePicker::make('created_at')
|
|
||||||
->label('Created At')
|
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
|
||||||
DateTimePicker::make('updated_at')
|
|
||||||
->label('Updated At')
|
|
||||||
->hiddenOn(['create'])
|
|
||||||
->disabled(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('comparison')
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('target_seconds')
|
|
||||||
->label('Target')
|
|
||||||
->formatStateUsing(fn (int $state): string => sprintf('%dh %02dm', intdiv($state, 3600), intdiv($state % 3600, 60)))
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('period')
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('type')
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('member.user.email')
|
|
||||||
->label('Member')
|
|
||||||
->placeholder('Every member')
|
|
||||||
->searchable(),
|
|
||||||
TextColumn::make('archived_at')
|
|
||||||
->dateTime()
|
|
||||||
->placeholder('Not archived')
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
TextColumn::make('organization.name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('created_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('updated_at')
|
|
||||||
->dateTime()
|
|
||||||
->sortable()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
|
||||||
->defaultSort('created_at', 'desc')
|
|
||||||
->filters([
|
|
||||||
SelectFilter::make('type')
|
|
||||||
->label('Type')
|
|
||||||
->options(collect(GoalType::cases())->mapWithKeys(fn (GoalType $type): array => [$type->value => $type->name])->all()),
|
|
||||||
SelectFilter::make('organization')
|
|
||||||
->label('Organization')
|
|
||||||
->relationship('organization', 'name')
|
|
||||||
->searchable(),
|
|
||||||
SelectFilter::make('organization_id')
|
|
||||||
->label('Organization ID')
|
|
||||||
->relationship('organization', 'id')
|
|
||||||
->searchable(),
|
|
||||||
])
|
|
||||||
->recordActions([
|
|
||||||
ViewAction::make(),
|
|
||||||
EditAction::make(),
|
|
||||||
DeleteAction::make(),
|
|
||||||
])
|
|
||||||
->toolbarActions([
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => Pages\ListGoals::route('/'),
|
|
||||||
'edit' => Pages\EditGoal::route('/{record}/edit'),
|
|
||||||
'view' => Pages\ViewGoal::route('/{record}'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\GoalResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\GoalResource;
|
|
||||||
use Filament\Actions;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
|
|
||||||
class EditGoal extends EditRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = GoalResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
Actions\DeleteAction::make()
|
|
||||||
->icon('heroicon-m-trash'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\GoalResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\GoalResource;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
|
||||||
|
|
||||||
class ListGoals extends ListRecords
|
|
||||||
{
|
|
||||||
protected static string $resource = GoalResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\GoalResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\GoalResource;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
|
||||||
|
|
||||||
class ViewGoal extends ViewRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = GoalResource::class;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
EditAction::make('edit')
|
|
||||||
->icon('heroicon-s-pencil'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,21 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\EditOrganizationInvitation;
|
use App\Filament\Resources\OrganizationInvitationResource\Pages;
|
||||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\ListOrganizationInvitations;
|
|
||||||
use App\Filament\Resources\OrganizationInvitationResource\Pages\ViewOrganizationInvitation;
|
|
||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
use App\Service\OrganizationInvitationService;
|
use App\Service\OrganizationInvitationService;
|
||||||
use Filament\Actions\BulkAction;
|
use Filament\Forms;
|
||||||
use Filament\Actions\BulkActionGroup;
|
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Actions\EditAction;
|
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
@@ -29,34 +22,34 @@ class OrganizationInvitationResource extends Resource
|
|||||||
|
|
||||||
protected static ?string $label = 'Invitations';
|
protected static ?string $label = 'Invitations';
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user-plus';
|
protected static ?string $navigationIcon = 'heroicon-o-user-plus';
|
||||||
|
|
||||||
protected static string|\UnitEnum|null $navigationGroup = 'Users';
|
protected static ?string $navigationGroup = 'Users';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 9;
|
protected static ?int $navigationSort = 9;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Form $form): Form
|
||||||
{
|
{
|
||||||
return $schema
|
return $form
|
||||||
->columns(1)
|
->columns(1)
|
||||||
->components([
|
->schema([
|
||||||
TextInput::make('email')
|
Forms\Components\TextInput::make('email')
|
||||||
->label('Email')
|
->label('Email')
|
||||||
->disabledOn(['edit'])
|
->disabledOn(['edit'])
|
||||||
->required(),
|
->required(),
|
||||||
Select::make('role')
|
Select::make('role')
|
||||||
->options(Role::class),
|
->options(Role::class),
|
||||||
Select::make('organization_id')
|
Forms\Components\Select::make('organization_id')
|
||||||
->label('Organization')
|
->label('Organization')
|
||||||
->relationship(name: 'organization', titleAttribute: 'name')
|
->relationship(name: 'organization', titleAttribute: 'name')
|
||||||
->searchable(['name'])
|
->searchable(['name'])
|
||||||
->disabledOn(['edit'])
|
->disabledOn(['edit'])
|
||||||
->required(),
|
->required(),
|
||||||
DateTimePicker::make('created_at')
|
Forms\Components\DateTimePicker::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
DateTimePicker::make('updated_at')
|
Forms\Components\DateTimePicker::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->hiddenOn(['create'])
|
->hiddenOn(['create'])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
@@ -67,17 +60,17 @@ class OrganizationInvitationResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('organization.name')
|
Tables\Columns\TextColumn::make('organization.name')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('email')
|
Tables\Columns\TextColumn::make('email')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('role'),
|
Tables\Columns\TextColumn::make('role'),
|
||||||
TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->label('Created At')
|
->label('Created At')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('updated_at')
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
->label('Updated At')
|
->label('Updated At')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable()
|
->sortable()
|
||||||
@@ -87,13 +80,13 @@ class OrganizationInvitationResource extends Resource
|
|||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
DeleteAction::make(),
|
Tables\Actions\DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
BulkAction::make('resend')
|
Tables\Actions\BulkAction::make('resend')
|
||||||
->label('Resend')
|
->label('Resend')
|
||||||
->action(function (Collection $records): void {
|
->action(function (Collection $records): void {
|
||||||
foreach ($records as $organizationInvite) {
|
foreach ($records as $organizationInvite) {
|
||||||
@@ -113,9 +106,9 @@ class OrganizationInvitationResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => ListOrganizationInvitations::route('/'),
|
'index' => Pages\ListOrganizationInvitations::route('/'),
|
||||||
'edit' => EditOrganizationInvitation::route('/{record}/edit'),
|
'edit' => Pages\EditOrganizationInvitation::route('/{record}/edit'),
|
||||||
'view' => ViewOrganizationInvitation::route('/{record}'),
|
'view' => Pages\ViewOrganizationInvitation::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user