Add project general settings danger zone #25

Merged
Twyco merged 4 commits from codex/project-general-settings-danger-zone into main 2026-04-13 16:07:19 +00:00
7 changed files with 101 additions and 72 deletions
Showing only changes of commit 73cee8178f - Show all commits
@@ -0,0 +1,28 @@
---
name: project-request-validation-style
description: "Always apply in this project when changing Laravel controllers or FormRequest classes. Enforces dedicated FormRequest usage for controller validation, validated-array access via Arr::get(), and forbids custom single-field getter methods or request helper extraction like boolean()/integer()/string() for app input handling."
---
# Project Request Validation Style
## When to Apply
Activate this skill whenever work in this project changes:
- Laravel controllers
- Laravel FormRequest classes
- Validation flow between controllers and requests
## Rules
- Do not add inline controller validation with `$request->validate(...)` when the endpoint has request-specific validation needs.
- Prefer dedicated `FormRequest` classes and use `$validated = $request->validated();` in controllers.
- When reading validated arrays, use `Arr::get($validated, 'key')` instead of direct array access.
- Do not use request helper extraction like `$request->boolean()`, `$request->integer()`, `$request->string()`, or similar helpers for application input handling.
- Do not add custom FormRequest getter helpers such as `points()`, `branchName()`, `ticketAttributes()`, or similar single-purpose accessors.
- Build controller payloads from the validated array directly with `Arr::get(...)`.
## Notes
- `Arr::get(...)` already returns `null` by default, so do not wrap it in redundant existence checks when `null` is acceptable.
- Keep validation logic in the FormRequest and payload shaping in the controller or dedicated action.
+3 -1
View File
@@ -37,6 +37,7 @@ This project has domain-specific skills available. You MUST activate the relevan
- `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.
- `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.
- `project-request-validation-style` — Always activate for this project when changing Laravel controllers or FormRequest classes. Use dedicated FormRequest classes for controller validation, access validated data through `$validated = $request->validated();` plus `Arr::get(...)`, avoid request helper extraction like `boolean()/integer()/string()`, and do not add custom single-field getter methods on FormRequest classes.
## Conventions
@@ -102,7 +103,7 @@ This project has domain-specific skills available. You MUST activate the relevan
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
=== php rules ===
@@ -209,6 +210,7 @@ Wayfinder generates TypeScript functions for Laravel routes. Import from `@/acti
# Inertia + Vue
Vue components must have a single root element.
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
</laravel-boost-guidelines>
+3 -11
View File
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Http\Requests\DeleteProjectRequest;
use App\Http\Requests\StoreProjectRequest;
use App\Http\Requests\SyncProjectPermissionsRequest;
use App\Models\Project;
@@ -12,7 +13,6 @@ use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
@@ -157,17 +157,9 @@ class ProjectController extends Controller
->with('status', "{$team->name} permissions were updated.");
}
public function destroy(Request $request, Project $project): RedirectResponse
public function destroy(DeleteProjectRequest $request, Project $project): RedirectResponse
{
$request->validate([
'confirmation_key' => [
'required',
'string',
Rule::in([$project->key]),
],
], [
'confirmation_key.in' => 'Enter the exact project key to confirm deletion.',
]);
$request->validated();
$projectName = $project->name;
@@ -109,7 +109,16 @@ class ProjectTicketController extends Controller
$validated = $request->validated();
$fieldValues = Arr::get($validated, 'field_values');
$ticketAttributes = $request->ticketAttributes();
$ticketAttributes = [
'acceptance_criteria' => Arr::get($validated, 'acceptance_criteria'),
'blocked_comment' => Arr::get($validated, 'blocked_comment'),
'description' => Arr::get($validated, 'description'),
'is_blocked' => Arr::get($validated, 'is_blocked', false),
'parent_ticket_id' => Arr::get($validated, 'parent_ticket_id'),
'ticket_priority_id' => Arr::get($validated, 'ticket_priority_id'),
'ticket_type_id' => Arr::get($validated, 'ticket_type_id'),
'title' => Arr::get($validated, 'title'),
];
$wasUpdated = $this->updateProjectTicket->handle(
$ticket,
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests;
use App\Models\Project;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class DeleteProjectRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
/**
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
/** @var Project $project */
$project = $this->route('project');
return [
'confirmation_key' => [
'required',
'string',
Rule::in([$project->key]),
],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'confirmation_key.in' => 'Enter the exact project key to confirm deletion.',
];
}
}
+5 -2
View File
@@ -4,6 +4,7 @@ namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rule;
class StoreProjectRequest extends FormRequest
@@ -18,9 +19,11 @@ class StoreProjectRequest extends FormRequest
protected function prepareForValidation(): void
{
if ($this->has('key')) {
$key = Arr::get($this->all(), 'key');
if ($key !== null) {
$this->merge([
'key' => strtoupper((string) $this->string('key')->trim()),
'key' => strtoupper(trim((string) $key)),
]);
}
}
@@ -71,14 +71,16 @@ class UpdateProjectTicketRequest extends FormRequest
{
return [
function (Validator $validator): void {
/** @var array<string, mixed> $validated */
$validated = $this->validated();
/** @var Project $project */
$project = $this->route('project');
/** @var Ticket $ticket */
$ticket = $this->route('ticket');
$parentTicketId = $this->input('parent_ticket_id');
$parentTicketId = is_numeric($parentTicketId) ? (int) $parentTicketId : null;
$parentTicketId = Arr::get($validated, 'parent_ticket_id');
if ($parentTicketId !== null) {
if ($parentTicketId === $ticket->id) {
@@ -105,12 +107,10 @@ class UpdateProjectTicketRequest extends FormRequest
}
}
$requestedTicketTypeId = $this->input('ticket_type_id');
$requestedTicketType = is_numeric($requestedTicketTypeId)
? TicketType::query()
->where('project_id', $project->id)
->find((int) $requestedTicketTypeId)
: null;
$requestedTicketTypeId = Arr::get($validated, 'ticket_type_id');
$requestedTicketType = TicketType::query()
->where('project_id', $project->id)
->find($requestedTicketTypeId);
if (
$requestedTicketType?->system_key !== 'parent'
@@ -119,61 +119,13 @@ class UpdateProjectTicketRequest extends FormRequest
$validator->errors()->add('ticket_type_id', 'Tickets with child tickets must keep a Parent type.');
}
if ($this->boolean('is_blocked') && blank($this->input('blocked_comment'))) {
if (Arr::get($validated, 'is_blocked', false) && blank(Arr::get($validated, 'blocked_comment'))) {
$validator->errors()->add('blocked_comment', 'Blocked tickets require a blocker comment.');
}
},
];
}
/**
* @return array{
* acceptance_criteria: string|null,
* blocked_comment: string|null,
* description: string|null,
* is_blocked: bool,
* parent_ticket_id: int|null,
* ticket_priority_id: int,
* ticket_type_id: int,
* title: string
* }
*/
public function ticketAttributes(): array
{
/** @var array{
* acceptance_criteria: string|null,
* blocked_comment: string|null,
* description: string|null,
* is_blocked: bool,
* parent_ticket_id: int|null,
* ticket_priority_id: int,
* ticket_type_id: int,
* title: string
* } $attributes
*/
$attributes = array_merge(
[
'acceptance_criteria' => null,
'blocked_comment' => null,
'description' => null,
'is_blocked' => false,
'parent_ticket_id' => null,
],
Arr::only($this->validated(), [
'acceptance_criteria',
'blocked_comment',
'description',
'is_blocked',
'parent_ticket_id',
'ticket_priority_id',
'ticket_type_id',
'title',
]),
);
return $attributes;
}
private function createsCycle(Ticket $ticket, Ticket $candidateParent): bool
{
$currentParent = $candidateParent;