diff --git a/backend/components/BaseController.php b/backend/components/BaseController.php index 18296b29c..19a53a29f 100644 --- a/backend/components/BaseController.php +++ b/backend/components/BaseController.php @@ -22,7 +22,7 @@ public function behaviors() 'class' => \yii\filters\AccessControl::class, 'rules' => [ 'adminActions'=>[ - 'allow' => true, + 'allow'=>true, 'roles' => ['@'], 'matchCallback' => function () { return \Yii::$app->user->identity->isAdmin; diff --git a/backend/config/web.php b/backend/config/web.php index 0a26c2907..3ee0be62e 100644 --- a/backend/config/web.php +++ b/backend/config/web.php @@ -14,39 +14,18 @@ '@npm' => '@vendor/npm-asset', ], 'modules' => [ - 'moderation' => [ - 'class' => 'app\modules\moderation\Module' - ], - 'speedprogramming' => [ - 'class' => 'app\modules\speedprogramming\Module', - ], - 'sales' => [ - 'class' => 'app\modules\sales\Module', - ], - 'content' => [ - 'class' => 'app\modules\content\Module', - ], - 'infrastructure' => [ - 'class' => 'app\modules\infrastructure\Module', - ], - 'smartcity' => [ - 'class' => 'app\modules\smartcity\Module', - ], - 'restapi' => [ - 'class' => 'app\modules\restapi\Module', - ], - 'settings' => [ - 'class' => 'app\modules\settings\Module', - ], - 'frontend' => [ - 'class' => 'app\modules\frontend\Module', - ], - 'gameplay' => [ - 'class' => 'app\modules\gameplay\Module', - ], - 'activity' => [ - 'class' => 'app\modules\activity\Module', - ], + 'administer' => ['class' => 'app\modules\administer\Module',], + 'moderation' => ['class' => 'app\modules\moderation\Module'], + 'speedprogramming' => ['class' => 'app\modules\speedprogramming\Module',], + 'sales' => ['class' => 'app\modules\sales\Module',], + 'content' => ['class' => 'app\modules\content\Module',], + 'infrastructure' => ['class' => 'app\modules\infrastructure\Module',], + 'smartcity' => ['class' => 'app\modules\smartcity\Module',], + 'restapi' => ['class' => 'app\modules\restapi\Module',], + 'settings' => ['class' => 'app\modules\settings\Module',], + 'frontend' => ['class' => 'app\modules\frontend\Module',], + 'gameplay' => ['class' => 'app\modules\gameplay\Module',], + 'activity' => ['class' => 'app\modules\activity\Module',], ], 'components' => [ 'i18n' => [ diff --git a/backend/migrations-init/m251207_102327_populate_mui_menu.php b/backend/migrations-init/m251207_102327_populate_mui_menu.php index 80c3c2fc2..77198aa64 100644 --- a/backend/migrations-init/m251207_102327_populate_mui_menu.php +++ b/backend/migrations-init/m251207_102327_populate_mui_menu.php @@ -42,15 +42,15 @@ class m251207_102327_populate_mui_menu extends Migration 'icon' => 'fas fa-money-check-alt', 'visibility' => 'admin', 'items' => [ - ['label' => 'Sales Dashboard', 'url' => ['/sales/default/index'], 'visibility' => 'admin' ], - ['label' => 'Customers', 'url' => ['/sales/player-customer/index'], 'visibility' => 'admin' ,], - ['label' => 'Subscriptions', 'url' => ['/sales/player-subscription/index'], 'visibility' => 'admin' ,], - ['label' => 'Player Products', 'url' => ['/sales/player-product/index'], 'visibility' => 'admin' ,], - ['label' => 'Products', 'url' => ['/sales/product/index'], 'visibility' => 'admin' ,], - ['label' => 'Prices', 'url' => ['/sales/price/index'], 'visibility' => 'admin' ,], - ['label' => 'Product Networks', 'url' => ['/sales/product-network/index'], 'visibility' => 'admin' ,], - ['label' => 'Payment History', 'url' => ['/sales/player-payment-history/index'], 'visibility' => 'admin' ,], - ['label' => 'Webhook', 'url' => ['/sales/stripe-webhook/index'], 'visibility' => 'admin' ,], + ['label' => 'Sales Dashboard', 'url' => ['/sales/default/index'], 'visibility' => 'admin'], + ['label' => 'Customers', 'url' => ['/sales/player-customer/index'], 'visibility' => 'admin',], + ['label' => 'Subscriptions', 'url' => ['/sales/player-subscription/index'], 'visibility' => 'admin',], + ['label' => 'Player Products', 'url' => ['/sales/player-product/index'], 'visibility' => 'admin',], + ['label' => 'Products', 'url' => ['/sales/product/index'], 'visibility' => 'admin',], + ['label' => 'Prices', 'url' => ['/sales/price/index'], 'visibility' => 'admin',], + ['label' => 'Product Networks', 'url' => ['/sales/product-network/index'], 'visibility' => 'admin',], + ['label' => 'Payment History', 'url' => ['/sales/player-payment-history/index'], 'visibility' => 'admin',], + ['label' => 'Webhook', 'url' => ['/sales/stripe-webhook/index'], 'visibility' => 'admin',], ] ], [ @@ -58,10 +58,10 @@ class m251207_102327_populate_mui_menu extends Migration 'url' => ['/speedprogramming/default/index'], 'icon' => 'fas fa-money-check-alt', 'visibility' => 'admin', - 'enabled'=>'0', + 'enabled' => '0', 'items' => [ - ['label' => 'Problems', 'url' => ['/speedprogramming/speed-problem/index'], 'visibility' => 'admin' ,], - ['label' => 'Solutions', 'url' => ['/speedprogramming/default/index'], 'visibility' => 'admin' ,], + ['label' => 'Problems', 'url' => ['/speedprogramming/speed-problem/index'], 'visibility' => 'admin',], + ['label' => 'Solutions', 'url' => ['/speedprogramming/default/index'], 'visibility' => 'admin',], ] ], [ @@ -108,7 +108,7 @@ class m251207_102327_populate_mui_menu extends Migration 'label' => ' SmartCity', 'url' => ['/smartcity/default/index'], 'visibility' => 'user', - 'enabled'=>0, + 'enabled' => 0, 'items' => [ ['label' => 'Infrastructure', 'url' => ['/smartcity/infrastructure/index'], 'visibility' => 'user',], ['label' => 'Infrastructure Targets', 'url' => ['/smartcity/infrastructure-target/index'], 'visibility' => 'user',], @@ -185,10 +185,10 @@ class m251207_102327_populate_mui_menu extends Migration ['label' => 'Hints', 'url' => ['/gameplay/hint/index'], 'visibility' => 'admin',], ['label' => 'Achievements', 'url' => ['/gameplay/achievement/index'], 'visibility' => 'admin',], ['label' => 'Badges', 'url' => ['/gameplay/badge/index'], 'visibility' => 'admin',], - ['label' => 'Tutorials', 'url' => ['/gameplay/tutorial/index'], 'visibility' => 'admin','enabled'=>0], - ['label' => 'Tutorial Target', 'url' => ['/gameplay/tutorial-target/index'], 'visibility' => 'admin','enabled'=>0], - ['label' => 'Tutorial Tasks', 'url' => ['/gameplay/tutorial-task/index'], 'visibility' => 'admin','enabled'=>0], - ['label' => 'Tutorial Task Dependencies', 'url' => ['/gameplay/tutorial-task-dependency/index'], 'visibility' => 'admin','enabled'=>0], + ['label' => 'Tutorials', 'url' => ['/gameplay/tutorial/index'], 'visibility' => 'admin', 'enabled' => 0], + ['label' => 'Tutorial Target', 'url' => ['/gameplay/tutorial-target/index'], 'visibility' => 'admin', 'enabled' => 0], + ['label' => 'Tutorial Tasks', 'url' => ['/gameplay/tutorial-task/index'], 'visibility' => 'admin', 'enabled' => 0], + ['label' => 'Tutorial Task Dependencies', 'url' => ['/gameplay/tutorial-task-dependency/index'], 'visibility' => 'admin', 'enabled' => 0], ['label' => 'Credentials', 'url' => ['/gameplay/credential/index'], 'visibility' => 'admin',], ], ], @@ -216,27 +216,34 @@ class m251207_102327_populate_mui_menu extends Migration ['label' => 'Users', 'url' => ['/settings/user/index'], 'visibility' => 'admin',], ], ], + [ + 'label' => ' Administer', + 'url' => ['/administer'], + 'visibility' => 'admin', + 'items' => [ + ['label' => 'Main', 'url' => ['/administer/default/index'], 'visibility' => 'admin',], + ['label' => 'Events', 'url' => ['/administer/events/index'], 'visibility' => 'admin',], + ], + ], + ]; /** * {@inheritdoc} */ public function safeUp() { - $root=0; - foreach($this->items as $menu) - { - $this->insert('mui_menu',['label'=>$menu['label'],'url'=>$menu['url'][0],'visibility'=>$menu['visibility'],'sort_order'=>$root++,'enabled'=>intval(@$menu['enabled'] ?? 1 )]); - $id=Yii::$app->db->getLastInsertID(); - $child=0; - foreach($menu['items'] as $item) - { - if(is_array($item)) - $this->insert('mui_menu',['label'=>$item['label'],'url'=>$item['url'][0],'visibility'=>$item['visibility'],'parent_id'=>$id,'sort_order'=>$child++,'enabled'=>intval(@$item['enabled'] ?? (@$menu['enabled'] ?? 1) )]); + $root = 0; + foreach ($this->items as $menu) { + $this->insert('mui_menu', ['label' => $menu['label'], 'url' => $menu['url'][0], 'visibility' => $menu['visibility'], 'sort_order' => $root++, 'enabled' => intval(@$menu['enabled'] ?? 1)]); + $id = Yii::$app->db->getLastInsertID(); + $child = 0; + foreach ($menu['items'] as $item) { + if (is_array($item)) + $this->insert('mui_menu', ['label' => $item['label'], 'url' => $item['url'][0], 'visibility' => $item['visibility'], 'parent_id' => $id, 'sort_order' => $child++, 'enabled' => intval(@$item['enabled'] ?? (@$menu['enabled'] ?? 1))]); else - $this->insert('mui_menu',['label'=>$item,'visibility'=>'admin','parent_id'=>$id,'sort_order'=>$child++]); + $this->insert('mui_menu', ['label' => $item, 'visibility' => 'admin', 'parent_id' => $id, 'sort_order' => $child++]); } } - } /** diff --git a/backend/migrations/m260205_183413_add_administer_menu_items.php b/backend/migrations/m260205_183413_add_administer_menu_items.php new file mode 100644 index 000000000..01bf8f5d1 --- /dev/null +++ b/backend/migrations/m260205_183413_add_administer_menu_items.php @@ -0,0 +1,47 @@ + ' Administer', + 'url' => ['/administer'], + 'visibility' => 'admin', + 'items' => [ + ['label' => 'Main', 'url' => ['/administer/default/index'], 'visibility' => 'admin',], + ['label' => 'Events', 'url' => ['/administer/events/index'], 'visibility' => 'admin',], + ], + ], + + ]; + /** + * {@inheritdoc} + */ + public function safeUp() + { + $root = 8; + foreach ($this->items as $menu) { + $this->insert('mui_menu', ['label' => $menu['label'], 'url' => $menu['url'][0], 'visibility' => $menu['visibility'], 'sort_order' => $root++, 'enabled' => intval(@$menu['enabled'] ?? 1)]); + $id = Yii::$app->db->getLastInsertID(); + $child = 0; + foreach ($menu['items'] as $item) { + if (is_array($item)) + $this->insert('mui_menu', ['label' => $item['label'], 'url' => $item['url'][0], 'visibility' => $item['visibility'], 'parent_id' => $id, 'sort_order' => $child++, 'enabled' => intval(@$item['enabled'] ?? (@$menu['enabled'] ?? 1))]); + else + $this->insert('mui_menu', ['label' => $item, 'visibility' => 'admin', 'parent_id' => $id, 'sort_order' => $child++]); + } + } + } + + + /** + * {@inheritdoc} + */ + public function safeDown() + { + echo "m260205_183413_add_administer_menu_items cannot be reverted.\n"; + } + +} diff --git a/backend/modules/administer/Module.php b/backend/modules/administer/Module.php new file mode 100644 index 000000000..663eefaa6 --- /dev/null +++ b/backend/modules/administer/Module.php @@ -0,0 +1,24 @@ + [ + 'class' => \yii\filters\AccessControl::class, + 'rules' => [ + 'authActions' => [ + 'allow' => \Yii::$app->user->identity && \Yii::$app->user->identity->isAdmin, + 'actions' => ['index'], + 'roles' => ['@'], + ], + ], + ], + ]); + } + + /** + * Renders the index view for the module + * @return string + */ + public function actionIndex() + { + return $this->render('index'); + } +} diff --git a/backend/modules/administer/controllers/EventsController.php b/backend/modules/administer/controllers/EventsController.php new file mode 100644 index 000000000..f135aba79 --- /dev/null +++ b/backend/modules/administer/controllers/EventsController.php @@ -0,0 +1,147 @@ + [ + 'class' => \yii\filters\AccessControl::class, + 'rules' => [ + 'authActions' => [ + 'allow' => \Yii::$app->user->identity && \Yii::$app->user->identity->isAdmin, + 'actions' => ['index', 'view'], + 'roles' => ['@'], + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::class, + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ]); + } + + public function actionIndex() + { + $dataProvider = new \yii\data\ArrayDataProvider([ + 'allModels' => EventModel::getAll(), + 'pagination' => ['pageSize' => 10], + ]); + + return $this->render('index', ['dataProvider' => $dataProvider]); + } + + public function actionView($name) + { + $model = $this->findModel($name); + + // Get actual event code + $eventCode = Yii::$app->db + ->createCommand("SHOW CREATE EVENT `{$model->Name}`") + ->queryOne(); + + return $this->render('view', [ + 'model' => $model, + 'eventCode' => $eventCode['Create Event'] ?? 'N/A', + ]); + } + + public function actionCreate() + { + $model = new EventModel(); + + if ($model->load(Yii::$app->request->post())) { + $newSql = str_replace(["\r\n", "\r"], "\n", $model->Event_comment); + + try { + Yii::$app->db->createCommand($newSql)->execute(); + + if (preg_match('/EVENT\s+`([^`]+)`/i', $newSql, $matches)) { + $eventName = $matches[1]; + Yii::$app->session->setFlash('success', "Event created successfully."); + return $this->redirect(['view', 'name' => $eventName]); + } else { + Yii::$app->session->setFlash('success', "Event created successfully."); + return $this->redirect(['index']); + } + } catch (\Exception $e) { + Yii::$app->session->setFlash('error', "Failed to create event: " . $e->getMessage()); + } + } + + return $this->render('create', [ + 'model' => $model, + ]); + } + + public function actionUpdate($name) + { + $model = $this->findModel($name); + + // Get current CREATE EVENT SQL + $oldSql = Yii::$app->db + ->createCommand("SHOW CREATE EVENT `{$model->Name}`") + ->queryOne()['Create Event']; + + if ($model->load(Yii::$app->request->post())) { + $newSql = str_replace(["\r\n", "\r"], "\n", $model->Event_comment); + + try { + // Try to drop old and create new event + Yii::$app->db->createCommand("DROP EVENT IF EXISTS `{$model->Name}`")->execute(); + Yii::$app->db->createCommand($newSql)->execute(); + + Yii::$app->session->setFlash('success', "Event updated successfully."); + return $this->redirect(['view', 'name' => $model->Name]); + } catch (\Exception $e) { + // If failed, recreate the old event + try { + Yii::$app->db->createCommand($oldSql)->execute(); + } catch (\Exception $rollback) { + Yii::$app->session->setFlash('error', "Failed to update event and rollback also failed: " . $rollback->getMessage()); + return $this->redirect(['view', 'name' => $model->Name]); + } + + Yii::$app->session->setFlash('error', "Failed to update event, rolled back to previous version: " . $e->getMessage()); + return $this->redirect(['view', 'name' => $model->Name]); + } + } + + // Pass current SQL to the form + $model->Event_comment = $oldSql; + + return $this->render('update', [ + 'model' => $model, + ]); + } + + public function actionDelete($name) + { + EventModel::dropEvent($name); + return $this->redirect(['index']); + } + + protected function findModel($name) + { + $events = EventModel::getAll(); + foreach ($events as $event) { + if ($event->Name === $name) { + return $event; + } + } + throw new NotFoundHttpException("Event not found: $name"); + } +} diff --git a/backend/modules/administer/models/EventModel.php b/backend/modules/administer/models/EventModel.php new file mode 100644 index 000000000..6fcdc37a5 --- /dev/null +++ b/backend/modules/administer/models/EventModel.php @@ -0,0 +1,98 @@ +Name; + } + public function rules() + { + return [[ + [ + 'Name', + 'Definer', + 'Time_zone', + 'Type', + 'Execute_at', + 'Interval_value', + 'Interval_field', + 'Starts', + 'Ends', + 'Status', + 'On_completion', + 'Created', + 'Last_altered', + 'Last_executed', + 'Event_comment', + 'Originator', + 'character_set_client', + 'collation_connection', + 'Database_collation' + ], + 'safe' + ]]; + } + + public static function getAll() + { + $rows = Yii::$app->db->createCommand('SHOW EVENTS')->queryAll(); + $models = []; + + foreach ($rows as $row) { + $model = new self(); + $model->Name = $row['Name']; + $model->Definer = $row['Definer']; + $model->Time_zone = $row['Time zone']; + $model->Type = $row['Type']; + $model->Execute_at = $row['Execute at']; + $model->Interval_value = $row['Interval value']; + $model->Interval_field = $row['Interval field']; + $model->Starts = $row['Starts']; + $model->Ends = $row['Ends']; + $model->Status = $row['Status']; + $model->Originator = $row['Originator']; + $model->character_set_client = $row['character_set_client']; + $model->collation_connection = $row['collation_connection']; + $model->Database_collation = $row['Database Collation']; + $models[] = $model; + } + + return $models; + } + + public static function dropEvent($name) + { + return Yii::$app->db->createCommand("DROP EVENT `$name`")->execute(); + } +} diff --git a/backend/modules/administer/views/default/index.php b/backend/modules/administer/views/default/index.php new file mode 100644 index 000000000..99249df50 --- /dev/null +++ b/backend/modules/administer/views/default/index.php @@ -0,0 +1,6 @@ +
+ Administer differrent operations of your platform installation. +
++ = Html::a('Create Event', ['create'], ['class' => 'btn btn-success']) ?> +
+ += GridView::widget([ + 'dataProvider' => $dataProvider, + 'columns' => [ + 'Name', + 'Status', + 'Time_zone', + 'Execute_at', + 'Starts', + 'Ends', + 'Interval_value', + 'Interval_field', + [ + 'class' => 'yii\grid\ActionColumn', + 'urlCreator' => function ($action, $model) { + return [$action, 'name' => $model->Name]; + }, + ], + ], +]);?> ++ = Html::a('Update', ['update', 'name' => $model->Name], ['class' => 'btn btn-primary']); ?> + = Html::a('Delete', ['delete', 'name' => $model->Name], [ + 'class' => 'btn btn-danger', + 'data-method' => 'post', + 'data-confirm' => 'Are you sure?', + ]); ?> +
+ + + = DetailView::widget([ + 'model' => $model, + 'attributes' => [ + 'Name', + 'Status', + 'Time_zone', + 'Execute_at', + 'Starts', + 'Ends', + 'Interval_value', + 'Interval_field', + 'Event_comment' + ], + ]); ?> + +