api: Newsletter-Anmeldung über die API eingestellt (410)

Der Endpunkt legte Abos ohne Bestätigungsweg an, die Spezifikation versprach
ein Double-Opt-in; seit dem Go-Live kein Aufruf. Antwortet jetzt mit 410,
die Ability ist für neue Tokens nicht mehr wählbar (Roadmap 4.3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-09 13:35:54 +00:00
parent 002dc08b5c
commit ccef83b952
6 changed files with 38 additions and 214 deletions

View file

@ -3,78 +3,21 @@
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\SubscribeNewsletterRequest;
use App\Models\NewsletterSubscription;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Newsletter-Anmeldung über die API — abgeschaltet am 09.10.2026
* (Roadmap 4.3). Der Endpunkt legte Abos ohne Bestätigungsweg an, obwohl die
* Spezifikation ein Double-Opt-in versprach, und wurde seit dem Go-Live nicht
* genutzt. Eine Anmeldung mit Double-Opt-in kommt mit dem Branchen-Digest
* (Roadmap 6.4); bis dahin antwortet die Route mit 410.
*/
class NewsletterSubscriptionController extends Controller
{
public function store(SubscribeNewsletterRequest $request): JsonResponse
public function store(): JsonResponse
{
$validated = $request->validated();
$email = mb_strtolower($validated['email']);
$subscription = DB::transaction(function () use ($request, $validated, $email): NewsletterSubscription {
$subscription = NewsletterSubscription::withoutGlobalScopes()
->where('portal', $validated['portal'])
->where('email', $email)
->lockForUpdate()
->first();
// Nur ein fremder, tatsächlich gesetzter Eigentümer blockiert (409).
// Verwaiste Datensätze (user_id NULL nach Konto-Löschung/Legacy-Import)
// dürfen neu gebunden werden — sonst wäre die portal+email-Kombination
// dauerhaft nicht mehr abonnierbar.
if ($subscription !== null && $subscription->user_id !== null && $subscription->user_id !== $request->user()->id) {
abort(409, 'This email address is already subscribed.');
}
if ($subscription === null) {
return NewsletterSubscription::withoutGlobalScopes()->create([
'portal' => $validated['portal'],
'email' => $email,
'user_id' => $request->user()->id,
'salutation_key' => $validated['salutation_key'] ?? null,
'first_name' => $validated['first_name'] ?? null,
'last_name' => $validated['last_name'] ?? null,
'ip_address' => $request->ip(),
'is_confirmed' => false,
'confirmation_token' => Str::random(32),
'subscribed_at' => now(),
'unsubscribed_at' => null,
]);
}
$attributes = [
'user_id' => $request->user()->id,
'salutation_key' => $validated['salutation_key'] ?? null,
'first_name' => $validated['first_name'] ?? null,
'last_name' => $validated['last_name'] ?? null,
'ip_address' => $request->ip(),
'subscribed_at' => now(),
'unsubscribed_at' => null,
];
if (! $subscription->is_confirmed) {
$attributes['confirmation_token'] = Str::random(32);
}
$subscription->update($attributes);
return $subscription;
});
return response()->json([
'message' => 'Newsletter subscription created.',
'data' => [
'id' => $subscription->id,
'portal' => $subscription->portal->value,
'email' => $subscription->email,
'is_confirmed' => $subscription->is_confirmed,
],
], 201);
'message' => 'Newsletter subscription via the API has been discontinued.',
], 410);
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1;
use App\Enums\Portal;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SubscribeNewsletterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user()?->tokenCan('newsletter:subscribe') === true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'portal' => ['required', Rule::in([
Portal::Presseecho->value,
Portal::Businessportal24->value,
])],
'email' => ['required', 'email', 'max:190'],
'salutation_key' => ['nullable', 'string', 'max:20'],
'first_name' => ['nullable', 'string', 'max:80'],
'last_name' => ['nullable', 'string', 'max:80'],
];
}
}