Added opaque join-token support across backend and frontend: new migration/model/service/endpoints, guest controllers now resolve tokens, and the demo seeder seeds a token. Tenant event details list/manage tokens with copy/revoke actions, and the guest PWA uses tokens end-to-end (routing, storage, uploads, achievements, etc.). Docs TODO updated to reflect completed steps.
This commit is contained in:
@@ -73,7 +73,7 @@ class MigrateToPackages extends Command
|
||||
'tenant_id' => $event->tenant_id,
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $freePackage->id,
|
||||
'type' => 'endcustomer_event',
|
||||
'type' => 'endcustomer',
|
||||
'provider_id' => 'migration',
|
||||
'price' => 0,
|
||||
'metadata' => ['migrated_from_credits' => true],
|
||||
|
||||
@@ -10,9 +10,45 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Support\ImageHelper;
|
||||
use App\Services\EventJoinTokenService;
|
||||
|
||||
class EventPublicController extends BaseController
|
||||
{
|
||||
public function __construct(private readonly EventJoinTokenService $joinTokenService)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: object|null, 1: \App\Models\EventJoinToken|null}
|
||||
*/
|
||||
private function resolvePublishedEvent(string $identifier, array $columns = ['id']): array
|
||||
{
|
||||
$event = DB::table('events')
|
||||
->where('slug', $identifier)
|
||||
->where('status', 'published')
|
||||
->first($columns);
|
||||
|
||||
if ($event) {
|
||||
return [$event, null];
|
||||
}
|
||||
|
||||
$joinToken = $this->joinTokenService->findActiveToken($identifier);
|
||||
if (! $joinToken) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$event = DB::table('events')
|
||||
->where('id', $joinToken->event_id)
|
||||
->where('status', 'published')
|
||||
->first($columns);
|
||||
|
||||
if (! $event) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [$event, $joinToken];
|
||||
}
|
||||
|
||||
private function getLocalized($value, $locale, $default = '') {
|
||||
if (is_string($value) && json_decode($value) !== null) {
|
||||
$data = json_decode($value, true);
|
||||
@@ -45,36 +81,46 @@ class EventPublicController extends BaseController
|
||||
|
||||
return $path; // fallback as-is
|
||||
}
|
||||
public function event(string $slug)
|
||||
public function event(string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first([
|
||||
'id', 'slug', 'name', 'default_locale', 'created_at', 'updated_at'
|
||||
[$event, $joinToken] = $this->resolvePublishedEvent($identifier, [
|
||||
'id',
|
||||
'slug',
|
||||
'name',
|
||||
'default_locale',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'event_type_id',
|
||||
]);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
|
||||
$locale = request()->query('locale', 'de');
|
||||
$locale = request()->query('locale', $event->default_locale ?? 'de');
|
||||
$nameData = json_decode($event->name, true);
|
||||
$localizedName = $nameData[$locale] ?? $nameData['de'] ?? $event->name;
|
||||
|
||||
// Get event type for icon
|
||||
$eventType = DB::table('events')
|
||||
->join('event_types', 'events.event_type_id', '=', 'event_types.id')
|
||||
->where('events.id', $event->id)
|
||||
->first(['event_types.slug as type_slug', 'event_types.name as type_name']);
|
||||
$eventType = null;
|
||||
if ($event->event_type_id) {
|
||||
$eventType = DB::table('event_types')
|
||||
->where('id', $event->event_type_id)
|
||||
->first(['slug as type_slug', 'name as type_name']);
|
||||
}
|
||||
|
||||
$locale = request()->query('locale', 'de');
|
||||
$eventTypeData = $eventType ? [
|
||||
'slug' => $eventType->type_slug,
|
||||
'name' => $this->getLocalized($eventType->type_name, $locale, 'Event'),
|
||||
'icon' => $eventType->type_slug === 'wedding' ? '❤️' : '👥'
|
||||
'icon' => $eventType->type_slug === 'wedding' ? 'heart' : 'guests',
|
||||
] : [
|
||||
'slug' => 'general',
|
||||
'name' => $this->getLocalized('Event', $locale, 'Event'),
|
||||
'icon' => '👥'
|
||||
'icon' => 'guests',
|
||||
];
|
||||
|
||||
if ($joinToken) {
|
||||
$this->joinTokenService->incrementUsage($joinToken);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'id' => $event->id,
|
||||
'slug' => $event->slug,
|
||||
@@ -83,12 +129,13 @@ class EventPublicController extends BaseController
|
||||
'created_at' => $event->created_at,
|
||||
'updated_at' => $event->updated_at,
|
||||
'type' => $eventTypeData,
|
||||
'join_token' => $joinToken?->token,
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
public function stats(string $slug)
|
||||
public function stats(string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
@@ -125,18 +172,20 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
|
||||
public function emotions(string $slug)
|
||||
public function emotions(string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
|
||||
$eventId = $event->id;
|
||||
|
||||
$rows = DB::table('emotions')
|
||||
->join('emotion_event_type', 'emotions.id', '=', 'emotion_event_type.emotion_id')
|
||||
->join('event_types', 'emotion_event_type.event_type_id', '=', 'event_types.id')
|
||||
->join('events', 'events.event_type_id', '=', 'event_types.id')
|
||||
->where('events.id', $event->id)
|
||||
->where('events.id', $eventId)
|
||||
->select([
|
||||
'emotions.id',
|
||||
'emotions.name',
|
||||
@@ -174,17 +223,19 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
|
||||
public function tasks(string $slug, Request $request)
|
||||
public function tasks(string $identifier, Request $request)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
|
||||
$eventId = $event->id;
|
||||
|
||||
$query = DB::table('tasks')
|
||||
->join('task_collection_task', 'tasks.id', '=', 'task_collection_task.task_id')
|
||||
->join('event_task_collection', 'task_collection_task.task_collection_id', '=', 'event_task_collection.task_collection_id')
|
||||
->where('event_task_collection.event_id', $event->id)
|
||||
->where('event_task_collection.event_id', $eventId)
|
||||
->select([
|
||||
'tasks.id',
|
||||
'tasks.title',
|
||||
@@ -258,9 +309,9 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
|
||||
public function photos(Request $request, string $slug)
|
||||
public function photos(Request $request, string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
@@ -404,18 +455,19 @@ class EventPublicController extends BaseController
|
||||
return response()->json(['liked' => true, 'likes_count' => $count]);
|
||||
}
|
||||
|
||||
public function upload(Request $request, string $slug)
|
||||
public function upload(Request $request, string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
$eventId = $event->id;
|
||||
|
||||
$deviceId = (string) $request->header('X-Device-Id', 'anon');
|
||||
$deviceId = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', $deviceId), 0, 64) ?: 'anon';
|
||||
|
||||
// Per-device cap per event (MVP: 50)
|
||||
$deviceCount = DB::table('photos')->where('event_id', $event->id)->where('guest_name', $deviceId)->count();
|
||||
$deviceCount = DB::table('photos')->where('event_id', $eventId)->where('guest_name', $deviceId)->count();
|
||||
if ($deviceCount >= 50) {
|
||||
return response()->json(['error' => ['code' => 'limit_reached', 'message' => 'Upload-Limit erreicht']], 429);
|
||||
}
|
||||
@@ -429,17 +481,17 @@ class EventPublicController extends BaseController
|
||||
]);
|
||||
|
||||
$file = $validated['photo'];
|
||||
$path = Storage::disk('public')->putFile("events/{$event->id}/photos", $file);
|
||||
$path = Storage::disk('public')->putFile("events/{$eventId}/photos", $file);
|
||||
$url = Storage::url($path);
|
||||
|
||||
// Generate thumbnail (JPEG) under photos/thumbs
|
||||
$baseName = pathinfo($path, PATHINFO_FILENAME);
|
||||
$thumbRel = "events/{$event->id}/photos/thumbs/{$baseName}_thumb.jpg";
|
||||
$thumbRel = "events/{$eventId}/photos/thumbs/{$baseName}_thumb.jpg";
|
||||
$thumbPath = ImageHelper::makeThumbnailOnDisk('public', $path, $thumbRel, 640, 82);
|
||||
$thumbUrl = $thumbPath ? Storage::url($thumbPath) : $url;
|
||||
|
||||
$id = DB::table('photos')->insertGetId([
|
||||
'event_id' => $event->id,
|
||||
'event_id' => $eventId,
|
||||
'task_id' => $validated['task_id'] ?? null,
|
||||
'guest_name' => $validated['guest_name'] ?? $deviceId,
|
||||
'file_path' => $url,
|
||||
@@ -447,7 +499,7 @@ class EventPublicController extends BaseController
|
||||
'likes_count' => 0,
|
||||
|
||||
// Handle emotion_id: prefer explicit ID, fallback to slug lookup, then default
|
||||
'emotion_id' => $this->resolveEmotionId($validated, $event->id),
|
||||
'emotion_id' => $this->resolveEmotionId($validated, $eventId),
|
||||
'is_featured' => 0,
|
||||
'metadata' => null,
|
||||
'created_at' => now(),
|
||||
@@ -502,9 +554,9 @@ class EventPublicController extends BaseController
|
||||
|
||||
return $defaultEmotion ?: 1; // Ultimate fallback to emotion ID 1 (assuming "Happy" exists)
|
||||
}
|
||||
public function achievements(Request $request, string $slug)
|
||||
public function achievements(Request $request, string $identifier)
|
||||
{
|
||||
$event = DB::table('events')->where('slug', $slug)->where('status', 'published')->first(['id']);
|
||||
[$event] = $this->resolvePublishedEvent($identifier, ['id']);
|
||||
if (! $event) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Event not found or not public']], 404);
|
||||
}
|
||||
@@ -718,4 +770,3 @@ class EventPublicController extends BaseController
|
||||
->header('ETag', $etag);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ namespace App\Http\Controllers\Api\Tenant;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Tenant\EventStoreRequest;
|
||||
use App\Http\Resources\Tenant\EventJoinTokenResource;
|
||||
use App\Http\Resources\Tenant\EventResource;
|
||||
use App\Models\Event;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\Photo;
|
||||
use App\Services\EventJoinTokenService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
@@ -18,6 +20,10 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EventController extends Controller
|
||||
{
|
||||
public function __construct(private readonly EventJoinTokenService $joinTokenService)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
@@ -283,12 +289,26 @@ class EventController extends Controller
|
||||
return response()->json(['error' => 'Event not found'], 404);
|
||||
}
|
||||
|
||||
$token = (string) Str::uuid();
|
||||
$link = url("/e/{$event->slug}?invite={$token}");
|
||||
$validated = $request->validate([
|
||||
'label' => ['nullable', 'string', 'max:255'],
|
||||
'expires_at' => ['nullable', 'date', 'after:now'],
|
||||
'usage_limit' => ['nullable', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
$attributes = array_filter([
|
||||
'label' => $validated['label'] ?? null,
|
||||
'expires_at' => $validated['expires_at'] ?? null,
|
||||
'usage_limit' => $validated['usage_limit'] ?? null,
|
||||
'created_by' => $request->user()?->id,
|
||||
], fn ($value) => ! is_null($value));
|
||||
|
||||
$joinToken = $this->joinTokenService->createToken($event, $attributes);
|
||||
|
||||
return response()->json([
|
||||
'link' => $link,
|
||||
'token' => $token,
|
||||
'link' => url("/e/{$event->slug}?invite={$joinToken->token}"),
|
||||
'token' => $joinToken->token,
|
||||
'token_url' => url('/e/'.$joinToken->token),
|
||||
'join_token' => new EventJoinTokenResource($joinToken),
|
||||
]);
|
||||
}
|
||||
public function bulkUpdateStatus(Request $request): JsonResponse
|
||||
|
||||
73
app/Http/Controllers/Api/Tenant/EventJoinTokenController.php
Normal file
73
app/Http/Controllers/Api/Tenant/EventJoinTokenController.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Tenant;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Tenant\EventJoinTokenResource;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventJoinToken;
|
||||
use App\Services\EventJoinTokenService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class EventJoinTokenController extends Controller
|
||||
{
|
||||
public function __construct(private readonly EventJoinTokenService $joinTokenService)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request, Event $event): JsonResponse
|
||||
{
|
||||
$this->authorizeEvent($request, $event);
|
||||
|
||||
$tokens = $event->joinTokens()
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
return EventJoinTokenResource::collection($tokens);
|
||||
}
|
||||
|
||||
public function store(Request $request, Event $event): JsonResponse
|
||||
{
|
||||
$this->authorizeEvent($request, $event);
|
||||
|
||||
$validated = $request->validate([
|
||||
'label' => ['nullable', 'string', 'max:255'],
|
||||
'expires_at' => ['nullable', 'date', 'after:now'],
|
||||
'usage_limit' => ['nullable', 'integer', 'min:1'],
|
||||
'metadata' => ['nullable', 'array'],
|
||||
]);
|
||||
|
||||
$token = $this->joinTokenService->createToken($event, array_merge($validated, [
|
||||
'created_by' => Auth::id(),
|
||||
]));
|
||||
|
||||
return (new EventJoinTokenResource($token))
|
||||
->response()
|
||||
->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Event $event, EventJoinToken $joinToken): JsonResponse
|
||||
{
|
||||
$this->authorizeEvent($request, $event);
|
||||
|
||||
if ($joinToken->event_id !== $event->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$reason = $request->input('reason');
|
||||
$token = $this->joinTokenService->revoke($joinToken, $reason);
|
||||
|
||||
return new EventJoinTokenResource($token);
|
||||
}
|
||||
|
||||
private function authorizeEvent(Request $request, Event $event): void
|
||||
{
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
|
||||
if ($event->tenant_id !== $tenantId) {
|
||||
abort(404, 'Event not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Models\Event;
|
||||
use App\Models\Photo;
|
||||
use App\Models\User;
|
||||
use App\Services\EventJoinTokenService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -13,6 +14,10 @@ use Illuminate\Support\Str;
|
||||
|
||||
class TenantController extends BaseController
|
||||
{
|
||||
public function __construct(private readonly EventJoinTokenService $joinTokenService)
|
||||
{
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$creds = $request->validate([
|
||||
@@ -145,7 +150,7 @@ class TenantController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
public function createInvite(int $id)
|
||||
public function createInvite(Request $request, int $id)
|
||||
{
|
||||
$u = Auth::user();
|
||||
$tenantId = $u->tenant_id ?? null;
|
||||
@@ -153,10 +158,16 @@ class TenantController extends BaseController
|
||||
if ($tenantId && $ev->tenant_id !== $tenantId) {
|
||||
return response()->json(['error' => ['code' => 'forbidden']], 403);
|
||||
}
|
||||
$token = Str::random(32);
|
||||
Cache::put('invite:'.$token, $ev->slug, now()->addDays(2));
|
||||
$link = url('/e/'.$ev->slug).'?t='.$token;
|
||||
return response()->json(['link' => $link]);
|
||||
|
||||
$joinToken = $this->joinTokenService->createToken($ev, [
|
||||
'created_by' => $u?->id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'link' => url('/e/'.$joinToken->token),
|
||||
'legacy_link' => url('/e/'.$ev->slug).'?invite='.$joinToken->token,
|
||||
'token' => $joinToken->token,
|
||||
]);
|
||||
}
|
||||
|
||||
public function eventPhotos(int $id)
|
||||
|
||||
@@ -9,15 +9,12 @@ use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Checkout\Session;
|
||||
use Stripe\StripeClient;
|
||||
use Exception;
|
||||
use PayPalHttp\Client;
|
||||
use PayPalHttp\HttpException;
|
||||
use PayPalCheckout\OrdersCreateRequest;
|
||||
use PayPalCheckout\OrdersCaptureRequest;
|
||||
use PayPalCheckout\OrdersGetRequest;
|
||||
use PayPalCheckout\Order;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\BlogPost;
|
||||
use App\Models\Package;
|
||||
@@ -71,7 +68,7 @@ class MarketingController extends Controller
|
||||
*/
|
||||
public function buyPackages(Request $request, $packageId)
|
||||
{
|
||||
Log::info('Buy packages called', ['auth' => Auth::check(), 'package_id' => $packageId]);
|
||||
Log::info('Buy packages called', ['auth' => Auth::check(), 'package_id' => $packageId, 'provider' => $request->input('provider', 'stripe')]);
|
||||
$package = Package::findOrFail($packageId);
|
||||
|
||||
if (!Auth::check()) {
|
||||
@@ -163,6 +160,8 @@ class MarketingController extends Controller
|
||||
],
|
||||
]);
|
||||
|
||||
Log::info('Stripe Checkout initiated', ['package_id' => $packageId, 'session_id' => $session->id, 'tenant_id' => $tenant->id]);
|
||||
|
||||
return redirect($session->url, 303);
|
||||
}
|
||||
|
||||
@@ -212,6 +211,8 @@ class MarketingController extends Controller
|
||||
$response = $ordersController->createOrder($createRequest);
|
||||
$order = $response->result;
|
||||
|
||||
Log::info('PayPal Checkout initiated', ['package_id' => $packageId, 'order_id' => $order->id, 'tenant_id' => $tenant->id]);
|
||||
|
||||
session(['paypal_order_id' => $order->id]);
|
||||
|
||||
foreach ($order->links as $link) {
|
||||
@@ -224,7 +225,7 @@ class MarketingController extends Controller
|
||||
} catch (HttpException $e) {
|
||||
Log::error('PayPal Orders API error: ' . $e->getMessage());
|
||||
return back()->with('error', 'Zahlung fehlgeschlagen');
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
Log::error('PayPal checkout error: ' . $e->getMessage());
|
||||
return back()->with('error', 'Zahlung fehlgeschlagen');
|
||||
}
|
||||
@@ -282,6 +283,9 @@ class MarketingController extends Controller
|
||||
*/
|
||||
public function success(Request $request, $packageId = null)
|
||||
{
|
||||
$provider = session('paypal_order_id') ? 'paypal' : 'stripe';
|
||||
Log::info('Payment Success: Provider processed', ['provider' => $provider, 'package_id' => $packageId]);
|
||||
|
||||
if (session('paypal_order_id')) {
|
||||
$orderId = session('paypal_order_id');
|
||||
$client = Client::create([
|
||||
@@ -299,6 +303,8 @@ class MarketingController extends Controller
|
||||
$captureResponse = $ordersController->captureOrder($captureRequest);
|
||||
$capture = $captureResponse->result;
|
||||
|
||||
Log::info('PayPal Capture completed', ['order_id' => $orderId, 'status' => $capture->status]);
|
||||
|
||||
if ($capture->status === 'COMPLETED') {
|
||||
$customId = $capture->purchaseUnits[0]->customId ?? null;
|
||||
if ($customId) {
|
||||
@@ -423,7 +429,7 @@ class MarketingController extends Controller
|
||||
|
||||
// Transform to array with translated strings for the current locale
|
||||
$markdown = $postModel->getTranslation('content', $locale) ?? $postModel->getTranslation('content', 'de') ?? '';
|
||||
$converter = new \League\CommonMark\CommonMarkConverter();
|
||||
$converter = new CommonMarkConverter();
|
||||
$contentHtml = (string) $converter->convert($markdown);
|
||||
|
||||
// Debug log for content_html
|
||||
|
||||
29
app/Http/Resources/Tenant/EventJoinTokenResource.php
Normal file
29
app/Http/Resources/Tenant/EventJoinTokenResource.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Tenant;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class EventJoinTokenResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'label' => $this->label,
|
||||
'token' => $this->token,
|
||||
'url' => url('/e/'.$this->token),
|
||||
'usage_limit' => $this->usage_limit,
|
||||
'usage_count' => $this->usage_count,
|
||||
'expires_at' => optional($this->expires_at)->toIso8601String(),
|
||||
'revoked_at' => optional($this->revoked_at)->toIso8601String(),
|
||||
'is_active' => $this->isActive(),
|
||||
'created_at' => optional($this->created_at)->toIso8601String(),
|
||||
'metadata' => $this->metadata ?? new \stdClass(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,11 @@ class Event extends Model
|
||||
return $this->belongsTo(EventPackage::class);
|
||||
}
|
||||
|
||||
public function joinTokens(): HasMany
|
||||
{
|
||||
return $this->hasMany(EventJoinToken::class);
|
||||
}
|
||||
|
||||
public function hasActivePackage(): bool
|
||||
{
|
||||
return $this->eventPackage && $this->eventPackage->isActive();
|
||||
|
||||
58
app/Models/EventJoinToken.php
Normal file
58
app/Models/EventJoinToken.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EventJoinToken extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'event_id',
|
||||
'token',
|
||||
'label',
|
||||
'usage_limit',
|
||||
'usage_count',
|
||||
'expires_at',
|
||||
'revoked_at',
|
||||
'created_by',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'expires_at' => 'datetime',
|
||||
'revoked_at' => 'datetime',
|
||||
'usage_limit' => 'integer',
|
||||
'usage_count' => 'integer',
|
||||
];
|
||||
|
||||
public function event(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Event::class);
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
if ($this->revoked_at !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->expires_at !== null && $this->expires_at->isPast()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->usage_limit !== null && $this->usage_count >= $this->usage_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
85
app/Services/EventJoinTokenService.php
Normal file
85
app/Services/EventJoinTokenService.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\EventJoinToken;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EventJoinTokenService
|
||||
{
|
||||
public function createToken(Event $event, array $attributes = []): EventJoinToken
|
||||
{
|
||||
return DB::transaction(function () use ($event, $attributes) {
|
||||
$tokenValue = $this->generateUniqueToken();
|
||||
|
||||
$payload = [
|
||||
'event_id' => $event->id,
|
||||
'token' => $tokenValue,
|
||||
'label' => Arr::get($attributes, 'label'),
|
||||
'usage_limit' => Arr::get($attributes, 'usage_limit'),
|
||||
'metadata' => Arr::get($attributes, 'metadata', []),
|
||||
];
|
||||
|
||||
if ($expiresAt = Arr::get($attributes, 'expires_at')) {
|
||||
$payload['expires_at'] = $expiresAt instanceof Carbon
|
||||
? $expiresAt
|
||||
: Carbon::parse($expiresAt);
|
||||
}
|
||||
|
||||
if ($createdBy = Arr::get($attributes, 'created_by')) {
|
||||
$payload['created_by'] = $createdBy;
|
||||
}
|
||||
|
||||
return EventJoinToken::create($payload);
|
||||
});
|
||||
}
|
||||
|
||||
public function revoke(EventJoinToken $joinToken, ?string $reason = null): EventJoinToken
|
||||
{
|
||||
$joinToken->revoked_at = now();
|
||||
|
||||
if ($reason) {
|
||||
$metadata = $joinToken->metadata ?? [];
|
||||
$metadata['revoked_reason'] = $reason;
|
||||
$joinToken->metadata = $metadata;
|
||||
}
|
||||
|
||||
$joinToken->save();
|
||||
|
||||
return $joinToken;
|
||||
}
|
||||
|
||||
public function incrementUsage(EventJoinToken $joinToken): void
|
||||
{
|
||||
$joinToken->increment('usage_count');
|
||||
}
|
||||
|
||||
public function findActiveToken(string $token): ?EventJoinToken
|
||||
{
|
||||
return EventJoinToken::query()
|
||||
->where('token', $token)
|
||||
->whereNull('revoked_at')
|
||||
->where(function ($query) {
|
||||
$query->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->whereNull('usage_limit')
|
||||
->orWhereColumn('usage_limit', '>', 'usage_count');
|
||||
})
|
||||
->first();
|
||||
}
|
||||
|
||||
protected function generateUniqueToken(int $length = 48): string
|
||||
{
|
||||
do {
|
||||
$token = Str::random($length);
|
||||
} while (EventJoinToken::where('token', $token)->exists());
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user