diff --git a/app/Audit/AbstractAuditLogFormatter.php b/app/Audit/AbstractAuditLogFormatter.php index 634b5a431..6fc8e7364 100644 --- a/app/Audit/AbstractAuditLogFormatter.php +++ b/app/Audit/AbstractAuditLogFormatter.php @@ -24,5 +24,22 @@ final public function setContext(AuditContext $ctx): void $this->ctx = $ctx; } + protected function getUserInfo(): string + { + if (!$this->ctx) { + return 'Unknown (unknown)'; + } + + $user_name = 'Unknown'; + if ($this->ctx->userFirstName || $this->ctx->userLastName) { + $user_name = trim(sprintf("%s %s", $this->ctx->userFirstName ?? '', $this->ctx->userLastName ?? '')) ?: 'Unknown'; + } elseif ($this->ctx->userEmail) { + $user_name = $this->ctx->userEmail; + } + + $user_id = $this->ctx->userId ?? 'unknown'; + return sprintf("%s (%s)", $user_name, $user_id); + } + abstract public function format($subject, array $change_set): ?string; } diff --git a/app/Audit/AuditContext.php b/app/Audit/AuditContext.php index 5150f59dc..8918543c5 100644 --- a/app/Audit/AuditContext.php +++ b/app/Audit/AuditContext.php @@ -21,6 +21,7 @@ public function __construct( public ?string $uiApp = null, public ?string $uiFlow = null, public ?string $route = null, + public ?string $rawRoute = null, public ?string $httpMethod = null, public ?string $clientIp = null, public ?string $userAgent = null, diff --git a/app/Audit/AuditEventListener.php b/app/Audit/AuditEventListener.php index 9bc4d7f32..230c50b52 100644 --- a/app/Audit/AuditEventListener.php +++ b/app/Audit/AuditEventListener.php @@ -16,6 +16,7 @@ use Doctrine\ORM\Event\OnFlushEventArgs; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Route; /** * Class AuditEventListener @@ -23,10 +24,11 @@ */ class AuditEventListener { + private const ROUTE_METHOD_SEPARATOR = '|'; public function onFlush(OnFlushEventArgs $eventArgs): void { - if (app()->environment('testing')){ + if (app()->environment('testing')) { return; } $em = $eventArgs->getObjectManager(); @@ -67,7 +69,7 @@ public function onFlush(OnFlushEventArgs $eventArgs): void /** * Get the appropriate audit strategy based on environment configuration */ - private function getAuditStrategy($em) + private function getAuditStrategy($em): ?IAuditStrategy { // Check if OTLP audit is enabled if (config('opentelemetry.enabled', false)) { @@ -97,7 +99,11 @@ private function buildAuditContext(): AuditContext //$ui = app()->bound('ui.context') ? app('ui.context') : []; $req = request(); - + + $route = Route::getRoutes()->match($req); + $method = $route->methods[0] ?? 'UNKNOWN'; + $rawRoute = $method . self::ROUTE_METHOD_SEPARATOR . $route->uri; + return new AuditContext( userId: $member?->getId(), userEmail: $member?->getEmail(), @@ -109,6 +115,7 @@ private function buildAuditContext(): AuditContext httpMethod: $req?->method(), clientIp: $req?->ip(), userAgent: $req?->userAgent(), + rawRoute: $rawRoute ); } } diff --git a/app/Audit/AuditLogFormatterFactory.php b/app/Audit/AuditLogFormatterFactory.php index 405e2e786..4536bbfd0 100644 --- a/app/Audit/AuditLogFormatterFactory.php +++ b/app/Audit/AuditLogFormatterFactory.php @@ -45,27 +45,36 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt if (count($subject) > 0) { $child_entity = $subject[0]; } - if (is_null($child_entity) && count($subject->getSnapshot()) > 0) { + if (is_null($child_entity) && isset($subject->getSnapshot()[0]) && count($subject->getSnapshot()) > 0) { $child_entity = $subject->getSnapshot()[0]; } $child_entity_formatter = $child_entity != null ? ChildEntityFormatterFactory::build($child_entity) : null; $formatter = new EntityCollectionUpdateAuditLogFormatter($child_entity_formatter); break; case IAuditStrategy::EVENT_ENTITY_CREATION: - $formatter = $this->getStrategyClass($subject, $eventType); + $formatter = $this->getFormatterByContext($subject, $eventType, $ctx); + if (is_null($formatter)) { + $formatter = $this->getStrategyClass($subject, $eventType); + } if(is_null($formatter)) { $formatter = new EntityCreationAuditLogFormatter(); } break; case IAuditStrategy::EVENT_ENTITY_DELETION: - $formatter = $this->getStrategyClass($subject, $eventType); + $formatter = $this->getFormatterByContext($subject, $eventType, $ctx); + if (is_null($formatter)) { + $formatter = $this->getStrategyClass($subject, $eventType); + } if(is_null($formatter)) { $child_entity_formatter = ChildEntityFormatterFactory::build($subject); $formatter = new EntityDeletionAuditLogFormatter($child_entity_formatter); } break; case IAuditStrategy::EVENT_ENTITY_UPDATE: - $formatter = $this->getStrategyClass($subject, $eventType); + $formatter = $this->getFormatterByContext($subject, $eventType, $ctx); + if (is_null($formatter)) { + $formatter = $this->getStrategyClass($subject, $eventType); + } if(is_null($formatter)) { $child_entity_formatter = ChildEntityFormatterFactory::build($subject); $formatter = new EntityUpdateAuditLogFormatter($child_entity_formatter); @@ -75,4 +84,39 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt $formatter->setContext($ctx); return $formatter; } + + private function getFormatterByContext(object $subject, string $event_type, AuditContext $ctx): ?IAuditLogFormatter + { + $class = get_class($subject); + $entity_config = $this->config['entities'][$class] ?? null; + + if (!$entity_config || !isset($entity_config['strategies'])) { + return null; + } + + foreach ($entity_config['strategies'] as $strategy) { + if (!$this->matchesStrategy($strategy, $ctx)) { + continue; + } + + $formatter_class = $strategy['formatter'] ?? null; + return $formatter_class ? new $formatter_class($event_type) : null; + } + + return null; + } + + private function matchesStrategy(array $strategy, AuditContext $ctx): bool + { + if (isset($strategy['route']) && !$this->routeMatches($strategy['route'], $ctx->rawRoute)) { + return false; + } + + return true; + } + + private function routeMatches(string $route, string $actual_route): bool + { + return strcmp($actual_route, $route) === 0; + } } diff --git a/app/Audit/ConcreteFormatters/FeaturedSpeakerAuditLogFormatter.php b/app/Audit/ConcreteFormatters/FeaturedSpeakerAuditLogFormatter.php new file mode 100644 index 000000000..b793766e9 --- /dev/null +++ b/app/Audit/ConcreteFormatters/FeaturedSpeakerAuditLogFormatter.php @@ -0,0 +1,97 @@ +event_type = $event_type; + } + + public function format($subject, array $change_set): ?string + { + if (!$subject instanceof FeaturedSpeaker) { + return null; + } + + try { + $speaker = $subject->getSpeaker(); + $speaker_email = $speaker ? ($speaker->getEmail() ?? 'unknown') : 'unknown'; + $speaker_name = $speaker ? sprintf("%s %s", $speaker->getFirstName() ?? '', $speaker->getLastName() ?? '') : 'Unknown'; + $speaker_name = trim($speaker_name) ?: $speaker_name; + $speaker_id = $speaker ? ($speaker->getId() ?? 'unknown') : 'unknown'; + + $summit = $subject->getSummit(); + $summit_name = $summit ? ($summit->getName() ?? 'Unknown Summit') : 'Unknown Summit'; + + $order = $subject->getOrder(); + + switch ($this->event_type) { + case IAuditStrategy::EVENT_ENTITY_CREATION: + return sprintf( + "Speaker '%s' (%s) added as featured speaker for Summit '%s' with display order %d by user %s", + $speaker_name, + $speaker_id, + $summit_name, + $order, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $changed_fields = []; + + if (isset($change_set['Order'])) { + $old_order = $change_set['Order'][0] ?? 'unknown'; + $new_order = $change_set['Order'][1] ?? 'unknown'; + $changed_fields[] = sprintf("display_order %s → %s", $old_order, $new_order); + } + if (isset($change_set['PresentationSpeakerID'])) { + $changed_fields[] = "speaker"; + } + + $fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties'; + return sprintf( + "Featured speaker '%s' (%s) updated (%s changed) by user %s", + $speaker_name, + $speaker_id, + $fields_str, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_DELETION: + return sprintf( + "Speaker '%s' (%s) removed from featured speakers list of Summit '%s' by user %s", + $speaker_name, + $speaker_id, + $summit_name, + $this->getUserInfo() + ); + } + } catch (\Exception $ex) { + Log::warning("FeaturedSpeakerAuditLogFormatter error: " . $ex->getMessage()); + } + + return null; + } +} diff --git a/app/Audit/ConcreteFormatters/PresentationFormatters/BasePresentationAuditLogFormatter.php b/app/Audit/ConcreteFormatters/PresentationFormatters/BasePresentationAuditLogFormatter.php new file mode 100644 index 000000000..6d548b07e --- /dev/null +++ b/app/Audit/ConcreteFormatters/PresentationFormatters/BasePresentationAuditLogFormatter.php @@ -0,0 +1,126 @@ +event_type = $event_type; + } + + protected function extractChangedFields(array $change_set): array + { + $changed_fields = []; + $old_status = null; + $new_status = null; + + if (isset($change_set['Title'])) { + $changed_fields[] = "title"; + } + if (isset($change_set['Abstract'])) { + $changed_fields[] = "abstract"; + } + if (isset($change_set['ProblemAddressed'])) { + $changed_fields[] = "problem_addressed"; + } + if (isset($change_set['AttendeesExpectedLearnt'])) { + $changed_fields[] = "attendees_expected_learnt"; + } + + if (isset($change_set['Status'])) { + $changed_fields[] = "status"; + $old_status = $change_set['Status'][0] ?? null; + $new_status = $change_set['Status'][1] ?? null; + } + if (isset($change_set['CategoryID']) || isset($change_set['category'])) { + $changed_fields[] = "track"; + } + if (isset($change_set['Published'])) { + $changed_fields[] = "published"; + } + if (isset($change_set['SelectionPlanID'])) { + $changed_fields[] = "selection_plan"; + } + + return [ + 'fields' => !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties', + 'old_status' => $old_status, + 'new_status' => $new_status, + ]; + } + + protected function getPresentationData(Presentation $subject): array + { + $creator = $subject->getCreator(); + $creator_name = $creator ? sprintf("%s %s", $creator->getFirstName() ?? '', $creator->getLastName() ?? '') : 'Unknown'; + $creator_name = trim($creator_name) ?: 'Unknown'; + + $category = $subject->getCategory(); + $category_name = $category ? $category->getTitle() : 'Unassigned Track'; + + $selection_plan = $subject->getSelectionPlan(); + $plan_name = $selection_plan ? $selection_plan->getName() : 'Unknown Plan'; + + return [ + 'title' => $subject->getTitle() ?? 'Unknown Presentation', + 'id' => $subject->getId() ?? 'unknown', + 'creator_name' => $creator_name, + 'category_name' => $category_name, + 'plan_name' => $plan_name, + ]; + } + + public function format($subject, array $change_set): ?string + { + if (!$subject instanceof Presentation) { + return null; + } + + try { + $data = $this->getPresentationData($subject); + + switch ($this->event_type) { + case IAuditStrategy::EVENT_ENTITY_CREATION: + return $this->formatCreation($data); + + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $extracted = $this->extractChangedFields($change_set); + return $this->formatUpdate($data, $extracted); + + case IAuditStrategy::EVENT_ENTITY_DELETION: + return $this->formatDeletion($data); + } + } catch (\Exception $ex) { + Log::warning(static::class . " error: " . $ex->getMessage()); + } + + return null; + } + + abstract protected function formatCreation(array $data): string; + + abstract protected function formatUpdate(array $data, array $extracted): string; + + abstract protected function formatDeletion(array $data): string; +} diff --git a/app/Audit/ConcreteFormatters/PresentationFormatters/PresentationEventApiAuditLogFormatter.php b/app/Audit/ConcreteFormatters/PresentationFormatters/PresentationEventApiAuditLogFormatter.php new file mode 100644 index 000000000..78cf0c3e2 --- /dev/null +++ b/app/Audit/ConcreteFormatters/PresentationFormatters/PresentationEventApiAuditLogFormatter.php @@ -0,0 +1,67 @@ +getUserInfo() + ); + } + + protected function formatUpdate(array $data, array $extracted): string + { + if ($extracted['old_status'] && $extracted['new_status']) { + return sprintf( + "Presentation '%s' (%s) status changed: %s → %s (%s changed) by user %s", + $data['title'], + $data['id'], + strtoupper($extracted['old_status']), + strtoupper($extracted['new_status']), + $extracted['fields'], + $this->getUserInfo() + ); + } + + return sprintf( + "Presentation '%s' (%s) modified (%s changed) by user %s", + $data['title'], + $data['id'], + $extracted['fields'], + $this->getUserInfo() + ); + } + + protected function formatDeletion(array $data): string + { + return sprintf( + "Presentation '%s' (%s) created by '%s' under track '%s' was removed by user %s", + $data['title'], + $data['id'], + $data['creator_name'], + $data['category_name'], + $this->getUserInfo() + ); + } +} diff --git a/app/Audit/ConcreteFormatters/PresentationFormatters/PresentationSubmissionAuditLogFormatter.php b/app/Audit/ConcreteFormatters/PresentationFormatters/PresentationSubmissionAuditLogFormatter.php new file mode 100644 index 000000000..1343f4c0f --- /dev/null +++ b/app/Audit/ConcreteFormatters/PresentationFormatters/PresentationSubmissionAuditLogFormatter.php @@ -0,0 +1,67 @@ +getUserInfo() + ); + } + + protected function formatUpdate(array $data, array $extracted): string + { + if ($extracted['old_status'] && $extracted['new_status']) { + return sprintf( + "Presentation '%s' (%s) status changed: %s → %s (%s changed) by user %s", + $data['title'], + $data['id'], + strtoupper($extracted['old_status']), + strtoupper($extracted['new_status']), + $extracted['fields'], + $this->getUserInfo() + ); + } + + return sprintf( + "Presentation '%s' (%s) updated (%s changed) by user %s", + $data['title'], + $data['id'], + $extracted['fields'], + $this->getUserInfo() + ); + } + + protected function formatDeletion(array $data): string + { + return sprintf( + "Presentation '%s' (%s) submitted by '%s' to track '%s' was deleted by user %s", + $data['title'], + $data['id'], + $data['creator_name'], + $data['category_name'], + $this->getUserInfo() + ); + } +} diff --git a/app/Audit/ConcreteFormatters/PresentationSpeakerAuditLogFormatter.php b/app/Audit/ConcreteFormatters/PresentationSpeakerAuditLogFormatter.php new file mode 100644 index 000000000..3d81a3f3c --- /dev/null +++ b/app/Audit/ConcreteFormatters/PresentationSpeakerAuditLogFormatter.php @@ -0,0 +1,107 @@ +event_type = $event_type; + } + + public function format($subject, array $change_set): ?string + { + if (!$subject instanceof PresentationSpeaker) { + return null; + } + + try { + $full_name = sprintf("%s %s", $subject->getFirstName() ?? 'Unknown', $subject->getLastName() ?? 'Unknown'); + $email = $subject->getEmail() ?? 'unknown'; + $speaker_id = $subject->getId() ?? 'unknown'; + + switch ($this->event_type) { + case IAuditStrategy::EVENT_ENTITY_CREATION: + $bio = $subject->getBio() ? sprintf(" - Bio: %s", mb_substr($subject->getBio(), 0, 50)) : ''; + return sprintf( + "Speaker '%s' (%s) created with email '%s'%s by user %s", + $full_name, + $speaker_id, + $email, + $bio, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $changed_fields = []; + if (isset($change_set['FirstName']) || isset($change_set['LastName'])) { + $changed_fields[] = "name"; + } + if (isset($change_set['Email'])) { + $changed_fields[] = "email"; + } + if (isset($change_set['Title'])) { + $changed_fields[] = "title"; + } + + if (isset($change_set['Country'])) { + $changed_fields[] = "country"; + } + if (isset($change_set['AvailableForBureau'])) { + $changed_fields[] = "available_for_bureau"; + } + if (isset($change_set['FundedTravel'])) { + $changed_fields[] = "funded_travel"; + } + if (isset($change_set['WillingToTravel'])) { + $changed_fields[] = "willing_to_travel"; + } + if (isset($change_set['WillingToPresentVideo'])) { + $changed_fields[] = "willing_to_present_video"; + } + + $fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties'; + return sprintf( + "Speaker '%s' (%s) updated (%s changed) by user %s", + $full_name, + $speaker_id, + $fields_str, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_DELETION: + return sprintf( + "Speaker '%s' (%s) with email '%s' was deleted by user %s", + $full_name, + $speaker_id, + $email, + $this->getUserInfo() + ); + } + } catch (\Exception $ex) { + Log::warning("PresentationSpeakerAuditLogFormatter error: " . $ex->getMessage()); + } + + return null; + } +} diff --git a/app/Audit/ConcreteFormatters/PresentationUserSubmissionAuditLogFormatter.php b/app/Audit/ConcreteFormatters/PresentationUserSubmissionAuditLogFormatter.php new file mode 100644 index 000000000..011a7395e --- /dev/null +++ b/app/Audit/ConcreteFormatters/PresentationUserSubmissionAuditLogFormatter.php @@ -0,0 +1,119 @@ +event_type = $event_type; + } + + public function format($subject, array $change_set): ?string + { + if (!$subject instanceof Presentation) { + return null; + } + + try { + $title = $subject->getTitle() ?? 'Unknown Presentation'; + $id = $subject->getId() ?? 'unknown'; + $creator = $subject->getCreator(); + $creator_name = $creator ? sprintf("%s %s", $creator->getFirstName() ?? '', $creator->getLastName() ?? '') : 'Unknown'; + $creator_name = trim($creator_name) ?: 'Unknown'; + $category = $subject->getCategory(); + $category_name = $category ? $category->getTitle() : 'Unassigned Track'; + $selection_plan = $subject->getSelectionPlan(); + $plan_name = $selection_plan ? $selection_plan->getName() : 'Unknown Plan'; + + switch ($this->event_type) { + case IAuditStrategy::EVENT_ENTITY_CREATION: + return sprintf( + "Presentation '%s' (%d) submitted by '%s' to track '%s' (Plan: %s) by user %s", + $title, + $id, + $creator_name, + $category_name, + $plan_name, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $changed_fields = []; + $old_status = null; + $new_status = null; + + if (isset($change_set['Title'])) { + $changed_fields[] = "title"; + } + if (isset($change_set['Abstract'])) { + $changed_fields[] = "abstract"; + } + if (isset($change_set['ProblemAddressed'])) { + $changed_fields[] = "problem_addressed"; + } + if (isset($change_set['AttendeesExpectedLearnt'])) { + $changed_fields[] = "attendees_expected_learnt"; + } + + if (isset($change_set['Status'])) { + $changed_fields[] = "status"; + $old_status = $change_set['Status'][0] ?? null; + $new_status = $change_set['Status'][1] ?? null; + } + if (isset($change_set['CategoryID']) || isset($change_set['category'])) { + $changed_fields[] = "track"; + } + if (isset($change_set['Published'])) { + $changed_fields[] = "published"; + } + if (isset($change_set['SelectionPlanID'])) { + $changed_fields[] = "selection_plan"; + } + + $fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties'; + + if ($old_status && $new_status) { + return sprintf( + "Presentation '%s' (%d) status changed: %s → %s (%s changed) by user %s", + $title, + $id, + strtoupper($old_status), + strtoupper($new_status), + $fields_str, + $this->getUserInfo() + ); + } + + return sprintf( + "Presentation '%s' (%d) updated (%s changed) by user %s", + $title, + $id, + $fields_str, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_DELETION: + return sprintf( + "Presentation '%s' (%d) submitted by '%s' to track '%s' was deleted by user %s", + $title, + $id, + $creator_name, + $category_name, + $this->getUserInfo() + ); + } + } catch (\Exception $ex) { + Log::warning("PresentationUserSubmissionAuditLogFormatter error: " . $ex->getMessage()); + } + + return null; + } +} diff --git a/app/Audit/ConcreteFormatters/SelectionPlanAuditLogFormatter.php b/app/Audit/ConcreteFormatters/SelectionPlanAuditLogFormatter.php new file mode 100644 index 000000000..1366ec613 --- /dev/null +++ b/app/Audit/ConcreteFormatters/SelectionPlanAuditLogFormatter.php @@ -0,0 +1,149 @@ +event_type = $event_type; + } + + private function formatDate($date): string + { + if ($date instanceof \DateTime) { + return $date->format('Y-m-d H:i:s'); + } + return $date ?? 'N/A'; + } + + public function format($subject, array $change_set): ?string + { + if (!$subject instanceof SelectionPlan) { + return null; + } + + try { + $name = $subject->getName() ?? 'Unknown Plan'; + $id = $subject->getId() ?? 'unknown'; + $summit = $subject->getSummit(); + $summit_name = $summit ? $summit->getName() : 'Unknown Summit'; + + switch ($this->event_type) { + case IAuditStrategy::EVENT_ENTITY_CREATION: + $submission_dates = $subject->hasSubmissionPeriodDefined() + ? sprintf( + "[%s - %s]", + $this->formatDate($subject->getSubmissionBeginDate()), + $this->formatDate($subject->getSubmissionEndDate()) + ) + : 'No dates set'; + + $selection_dates = $subject->hasSelectionPeriodDefined() + ? sprintf( + "[%s - %s]", + $this->formatDate($subject->getSelectionBeginDate()), + $this->formatDate($subject->getSelectionEndDate()) + ) + : 'No dates set'; + + return sprintf( + "Selection Plan '%s' (%d) created for Summit '%s' with CFP period: %s, selection period: %s by user %s", + $name, + $id, + $summit_name, + $submission_dates, + $selection_dates, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $changed_fields = []; + + if (isset($change_set['Name'])) { + $changed_fields[] = "name"; + } + if (isset($change_set['Enabled'])) { + $status = $change_set['Enabled'][1] ? 'enabled' : 'disabled'; + $changed_fields[] = "status ($status)"; + } + if (isset($change_set['IsHidden'])) { + $changed_fields[] = "visibility"; + } + if (isset($change_set['AllowNewPresentations'])) { + $changed_fields[] = "allow_new_presentations"; + } + if (isset($change_set['AllowProposedSchedules'])) { + $changed_fields[] = "allow_proposed_schedules"; + } + if (isset($change_set['AllowTrackChangeRequests'])) { + $changed_fields[] = "allow_track_change_requests"; + } + if (isset($change_set['SubmissionBeginDate']) || isset($change_set['SubmissionEndDate'])) { + $old_begin = isset($change_set['SubmissionBeginDate']) ? $this->formatDate($change_set['SubmissionBeginDate'][0]) : 'N/A'; + $new_begin = isset($change_set['SubmissionBeginDate']) ? $this->formatDate($change_set['SubmissionBeginDate'][1]) : 'N/A'; + $old_end = isset($change_set['SubmissionEndDate']) ? $this->formatDate($change_set['SubmissionEndDate'][0]) : 'N/A'; + $new_end = isset($change_set['SubmissionEndDate']) ? $this->formatDate($change_set['SubmissionEndDate'][1]) : 'N/A'; + $changed_fields[] = sprintf("CFP period [%s - %s] → [%s - %s]", $old_begin, $old_end, $new_begin, $new_end); + } + if (isset($change_set['VotingBeginDate']) || isset($change_set['VotingEndDate'])) { + $changed_fields[] = "voting period"; + } + if (isset($change_set['SelectionBeginDate']) || isset($change_set['SelectionEndDate'])) { + $changed_fields[] = "selection period"; + } + if (isset($change_set['MaxSubmissionAllowedPerUser'])) { + $old_val = $change_set['MaxSubmissionAllowedPerUser'][0] ?? 0; + $new_val = $change_set['MaxSubmissionAllowedPerUser'][1] ?? 0; + $changed_fields[] = sprintf("max_submissions (%d → %d)", $old_val, $new_val); + } + if (isset($change_set['SubmissionPeriodDisclaimer'])) { + $changed_fields[] = "disclaimer"; + } + + $fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties'; + return sprintf( + "Selection Plan '%s' (%d) for Summit '%s' updated (%s changed) by user %s", + $name, + $id, + $summit_name, + $fields_str, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_DELETION: + return sprintf( + "Selection Plan '%s' (%d) for Summit '%s' was deleted by user %s", + $name, + $id, + $summit_name, + $this->getUserInfo() + ); + } + } catch (\Exception $ex) { + Log::warning("SelectionPlanAuditLogFormatter error: " . $ex->getMessage()); + } + + return null; + } +} diff --git a/app/Audit/ConcreteFormatters/SpeakerAssistanceAuditLogFormatter.php b/app/Audit/ConcreteFormatters/SpeakerAssistanceAuditLogFormatter.php new file mode 100644 index 000000000..857b9728a --- /dev/null +++ b/app/Audit/ConcreteFormatters/SpeakerAssistanceAuditLogFormatter.php @@ -0,0 +1,119 @@ +event_type = $event_type; + } + + public function format($subject, array $change_set): ?string + { + if (!$subject instanceof PresentationSpeakerSummitAssistanceConfirmationRequest) { + return null; + } + + try { + $speaker = $subject->getSpeaker(); + $speaker_name = $speaker ? sprintf("%s %s", $speaker->getFirstName() ?? '', $speaker->getLastName() ?? '') : 'Unknown'; + $speaker_email = $speaker ? ($speaker->getEmail() ?? 'unknown') : 'unknown'; + $speaker_name = trim($speaker_name) ?: $speaker_email; + $speaker_id = $speaker ? ($speaker->getId() ?? 'unknown') : 'unknown'; + + $summit = $subject->getSummit(); + $summit_name = $summit ? ($summit->getName() ?? 'Unknown Summit') : 'Unknown Summit'; + + $is_confirmed = $subject->isConfirmed(); + $is_registered = $subject->isRegistered(); + + switch ($this->event_type) { + case IAuditStrategy::EVENT_ENTITY_CREATION: + $status = $is_confirmed ? 'confirmed' : 'pending'; + $registration_status = $is_registered ? 'registered' : 'unregistered'; + return sprintf( + "Speaker assistance created for '%s' (%s) on Summit '%s' [confirmation: %s, registration: %s] by user %s", + $speaker_name, + $speaker_id, + $summit_name, + $status, + $registration_status, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $changed_fields = []; + + if (isset($change_set['IsConfirmed'])) { + $old_status = $change_set['IsConfirmed'][0] ? 'confirmed' : 'pending'; + $new_status = $change_set['IsConfirmed'][1] ? 'confirmed' : 'pending'; + $changed_fields[] = sprintf("confirmation %s → %s", $old_status, $new_status); + } + if (isset($change_set['CheckedIn'])) { + $old_status = $change_set['CheckedIn'][0] ? 'checked_in' : 'not_checked_in'; + $new_status = $change_set['CheckedIn'][1] ? 'checked_in' : 'not_checked_in'; + $changed_fields[] = sprintf("check_in %s → %s", $old_status, $new_status); + } + if (isset($change_set['RegisteredForSummit'])) { + $old_status = $change_set['RegisteredForSummit'][0] ? 'registered' : 'unregistered'; + $new_status = $change_set['RegisteredForSummit'][1] ? 'registered' : 'unregistered'; + $changed_fields[] = sprintf("registration %s → %s", $old_status, $new_status); + } + if (isset($change_set['OnSitePhoneNumber'])) { + $changed_fields[] = "on_site_phone"; + } + if (isset($change_set['ConfirmationDate'])) { + $changed_fields[] = "confirmation_date"; + } + + $fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties'; + return sprintf( + "Speaker assistance for '%s' (%s) on Summit '%s' updated (%s changed) by user %s", + $speaker_name, + $speaker_id, + $summit_name, + $fields_str, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_DELETION: + $status = $is_confirmed ? 'confirmed' : 'pending'; + $registration_status = $is_registered ? 'registered' : 'unregistered'; + return sprintf( + "Speaker assistance for '%s' (%s) on Summit '%s' [confirmation: %s, registration: %s] was deleted by user %s", + $speaker_name, + $speaker_id, + $summit_name, + $status, + $registration_status, + $this->getUserInfo() + ); + } + } catch (\Exception $ex) { + Log::warning("SpeakerAssistanceAuditLogFormatter error: " . $ex->getMessage()); + } + + return null; + } +} diff --git a/app/Audit/ConcreteFormatters/SpeakerRegistrationRequestAuditLogFormatter.php b/app/Audit/ConcreteFormatters/SpeakerRegistrationRequestAuditLogFormatter.php new file mode 100644 index 000000000..21b8eac5a --- /dev/null +++ b/app/Audit/ConcreteFormatters/SpeakerRegistrationRequestAuditLogFormatter.php @@ -0,0 +1,99 @@ +event_type = $event_type; + } + + public function format($subject, array $change_set): ?string + { + if (!$subject instanceof SpeakerRegistrationRequest) { + return null; + } + + try { + $email = $subject->getEmail() ?? 'unknown'; + $speaker = $subject->getSpeaker(); + $speaker_name = $speaker ? sprintf("%s %s", $speaker->getFirstName() ?? '', $speaker->getLastName() ?? '') : 'Unknown'; + $speaker_name = trim($speaker_name) ?: 'Unknown'; + $is_confirmed = $subject->isConfirmed(); + + switch ($this->event_type) { + case IAuditStrategy::EVENT_ENTITY_CREATION: + return sprintf( + "Speaker registration request created for email '%s' by user %s", + $email, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $changed_fields = []; + + if (isset($change_set['Email'])) { + $old_email = $change_set['Email'][0] ?? 'N/A'; + $new_email = $change_set['Email'][1] ?? 'N/A'; + $changed_fields[] = sprintf("email %s → %s", $old_email, $new_email); + } + if (isset($change_set['IsConfirmed'])) { + $old_status = $change_set['IsConfirmed'][0] ? 'confirmed' : 'pending'; + $new_status = $change_set['IsConfirmed'][1] ? 'confirmed' : 'pending'; + $changed_fields[] = sprintf("status %s → %s", $old_status, $new_status); + } + if (isset($change_set['ConfirmationDate'])) { + $changed_fields[] = "confirmation_date"; + } + if (isset($change_set['ProposerID'])) { + $changed_fields[] = "proposer"; + } + if (isset($change_set['SpeakerID'])) { + $changed_fields[] = "speaker"; + } + + $fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties'; + return sprintf( + "Speaker registration request for '%s' (%s changed) by user %s", + $email, + $fields_str, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_DELETION: + $status = $is_confirmed ? 'confirmed' : 'pending'; + return sprintf( + "Speaker registration request for email '%s' (status: %s) was deleted by user %s", + $email, + $status, + $this->getUserInfo() + ); + } + } catch (\Exception $ex) { + Log::warning("SpeakerRegistrationRequestAuditLogFormatter error: " . $ex->getMessage()); + } + + return null; + } +} diff --git a/app/Audit/ConcreteFormatters/SubmissionInvitationAuditLogFormatter.php b/app/Audit/ConcreteFormatters/SubmissionInvitationAuditLogFormatter.php new file mode 100644 index 000000000..260795fbd --- /dev/null +++ b/app/Audit/ConcreteFormatters/SubmissionInvitationAuditLogFormatter.php @@ -0,0 +1,107 @@ +event_type = $event_type; + } + + public function format($subject, array $change_set): ?string + { + if (!$subject instanceof SummitSubmissionInvitation) { + return null; + } + + try { + $email = $subject->getEmail() ?? 'unknown'; + $first_name = $subject->getFirstName() ?? 'Unknown'; + $last_name = $subject->getLastName() ?? ''; + $full_name = trim(sprintf("%s %s", $first_name, $last_name)) ?: 'Unknown'; + $is_sent = $subject->isSent(); + $speaker = $subject->getSpeaker(); + $speaker_name = $speaker ? sprintf("%s %s", $speaker->getFirstName() ?? '', $speaker->getLastName() ?? '') : 'None'; + $speaker_name = trim($speaker_name) ?: 'None'; + $id = $subject->getId() ?? 'unknown'; + + switch ($this->event_type) { + case IAuditStrategy::EVENT_ENTITY_CREATION: + $sent_status = $is_sent ? 'sent' : 'not sent'; + return sprintf( + "Submission invitation created for '%s' (%s) with email '%s' [status: %s] by user %s", + $full_name, + $id, + $email, + $sent_status, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_UPDATE: + $changed_fields = []; + + if (isset($change_set['FirstName']) || isset($change_set['LastName'])) { + $changed_fields[] = "name"; + } + if (isset($change_set['Email'])) { + $old_email = $change_set['Email'][0] ?? 'N/A'; + $new_email = $change_set['Email'][1] ?? 'N/A'; + $changed_fields[] = sprintf("email %s to %s", $old_email, $new_email); + } + if (isset($change_set['SentDate'])) { + $old_status = isset($change_set['SentDate'][0]) && $change_set['SentDate'][0] ? 'sent' : 'not sent'; + $new_status = isset($change_set['SentDate'][1]) && $change_set['SentDate'][1] ? 'sent' : 'not sent'; + $changed_fields[] = sprintf("status %s to %s", $old_status, $new_status); + } + if (isset($change_set['SpeakerID'])) { + $changed_fields[] = "speaker"; + } + + $fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties'; + return sprintf( + "Submission invitation for '%s' (%s) updated (%s changed) by user %s", + $email, + $id, + $fields_str, + $this->getUserInfo() + ); + + case IAuditStrategy::EVENT_ENTITY_DELETION: + $sent_status = $is_sent ? 'sent' : 'pending'; + return sprintf( + "Submission invitation for '%s' (%s) with email '%s' [status: %s] was deleted by user %s", + $full_name, + $id, + $email, + $sent_status, + $this->getUserInfo() + ); + } + } catch (\Exception $ex) { + Log::warning("SubmissionInvitationAuditLogFormatter error: " . $ex->getMessage()); + } + + return null; + } +} diff --git a/app/Models/Foundation/Summit/SelectionPlan.php b/app/Models/Foundation/Summit/SelectionPlan.php index d68e1dab7..c602dc9e0 100644 --- a/app/Models/Foundation/Summit/SelectionPlan.php +++ b/app/Models/Foundation/Summit/SelectionPlan.php @@ -743,6 +743,10 @@ public function hasSelectionPeriodDefined():bool{ return !is_null($this->selection_begin_date) && !is_null($this->selection_end_date); } + public function hasSubmissionPeriodDefined():bool{ + return !is_null($this->submission_begin_date) && !is_null($this->submission_end_date); + } + /** * @return bool */ diff --git a/app/Swagger/SummitRegistrationSchemas.php b/app/Swagger/SummitRegistrationSchemas.php index e9440c4e6..2d2860b2a 100644 --- a/app/Swagger/SummitRegistrationSchemas.php +++ b/app/Swagger/SummitRegistrationSchemas.php @@ -6,43 +6,6 @@ // Badge Types -#[OA\Schema( - schema: "SummitBadgeType", - description: "Summit badge type", - type: "object", - properties: [ - new OA\Property(property: "id", type: "integer", example: 1), - new OA\Property(property: "created", type: "integer", description: "Unix timestamp", example: 1640995200), - new OA\Property(property: "last_edited", type: "integer", description: "Unix timestamp", example: 1640995200), - new OA\Property(property: "name", type: "string", example: "Attendee"), - new OA\Property(property: "description", type: "string", example: "Standard attendee badge"), - new OA\Property(property: "template_content", type: "string", nullable: true, example: "Badge template content"), - new OA\Property(property: "is_default", type: "boolean", example: false), - new OA\Property(property: "summit_id", type: "integer", example: 1), - new OA\Property( - property: "access_levels", - type: "array", - items: new OA\Items(type: "integer"), - description: "SummitAccessLevelType IDs, full objects when expanded" - ), - new OA\Property( - property: "badge_features", - type: "array", - items: new OA\Items(type: "integer"), - description: "SummitBadgeFeatureType IDs, full objects when expanded", - ), - new OA\Property( - property: "allowed_view_types", - type: "array", - items: new OA\Items(type: "integer"), - description: "SummitBadgeViewType IDs, full objects when expanded", - ), - ], -)] -class SummitBadgeType -{ -} - #[OA\Schema( schema: "PaginatedSummitBadgeTypesResponse", description: "Paginated list of summit badge types", diff --git a/config/audit_log.php b/config/audit_log.php index bb26f9521..9b576c608 100644 --- a/config/audit_log.php +++ b/config/audit_log.php @@ -5,6 +5,51 @@ \models\main\SummitMemberSchedule::class => [ 'enabled' => true, 'strategy' => \App\Audit\ConcreteFormatters\SummitMemberScheduleAuditLogFormatter::class, + ], + \models\summit\PresentationSpeaker::class => [ + 'enabled' => true, + 'strategy' => \App\Audit\ConcreteFormatters\PresentationSpeakerAuditLogFormatter::class, + ], + \models\summit\Presentation::class => [ + 'enabled' => true, + 'strategies' => [ + [ + 'route' => 'POST|api/v1/summits/{id}/events', + 'formatter' => \App\Audit\ConcreteFormatters\PresentationFormatters\PresentationEventApiAuditLogFormatter::class, + ], + [ + 'route' => 'PUT|api/v1/summits/{id}/events/{event_id}', + 'formatter' => \App\Audit\ConcreteFormatters\PresentationFormatters\PresentationEventApiAuditLogFormatter::class, + ], + [ + 'route' => 'POST|api/v1/summits/{id}/presentations', + 'formatter' => \App\Audit\ConcreteFormatters\PresentationFormatters\PresentationSubmissionAuditLogFormatter::class, + ], + [ + 'route' => 'PUT|api/v1/summits/{id}/presentations/{presentation_id}', + 'formatter' => \App\Audit\ConcreteFormatters\PresentationFormatters\PresentationSubmissionAuditLogFormatter::class, + ], + ] + ], + \App\Models\Foundation\Summit\SelectionPlan::class => [ + 'enabled' => true, + 'strategy' => \App\Audit\ConcreteFormatters\SelectionPlanAuditLogFormatter::class, + ], + \models\summit\SpeakerRegistrationRequest::class => [ + 'enabled' => true, + 'strategy' => \App\Audit\ConcreteFormatters\SpeakerRegistrationRequestAuditLogFormatter::class, + ], + \models\summit\SummitSubmissionInvitation::class => [ + 'enabled' => true, + 'strategy' => \App\Audit\ConcreteFormatters\SubmissionInvitationAuditLogFormatter::class, + ], + \App\Models\Foundation\Summit\Speakers\FeaturedSpeaker::class => [ + 'enabled' => true, + 'strategy' => \App\Audit\ConcreteFormatters\FeaturedSpeakerAuditLogFormatter::class, + ], + \models\summit\PresentationSpeakerSummitAssistanceConfirmationRequest::class => [ + 'enabled' => true, + 'strategy' => \App\Audit\ConcreteFormatters\SpeakerAssistanceAuditLogFormatter::class, ] ] ];