fix: final review wave — public routes, inbox 419, url schemes, deep_link, tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
nikita.hohlov
2026-09-04 09:09:07 -03:00
parent dd714a4be1
commit cbb43714ae
20 changed files with 123 additions and 20 deletions

View File

@@ -36,4 +36,13 @@ enum EventState: string
array_filter(self::cases(), fn (self $s) => ! $s->isTerminal()),
));
}
/** @return list<string> */
public static function badgeValues(): array
{
return array_values(array_map(
fn (self $s) => $s->value,
array_filter(self::cases(), fn (self $s) => $s->countsInBadge()),
));
}
}

View File

@@ -91,7 +91,7 @@ class InboxController extends Controller
/** Пропускает только относительные пути и http(s)-ссылки; прочее (напр. javascript:) отбрасывает. */
public static function safeLink(?string $link): ?string
{
return $link !== null && preg_match('#^(/(?!/)|https?://)#i', $link) === 1 ? $link : null;
return $link !== null && preg_match('#^(/(?![/\\\\])|https?://)#i', $link) === 1 ? $link : null;
}
public static function relative(int $diff): string

View File

@@ -31,8 +31,8 @@ class ChannelsController extends Controller
$data = $request->validate([
'type' => ['required', 'in:webhook'],
'config' => ['required', 'array'],
'config.deliver_url' => ['required', 'url'],
'config.presence_url' => ['required', 'url'],
'config.deliver_url' => ['required', 'url:http,https'],
'config.presence_url' => ['required', 'url:http,https'],
], ['type.in' => 'Через API добавляется только webhook; Telegram подключается через /start.']);
$channel = $user->channels()->create([

View File

@@ -10,9 +10,11 @@ use App\Ingest\EventUpserter;
use App\Models\Event;
use App\Models\Source;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class EventsController extends Controller
{
@@ -22,11 +24,14 @@ class EventsController extends Controller
{
/** @var User $user */
$user = $request->attributes->get('user');
$request->validate([
'state' => ['sometimes', Rule::in(array_map(fn ($s) => $s->value, EventState::cases()))],
]);
$states = $request->query('state')
? [$request->query('state')]
: EventState::nonTerminalValues();
$events = $user->events()->whereIn('state', $states)->orderBy('due_date')->orderBy('due_instant')->get();
$events = $user->events()->with('source')->whereIn('state', $states)->orderBy('due_date')->orderBy('due_instant')->get();
return response()->json(['events' => $events->map(fn (Event $e) => ApiPresenter::present($e) + ['source' => $e->source->name])->values()]);
}
@@ -66,7 +71,7 @@ class EventsController extends Controller
'source_ref' => 'manual:'.Str::ulid(),
'topic' => '',
'due_at' => $dueAt,
'fire_at' => \Carbon\CarbonImmutable::createFromFormat('Y-m-d', $data['due_date'])->subDays(30)->format('Y-m-d'),
'fire_at' => CarbonImmutable::createFromFormat('Y-m-d', $data['due_date'])->subDays(30)->format('Y-m-d'),
'after_due' => 'keep',
'payload' => array_filter([
'title' => $data['title'],

View File

@@ -10,7 +10,7 @@ use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
use Illuminate\Validation\ValidationException;
class ProfileController extends Controller
{
@@ -33,7 +33,7 @@ class ProfileController extends Controller
$start = $data['quiet_start'] ?? substr($user->quiet_start, 0, 5);
$end = $data['quiet_end'] ?? substr($user->quiet_end, 0, 5);
if ($end >= $start) {
throw \Illuminate\Validation\ValidationException::withMessages([
throw ValidationException::withMessages([
'quiet_end' => 'Тихие часы должны переходить через полночь: quiet_end раньше quiet_start',
]);
}
@@ -64,7 +64,7 @@ class ProfileController extends Controller
'tz' => $user->tz,
'quiet_start' => substr($user->quiet_start, 0, 5),
'quiet_end' => substr($user->quiet_end, 0, 5),
'badge' => $user->events()->whereIn('state', [EventState::Today->value, EventState::Overdue->value])->count(),
'badge' => $user->events()->whereIn('state', EventState::badgeValues())->count(),
];
}
}

View File

@@ -27,7 +27,7 @@ class UpsertEventsRequest extends FormRequest
'events.*.payload' => ['required', 'array'],
'events.*.payload.title' => ['required', 'string'],
'events.*.payload.subtitle' => ['sometimes', 'nullable', 'string'],
'events.*.payload.deep_link' => ['sometimes', 'nullable', 'string', 'max:2048', 'regex:#^(/(?!/)|https?://)#'],
'events.*.payload.deep_link' => ['sometimes', 'nullable', 'string', 'max:2048', 'regex:#^(/(?![/\\\\])|https?://)#i'],
'events.*.payload.done_label' => ['sometimes', 'nullable', 'string', 'max:64'],
];
}