Compare commits

..
21 Commits
Author SHA1 Message Date
Twyco 149748b03e Merge pull request 'fix: make project template inheritance granular and safe' (#107) from fix/kan-14 into epic/kan-3
linter / quality (pull_request) Successful in 2m59s
tests / ci (8.4) (pull_request) Successful in 3m17s
Reviewed-on: #107
2026-09-08 10:24:16 +00:00
Nico Schröder 3122e00ed8 fix: make project template inheritance granular and safe
linter / quality (pull_request) Successful in 2m52s
tests / ci (8.4) (pull_request) Successful in 3m15s
2026-09-08 12:03:07 +02:00
Twyco 0f125f17a3 Merge branch 'main' into epic/kan-3
linter / quality (pull_request) Successful in 1m30s
tests / ci (8.4) (pull_request) Successful in 1m54s
2026-06-25 01:25:53 +02:00
Twyco 5e7be28daf Merge pull request 'Fix KAN-3 project template flows' (#103) from codex/kan-3-template-fixes into epic/kan-3
linter / quality (pull_request) Successful in 1m36s
tests / ci (8.4) (pull_request) Successful in 1m51s
Reviewed-on: OpenSource/KanbanGit#103
2026-06-24 10:56:35 +00:00
Twyco d250f0b380 Fix KAN-3 project template flows
linter / quality (pull_request) Successful in 1m31s
tests / ci (8.4) (pull_request) Successful in 1m47s
2026-06-24 01:16:25 +02:00
Twyco c19cf34505 Merge pull request 'kan-11' (#99) from koda/kan-11-restliche-projekt-konfiguration-auf-templates-umstellen into epic/kan-3
linter / quality (pull_request) Successful in 1m36s
tests / ci (8.4) (pull_request) Failing after 1m51s
Reviewed-on: OpenSource/KanbanGit#99
2026-06-23 22:41:16 +00:00
Twyco 37b2e6034f Merge pull request 'koda/kan-10-edit-templates' (#102) from koda/kan-10-edit-templates into epic/kan-3
linter / quality (pull_request) Successful in 1m31s
tests / ci (8.4) (pull_request) Successful in 1m52s
Reviewed-on: OpenSource/KanbanGit#102
2026-06-23 22:40:46 +00:00
Twyco 84e427ef5d Merge pull request 'koda/kan-9-switchreset-template-for-existing-projects' (#101) from koda/kan-9-switchreset-template-for-existing-projects into epic/kan-3
linter / quality (pull_request) Successful in 1m31s
tests / ci (8.4) (pull_request) Successful in 1m42s
Reviewed-on: OpenSource/KanbanGit#101
2026-06-23 22:40:38 +00:00
Twyco 95b53ba74b Merge pull request 'Add template context to API and MCP project payloads' (#100) from koda/kan-8-add-project-template-to-mcp-api into epic/kan-3
linter / quality (pull_request) Successful in 1m24s
tests / ci (8.4) (pull_request) Successful in 1m46s
Reviewed-on: OpenSource/KanbanGit#100
2026-06-23 22:40:29 +00:00
Twyco a8952d7867 Merge pull request 'KAN-7 Made Templates Overwritable' (#89) from koda/kan-7-made-templates-overwritable into epic/kan-3
linter / quality (pull_request) Successful in 1m28s
tests / ci (8.4) (pull_request) Successful in 1m48s
Reviewed-on: OpenSource/KanbanGit#89
Reviewed-by: Eric <eric-peters6@gmx.de>
2026-06-23 21:17:52 +00:00
Koda 2390daf2de Move remaining project metadata ownership to templates
linter / quality (pull_request) Successful in 1m26s
tests / ci (8.4) (pull_request) Successful in 1m49s
2026-06-05 20:31:38 +00:00
Twyco b1357cf854 Merge pull request 'KAN-6 Project rely on project templates' (#88) from koda/kan-6-project-rely-on-project-templates into epic/kan-3
Reviewed-on: OpenSource/KanbanGit#88
2026-06-04 20:06:22 +00:00
Twyco 2c229c9b39 Merge pull request 'KAN-5 Create Template from Project & Create Project form Template' (#87) from koda/kan-5-create-template-from-project-create-project-form-template into epic/kan-3
Reviewed-on: OpenSource/KanbanGit#87
2026-06-04 19:44:32 +00:00
Twyco 9a7dd7fb59 Merge pull request 'KAN-4 Duplicate Project' (#86) from koda/kan-4-duplicate-project into epic/kan-3
Reviewed-on: OpenSource/KanbanGit#86
Reviewed-by: Twyco <twyco@icloud.com>
2026-06-04 19:21:11 +00:00
Koda 39a039cd1e Add editable project template flow 2026-05-25 11:12:55 +00:00
Koda e38c77198d Add template switch and reset flows for existing projects 2026-05-25 08:31:35 +00:00
Koda ee9b9a2765 Add template context to API and MCP project payloads 2026-05-25 08:19:09 +00:00
Koda 57711b9659 Show template inheritance overrides 2026-05-25 01:53:03 +00:00
Koda 5e6426067a Make projects depend on templates 2026-05-25 01:41:27 +00:00
Koda 58d2edcacd Add reusable project templates 2026-05-25 01:26:08 +00:00
Koda 3b17c9ee14 Add project duplication flow
linter / quality (pull_request) Successful in 2m11s
tests / ci (8.4) (pull_request) Successful in 1m46s
2026-05-24 22:36:21 +00:00
31 changed files with 798 additions and 1902 deletions
-96
View File
@@ -1,96 +0,0 @@
---
name: mcp-development
description: "Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP."
license: MIT
metadata:
author: laravel
---
# MCP Development
## Documentation
Use `search-docs` for detailed Laravel MCP patterns and documentation.
## Basic Usage
Register MCP servers in `routes/ai.php`:
<!-- Register MCP Server -->
```php
use Laravel\Mcp\Facades\Mcp;
Mcp::web();
```
### Creating MCP Primitives
Create MCP tools, resources, prompts, and servers using artisan commands:
```bash
php artisan make:mcp-tool ToolName # Create a tool
php artisan make:mcp-resource ResourceName # Create a resource
php artisan make:mcp-prompt PromptName # Create a prompt
php artisan make:mcp-server ServerName # Create a server
```
After creating primitives, register them in your server's `$tools`, `$resources`, or `$prompts` properties.
### Tools
<!-- MCP Tool Example -->
```php
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Request;
use Laravel\Mcp\Server\Response;
class MyTool extends Tool
{
public function handle(Request $request): Response
{
return new Response(['result' => 'success']);
}
}
```
### Registering Primitives in a Server
Each MCP server must explicitly declare the tools, resources, and prompts it exposes.
<!-- Register Primitives in MCP Server -->
```php
use Laravel\Mcp\Server;
class AppServer extends Server
{
protected array $tools = [
\App\Mcp\Tools\MyTool::class,
];
protected array $resources = [
\App\Mcp\Resources\MyResource::class,
];
protected array $prompts = [
\App\Mcp\Prompts\MyPrompt::class,
];
}
```
## Verification
1. Check `routes/ai.php` for proper registration
2. Test tool via MCP client
## Common Pitfalls
- Running `mcp:start` command (it hangs waiting for input)
- Using HTTPS locally with Node-based MCP clients
- Not using `search-docs` for the latest MCP documentation
- Not registering MCP server routes in `routes/ai.php`
- Do not register `ai.php` in `bootstrap.php`; it is registered automatically.
- OAuth registration supports custom URI schemes (e.g., `cursor://`, `vscode://`) for native desktop clients via `mcp.custom_schemes` config
+7 -6
View File
@@ -25,6 +25,7 @@
**/compose.*.y*ml
!**/composer.json
!**/composer.lock
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
@@ -40,9 +41,9 @@ vendor
LICENSE
README.md
# Runtime-generated data must not become part of an image layer.
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/testing/*
storage/framework/views/*
storage/logs/*
# Ignore development-related files and directories
# For development environment, we will use mount bind to mount the source code to the container
# So we don't need to copy the source code to the container for development
tests/
phpunit.xml
phpunit.xml.dist
+1 -17
View File
@@ -10,7 +10,7 @@ APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
LOG_STACK=stderr
LOG_STACK=daily
# Optional Discord error logging (append `,discord_errors` to LOG_STACK to enable it)
LOG_DISCORD_WEBHOOK_URL=
@@ -38,22 +38,6 @@ MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="${APP_NAME}"
CACHE_STORE=database
QUEUE_CONNECTION=database
SESSION_DRIVER=database
FILESYSTEM_DISK=public
AVATAR_FILESYSTEM_DISK=public
AVATAR_URLS_TEMPORARY=false
AVATAR_URL_EXPIRATION_MINUTES=15
# Production: set AVATAR_FILESYSTEM_DISK=s3 and inject these as runtime secrets.
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=eu-central-1
AWS_BUCKET=
AWS_URL=
AWS_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
@@ -1,18 +0,0 @@
name: test_and_check_app
description: Build the test image and run all application checks
runs:
using: composite
steps:
- name: Test and check app
shell: bash
run: |
set -Eeuo pipefail
docker buildx build \
--file Dockerfile \
--target test \
--tag "local/kanbangit-test:${GITHUB_SHA}" \
--progress plain \
--load \
.
-130
View File
@@ -1,130 +0,0 @@
name: CI/CD
on:
push:
pull_request:
permissions:
contents: read
env:
REGISTRY: git.twyco.de
jobs:
test_and_check_app:
name: test_and_check_app
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Run required application checks
uses: ./.github/tests/test_and_check_app
publish:
name: Publish immutable image
if: >-
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
needs:
- test_and_check_app
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
- name: Validate ref and calculate image name
id: image
shell: bash
run: |
set -Eeuo pipefail
image_path="$(printf '%s' "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
image="${REGISTRY}/${image_path}"
case "${GITHUB_REF}" in
refs/heads/main)
printf -v run_attempt '%03d' "${GITHUB_RUN_ATTEMPT}"
tag="main-${GITHUB_RUN_NUMBER}-${run_attempt}-${GITHUB_SHA:0:12}"
environment="staging"
;;
refs/tags/*)
tag="${GITHUB_REF#refs/tags/}"
if [[ ! "${tag}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "Production tags must match vMAJOR.MINOR.PATCH." >&2
exit 1
fi
git fetch --no-tags origin main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then
echo "Production tag must point to a commit contained in main." >&2
exit 1
fi
environment="production"
;;
*)
echo "Unsupported ref: ${GITHUB_REF}" >&2
exit 1
;;
esac
{
echo "image=${image}"
echo "tag=${tag}"
echo "environment=${environment}"
} >> "${GITHUB_OUTPUT}"
- name: Log in to private registry
shell: bash
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -Eeuo pipefail
if [[ -z "${REGISTRY_USERNAME}" || -z "${REGISTRY_TOKEN}" ]]; then
echo "Registry credentials are missing." >&2
exit 1
fi
printf '%s' "${REGISTRY_TOKEN}" |
docker login \
--username "${REGISTRY_USERNAME}" \
--password-stdin \
"${REGISTRY}"
- name: Build and publish runtime image
shell: bash
env:
IMAGE: ${{ steps.image.outputs.image }}
IMAGE_TAG: ${{ steps.image.outputs.tag }}
DEPLOYMENT_ENVIRONMENT: ${{ steps.image.outputs.environment }}
run: |
set -Eeuo pipefail
if [[ "${DEPLOYMENT_ENVIRONMENT}" == "production" ]] &&
docker buildx imagetools inspect "${IMAGE}:${IMAGE_TAG}" >/dev/null 2>&1; then
echo "Production image ${IMAGE}:${IMAGE_TAG} already exists and will not be overwritten." >&2
exit 1
fi
docker buildx build \
--file Dockerfile \
--target runtime \
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
--label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
--label "de.twyco.environment=${DEPLOYMENT_ENVIRONMENT}" \
--tag "${IMAGE}:${IMAGE_TAG}" \
--push \
.
- name: Remove registry credentials
if: ${{ always() }}
shell: bash
run: docker logout "${REGISTRY}" >/dev/null 2>&1 || true
+39
View File
@@ -0,0 +1,39 @@
name: linter
on:
push:
branches:
- main
- staging
pull_request:
permissions:
contents: write
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Install Dependencies
run: |
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
npm install
- name: Run Pint
run: composer lint
- name: Lint Frontend
run: npm run lint
# - name: Commit Changes
# uses: stefanzweifel/git-auto-commit-action@v7
# with:
# commit_message: fix code style
# commit_options: '--no-verify'
+49
View File
@@ -0,0 +1,49 @@
name: tests
on:
push:
branches:
- main
- staging
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.4']
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer:v2
coverage: xdebug
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install Node Dependencies
run: npm i
- name: Install Dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Copy Environment File
run: cp .env.example .env
- name: Generate Application Key
run: php artisan key:generate
- name: Build Assets
run: npm run build
- name: Tests
run: ./vendor/bin/phpunit
+2 -2
View File
@@ -13,10 +13,10 @@ This application is a Laravel application and its main Laravel ecosystems packag
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v2
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13
- laravel/mcp (MCP) - v0
- laravel/prompts (PROMPTS) - v0
- laravel/wayfinder (WAYFINDER) - v0
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pail (PAIL) - v1
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
@@ -34,7 +34,7 @@ This project has domain-specific skills available. You MUST activate the relevan
- `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.
- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns.
- `mcp-development` — Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP.
- `project-route-structure` — Always apply in this project when creating, moving, or refactoring Laravel route definitions in routes/**/*.php; changing route prefixes, names, middleware, scoped bindings, or route files; or adding frontend-facing endpoints that Wayfinder generates. Documents the KanbanGit route-file layout and grouping conventions.
- `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task
- `inertia-vue-development` — Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation.
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
-109
View File
@@ -1,109 +0,0 @@
ARG COMPOSER_IMAGE=composer:2.9.5@sha256:698d3801b2a622ace460c4743c781282fcbcb733a4cbf8b31c44731e846585e8
ARG NODE_IMAGE=node:22-bookworm-slim@sha256:d649c27dae7ba0137b3cef5dd75baa422c08dc3d9e3fc0c23dfb172dc3cc6436
ARG PHP_IMAGE=php:8.4-fpm-bookworm@sha256:c5fb7a0c02f4efe280691910c8b734995fa83598cdcf3115ef5dcb2e4617681c
FROM ${COMPOSER_IMAGE} AS composer_binary
FROM ${NODE_IMAGE} AS node_binary
FROM ${PHP_IMAGE} AS php_base
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gettext-base \
git \
libfcgi-bin \
libfreetype6-dev \
libicu-dev \
libjpeg62-turbo-dev \
libmagickwand-dev \
libonig-dev \
libpng-dev \
libpq-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
libzip-dev \
nginx \
procps \
unzip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" \
bcmath \
gd \
intl \
opcache \
pcntl \
pdo_mysql \
pdo_pgsql \
pdo_sqlite \
pgsql \
soap \
zip \
&& pecl install imagick \
&& docker-php-ext-enable imagick \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY --from=composer_binary /usr/bin/composer /usr/local/bin/composer
WORKDIR /var/www
FROM php_base AS build
COPY --from=node_binary /usr/local/ /usr/local/
COPY . /var/www
RUN composer install --prefer-dist --no-interaction --no-progress \
&& npm ci \
&& npm run build
FROM build AS test
RUN touch .env \
&& composer audit --locked --no-interaction \
&& COMPOSER_ALLOW_SUPERUSER=1 composer ci:check
FROM build AS production_builder
RUN composer install \
--classmap-authoritative \
--no-dev \
--no-interaction \
--no-progress \
--prefer-dist \
&& rm -rf node_modules tests phpunit.xml
FROM php_base AS runtime
COPY docker/entrypoint.sh /usr/local/bin/kanbangit-entrypoint
COPY docker/nginx/nginx.conf.template /etc/nginx/nginx.conf.template
COPY docker/php-fpm-healthcheck.sh /usr/local/bin/php-fpm-healthcheck
COPY --from=production_builder --chown=www-data:www-data /var/www /var/www
RUN chmod +x /usr/local/bin/kanbangit-entrypoint /usr/local/bin/php-fpm-healthcheck \
&& mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
&& printf '%s\n' \
'[global]' \
'daemonize = no' \
'' \
'[www]' \
'pm.status_path = /status' \
> /usr/local/etc/php-fpm.d/zz-kanbangit.conf \
&& mkdir -p \
/var/www/bootstrap/cache \
/var/www/storage/app/public \
/var/www/storage/framework/cache \
/var/www/storage/framework/sessions \
/var/www/storage/framework/testing \
/var/www/storage/framework/views \
/var/www/storage/logs \
&& ln -s ../storage/app/public /var/www/public/storage \
&& chown -R www-data:www-data /var/www/bootstrap/cache /var/www/storage
USER www-data
EXPOSE 8080 9000
ENTRYPOINT ["kanbangit-entrypoint"]
CMD ["php-fpm"]
@@ -7,9 +7,9 @@ use App\Http\Resources\TicketPriorityResource;
use App\Http\Resources\TicketReferenceResource;
use App\Http\Resources\TicketStatusResource;
use App\Http\Resources\TicketTypeResource;
use App\Models\Ticket;
use App\Http\Resources\UserResource;
use App\Models\Project;
use App\Models\Ticket;
use App\Models\User;
class BuildProjectTicketOptionsPayload
+3 -11
View File
@@ -30,10 +30,9 @@ class UpdateUserAvatar
$color = $this->colorFor($user);
$path = sprintf('avatars/generated/%d-%s.svg', $user->id, (string) Str::uuid());
$stored = Storage::disk($this->avatarDisk())->put(
$stored = Storage::disk('public')->put(
$path,
$this->svg($this->initials($user->name), $color),
['ContentType' => 'image/svg+xml'],
);
if (! $stored) {
@@ -54,7 +53,7 @@ class UpdateUserAvatar
public function upload(User $user, UploadedFile $avatar): User
{
$oldPath = $user->avatar_path;
$path = $avatar->store('avatars/uploads', $this->avatarDisk());
$path = $avatar->store('avatars/uploads', 'public');
if (! is_string($path)) {
throw new RuntimeException('Uploaded avatar could not be stored.');
@@ -91,14 +90,7 @@ class UpdateUserAvatar
return;
}
Storage::disk($this->avatarDisk())->delete($path);
}
private function avatarDisk(): string
{
$disk = config('filesystems.avatar_disk', 'public');
return is_string($disk) && $disk !== '' ? $disk : 'public';
Storage::disk('public')->delete($path);
}
private function initials(string $name): string
-11
View File
@@ -8,7 +8,6 @@ use App\Models\User;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
#[Signature('app:create-admin-user {name=Admin} {email=admin@kanbangit.test} {password=test}')]
@@ -26,9 +25,6 @@ class CreateAdminUser extends Command
*/
public function handle(): int
{
DB::beginTransaction();
try {
$user = User::create([
'name' => $this->argument('name'),
'email' => $this->argument('email'),
@@ -42,14 +38,7 @@ class CreateAdminUser extends Command
]);
$this->info(sprintf('Admin User created with emil=\'%s\' and password=\'%s\'', $this->argument('email'), $this->argument('password')));
DB::commit();
return self::SUCCESS;
} catch (\Exception $exception) {
$this->error('Admin User could not be created: ', $exception->getMessage());
DB::rollBack();
return self::FAILURE;
}
}
}
+3 -19
View File
@@ -77,28 +77,12 @@ class User extends Authenticatable
protected function avatar(): Attribute
{
return Attribute::get(
fn (): ?string => $this->avatarUrl(),
fn (): ?string => filled($this->avatar_path)
? Storage::disk('public')->url($this->avatar_path)
: null,
);
}
private function avatarUrl(): ?string
{
if (blank($this->avatar_path)) {
return null;
}
$diskName = config('filesystems.avatar_disk', 'public');
$disk = Storage::disk(is_string($diskName) && $diskName !== '' ? $diskName : 'public');
if ((bool) config('filesystems.avatar_urls_temporary', false)) {
$expirationMinutes = max(1, (int) config('filesystems.avatar_url_expiration_minutes', 15));
return $disk->temporaryUrl($this->avatar_path, now()->addMinutes($expirationMinutes));
}
return $disk->url($this->avatar_path);
}
public function permissionAssignments(): MorphMany
{
return $this->morphMany(PermissionAssignment::class, 'assignee');
-1
View File
@@ -9,7 +9,6 @@
"skills": [
"fortify-development",
"laravel-best-practices",
"mcp-development",
"wayfinder-development",
"inertia-vue-development",
"tailwindcss-development"
-2
View File
@@ -17,8 +17,6 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '*');
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
$middleware->web(append: [
+3 -4
View File
@@ -12,11 +12,10 @@
"php": "^8.4",
"inertiajs/inertia-laravel": "^2.0",
"laravel/fortify": "^1.34",
"laravel/framework": "^v13.26.1",
"laravel/framework": "^13.0",
"laravel/mcp": "^0.6",
"laravel/tinker": "^3.0",
"laravel/wayfinder": "^0.1.14",
"league/flysystem-aws-s3-v3": "^3.0",
"scalar/laravel": "^0.2.1"
},
"require-dev": {
@@ -52,7 +51,7 @@
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"@php artisan dev"
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"dev:ssr": [
"npm run build:ssr",
@@ -67,8 +66,8 @@
],
"ci:check": [
"Composer\\Config::disableProcessTimeout",
"npm audit --audit-level=high",
"npm run lint:check",
"npm run format:check",
"npm run types:check",
"@test"
],
Generated
+332 -791
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -15,12 +15,6 @@ return [
'default' => env('FILESYSTEM_DISK', 'local'),
'avatar_disk' => env('AVATAR_FILESYSTEM_DISK', env('FILESYSTEM_DISK', 'public')),
'avatar_urls_temporary' => env('AVATAR_URLS_TEMPORARY', false),
'avatar_url_expiration_minutes' => env('AVATAR_URL_EXPIRATION_MINUTES', 15),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
+12 -32
View File
@@ -1,38 +1,33 @@
services:
web:
container_name: kanbangit-web
image: kanbangit-app:local
build:
context: .
dockerfile: Dockerfile
target: runtime
command: nginx
dockerfile: ./docker/nginx/Dockerfile
restart: unless-stopped
ports:
- "8080:8080"
- "8080:80"
volumes:
- kanbangit-storage:/var/www/storage:ro
- kanbangit-public-assets:/var/www/public/build:ro
env_file:
- .env.production
environment:
PHP_FPM_UPSTREAM: php-fpm:9000
networks:
- kanbangit-web
healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8080/up"]
interval: 10s
timeout: 5s
retries: 3
depends_on:
php-fpm:
condition: service_healthy
php-fpm:
container_name: kanbangit-php-fpm
image: kanbangit-app:local
build:
context: .
dockerfile: Dockerfile
target: runtime
dockerfile: ./docker/php-fpm/Dockerfile
target: production
restart: unless-stopped
volumes:
- kanbangit-storage:/var/www/storage
- kanbangit-public-assets:/var/www/public/build
env_file:
- .env.production
networks:
@@ -47,23 +42,6 @@ services:
mariadb:
condition: service_healthy
worker:
container_name: kanbangit-worker
image: kanbangit-app:local
build:
context: .
dockerfile: Dockerfile
target: runtime
command: php artisan queue:work --sleep=3 --tries=3 --timeout=60 --max-time=3600
restart: unless-stopped
env_file:
- .env.production
networks:
- kanbangit-db
depends_on:
mariadb:
condition: service_healthy
mariadb:
container_name: kanbangit-db
image: mariadb:11.4
@@ -87,3 +65,5 @@ networks:
volumes:
mariadb-data-production:
kanbangit-storage:
kanbangit-public-assets:
-26
View File
@@ -1,26 +0,0 @@
#!/bin/sh
set -eu
if [ "${1:-}" = "nginx" ]; then
: "${PHP_FPM_UPSTREAM:=127.0.0.1:9000}"
export PHP_FPM_UPSTREAM
envsubst '${PHP_FPM_UPSTREAM}' \
< /etc/nginx/nginx.conf.template \
> /tmp/nginx.conf
exec nginx -c /tmp/nginx.conf -g 'daemon off;'
fi
mkdir -p \
bootstrap/cache \
storage/framework/cache \
storage/framework/sessions \
storage/framework/testing \
storage/framework/views \
storage/logs
php artisan config:cache --no-ansi
php artisan route:cache --no-ansi
exec "$@"
+20
View File
@@ -0,0 +1,20 @@
FROM php:8.4-cli-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates git unzip \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get update && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sS https://getcomposer.org/installer | php -- \
--install-dir=/usr/local/bin --filename=composer
WORKDIR /var/www
COPY . /var/www
RUN composer install --no-dev --prefer-dist --no-interaction --no-progress --optimize-autoloader
RUN npm ci && npm run build
FROM nginx:alpine
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /var/www/public /var/www/public
WORKDIR /var/www/public
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+33
View File
@@ -0,0 +1,33 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
client_max_body_size 100m;
server {
listen 80;
server_name localhost;
root /var/www/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ^~ /storage/ {
alias /var/www/storage/app/public/;
try_files $uri =404;
access_log off;
expires 7d;
add_header Cache-Control "public";
}
location ~ \.php$ {
fastcgi_pass php-fpm:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
}
-58
View File
@@ -1,58 +0,0 @@
pid /tmp/nginx.pid;
error_log /dev/stderr warn;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
client_body_temp_path /tmp/client_body;
proxy_temp_path /tmp/proxy;
fastcgi_temp_path /tmp/fastcgi;
uwsgi_temp_path /tmp/uwsgi;
scgi_temp_path /tmp/scgi;
sendfile on;
keepalive_timeout 65;
client_max_body_size 3m;
server {
listen 8080;
server_name _;
root /var/www/public;
index index.php;
location = /nginx-health {
access_log off;
default_type text/plain;
return 200 'ok';
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /index.php {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY '';
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host;
fastcgi_param HTTP_X_FORWARDED_PORT $http_x_forwarded_port;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_pass ${PHP_FPM_UPSTREAM};
}
location ~ \.php$ {
return 404;
}
location ~ /\. {
deny all;
}
}
}
-7
View File
@@ -1,7 +0,0 @@
#!/bin/sh
set -eu
SCRIPT_NAME=/status \
SCRIPT_FILENAME=/status \
REQUEST_METHOD=GET \
cgi-fcgi -bind -connect 127.0.0.1:9000 2>/dev/null | grep -q '^pool:'
+66
View File
@@ -0,0 +1,66 @@
FROM php:8.4-fpm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
unzip \
libpq-dev \
libonig-dev \
libssl-dev \
libxml2-dev \
libcurl4-openssl-dev \
libicu-dev \
libzip-dev \
libmagickwand-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql \
pdo_pgsql \
pgsql \
opcache \
intl \
zip \
bcmath \
soap \
&& docker-php-ext-configure gd --with-jpeg --with-freetype \
&& docker-php-ext-install -j$(nproc) gd \
&& pecl install imagick \
&& docker-php-ext-enable imagick \
&& apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /var/www
COPY . /var/www
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& composer install --no-dev --optimize-autoloader --no-interaction --no-progress --prefer-dist
FROM php:8.4-fpm AS production
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
libicu-dev \
libzip-dev \
libfcgi-bin \
procps \
libmagickwand-dev \
&& apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN curl -o /usr/local/bin/php-fpm-healthcheck \
https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/master/php-fpm-healthcheck \
&& chmod +x /usr/local/bin/php-fpm-healthcheck
COPY ./docker/php-fpm/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
COPY ./storage /var/www/storage-init
COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
COPY --from=builder /usr/local/bin/docker-php-ext-* /usr/local/bin/
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN printf '%s\n' \
'[global]' \
'daemonize = no' \
'' \
'[www]' \
'pm.status_path = /status' \
> /usr/local/etc/php-fpm.d/zz-docker.conf
COPY --from=builder /var/www /var/www
WORKDIR /var/www
RUN chown -R www-data:www-data /var/www
USER www-data
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
EXPOSE 9000
CMD ["php-fpm"]
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -e
if [ ! "$(ls -A /var/www/storage)" ]; then
echo "Initializing storage directory..."
cp -R /var/www/storage-init/. /var/www/storage
chown -R www-data:www-data /var/www/storage
fi
rm -rf /var/www/storage-init
php artisan config:cache
php artisan route:cache
exec "$@"
-3
View File
@@ -37,10 +37,7 @@ const eslintConfig = [
'bootstrap/cache/**',
'public/build/**',
'public/hot',
'resources/js/actions/**',
'resources/js/components/ui/**',
'resources/js/routes/**',
'resources/js/wayfinder/**',
'resources/views/mail/**',
],
},
+198 -497
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -19,10 +19,7 @@
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_KEY" value="base64:11ujGYe5vzbwqcdFDVQC6PuDgd7c8PKD9kWt9HH9Bvs="/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="AVATAR_FILESYSTEM_DISK" value="public"/>
<env name="AVATAR_URLS_TEMPORARY" value="false"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
+3 -36
View File
@@ -5,14 +5,11 @@ namespace Tests\Feature\Models;
use App\Models\Team;
use App\Models\User;
use Database\Seeders\UserSeeder;
use DateTimeInterface;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Mockery;
use Tests\TestCase;
class UserTest extends TestCase
@@ -35,13 +32,9 @@ class UserTest extends TestCase
]);
}
public function test_user_avatar_attribute_returns_configured_storage_url(): void
public function test_user_avatar_attribute_returns_public_storage_url(): void
{
config([
'filesystems.avatar_disk' => 'avatar-test',
'filesystems.avatar_urls_temporary' => false,
]);
Storage::fake('avatar-test');
Storage::fake('public');
$user = User::factory()->create([
'avatar_path' => 'avatars/generated/example.svg',
@@ -50,38 +43,12 @@ class UserTest extends TestCase
]);
$this->assertSame(
Storage::disk('avatar-test')->url('avatars/generated/example.svg'),
Storage::disk('public')->url('avatars/generated/example.svg'),
$user->avatar,
);
$this->assertArrayNotHasKey('avatar_path', $user->toArray());
}
public function test_user_avatar_attribute_returns_temporary_url_for_private_storage(): void
{
config([
'filesystems.avatar_disk' => 's3',
'filesystems.avatar_urls_temporary' => true,
'filesystems.avatar_url_expiration_minutes' => 10,
]);
$disk = Mockery::mock(FilesystemAdapter::class);
$disk->shouldReceive('temporaryUrl')
->once()
->with(
'avatars/uploads/private.png',
Mockery::on(fn (mixed $expiration): bool => $expiration instanceof DateTimeInterface),
)
->andReturn('https://object-storage.test/signed-avatar');
Storage::shouldReceive('disk')->once()->with('s3')->andReturn($disk);
$user = User::factory()->create([
'avatar_path' => 'avatars/uploads/private.png',
'avatar_is_generated' => false,
]);
$this->assertSame('https://object-storage.test/signed-avatar', $user->avatar);
}
public function test_user_can_identify_uploaded_avatars(): void
{
$generated = User::factory()->make([
+4 -5
View File
@@ -72,15 +72,14 @@ class ProfileUpdateTest extends TestCase
public function test_user_can_upload_a_profile_picture()
{
config(['filesystems.avatar_disk' => 'avatar-test']);
Storage::fake('avatar-test');
Storage::fake('public');
$user = User::factory()->create([
'avatar_path' => 'avatars/generated/old.svg',
'avatar_color' => '#F97316',
'avatar_is_generated' => true,
]);
Storage::disk('avatar-test')->put($user->avatar_path, '<svg>OU</svg>');
Storage::disk('public')->put($user->avatar_path, '<svg>OU</svg>');
$response = $this
->actingAs($user)
@@ -101,8 +100,8 @@ class ProfileUpdateTest extends TestCase
$this->assertSame('#F97316', $user->avatar_color);
$this->assertNotSame('avatars/generated/old.svg', $user->avatar_path);
$this->assertStringStartsWith('avatars/uploads/', $user->avatar_path);
Storage::disk('avatar-test')->assertMissing('avatars/generated/old.svg');
Storage::disk('avatar-test')->assertExists($user->avatar_path);
Storage::disk('public')->assertMissing('avatars/generated/old.svg');
Storage::disk('public')->assertExists($user->avatar_path);
}
public function test_user_can_delete_their_uploaded_profile_picture()