From 582a8aa2a2f5ffadbb818f554f377183f4aba926 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Sun, 3 Nov 2024 13:33:16 +0200 Subject: [PATCH 01/38] introduce player tokens issue #1276 and #665 --- ...41103_000427_create_player_token_table.php | 34 ++++++++++++++++++ ...5725_create_player_token_history_table.php | 35 +++++++++++++++++++ ...105924_create_tai_player_token_trigger.php | 29 +++++++++++++++ ...105954_create_tau_player_token_trigger.php | 31 ++++++++++++++++ ...110652_create_tad_player_token_trigger.php | 29 +++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 backend/migrations/m241103_000427_create_player_token_table.php create mode 100644 backend/migrations/m241103_105725_create_player_token_history_table.php create mode 100644 backend/migrations/m241103_105924_create_tai_player_token_trigger.php create mode 100644 backend/migrations/m241103_105954_create_tau_player_token_trigger.php create mode 100644 backend/migrations/m241103_110652_create_tad_player_token_trigger.php diff --git a/backend/migrations/m241103_000427_create_player_token_table.php b/backend/migrations/m241103_000427_create_player_token_table.php new file mode 100644 index 000000000..9b3428b86 --- /dev/null +++ b/backend/migrations/m241103_000427_create_player_token_table.php @@ -0,0 +1,34 @@ +createTable('{{%player_token}}', [ + 'player_id' => $this->integer()->unsigned()->notNull(), + 'type' => $this->string(32)->notNull()->defaultValue('API'), + 'token' => $this->string(128)->notNull()->unique(), + 'description' => $this->text()->notNull()->defaultValue(""), + 'expires_at' => $this->dateTime(), + 'created_at' => $this->timestamp(), + ]); + $this->addPrimaryKey('player_token-pk', 'player_token', ['player_id', 'type']); + $this->addForeignKey('fk-player_token-player_id-player', '{{%player_token}}', 'player_id', 'player', 'id', 'CASCADE', 'CASCADE'); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropTable('{{%player_token}}'); + } +} diff --git a/backend/migrations/m241103_105725_create_player_token_history_table.php b/backend/migrations/m241103_105725_create_player_token_history_table.php new file mode 100644 index 000000000..a4c95c479 --- /dev/null +++ b/backend/migrations/m241103_105725_create_player_token_history_table.php @@ -0,0 +1,35 @@ +createTable('{{%player_token_history}}', [ + 'id'=>$this->primaryKey(), + 'player_id' => $this->integer()->unsigned()->notNull(), + 'type' => $this->string(32)->notNull()->defaultValue('API'), + 'token' => $this->string(128)->notNull()->unique(), + 'description' => $this->text()->notNull()->defaultValue(''), + 'expires_at' => $this->dateTime(), + 'created_at' => $this->timestamp(), + 'ts' => $this->timestamp(), + ]); + $this->addForeignKey('fk-player_token_history-player_id-player', '{{%player_token_history}}', 'player_id', 'player', 'id', 'CASCADE', 'CASCADE'); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropTable('{{%player_token}}'); + } +} diff --git a/backend/migrations/m241103_105924_create_tai_player_token_trigger.php b/backend/migrations/m241103_105924_create_tai_player_token_trigger.php new file mode 100644 index 000000000..baee66820 --- /dev/null +++ b/backend/migrations/m241103_105924_create_tai_player_token_trigger.php @@ -0,0 +1,29 @@ +db->createCommand($this->DROP_SQL)->execute(); + $this->db->createCommand($this->CREATE_SQL)->execute(); + } + + public function down() + { + $this->db->createCommand($this->DROP_SQL)->execute(); + } +} diff --git a/backend/migrations/m241103_105954_create_tau_player_token_trigger.php b/backend/migrations/m241103_105954_create_tau_player_token_trigger.php new file mode 100644 index 000000000..a26596dbb --- /dev/null +++ b/backend/migrations/m241103_105954_create_tau_player_token_trigger.php @@ -0,0 +1,31 @@ +db->createCommand($this->DROP_SQL)->execute(); + $this->db->createCommand($this->CREATE_SQL)->execute(); + } + + public function down() + { + $this->db->createCommand($this->DROP_SQL)->execute(); + } +} diff --git a/backend/migrations/m241103_110652_create_tad_player_token_trigger.php b/backend/migrations/m241103_110652_create_tad_player_token_trigger.php new file mode 100644 index 000000000..bb599fc34 --- /dev/null +++ b/backend/migrations/m241103_110652_create_tad_player_token_trigger.php @@ -0,0 +1,29 @@ +db->createCommand($this->DROP_SQL)->execute(); + $this->db->createCommand($this->CREATE_SQL)->execute(); + } + + public function down() + { + $this->db->createCommand($this->DROP_SQL)->execute(); + } +} From 7fffaf357ce32596c1b830820a5455251865c255 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Sun, 3 Nov 2024 13:34:05 +0200 Subject: [PATCH 02/38] When a player is archived, show their archived stream --- .../activity/models/ArchivedStream.php | 124 ++++++++++++++++++ .../frontend/views/profile/_activity_tab.php | 23 ++-- 2 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 backend/modules/activity/models/ArchivedStream.php diff --git a/backend/modules/activity/models/ArchivedStream.php b/backend/modules/activity/models/ArchivedStream.php new file mode 100644 index 000000000..808abe9c2 --- /dev/null +++ b/backend/modules/activity/models/ArchivedStream.php @@ -0,0 +1,124 @@ +'', + 'challenge'=>'', + 'treasure'=>'', + 'finding'=>'', + 'question'=>'', + 'team_player'=>'', + 'user'=>'', + 'report'=>'', + 'badge'=>'', + ]; + + public $ts_ago; + public $pub=true; + + public static function tableName() + { + return 'archived_stream'; + } + + public function getIcon() + { + return self::MODEL_ICONS[$this->model]; + } + + public function getPrefix() + { + return sprintf(" %s %s", Url::to('//'.Yii::$app->sys->offense_domain.'/images/avatars/'.$this->player->profile->avtr),$this->player->profile->link,$this->icon); + } + + public function Title(bool $pub=true) + { + return $this->pub ? $this->pubtitle : $this->title; + } + + public function getFormatted(bool $pub=true) + { + if(!Yii::$app->user->isGuest && (Yii::$app->user->id === $this->player_id || Yii::$app->user->identity->isAdmin)) + $this->pub=false; + return $this->{$this->model.'Message'}; + } + + public function getSuffix() + { + if($this->points != 0) + return sprintf(" for %d points", $this->points); + return ""; + } + + public function getBadgeMessage() + { + return sprintf("%s got the badge [%s]%s", $this->prefix, Badge::findOne(['id'=>$this->model_id])->name, $this->suffix); + } + + + public function getHeadshotMessage() + { + $headshot=\app\modules\activity\models\Headshot::findOne(['target_id'=>$this->model_id, 'player_id'=>$this->player_id]); + if($headshot->target->timer===0 || $headshot->timer===0) + return sprintf("%s managed to headshot [%s]%s", $this->prefix, Html::a(Target::findOne(['id'=>$this->model_id])->name, ['/infrastructure/target/full-view', 'id'=>$this->model_id]), $this->suffix); + + return sprintf("%s managed to headshot [%s] in %s minutes%s", $this->prefix, Html::a(Target::findOne(['id'=>$this->model_id])->name, ['/infrastructure/target/full-view', 'id'=>$this->model_id]), Yii::$app->formatter->asDuration($headshot->timer), number_format($headshot->timer / 60), $this->suffix); + } + + public function getChallengeMessage() + { + $csolver=\app\modules\activity\models\ChallengeSolver::findOne(['challenge_id'=>$this->model_id, 'player_id'=>$this->player_id]); + if($csolver->challenge->timer===0) + return sprintf("%s managed to complete the challenge [%s]%s", $this->prefix, Html::a(\app\modules\gameplay\models\Challenge::findOne(['id'=>$this->model_id])->name, ['/gameplay/challenge/view', 'id'=>$this->model_id]), $this->suffix); + + return sprintf("%s managed to complete the challenge [%s] in %s minutes%s", $this->prefix, Html::a(\app\modules\gameplay\models\Challenge::findOne(['id'=>$this->model_id])->name, ['/gameplay/challenge/view', 'id'=>$this->model_id]), Yii::$app->formatter->asDuration($csolver->timer),number_format($csolver->timer / 60), $this->suffix); + } + + public function getReportMessage() + { + return sprintf("%s Reported %s%s", $this->prefix, $this->Title($this->pub), $this->suffix); + } + + public function getQuestionMessage() + { + return sprintf("%s Answered the question of %s [%s] %s", $this->prefix, \app\modules\gameplay\models\Question::findOne($this->model_id)->challenge->name,\app\modules\gameplay\models\Question::findOne($this->model_id)->name, $this->suffix); + } + + public function getFindingMessage() + { + return $this->defaultMessage; + } + + public function getTreasureMessage() + { + return $this->defaultMessage; + } + + public function getUserMessage() + { + return $this->defaultMessage; + } + + public function getDefaultMessage() + { + return sprintf("%s %s%s", $this->prefix, $this->Title($this->pub), $this->suffix); + } +} diff --git a/backend/modules/frontend/views/profile/_activity_tab.php b/backend/modules/frontend/views/profile/_activity_tab.php index d992d6b0b..4b7c84fe3 100644 --- a/backend/modules/frontend/views/profile/_activity_tab.php +++ b/backend/modules/frontend/views/profile/_activity_tab.php @@ -1,12 +1,17 @@ -
Activity Stream
- -

owner->streams as $stream) -{ - echo "",$stream->formatted," ",Yii::$app->formatter->asRelativeTime($stream->ts),"
"; - if(($i++)>20) break; +if ($model->owner->status === 0) { + $streams = $model->owner->archivedStreams; + $TITLE="Archived activity stream"; +} else { + $streams = $model->owner->streams; + $TITLE="Activity stream"; } ?> -

+
+

", $stream->formatted, " ", Yii::$app->formatter->asRelativeTime($stream->ts), "
"; + if (($i++) > 20) break; + } + ?>

\ No newline at end of file From 14c49cdadafe0cff377ff9da97373c40cdb08277 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Sun, 3 Nov 2024 13:36:26 +0200 Subject: [PATCH 03/38] add player token crud --- .../controllers/PlayerTokenController.php | 137 ++++++++++++++++++ .../PlayerTokenHistoryController.php | 93 ++++++++++++ backend/modules/frontend/models/PlayerAR.php | 8 + .../modules/frontend/models/PlayerToken.php | 125 ++++++++++++++++ .../frontend/models/PlayerTokenHistory.php | 83 +++++++++++ .../models/PlayerTokenHistoryQuery.php | 34 +++++ .../models/PlayerTokenHistorySearch.php | 92 ++++++++++++ .../frontend/models/PlayerTokenQuery.php | 34 +++++ .../frontend/models/PlayerTokenSearch.php | 84 +++++++++++ .../views/player-token-history/_form.php | 35 +++++ .../views/player-token-history/_search.php | 44 ++++++ .../views/player-token-history/index.php | 56 +++++++ .../frontend/views/player-token/_form.php | 38 +++++ .../frontend/views/player-token/_search.php | 38 +++++ .../frontend/views/player-token/create.php | 20 +++ .../frontend/views/player-token/index.php | 50 +++++++ .../frontend/views/player-token/update.php | 23 +++ .../frontend/views/player-token/view.php | 40 +++++ backend/views/layouts/main.php | 2 + 19 files changed, 1036 insertions(+) create mode 100644 backend/modules/frontend/controllers/PlayerTokenController.php create mode 100644 backend/modules/frontend/controllers/PlayerTokenHistoryController.php create mode 100644 backend/modules/frontend/models/PlayerToken.php create mode 100644 backend/modules/frontend/models/PlayerTokenHistory.php create mode 100644 backend/modules/frontend/models/PlayerTokenHistoryQuery.php create mode 100644 backend/modules/frontend/models/PlayerTokenHistorySearch.php create mode 100644 backend/modules/frontend/models/PlayerTokenQuery.php create mode 100644 backend/modules/frontend/models/PlayerTokenSearch.php create mode 100644 backend/modules/frontend/views/player-token-history/_form.php create mode 100644 backend/modules/frontend/views/player-token-history/_search.php create mode 100644 backend/modules/frontend/views/player-token-history/index.php create mode 100644 backend/modules/frontend/views/player-token/_form.php create mode 100644 backend/modules/frontend/views/player-token/_search.php create mode 100644 backend/modules/frontend/views/player-token/create.php create mode 100644 backend/modules/frontend/views/player-token/index.php create mode 100644 backend/modules/frontend/views/player-token/update.php create mode 100644 backend/modules/frontend/views/player-token/view.php diff --git a/backend/modules/frontend/controllers/PlayerTokenController.php b/backend/modules/frontend/controllers/PlayerTokenController.php new file mode 100644 index 000000000..d0dd0ef42 --- /dev/null +++ b/backend/modules/frontend/controllers/PlayerTokenController.php @@ -0,0 +1,137 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ] + ); + } + + /** + * Lists all PlayerToken models. + * + * @return string + */ + public function actionIndex() + { + $searchModel = new PlayerTokenSearch(); + $dataProvider = $searchModel->search($this->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Displays a single PlayerToken model. + * @param int $player_id Player ID + * @param string $type Type + * @return string + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionView($player_id, $type) + { + return $this->render('view', [ + 'model' => $this->findModel($player_id, $type), + ]); + } + + /** + * Creates a new PlayerToken model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return string|\yii\web\Response + */ + public function actionCreate() + { + $model = new PlayerToken(); + if ($this->request->isPost) { + if ($model->load($this->request->post()) && $model->save()) { + return $this->redirect(['view', 'player_id' => $model->player_id, 'type' => $model->type]); + } + } else { + $model->loadDefaultValues(); + } + + return $this->render('create', [ + 'model' => $model, + ]); + } + + /** + * Updates an existing PlayerToken model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param int $player_id Player ID + * @param string $type Type + * @return string|\yii\web\Response + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionUpdate($player_id, $type) + { + $model = $this->findModel($player_id, $type); + + if ($this->request->isPost && $model->load($this->request->post()) && $model->save()) { + return $this->redirect(['view', 'player_id' => $model->player_id, 'type' => $model->type]); + } + + return $this->render('update', [ + 'model' => $model, + ]); + } + + /** + * Deletes an existing PlayerToken model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param int $player_id Player ID + * @param string $type Type + * @return \yii\web\Response + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionDelete($player_id, $type) + { + $this->findModel($player_id, $type)->delete(); + + return $this->redirect(['index']); + } + + /** + * Finds the PlayerToken model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param int $player_id Player ID + * @param string $type Type + * @return PlayerToken the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($player_id, $type) + { + if (($model = PlayerToken::findOne(['player_id' => $player_id, 'type' => $type])) !== null) { + return $model; + } + + throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exist.')); + } +} diff --git a/backend/modules/frontend/controllers/PlayerTokenHistoryController.php b/backend/modules/frontend/controllers/PlayerTokenHistoryController.php new file mode 100644 index 000000000..8a785e5f5 --- /dev/null +++ b/backend/modules/frontend/controllers/PlayerTokenHistoryController.php @@ -0,0 +1,93 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['POST'], + 'truncate' => ['POST'], + ], + ], + ] + ); + } + + /** + * Lists all PlayerTokenHistory models. + * + * @return string + */ + public function actionIndex() + { + $searchModel = new PlayerTokenHistorySearch(); + $dataProvider = $searchModel->search($this->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Deletes an existing PlayerTokenHistory model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param int $id ID + * @return \yii\web\Response + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } + + /** + * Deletes All PlayerTokenHistory models. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @return \yii\web\Response + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionTruncate() + { + if(PlayerTokenHistory::deleteAll()!==0) + \Yii::$app->session->addFlash('success','Token History truncated'); + return $this->redirect(['index']); + } + + /** + * Finds the PlayerTokenHistory model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param int $id ID + * @return PlayerTokenHistory the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = PlayerTokenHistory::findOne(['id' => $id])) !== null) { + return $model; + } + + throw new NotFoundHttpException(\Yii::t('app', 'The requested page does not exist.')); + } +} diff --git a/backend/modules/frontend/models/PlayerAR.php b/backend/modules/frontend/models/PlayerAR.php index a9dde7b27..e05395fba 100644 --- a/backend/modules/frontend/models/PlayerAR.php +++ b/backend/modules/frontend/models/PlayerAR.php @@ -10,6 +10,7 @@ use app\modules\activity\models\SpinQueue; use app\modules\activity\models\Report; use app\modules\activity\models\Stream; +use app\modules\activity\models\ArchivedStream; use app\modules\gameplay\models\Hint; use app\modules\gameplay\models\Finding; use app\modules\gameplay\models\Treasure; @@ -327,6 +328,13 @@ public function getStreams() { return $this->hasMany(Stream::class, ['player_id' => 'id'])->orderBy(['ts'=>SORT_DESC,'id'=>SORT_DESC]); } + /** + * @return \yii\db\ActiveQuery + */ + public function getArchivedStreams() + { + return $this->hasMany(ArchivedStream::class, ['player_id' => 'id'])->orderBy(['ts'=>SORT_DESC,'id'=>SORT_DESC]); + } /** * @return \yii\db\ActiveQuery diff --git a/backend/modules/frontend/models/PlayerToken.php b/backend/modules/frontend/models/PlayerToken.php new file mode 100644 index 000000000..74864eb6d --- /dev/null +++ b/backend/modules/frontend/models/PlayerToken.php @@ -0,0 +1,125 @@ +isNewRecord) //for checking this code is on model search or not + { + $this->type='API'; + if($this->description===null) + $this->description=(string)$this->description; + $this->token=Yii::$app->security->generateRandomString(30); + $this->expires_at=\Yii::$app->formatter->asDatetime(new \DateTime('NOW + 60 days'), 'php:Y-m-d H:i:s'); + } + } + public function behaviors() + { + return [ + 'typecast' => [ + 'class' => AttributeTypecastBehavior::class, + 'typecastAfterValidate' => true, + 'typecastBeforeSave' => true, + 'typecastAfterFind' => true, + ], + 'timestamp' => [ + 'class' => TimestampBehavior::class, + 'createdAtAttribute' => 'created_at', + 'updatedAtAttribute' => 'created_at', + 'value' => new Expression('NOW()'), + 'preserveNonEmptyValues' => true, + ], + ]; + } + /** + * {@inheritdoc} + */ + public function rules() + { + return [ + ['token', 'default', 'value' => Yii::$app->security->generateRandomString(20)], + ['description', 'default', 'skipOnEmpty'=>false, 'skipOnError'=>false,'value' => 'auto-generated by backend'], + [['expires_at', 'created_at'], 'default', 'value' => \Yii::$app->formatter->asDatetime(new \DateTime('NOW + 30 days'), 'php:Y-m-d H:i:s')], + [['type'], 'default', 'value' => 'API'], + [['player_id', 'type', 'token'], 'required'], + [['player_id'], 'integer'], + [['expires_at', 'created_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'], + [['expires_at', 'created_at','description'], 'safe'], + [['type'], 'string', 'max' => 32], + [['token'], 'string', 'max' => 128], + [['description'], 'string'], + [['token'], 'unique'], + [['player_id', 'type'], 'unique', 'targetAttribute' => ['player_id', 'type']], + [['player_id'], 'exist', 'skipOnError' => true, 'targetClass' => Player::class, 'targetAttribute' => ['player_id' => 'id']], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels() + { + return [ + 'player_id' => Yii::t('app', 'Player ID'), + 'type' => Yii::t('app', 'Type'), + 'token' => Yii::t('app', 'Token'), + 'expires_at' => Yii::t('app', 'Expires At'), + 'created_at' => Yii::t('app', 'Created At'), + ]; + } + + /** + * Gets query for [[Player]]. + * + * @return \yii\db\ActiveQuery|yii\db\ActiveQuery + */ + public function getPlayer() + { + return $this->hasOne(Player::class, ['id' => 'player_id']); + } + + /** + * {@inheritdoc} + * @return PlayerTokenQuery the active query used by this AR class. + */ + public static function find() + { + return new PlayerTokenQuery(get_called_class()); + } + + public function getTypes() + { + return [ + 'API'=>'API', + 'password_reset'=>'Password Reset', + 'email_verification'=>'Email Verification' + ]; + } +} diff --git a/backend/modules/frontend/models/PlayerTokenHistory.php b/backend/modules/frontend/models/PlayerTokenHistory.php new file mode 100644 index 000000000..33bb022ef --- /dev/null +++ b/backend/modules/frontend/models/PlayerTokenHistory.php @@ -0,0 +1,83 @@ + 32], + [['token'], 'string', 'max' => 128], + [['token'], 'unique'], + [['player_id'], 'exist', 'skipOnError' => true, 'targetClass' => Player::class, 'targetAttribute' => ['player_id' => 'id']], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('app', 'ID'), + 'player_id' => Yii::t('app', 'Player ID'), + 'type' => Yii::t('app', 'Type'), + 'token' => Yii::t('app', 'Token'), + 'description' => Yii::t('app', 'Description'), + 'expires_at' => Yii::t('app', 'Expires At'), + 'created_at' => Yii::t('app', 'Created At'), + 'ts' => Yii::t('app', 'Ts'), + ]; + } + + /** + * Gets query for [[Player]]. + * + * @return \yii\db\ActiveQuery|yii\db\ActiveQuery + */ + public function getPlayer() + { + return $this->hasOne(Player::class, ['id' => 'player_id']); + } + + /** + * {@inheritdoc} + * @return PlayerTokenHistoryQuery the active query used by this AR class. + */ + public static function find() + { + return new PlayerTokenHistoryQuery(get_called_class()); + } +} diff --git a/backend/modules/frontend/models/PlayerTokenHistoryQuery.php b/backend/modules/frontend/models/PlayerTokenHistoryQuery.php new file mode 100644 index 000000000..d2d05872c --- /dev/null +++ b/backend/modules/frontend/models/PlayerTokenHistoryQuery.php @@ -0,0 +1,34 @@ +andWhere('[[status]]=1'); + }*/ + + /** + * {@inheritdoc} + * @return PlayerTokenHistory[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * {@inheritdoc} + * @return PlayerTokenHistory|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} diff --git a/backend/modules/frontend/models/PlayerTokenHistorySearch.php b/backend/modules/frontend/models/PlayerTokenHistorySearch.php new file mode 100644 index 000000000..feaa3ff66 --- /dev/null +++ b/backend/modules/frontend/models/PlayerTokenHistorySearch.php @@ -0,0 +1,92 @@ +joinWith('player'); + + // add conditions that should always apply here + + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + ]); + + $this->load($params); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + // grid filtering conditions + $query->andFilterWhere([ + 'id' => $this->id, + 'player_id' => $this->player_id, + 'expires_at' => $this->expires_at, + 'created_at' => $this->created_at, + 'ts' => $this->ts, + ]); + + $query->andFilterWhere(['like', 'player_token.type', $this->type]) + ->andFilterWhere(['like', 'token', $this->token]) + ->andFilterWhere(['like', 'description', $this->description]) + ->andFilterWhere(['like', 'player.username', $this->username]); + $query->andFilterWhere(['like', 'player_token.type', $this->type]) + ->andFilterWhere(['like', 'token', $this->token]) + ->andFilterWhere(['like', 'description', $this->description]) + ->andFilterWhere(['like', 'player.username', $this->username]); + $dataProvider->setSort([ + 'attributes' => array_merge( + $dataProvider->getSort()->attributes, + [ + 'username' => [ + 'asc' => ['player.username' => SORT_ASC], + 'desc' => ['player.username' => SORT_DESC], + ], + ] + ), + ]); + + return $dataProvider; + } +} diff --git a/backend/modules/frontend/models/PlayerTokenQuery.php b/backend/modules/frontend/models/PlayerTokenQuery.php new file mode 100644 index 000000000..b8dd7371e --- /dev/null +++ b/backend/modules/frontend/models/PlayerTokenQuery.php @@ -0,0 +1,34 @@ +andWhere('[[status]]=1'); + }*/ + + /** + * {@inheritdoc} + * @return PlayerToken[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * {@inheritdoc} + * @return PlayerToken|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} diff --git a/backend/modules/frontend/models/PlayerTokenSearch.php b/backend/modules/frontend/models/PlayerTokenSearch.php new file mode 100644 index 000000000..d1948b17e --- /dev/null +++ b/backend/modules/frontend/models/PlayerTokenSearch.php @@ -0,0 +1,84 @@ +joinWith(['player']); + + // add conditions that should always apply here + + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + ]); + + $this->load($params); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + // grid filtering conditions + $query->andFilterWhere([ + 'player_id' => $this->player_id, + 'expires_at' => $this->expires_at, + 'created_at' => $this->created_at, + ]); + + $query->andFilterWhere(['like', 'player_token.type', $this->type]) + ->andFilterWhere(['like', 'token', $this->token]) + ->andFilterWhere(['like', 'description', $this->description]) + ->andFilterWhere(['like', 'player.username', $this->username]); + $dataProvider->setSort([ + 'attributes' => array_merge( + $dataProvider->getSort()->attributes, + [ + 'username' => [ + 'asc' => ['player.username' => SORT_ASC], + 'desc' => ['player.username' => SORT_DESC], + ], + ] + ), + ]); + + return $dataProvider; + } +} diff --git a/backend/modules/frontend/views/player-token-history/_form.php b/backend/modules/frontend/views/player-token-history/_form.php new file mode 100644 index 000000000..a4f58e891 --- /dev/null +++ b/backend/modules/frontend/views/player-token-history/_form.php @@ -0,0 +1,35 @@ + + +
+ + + + field($model, 'player_id')->textInput() ?> + + field($model, 'type')->textInput(['maxlength' => true]) ?> + + field($model, 'token')->textInput(['maxlength' => true]) ?> + + field($model, 'description')->textarea(['rows' => 6]) ?> + + field($model, 'expires_at')->textInput() ?> + + field($model, 'created_at')->textInput() ?> + + field($model, 'ts')->textInput() ?> + +
+ 'btn btn-success']) ?> +
+ + + +
diff --git a/backend/modules/frontend/views/player-token-history/_search.php b/backend/modules/frontend/views/player-token-history/_search.php new file mode 100644 index 000000000..266f7d733 --- /dev/null +++ b/backend/modules/frontend/views/player-token-history/_search.php @@ -0,0 +1,44 @@ + + + diff --git a/backend/modules/frontend/views/player-token-history/index.php b/backend/modules/frontend/views/player-token-history/index.php new file mode 100644 index 000000000..76b52eb09 --- /dev/null +++ b/backend/modules/frontend/views/player-token-history/index.php @@ -0,0 +1,56 @@ +title = Yii::t('app', 'Player Token Histories'); +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+

+ 'btn btn-danger', + 'data' => [ + 'confirm' => Yii::t('app', 'Are you sure you want to delete all records?'), + 'method' => 'post', + ], + ]) ?> +

+ + render('_search', ['model' => $searchModel]); + ?> + + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + ['class' => 'app\components\columns\ProfileColumn', 'idkey' => 'player.profile.id', 'attribute' => 'username', 'field' => 'player.username'], + 'type', + 'token', + 'description:ntext', + 'expires_at', + 'created_at', + 'ts', + [ + 'class' => ActionColumn::class, + 'urlCreator' => function ($action, PlayerTokenHistory $model, $key, $index, $column) { + return Url::toRoute([$action, 'id' => $model->id]); + } + ], + ], + ]); ?> + + + +
\ No newline at end of file diff --git a/backend/modules/frontend/views/player-token/_form.php b/backend/modules/frontend/views/player-token/_form.php new file mode 100644 index 000000000..019a48f4f --- /dev/null +++ b/backend/modules/frontend/views/player-token/_form.php @@ -0,0 +1,38 @@ + + +
+ + +
+
field($model, 'player_id')->widget(AutocompleteAjax::class, [ + 'multiple' => false, + 'url' => ['/frontend/player/ajax-search'], + 'options' => ['placeholder' => 'Find player by email, username, id or profile.'] + ])->Label('Player')->hint('Choose the player for the token') ?>
+
field($model, 'description')->textInput(['maxlength' => true,'placeholder'=>'my token description'])->hint("A small description for the token") ?>
+
+ +
+
field($model, 'type')->dropDownList($model->types)->hint('The type of this token') ?>
+
field($model, 'token')->textInput(['maxlength' => true]) ?>
+
field($model, 'expires_at')->textInput()->hint('Token expiration date (default: in 60 days)') ?>
+
+ + +
+ 'btn btn-success']) ?> +
+ + + +
diff --git a/backend/modules/frontend/views/player-token/_search.php b/backend/modules/frontend/views/player-token/_search.php new file mode 100644 index 000000000..c273a4bb4 --- /dev/null +++ b/backend/modules/frontend/views/player-token/_search.php @@ -0,0 +1,38 @@ + + + diff --git a/backend/modules/frontend/views/player-token/create.php b/backend/modules/frontend/views/player-token/create.php new file mode 100644 index 000000000..18b515f35 --- /dev/null +++ b/backend/modules/frontend/views/player-token/create.php @@ -0,0 +1,20 @@ +title = Yii::t('app', 'Create Player Token'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Player Tokens'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ + render('_form', [ + 'model' => $model, + ]) ?> + +
diff --git a/backend/modules/frontend/views/player-token/index.php b/backend/modules/frontend/views/player-token/index.php new file mode 100644 index 000000000..99be498aa --- /dev/null +++ b/backend/modules/frontend/views/player-token/index.php @@ -0,0 +1,50 @@ +title = Yii::t('app', 'Player Tokens'); +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ +

+ 'btn btn-success']) ?> +

+ + + render('_search', ['model' => $searchModel]); ?> + + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + + ['class' => 'app\components\columns\ProfileColumn', 'idkey' => 'player.profile.id', 'attribute' => 'username', 'field' => 'player.username'], + 'type', + 'token', + 'description', + 'expires_at', + 'created_at', + [ + 'class' => ActionColumn::class, + 'urlCreator' => function ($action, PlayerToken $model, $key, $index, $column) { + return Url::toRoute([$action, 'player_id' => $model->player_id, 'type' => $model->type]); + } + ], + ], + ]); ?> + + + +
diff --git a/backend/modules/frontend/views/player-token/update.php b/backend/modules/frontend/views/player-token/update.php new file mode 100644 index 000000000..2f98c0fb3 --- /dev/null +++ b/backend/modules/frontend/views/player-token/update.php @@ -0,0 +1,23 @@ +title = Yii::t('app', 'Update Player Token: {name}', [ + 'name' => $model->player_id, +]); +$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Player Tokens'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => $model->player_id, 'url' => ['view', 'player_id' => $model->player_id, 'type' => $model->type]]; +$this->params['breadcrumbs'][] = Yii::t('app', 'Update'); +?> +
+ +

title) ?>

+ + render('_form', [ + 'model' => $model, + ]) ?> + +
diff --git a/backend/modules/frontend/views/player-token/view.php b/backend/modules/frontend/views/player-token/view.php new file mode 100644 index 000000000..d1edc077b --- /dev/null +++ b/backend/modules/frontend/views/player-token/view.php @@ -0,0 +1,40 @@ +title = $model->player_id; +$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Player Tokens'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +\yii\web\YiiAsset::register($this); +?> +
+ +

title) ?>

+ +

+ $model->player_id, 'type' => $model->type], ['class' => 'btn btn-primary']) ?> + $model->player_id, 'type' => $model->type], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => Yii::t('app', 'Are you sure you want to delete this item?'), + 'method' => 'post', + ], + ]) ?> +

+ + $model, + 'attributes' => [ + 'player_id', + 'type', + 'token', + 'expires_at', + 'created_at', + ], + ]) ?> + +
diff --git a/backend/views/layouts/main.php b/backend/views/layouts/main.php index 803332003..38e9e23d3 100644 --- a/backend/views/layouts/main.php +++ b/backend/views/layouts/main.php @@ -161,6 +161,8 @@ ['label' => 'Players', 'url' => ['/frontend/player/index'], 'visible' => !Yii::$app->user->isGuest,], ['label' => 'Profiles', 'url' => ['/frontend/profile/index'], 'visible' => !Yii::$app->user->isGuest,], ['label' => 'Player Metadata', 'url' => ['/frontend/player-metadata/index'], 'visible' => !Yii::$app->user->isGuest,], + ['label' => 'Player Tokens', 'url' => ['/frontend/player-token/index'], 'visible' => !Yii::$app->user->isGuest,], + ['label' => 'Player Tokens History', 'url' => ['/frontend/player-token-history/index'], 'visible' => !Yii::$app->user->isGuest,], ['label' => 'Player Last', 'url' => ['/frontend/player-last/index'], 'visible' => !Yii::$app->user->isGuest,], ['label' => 'Player SSL', 'url' => ['/frontend/player-ssl/index'], 'visible' => !Yii::$app->user->isGuest,], ['label' => 'Player Spins', 'url' => ['/frontend/player-spin/index'], 'visible' => !Yii::$app->user->isGuest,], From 586764740c8a6ee422602d0e6cdc0639519ee291 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:53:50 +0200 Subject: [PATCH 04/38] add expire tokens procedure --- ..._create_expire_player_tokens_procedure.php | 33 +++++++++++++++++++ ...reate_ev_player_token_expiration_event.php | 28 ++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 backend/migrations/m241103_121625_create_expire_player_tokens_procedure.php create mode 100644 backend/migrations/m241103_121633_create_ev_player_token_expiration_event.php diff --git a/backend/migrations/m241103_121625_create_expire_player_tokens_procedure.php b/backend/migrations/m241103_121625_create_expire_player_tokens_procedure.php new file mode 100644 index 000000000..e601ef329 --- /dev/null +++ b/backend/migrations/m241103_121625_create_expire_player_tokens_procedure.php @@ -0,0 +1,33 @@ +0 THEN + START TRANSACTION; + INSERT INTO notification (player_id,category,title,body,archived,created_at,updated_at) SELECT player_id,'info','Token expiration',CONCAT(type,' Token [',description,'] expired at ',expires_at),0,tnow,tnow FROM player_token WHERE expires_atdb->createCommand($this->DROP_SQL)->execute(); + $this->db->createCommand($this->CREATE_SQL)->execute(); + } + + public function down() + { + $this->db->createCommand($this->DROP_SQL)->execute(); + } +} diff --git a/backend/migrations/m241103_121633_create_ev_player_token_expiration_event.php b/backend/migrations/m241103_121633_create_ev_player_token_expiration_event.php new file mode 100644 index 000000000..69788c501 --- /dev/null +++ b/backend/migrations/m241103_121633_create_ev_player_token_expiration_event.php @@ -0,0 +1,28 @@ +db->createCommand($this->DROP_SQL)->execute(); + $this->db->createCommand($this->CREATE_SQL)->execute(); + } + + public function down() + { + $this->db->createCommand($this->DROP_SQL)->execute(); + } +} From 02bf44396b8be57a4734d7ec006c08d1ca0efe73 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:54:12 +0200 Subject: [PATCH 05/38] remove unique from token on history --- .../m241103_105725_create_player_token_history_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/migrations/m241103_105725_create_player_token_history_table.php b/backend/migrations/m241103_105725_create_player_token_history_table.php index a4c95c479..54bed7c0c 100644 --- a/backend/migrations/m241103_105725_create_player_token_history_table.php +++ b/backend/migrations/m241103_105725_create_player_token_history_table.php @@ -16,7 +16,7 @@ public function safeUp() 'id'=>$this->primaryKey(), 'player_id' => $this->integer()->unsigned()->notNull(), 'type' => $this->string(32)->notNull()->defaultValue('API'), - 'token' => $this->string(128)->notNull()->unique(), + 'token' => $this->string(128)->notNull(), 'description' => $this->text()->notNull()->defaultValue(''), 'expires_at' => $this->dateTime(), 'created_at' => $this->timestamp(), From 932131f5829adf1c49b18e0794bf5a2a9a6bdc14 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:54:38 +0200 Subject: [PATCH 06/38] add api related url migrations --- .../m241103_021658_target_api_url_routes.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 backend/migrations-init/m241103_021658_target_api_url_routes.php diff --git a/backend/migrations-init/m241103_021658_target_api_url_routes.php b/backend/migrations-init/m241103_021658_target_api_url_routes.php new file mode 100644 index 000000000..a22475004 --- /dev/null +++ b/backend/migrations-init/m241103_021658_target_api_url_routes.php @@ -0,0 +1,33 @@ +upsert('url_route',['source'=>'profile/generate-token','destination'=>'profile/generate-token','weight'=>339]); + $this->upsert('url_route',['source'=>'api/targets','destination'=>'api/target/index','weight'=>642]); + $this->upsert('url_route',['source'=>'api/target/claim','destination'=>'api/target/claim','weight'=>643]); + $this->upsert('url_route',['source'=>'api/target/instances','destination'=>'api/target/instances','weight'=>643]); + $this->upsert('url_route',['source'=>'api/target/','destination'=>'api/target/view','weight'=>644]); + $this->upsert('url_route',['source'=>'api/target//spin','destination'=>'api/target/spin','weight'=>645]); + $this->upsert('url_route',['source'=>'api/target//spawn','destination'=>'api/target/spawn','weight'=>646]); + $this->upsert('url_route',['source'=>'api/target//shut','destination'=>'api/target/shut','weight'=>647]); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + echo "m241103_021658_target_api_url_routes cannot be reverted.\n"; + } + +} From 89802b76a044c889d29db8b10eef07233beee6f9 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:55:06 +0200 Subject: [PATCH 07/38] add sysconfig --- docs/Sysconfig-Keys.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Sysconfig-Keys.md b/docs/Sysconfig-Keys.md index ec0fafcdb..26cdb4e80 100644 --- a/docs/Sysconfig-Keys.md +++ b/docs/Sysconfig-Keys.md @@ -76,6 +76,9 @@ * `event_end_notification_body`: The body that will be used to send a notification to all players when the event ends * `plus_writeups`: Number to add to the headshots to allow for writeup activations (eg. a value of `2` means that the player can have `player_headshots+2` writeups active at most). A value of `0` means that the player can have only as many writeups active as its own number of headshots. +* `api_bearer_enable` Enable Bearer authorizations API operations +* `api_claim_timeout` set the rate limit for the api claim. One request per `api_claim_timeout`+1 seconds +* `api_target_instances_timeout` set the rate limit for the target instances endpoint. One request per `api_target_instances_timeout`+1 seconds ## mail configuration * `mail_from` Email address used to send registration and password reset mails from * `mail_fromName` The name appeared on the email send for registration and password resets From b03c65f6bb0202560d558f9cd8beffeec446786e Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:56:34 +0200 Subject: [PATCH 08/38] introduce api tokens --- frontend/controllers/ProfileController.php | 48 +++++- frontend/models/PlayerAR.php | 8 + frontend/models/PlayerToken.php | 124 ++++++++++++++ frontend/models/PlayerTokenQuery.php | 34 ++++ frontend/themes/material/profile/_card.php | 187 +++++++++++---------- 5 files changed, 305 insertions(+), 96 deletions(-) create mode 100644 frontend/models/PlayerToken.php create mode 100644 frontend/models/PlayerTokenQuery.php diff --git a/frontend/controllers/ProfileController.php b/frontend/controllers/ProfileController.php index 54b31365d..ddc45f75a 100644 --- a/frontend/controllers/ProfileController.php +++ b/frontend/controllers/ProfileController.php @@ -5,6 +5,7 @@ use app\models\PlayerDisconnectQueue; use Yii; use app\models\Profile; +use app\models\PlayerToken; use yii\data\ActiveDataProvider; use \app\modules\target\models\Target; use \app\modules\game\models\Headshot; @@ -16,6 +17,7 @@ use yii\web\Response; use yii\web\BadRequestHttpException; use yii\base\InvalidArgumentException; +use yii\base\UserException; use yii\web\UploadedFile; class ProfileController extends \app\components\BaseController @@ -28,32 +30,32 @@ public function behaviors() return ArrayHelper::merge(parent::behaviors(), [ 'access' => [ 'class' => AccessControl::class, - 'only' => ['badge', 'index', 'invite', 'me', 'delete', 'ovpn', 'revoke', 'settings', 'notifications', 'hints', 'disconnect'], + 'only' => ['badge', 'index', 'invite', 'me', 'delete', 'ovpn', 'revoke', 'settings', 'notifications', 'hints', 'disconnect','generate-token'], 'rules' => [ 'eventActive' => [ - 'actions' => ['badge', 'index', 'notifications', 'hints', 'ovpn', 'settings', 'invite', 'revoke', 'disconnect','delete'], + 'actions' => ['badge', 'index', 'notifications', 'hints', 'ovpn', 'settings', 'invite', 'revoke', 'disconnect','delete','generate-token'], ], 'eventStartEnd' => [ 'actions' => [''], ], 'eventStart' => [ - 'actions' => ['ovpn', 'revoke', 'disconnect','delete'], + 'actions' => ['ovpn', 'revoke', 'disconnect','delete','generate-token'], ], 'eventEnd' => [ - 'actions' => ['ovpn', 'revoke', 'disconnect','delete'], + 'actions' => ['ovpn', 'revoke', 'disconnect','delete','generate-token'], ], 'teamsAccess' => [ - 'actions' => ['ovpn', 'disconnect'], + 'actions' => ['ovpn', 'disconnect','generate-token'], ], 'disabledRoute' => [ - 'actions' => ['badge', 'me', 'delete','index', 'notifications', 'hints', 'ovpn', 'settings', 'invite', 'revoke', 'disconnect'], + 'actions' => ['badge', 'me', 'delete','index', 'notifications', 'hints', 'ovpn', 'settings', 'invite', 'revoke', 'disconnect','generate-token'], ], [ 'actions' => ['index', 'badge'], 'allow' => true, ], [ - 'actions' => ['ovpn', 'me', 'delete','settings', 'notifications', 'hints', 'revoke', 'disconnect'], + 'actions' => ['ovpn', 'me', 'delete','settings', 'notifications', 'hints', 'revoke', 'disconnect','generate-token'], 'allow' => true, 'roles' => ['@'] ], @@ -71,7 +73,6 @@ public function behaviors() return \Yii::$app->getResponse()->redirect([Yii::$app->sys->default_homepage]); } ], - ], ], [ @@ -84,6 +85,7 @@ public function behaviors() 'revoke' => ['POST'], 'disconnect' => ['POST'], 'delete' => ['POST'], + 'generate-token'=>['POST'], ], ], ]); @@ -250,6 +252,36 @@ public function actionDisconnect() return $this->redirect(['/profile/me']); } + /** + * Generate API Token + */ + public function actionGenerateToken() + { + if(boolval(\Yii::$app->sys->api_bearer_enable)!==true) + { + \Yii::$app->session->addFlash('warning', \Yii::t('app', "Operation disabled.")); + return $this->redirect(['/profile/me']); + } + if (Yii::$app->user->identity->apiToken!==null) { + \Yii::$app->session->addFlash('warning', \Yii::t('app', "There is already an API token.")); + return $this->redirect(['/profile/me']); + } + try { + $pt=new PlayerToken(); + $pt->player_id=Yii::$app->user->identity->id; + $pt->type='API'; + $pt->description='generated by player'; + if(!$pt->validate('token') || !$pt->save()) + { + throw new UserException(\Yii::t('app','Failed to generate API token')); + } + \Yii::$app->session->addFlash('success', \Yii::t('app', "API Token generated.")); + } catch (\Exception $e) { + \Yii::$app->session->addFlash('error', \Yii::t('app', "Failed to generate API token.")); + } + return $this->redirect(['/profile/me']); + } + public function actionOvpn($id) { diff --git a/frontend/models/PlayerAR.php b/frontend/models/PlayerAR.php index 4448e5726..599dc1ace 100644 --- a/frontend/models/PlayerAR.php +++ b/frontend/models/PlayerAR.php @@ -240,6 +240,14 @@ public function getDisconnectQueue() return $this->hasOne(PlayerDisconnectQueue::class, ['player_id' => 'id']); } + /** + * @return \yii\db\ActiveQuery + */ + public function getApiToken() + { + return $this->hasOne(PlayerToken::class, ['player_id' => 'id'])->andWhere(['type'=>'API']); + } + /** * @return \yii\db\ActiveQuery */ diff --git a/frontend/models/PlayerToken.php b/frontend/models/PlayerToken.php new file mode 100644 index 000000000..e7457792a --- /dev/null +++ b/frontend/models/PlayerToken.php @@ -0,0 +1,124 @@ +isNewRecord) //for checking this code is on model search or not + { + $this->type='API'; + $this->token=Yii::$app->security->generateRandomString(30); + $this->expires_at=\Yii::$app->formatter->asDatetime(new \DateTime('NOW + 60 days'), 'php:Y-m-d H:i:s'); + } + } + public function behaviors() + { + return [ + 'typecast' => [ + 'class' => AttributeTypecastBehavior::class, + 'attributeTypes' => [ + 'player_id' => AttributeTypecastBehavior::TYPE_INTEGER, + ], + 'typecastAfterValidate' => true, + 'typecastBeforeSave' => true, + 'typecastAfterFind' => true, + ], + 'timestamp' => [ + 'class' => TimestampBehavior::class, + 'createdAtAttribute' => 'created_at', + 'updatedAtAttribute' => 'created_at', + 'value' => new Expression('NOW()'), + 'preserveNonEmptyValues' => true, + ], + ]; + } + /** + * {@inheritdoc} + */ + public function rules() + { + return [ + ['token', 'default', 'value' => Yii::$app->security->generateRandomString(30)], + [['expires_at'], 'default', 'value' => \Yii::$app->formatter->asDatetime(new \DateTime('NOW + 30 days'), 'php:Y-m-d H:i:s')], + [['type'], 'default', 'value' => 'API'], + [['player_id', 'type', 'token'], 'required'], + [['player_id'], 'integer'], + [['expires_at', 'created_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'], + [['expires_at', 'created_at','description'], 'safe'], + [['type'], 'string', 'max' => 32], + [['token'], 'string', 'max' => 128], + [['token'], 'unique'], + [['player_id', 'type'], 'unique', 'targetAttribute' => ['player_id', 'type']], + [['player_id'], 'exist', 'skipOnError' => true, 'targetClass' => Player::class, 'targetAttribute' => ['player_id' => 'id']], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels() + { + return [ + 'player_id' => Yii::t('app', 'Player ID'), + 'type' => Yii::t('app', 'Type'), + 'token' => Yii::t('app', 'Token'), + 'expires_at' => Yii::t('app', 'Expires At'), + 'created_at' => Yii::t('app', 'Created At'), + ]; + } + + /** + * Gets query for [[Player]]. + * + * @return \yii\db\ActiveQuery|yii\db\ActiveQuery + */ + public function getPlayer() + { + return $this->hasOne(Player::class, ['id' => 'player_id']); + } + + /** + * {@inheritdoc} + * @return PlayerTokenQuery the active query used by this AR class. + */ + public static function find() + { + return new PlayerTokenQuery(get_called_class()); + } + + public function getTypes() + { + return [ + 'API'=>'API', + 'password_reset'=>'Password Reset', + 'email_verification'=>'Email Verification' + ]; + } +} diff --git a/frontend/models/PlayerTokenQuery.php b/frontend/models/PlayerTokenQuery.php new file mode 100644 index 000000000..c205a791a --- /dev/null +++ b/frontend/models/PlayerTokenQuery.php @@ -0,0 +1,34 @@ +andWhere('[[status]]=1'); + }*/ + + /** + * {@inheritdoc} + * @return PlayerToken[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * {@inheritdoc} + * @return PlayerToken|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} diff --git a/frontend/themes/material/profile/_card.php b/frontend/themes/material/profile/_card.php index 1955e465e..ea3e09e62 100644 --- a/frontend/themes/material/profile/_card.php +++ b/frontend/themes/material/profile/_card.php @@ -1,4 +1,5 @@ 'post', ],]], */ -if(\Yii::$app->user->identity->sSL && $profile->isMine) - $profile_actions=$profile->vpnItems; -$profile_actions['badge']=['encode'=>false, 'label'=>"  Your badge URL", 'url'=>Url::to(['profile/badge','id'=>$profile->id],true), 'linkOptions'=>['class'=>'copy-to-clipboard','swal-data'=>'Copied to clipboard!']]; -$profile_actions['edit']= ['encode'=>false, 'label'=>"  Edit your profile settings", 'url'=>['profile/settings'], 'linkOptions'=>['alt'=>'Edit profile and account settings']]; -$profile_actions['profileurl']=['encode'=>false, 'label'=>'  Your profile URL','url'=>Url::to(['profile/index', 'id'=>$profile->id], 'https'),'linkOptions'=>['class'=>'copy-to-clipboard','swal-data'=>'Copied to clipboard!']]; -$profile_actions['inviteurl']=['encode'=>false, 'label'=>'  Your invite URL','url'=>Url::to(['profile/invite', 'id'=>$profile->id],true),'linkOptions'=>['class'=>'copy-to-clipboard','swal-data'=>'Copied to clipboard!']]; +if (\Yii::$app->user->identity->sSL && $profile->isMine) + $profile_actions = $profile->vpnItems; -if(\Yii::$app->user->identity->sSL && (time()-strtotime(\Yii::$app->user->identity->sSL->ts))>=300) - $profile_actions['revoke']=['encode'=>false, 'label'=>"  Regenerate VPN Keys (revoke)", 'url'=>Url::to(['profile/revoke'],true), 'linkOptions'=>['class'=>'text-danger','data-swType'=>'question','data'=>['confirm'=>'You are about to revoke your old keys and generate a new pair!','method'=>'POST']]]; +$profile_actions['badge'] = ['encode' => false, 'label' => "  Your badge URL", 'url' => Url::to(['profile/badge', 'id' => $profile->id], true), 'linkOptions' => ['class' => 'copy-to-clipboard', 'swal-data' => 'Copied to clipboard!']]; +$profile_actions['edit'] = ['encode' => false, 'label' => "  Edit your profile settings", 'url' => ['profile/settings'], 'linkOptions' => ['alt' => 'Edit profile and account settings']]; +$profile_actions['profileurl'] = ['encode' => false, 'label' => '  Your profile URL', 'url' => Url::to(['profile/index', 'id' => $profile->id], 'https'), 'linkOptions' => ['class' => 'copy-to-clipboard', 'swal-data' => 'Copied to clipboard!']]; +$profile_actions['inviteurl'] = ['encode' => false, 'label' => '  Your invite URL', 'url' => Url::to(['profile/invite', 'id' => $profile->id], true), 'linkOptions' => ['class' => 'copy-to-clipboard', 'swal-data' => 'Copied to clipboard!']]; -if(Yii::$app->user->identity->onVPN && Yii::$app->user->identity->disconnectQueue===null) - $profile_actions['disconnect']=['encode'=>false, 'label'=>"  Disconnect your VPN", 'url'=>Url::to(['profile/disconnect'],true), 'linkOptions'=>['class'=>'text-danger','data-swType'=>'question','data'=>['confirm'=>'You are about to disconnect your current VPN connection! You will receive another notification once the process is completed!','method'=>'POST']]]; +if (\Yii::$app->sys->api_bearer_enable === true) { + if (\Yii::$app->user->identity->apiToken === null) { + $profile_actions['generate-token'] = ['encode' => false, 'label' => "  Generate API Token", 'url' => Url::to(['profile/generate-token'], true), 'linkOptions' => ['class' => 'text-danger', 'data-swType' => 'question', 'data' => ['confirm' => 'You are about to generate a new API token! Are you sure?', 'method' => 'POST']]]; + } else { + $profile_actions['copy-token'] = ['encode' => false, 'label' => "  Copy API Token", 'url' => $profile->owner->apiToken->token, 'linkOptions' => ['class' => 'copy-to-clipboard', 'swal-data' => 'API token copied to clipboard!']]; + } +} +if (\Yii::$app->user->identity->sSL && (time() - strtotime(\Yii::$app->user->identity->sSL->ts)) >= 300) + $profile_actions['revoke'] = ['encode' => false, 'label' => "  Regenerate VPN Keys (revoke)", 'url' => Url::to(['profile/revoke'], true), 'linkOptions' => ['class' => 'text-danger', 'data-swType' => 'question', 'data' => ['confirm' => 'You are about to revoke your old keys and generate a new pair!', 'method' => 'POST']]]; -$profile_actions['delete']=['encode'=>false, 'label'=>"  Delete your account", 'url'=>Url::to(['profile/delete'],true), 'linkOptions'=>['class'=>'text-danger','data-swType'=>'error','data'=>['confirm'=>'You are about to delete your account! This is irreversible and will cause you loss of all your progress.','method'=>'POST']]]; +if (Yii::$app->user->identity->onVPN && Yii::$app->user->identity->disconnectQueue === null) + $profile_actions['disconnect'] = ['encode' => false, 'label' => "  Disconnect your VPN", 'url' => Url::to(['profile/disconnect'], true), 'linkOptions' => ['class' => 'text-danger', 'data-swType' => 'question', 'data' => ['confirm' => 'You are about to disconnect your current VPN connection! You will receive another notification once the process is completed!', 'method' => 'POST']]]; +$profile_actions['delete'] = ['encode' => false, 'label' => "  Delete your account", 'url' => Url::to(['profile/delete'], true), 'linkOptions' => ['class' => 'text-danger', 'data-swType' => 'error', 'data' => ['confirm' => 'You are about to delete your account! This is irreversible and will cause you loss of all your progress.', 'method' => 'POST']]]; -if(array_key_exists('subscription',Yii::$app->modules)!==false) -{ - $subscription=Yii::$app->getModule('subscription'); -} -else { - $subscription=new \app\models\DummySubscription; + +if (array_key_exists('subscription', Yii::$app->modules) !== false) { + $subscription = Yii::$app->getModule('subscription'); +} else { + $subscription = new \app\models\DummySubscription; } ?>
- isMine):?>user->identity->isVip && Yii::$app->sys->all_players_vip==false):?>VIP -
Level experience->id?> / experience->name?>
-

owner->username)?>

+ isMine): ?>user->identity->isVip && Yii::$app->sys->all_players_vip == false): ?>VIP +
Level experience->id ?> / experience->name ?>
+

owner->username) ?>

- bio)?>isMine):?>  - sprintf('Checkout my profile at %s! %s', Html::encode(\Yii::$app->sys->{"event_name"}), $profile->braggingRights), - 'url'=>Url::to(['profile/index', 'id'=>$profile->id], 'https'), - 'linkOptions'=>['class'=>'profile-tweet', 'target'=>'_blank', 'style'=>'font-size: 1.3em;', 'rel'=>'noopener noreferrer nofollow'], - ]);?> - - sprintf('Checkout the profile of %s at %s', $profile->twitterHandle, Html::encode(\Yii::$app->sys->{"event_name"})), - 'linkOptions'=>['class'=>'profile-tweet', 'target'=>'_blank', 'style'=>'font-size: 1.3em;','rel'=>'noopener noreferrer nofollow'], - ]);?> - + bio) ?>isMine): ?>  + sprintf('Checkout my profile at %s! %s', Html::encode(\Yii::$app->sys->{"event_name"}), $profile->braggingRights), + 'url' => Url::to(['profile/index', 'id' => $profile->id], 'https'), + 'linkOptions' => ['class' => 'profile-tweet', 'target' => '_blank', 'style' => 'font-size: 1.3em;', 'rel' => 'noopener noreferrer nofollow'], + ]); ?> + + sprintf('Checkout the profile of %s at %s', $profile->twitterHandle, Html::encode(\Yii::$app->sys->{"event_name"})), + 'linkOptions' => ['class' => 'profile-tweet', 'target' => '_blank', 'style' => 'font-size: 1.3em;', 'rel' => 'noopener noreferrer nofollow'], + ]); ?> +

- exists && \Yii::$app->sys->all_players_vip!==true):?> - isActive):?> -
product ? $subscription->product->name: "subscription"?> expires in expires?>
- 'btn btn-block btn-info font-weight-bold']);?> - -

Your product ? $subscription->product->name: "subscription"?> has expired
'btn btn-warning text-dark text-bold col-md-12']);?>

- - - isMine || (!Yii::$app->user->isGuest && Yii::$app->user->identity->isAdmin)):?> - '  Profile Actions', - 'containerOptions'=>['class'=>'row d-flex'], - 'options'=>['class'=>'btn-primary text-bold btn-block','style'=>'color: black'], - 'encodeLabel'=>false, - 'dropdown' => [ - 'items' => $profile_actions, - ], - ]); - ?> - + exists && \Yii::$app->sys->all_players_vip !== true): ?> + isActive): ?> +
product ? $subscription->product->name : "subscription" ?> expires in expires ?>
+ 'btn btn-block btn-info font-weight-bold']); ?> + +

Your product ? $subscription->product->name : "subscription" ?> has expired
'btn btn-warning text-dark text-bold col-md-12']); ?>

+ + + isMine || (!Yii::$app->user->isGuest && Yii::$app->user->identity->isAdmin)): ?> + '  Profile Actions', + 'containerOptions' => ['class' => 'row d-flex'], + 'options' => ['class' => 'btn-primary text-bold btn-block', 'style' => 'color: black'], + 'encodeLabel' => false, + 'dropdown' => [ + 'items' => $profile_actions, + ], + ]); + ?> + -
- -owner->networks && $profile->isMine):?> -
+ +
+ + owner->networks && $profile->isMine): ?> +
- +
-
+ \ No newline at end of file From d839b8064ebfb07a22d5f4fc097b757781bb673f Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:57:36 +0200 Subject: [PATCH 09/38] add api operations --- frontend/models/Player.php | 4 +- .../modules/api/actions/ShutRestAction.php | 60 ++++++ .../modules/api/actions/SpawnRestAction.php | 101 ++++++++++ .../modules/api/actions/SpinRestAction.php | 68 +++++++ .../api/controllers/HeadshotController.php | 112 +++++------ .../api/controllers/TargetController.php | 184 ++++++++++++++++++ frontend/modules/api/models/Target.php | 49 +++++ .../modules/api/models/TargetInstance.php | 34 ++++ .../api/models/TargetInstanceQuery.php | 34 ++++ 9 files changed, 584 insertions(+), 62 deletions(-) create mode 100644 frontend/modules/api/actions/ShutRestAction.php create mode 100644 frontend/modules/api/actions/SpawnRestAction.php create mode 100644 frontend/modules/api/actions/SpinRestAction.php create mode 100644 frontend/modules/api/controllers/TargetController.php create mode 100644 frontend/modules/api/models/Target.php create mode 100644 frontend/modules/api/models/TargetInstance.php create mode 100644 frontend/modules/api/models/TargetInstanceQuery.php diff --git a/frontend/models/Player.php b/frontend/models/Player.php index ef13f02e7..3788ef076 100644 --- a/frontend/models/Player.php +++ b/frontend/models/Player.php @@ -81,9 +81,11 @@ public static function findIdentity($id) */ public static function findIdentityByAccessToken($token, $type = null) { - throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.'); + if(($model=PlayerToken::findOne(['token' => $token,'type'=>'API']))!==null) + return static::findOne($model->player_id); } + /** * Finds player by username * diff --git a/frontend/modules/api/actions/ShutRestAction.php b/frontend/modules/api/actions/ShutRestAction.php new file mode 100644 index 000000000..43a555bba --- /dev/null +++ b/frontend/modules/api/actions/ShutRestAction.php @@ -0,0 +1,60 @@ +response->format=\yii\web\Response::FORMAT_JSON; + \Yii::$app->response->statusCode = 200; + try + { + $target=$this->findTarget($id); + if($target->status!=='online') + { + \Yii::$app->response->statusCode = 222; + throw new UserException(\Yii::t('app',"Target is not online yet!")); + } + $ti=TargetInstance::findOne(['player_id'=>Yii::$app->user->id,'target_id'=>$id]); + // Check if the instance exists + if($ti!==null && $ti->reboot!==2) + { + $ti->reboot=2; + $ti->save(); + \Yii::$app->response->statusCode = 201; + } + else + { + \Yii::$app->response->statusCode = 422; + } + + } + catch(\Exception $e) + { + } + + return []; + } + + private function findTarget($id) + { + if(($model=Target::findOne($id))!==NULL) + { + return $model; + } + + throw new NotFoundHttpException(\Yii::t('app','The requested target does not exist.')); + + } + +} diff --git a/frontend/modules/api/actions/SpawnRestAction.php b/frontend/modules/api/actions/SpawnRestAction.php new file mode 100644 index 000000000..a8430874a --- /dev/null +++ b/frontend/modules/api/actions/SpawnRestAction.php @@ -0,0 +1,101 @@ +response->format=\yii\web\Response::FORMAT_JSON; + \Yii::$app->response->statusCode = 200; + + try + { + \Yii::$app->response->statusCode = 201; + $target=$this->findTarget($id); + if($this->actionAllowedFor($target->id)) + { + \Yii::$app->response->statusCode = 403; + throw new UserException(\Yii::t('app','Target not allowed to spawn private instances!')); + } + if(!$this->actionAllowedBy()) + { + \Yii::$app->response->statusCode = 403; + throw new UserException(\Yii::t('app','Player not allowed to spawn private instances!')); + } + + if($target->status!=='online') + { + \Yii::$app->response->statusCode = 422; + throw new UserException(\Yii::t('app','Target did not start, target is not online yet!')); + } + + $ti=TargetInstance::findOne(Yii::$app->user->id); + // Check if user has already a started instance + if($ti!==null) + { + if($ti->target_id!=$id) + { + \Yii::$app->response->statusCode = 201; + $ti->reboot=2; + $ti->save(); + } + else + { + \Yii::$app->response->statusCode = 200; + } + + return []; + } + + $ti=new TargetInstance; + $ti->player_id=Yii::$app->user->id; + $ti->target_id=$id; + // pick the least used server currently + $ti->server_id=intval(Yii::$app->db->createCommand('select id from server t1 left join target_instance t2 on t1.id=t2.server_id group by t1.id order by count(t2.server_id) limit 1')->queryScalar()); + if(!$ti->save()) + { + \Yii::$app->response->statusCode = 422; + throw new UserException(\Yii::t('app','Failed to spawn new target instance for you.')); + } + } + catch(\Exception $e) + { + } + + return []; + } + + protected function actionAllowedFor($id) + { + $model=$this->findTarget($id); + $action=sprintf('/target/%d/spawn',$model->id); + return $model->instance_allowed && Yii::$app->DisabledRoute->disabled($action); + } + + protected function actionAllowedBy() + { + return (Yii::$app->user->identity->isVip===true || Yii::$app->sys->all_players_vip===true); + } + + private function findTarget($id) + { + if(($model=Target::findOne($id))!==NULL) + { + return $model; + } + + throw new NotFoundHttpException(\Yii::t('app','The requested target does not exist.')); + + } +} diff --git a/frontend/modules/api/actions/SpinRestAction.php b/frontend/modules/api/actions/SpinRestAction.php new file mode 100644 index 000000000..2b671a582 --- /dev/null +++ b/frontend/modules/api/actions/SpinRestAction.php @@ -0,0 +1,68 @@ + 'yii\rest\Serializer', + 'collectionEnvelope' => 'items', + ]; + + + public function run($id) + { + + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + \Yii::$app->response->statusCode = 200; + try { + $target = $this->findModelProgress($id); + $module = \Yii::$app->getModule('target'); + + $module->checkNetwork($target); + + if (Yii::$app->user->identity->instance !== NULL && Yii::$app->user->identity->instance->target_id === $target->id) { + Yii::$app->user->identity->instance->updateAttributes(['reboot' => 1]); + \Yii::$app->response->statusCode = 201; + return []; + } + + $this->checkSpinable($target); + + $playerSpin = Yii::$app->user->identity->profile->spins; + $SQ = new \app\modules\target\models\SpinQueue; + $SQ->player_id = (int) \Yii::$app->user->id; + $SQ->target_id = $target->id; + $playerSpin->counter = intval($playerSpin->counter) + 1; + $playerSpin->total = intval($playerSpin->total) + 1; + if ($SQ->save() !== false && $playerSpin->save() !== false) { + \Yii::$app->response->statusCode = 201; + } else + throw new NotFoundHttpException(\Yii::t('app', 'Failed to queue target for restart.')); + } catch (\Exception $e) { + \Yii::$app->response->statusCode = 422; + } + return []; + } + + protected function findModelProgress($id) + { + if (($model = Target::find()->player_progress(\Yii::$app->user->id)->where(['t.id' => $id])->one()) !== null) { + return $model; + } + throw new NotFoundHttpException(\Yii::t('app', 'The requested target does not exist.')); + } + + protected function checkSpinable($target) + { + if ($target->spinable !== true) + throw new NotFoundHttpException(\Yii::t('app', 'Not allowed to spin target. Target cannot not be spined.')); + } +} diff --git a/frontend/modules/api/controllers/HeadshotController.php b/frontend/modules/api/controllers/HeadshotController.php index eba1be3fc..0984c7c0b 100644 --- a/frontend/modules/api/controllers/HeadshotController.php +++ b/frontend/modules/api/controllers/HeadshotController.php @@ -6,53 +6,42 @@ use yii\helpers\ArrayHelper; use yii\data\ActiveDataProvider; use yii\data\ActiveDataFilter; + class HeadshotController extends \yii\rest\ActiveController { - public $modelClass="\app\modules\api\models\Headshot"; - public $serializer=[ - 'class' => 'yii\rest\Serializer', - 'collectionEnvelope' => 'items', - ]; -// public function behaviors() -// { -// return ArrayHelper::merge(parent::behaviors(), [ -// [ -// 'class' => 'yii\filters\ContentNegotiator', -// 'formats' => [ -// 'application/json' => \yii\web\Response::FORMAT_JSON, -// 'application/xml' => \yii\web\Response::FORMAT_XML, -// ], -// ], -// ]); -// } + public $modelClass = "\app\modules\api\models\Headshot"; + public $serializer = [ + 'class' => 'yii\rest\Serializer', + 'collectionEnvelope' => 'items', + ]; public function actions() { - $actions = parent::actions(); + $actions = parent::actions(); - // disable the "delete", "create", "view","update" actions - unset($actions['delete'], $actions['create'],$actions['view'],$actions['update']); - $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider']; + // disable the "delete", "create", "view","update" actions + unset($actions['delete'], $actions['create'], $actions['view'], $actions['update']); + $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider']; - return $actions; + return $actions; } public function prepareDataProvider() { - \Yii::$app->response->format=\yii\web\Response::FORMAT_JSON; + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; $requestParams = Yii::$app->getRequest()->getBodyParams(); if (empty($requestParams)) { - $requestParams = Yii::$app->getRequest()->getQueryParams(); + $requestParams = Yii::$app->getRequest()->getQueryParams(); } $filter = new ActiveDataFilter([ - 'searchModel' => '\app\modules\api\models\HeadshotSearch', - 'attributeMap' => [ - 'profile_id' => 'profile.id', - 'target_name' => 't.name', - 'created_at' => 'headshot.created_at', - 'timer' => 'headshot.timer' - ] + 'searchModel' => '\app\modules\api\models\HeadshotSearch', + 'attributeMap' => [ + 'profile_id' => 'profile.id', + 'target_name' => 't.name', + 'created_at' => 'headshot.created_at', + 'timer' => 'headshot.timer' + ] ]); $filterCondition = null; @@ -61,47 +50,48 @@ public function prepareDataProvider() // if you prefer JSON in request body, // use Yii::$app->request->getBodyParams() below: if ($filter->load(\Yii::$app->request->get())) { - $filterCondition = $filter->build(); - if ($filterCondition === false) { - // Serializer would get errors out of it - return $filter; - } + $filterCondition = $filter->build(); + if ($filterCondition === false) { + // Serializer would get errors out of it + return $filter; + } } - $query=\app\modules\api\models\Headshot::find()->rest(); + $query = \app\modules\api\models\Headshot::find()->rest(); if ($filterCondition !== null) { - $query->andWhere($filterCondition); + $query->andWhere($filterCondition); } - $dataProvider=new ActiveDataProvider([ - 'query'=>$query, + $dataProvider = new ActiveDataProvider([ + 'query' => $query, 'pagination' => [ - 'pageSizeLimit' => [1,100], - 'defaultPageSize'=>10, - 'params' => $requestParams, + 'pageSizeLimit' => [1, 100], + 'defaultPageSize' => 10, + 'params' => $requestParams, ], 'sort' => [ 'params' => $requestParams, ], ]); + $dataProvider->setSort([ - 'defaultOrder' => ['created_at' => SORT_DESC], - 'attributes' => array_merge( - $dataProvider->getSort()->attributes, - [ - 'profile_id' => [ - 'asc' => ['profile.id' => SORT_ASC], - 'desc' => ['profile.id' => SORT_DESC], - ], - 'created_at' => [ - 'asc' => ['headshot.created_at' => SORT_ASC], - 'desc' => ['headshot.created_at' => SORT_DESC], - ], - 'target_name' => [ - 'asc' => ['target.name' => SORT_ASC], - 'desc' => ['target.name' => SORT_DESC], - ], - ] - ), + 'defaultOrder' => ['created_at' => SORT_DESC], + 'attributes' => array_merge( + $dataProvider->getSort()->attributes, + [ + 'profile_id' => [ + 'asc' => ['profile.id' => SORT_ASC], + 'desc' => ['profile.id' => SORT_DESC], + ], + 'created_at' => [ + 'asc' => ['headshot.created_at' => SORT_ASC], + 'desc' => ['headshot.created_at' => SORT_DESC], + ], + 'target_name' => [ + 'asc' => ['target.name' => SORT_ASC], + 'desc' => ['target.name' => SORT_DESC], + ], + ] + ), ]); return $dataProvider; } diff --git a/frontend/modules/api/controllers/TargetController.php b/frontend/modules/api/controllers/TargetController.php new file mode 100644 index 000000000..84081b460 --- /dev/null +++ b/frontend/modules/api/controllers/TargetController.php @@ -0,0 +1,184 @@ + 'yii\rest\Serializer', + 'collectionEnvelope' => 'items', + ]; + public function behaviors() + { + \Yii::$app->user->enableSession = false; + \Yii::$app->user->loginUrl = null; + + return ArrayHelper::merge(parent::behaviors(), [ + 'authenticator' => [ + 'authMethods' => [ + HttpBearerAuth::class, + ], + ], + 'content' => [ + 'class' => yii\filters\ContentNegotiator::class, + 'formats' => [ + 'application/json' => \yii\web\Response::FORMAT_JSON, + ], + ], + 'access' => [ + 'class' => AccessControl::class, + 'rules' => [ + [ //api_bearer_disable + 'allow' => false, + 'matchCallback' => function () { + return \Yii::$app->sys->api_bearer_enable !== true; + } + ], + [ + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + ]); + } + + public function actions() + { + $actions = parent::actions(); + // disable the "delete", "create", "view","update" actions + unset($actions['delete'], $actions['create'], $actions['update'], $actions['index']); + //$actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider']; + $actions['spin']['class'] = 'app\modules\api\actions\SpinRestAction'; + $actions['spawn']['class'] = 'app\modules\api\actions\SpawnRestAction'; + $actions['shut']['class'] = 'app\modules\api\actions\ShutRestAction'; + + return $actions; + } + public function actionInstances() + { + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + $response = Yii::$app->getResponse(); + if (\Yii::$app->cache->memcache->get("api_target_instances:" . \Yii::$app->user->id) !== false) { + $response->statusCode = 429; + return []; + } + + $teamInstances = \app\modules\api\models\TargetInstance::find()->rest()->where(['target_instance.player_id'=>Yii::$app->user->identity->id]); + + if ((Yii::$app->sys->teams || Yii::$app->sys->team_visible_instances) && Yii::$app->user->identity->teamPlayer) { + $TP = TeamPlayer::find()->where(['team_id' => Yii::$app->user->identity->teamPlayer->team_id])->orderBy(['approved' => SORT_DESC, 'ts' => SORT_ASC]); + $teamPlayers = ArrayHelper::getColumn($TP->all(), 'player_id'); + $teamInstances->leftJoin('team_player', 'target_instance.player_id=team_player.player_id'); + if (\Yii::$app->sys->team_visible_instances === true) { + $teamInstances->orWhere(['in', 'target_instance.player_id', $teamPlayers]); + + } + else + $teamInstances->orWhere(['team_allowed' => 1,'target_instance.player_id'=>$teamPlayers]); + } + + $dataProvider = new ActiveDataProvider([ + 'query' => $teamInstances, + 'pagination' => false, + ]); + \Yii::$app->cache->memcache->set("api_target_instances:" . \Yii::$app->user->id, time(), intval(\Yii::$app->sys->api_target_instances_timeout) + 1); + + return $dataProvider; + } + + // Do Claim operation + public function actionClaim() + { + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + $response = Yii::$app->getResponse(); + if (\Yii::$app->cache->memcache->get("api_claim:" . \Yii::$app->user->id) !== false) { + $response->statusCode = 429; + return []; + } + \Yii::$app->cache->memcache->set("api_claim:" . \Yii::$app->user->id, time(), intval(\Yii::$app->sys->api_claim_timeout) + 1); + $string = Yii::$app->getRequest()->getBodyParam('hash'); + if (trim($string) === "" || $string === null) { + $response->statusCode = 422; + return [Yii::t('app', 'Need to provide hash value.')]; + } + + $treasure = Treasure::find()->claimable()->byCode($string)->one(); + if ($treasure !== null && Treasure::find()->byCode($string)->claimable()->notBy((int) Yii::$app->user->id)->one() === null) { + $response->statusCode = 422; + return [\Yii::t('app', 'Flag claimed before')]; + } elseif ($treasure === null) { + Yii::$app->counters->increment('failed_claims'); + $response->statusCode = 422; + return [\Yii::t('app', 'Flag does not exist!')]; + } + + $player_progress = TPS::findOne(['id' => $treasure->target_id, 'player_id' => Yii::$app->user->id]); + if ((Yii::$app->sys->force_findings_to_claim || $treasure->target->require_findings) && $player_progress === null && intval($treasure->target->getFindings()->count()) > 0) { + Yii::$app->counters->increment('claim_no_finding'); + $response->statusCode = 422; + return [\Yii::t('app', 'You need to discover at least one service before claiming a flag for this system.')]; + } + + try { + $module = \Yii::$app->getModule('target'); + $module->checkNetwork($treasure->target); + } catch (\Throwable $e) { + \Yii::$app->response->statusCode = 422; + return [\Yii::t('app', "You cannot claim this flag. You don't have access to this network.")]; + } + + Yii::$app->counters->increment('claims'); + $this->doClaim($treasure); + \Yii::$app->response->statusCode = 201; + return ['OK']; + } + + protected function doClaim($treasure) + { + $connection = Yii::$app->db; + $transaction = $connection->beginTransaction(); + try { + $PT = new PlayerTreasure(); + $PT->player_id = (int) Yii::$app->user->id; + $PT->treasure_id = $treasure->id; + $PT->save(); + if ($treasure->appears !== -1) { + $treasure->updateAttributes(['appears' => intval($treasure->appears) - 1]); + } + $transaction->commit(); + $this->doOndemand($treasure->target); + $PT->refresh(); + Yii::$app->session->setFlash('success', sprintf(\Yii::t('app', 'Flag [%s] claimed for %s points'), $treasure->name, number_format($PT->points))); + } catch (\Exception $e) { + $transaction->rollBack(); + Yii::$app->session->setFlash('error', \Yii::t('app', 'Flag failed')); + throw $e; + } catch (\Throwable $e) { + $transaction->rollBack(); + throw $e; + } + } + + protected function doOndemand($target) + { + if ($target->ondemand && $target->ondemand->state > 0) { + $target->ondemand->updateAttributes(['heartbeat' => new \yii\db\Expression('NOW()')]); + } + } +} diff --git a/frontend/modules/api/models/Target.php b/frontend/modules/api/models/Target.php new file mode 100644 index 000000000..aa48d3199 --- /dev/null +++ b/frontend/modules/api/models/Target.php @@ -0,0 +1,49 @@ +total_findings = count($this->findings); + $this->total_treasures = count($this->treasures); + if (Yii::$app->user->identity->instance) + $this->ip = long2ip(Yii::$app->user->identity->instance->ip); + else if($this->ondemand && $this->ondemand->state==1) + { + $this->ip = long2ip($this->ip); + } + else + $this->ip=0; + } +} diff --git a/frontend/modules/api/models/TargetInstance.php b/frontend/modules/api/models/TargetInstance.php new file mode 100644 index 000000000..9a9b56bd3 --- /dev/null +++ b/frontend/modules/api/models/TargetInstance.php @@ -0,0 +1,34 @@ +select(['target_id', 'target_instance.ip', new \yii\db\Expression('INET_NTOA(target_instance.ip) as ipstr'), 't.name as hostname', 't.fqdn', 'p.username as owner'])->joinWith(['target t', 'player p']); + } + + /** + * {@inheritdoc} + * @return Headshot[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * {@inheritdoc} + * @return Headshot|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} From 92ccff030df2150f9cdf1ddfc26056eb8a8c91e3 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:57:52 +0200 Subject: [PATCH 10/38] display the url --- .../themes/material/modules/team/views/default/view.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/themes/material/modules/team/views/default/view.php b/frontend/themes/material/modules/team/views/default/view.php index 74f5444d6..f977524bc 100644 --- a/frontend/themes/material/modules/team/views/default/view.php +++ b/frontend/themes/material/modules/team/views/default/view.php @@ -19,14 +19,14 @@

[name) ?>]

getTeamPlayers()->count() < Yii::$app->sys->members_per_team): ?>

- invite && !$team->inviteonly): ?> + owner_id === Yii::$app->user->id || ($team->invite && !$team->inviteonly)): ?> owner_id === Yii::$app->user->id) $class .= ' copy-to-clipboard'; ?> $team->invite->token], 'https'), Url::to(['/team/default/invite', 'token' => $team->invite->token], 'https'), ['class' => $class, 'swal-data' => 'Copied to clipboard!']); ?> recruitment) ?> - user->identity->isAdmin || ($team->owner_id === Yii::$app->user->id /*&& \Yii::$app->cache->memcache->get('team_renewed:' . $team->id) === false*/)): ?> + owner_id === Yii::$app->user->id && \Yii::$app->cache->memcache->get('team_renewed:' . $team->id) === false)): ?> ', Url::to(['renew', 'token' => $team->token]), ['data-method' => 'POST', 'title' => 'Regenerate invite URL', 'rel' => "tooltip",]) ?>

@@ -106,7 +106,7 @@ 'template' => '{approve} {reject}', 'visibleButtons' => [ 'approve' => function ($model) { - if ($model->approved === 0 && Yii::$app->user->identity->teamLeader !== null) + if ($model->approved === 0 && Yii::$app->user->identity->teamLeader !== null && Yii::$app->user->identity->teamLeader->id === $model->team_id) return true; return false; }, From c58491719f927c96a6a4852f50b76a08248ceddf Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:58:22 +0200 Subject: [PATCH 11/38] dont allow spawn if not VIP --- frontend/modules/target/actions/SpawnRestAction.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/modules/target/actions/SpawnRestAction.php b/frontend/modules/target/actions/SpawnRestAction.php index f316008c4..1df4621f5 100644 --- a/frontend/modules/target/actions/SpawnRestAction.php +++ b/frontend/modules/target/actions/SpawnRestAction.php @@ -27,6 +27,10 @@ public function run($id) { throw new UserException(\Yii::t('app','Target not allowed to spawn private instances!')); } + if(!$this->actionAllowedBy()) + { + throw new UserException(\Yii::t('app','You are not allowed to spawn private instances!')); + } if($target->status!=='online') { @@ -76,6 +80,11 @@ protected function actionAllowedFor($id) return $model->instance_allowed && Yii::$app->DisabledRoute->disabled($action); } + protected function actionAllowedBy() + { + return (Yii::$app->user->identity->isVip===true || Yii::$app->sys->all_players_vip===true); + } + private function findTarget($id) { if(($model=Target::findOne($id))!==NULL) From 2a3ddbcf7dd20e3c3e423a772cbc1625cbfb4ae2 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 11:58:38 +0200 Subject: [PATCH 12/38] add some more typecasting --- frontend/modules/target/models/TargetAR.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/modules/target/models/TargetAR.php b/frontend/modules/target/models/TargetAR.php index 58deda8b2..def7c8e1d 100644 --- a/frontend/modules/target/models/TargetAR.php +++ b/frontend/modules/target/models/TargetAR.php @@ -51,7 +51,7 @@ * @property TargetVolume[] $targetVolumes * @property Treasure[] $treasures * @property Headshot[] $headshots - * @property Ondemand[] $ondemand + * @property Ondemand $ondemand * @property Writeup[] $writeups */ class TargetAR extends \app\models\ActiveRecordReadOnly @@ -103,6 +103,8 @@ public function behaviors() 'average_rating' => AttributeTypecastBehavior::TYPE_INTEGER, 'ip' => AttributeTypecastBehavior::TYPE_INTEGER, 'active' => AttributeTypecastBehavior::TYPE_BOOLEAN, + 'rootable' => AttributeTypecastBehavior::TYPE_BOOLEAN, + 'healthcheck' => AttributeTypecastBehavior::TYPE_BOOLEAN, 'writeup_allowed'=> AttributeTypecastBehavior::TYPE_BOOLEAN, 'timer'=> AttributeTypecastBehavior::TYPE_BOOLEAN, 'player_spin'=> AttributeTypecastBehavior::TYPE_BOOLEAN, From 3be2f8c58dff5dcc59fc53aba0a6e2b545b33586 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 12:19:51 +0200 Subject: [PATCH 13/38] remove url route that was never used --- .../migrations-init/m241103_021658_target_api_url_routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/migrations-init/m241103_021658_target_api_url_routes.php b/backend/migrations-init/m241103_021658_target_api_url_routes.php index a22475004..20ea20fd9 100644 --- a/backend/migrations-init/m241103_021658_target_api_url_routes.php +++ b/backend/migrations-init/m241103_021658_target_api_url_routes.php @@ -13,7 +13,7 @@ class m241103_021658_target_api_url_routes extends Migration public function safeUp() { $this->upsert('url_route',['source'=>'profile/generate-token','destination'=>'profile/generate-token','weight'=>339]); - $this->upsert('url_route',['source'=>'api/targets','destination'=>'api/target/index','weight'=>642]); + //$this->upsert('url_route',['source'=>'api/targets','destination'=>'api/target/index','weight'=>642]); $this->upsert('url_route',['source'=>'api/target/claim','destination'=>'api/target/claim','weight'=>643]); $this->upsert('url_route',['source'=>'api/target/instances','destination'=>'api/target/instances','weight'=>643]); $this->upsert('url_route',['source'=>'api/target/','destination'=>'api/target/view','weight'=>644]); From 21c016886a837d9df6c2118e2010d66643c542fa Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 12:25:09 +0200 Subject: [PATCH 14/38] the archive is back online we can remove our mirror --- contrib/Dockerfile-mariadb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/Dockerfile-mariadb b/contrib/Dockerfile-mariadb index 32a3e566e..5b1d5295c 100644 --- a/contrib/Dockerfile-mariadb +++ b/contrib/Dockerfile-mariadb @@ -17,7 +17,7 @@ COPY contrib/findingsd.sql /docker-entrypoint-initdb.d/05.sql COPY contrib/entrypoint-mariadb.sh /usr/local/bin/docker-entrypoint.sh WORKDIR / RUN set -ex \ - && echo "deb http://ftp.cc.uoc.gr/mirrors/mariadb/mariadb-11.4.3/repo/ubuntu noble main main/debug" >>/etc/apt/sources.list.d/mariadb.list \ +# && echo "deb http://ftp.cc.uoc.gr/mirrors/mariadb/mariadb-11.4.3/repo/ubuntu noble main main/debug" >>/etc/apt/sources.list.d/mariadb.list \ && apt-get update \ && apt-get install -y build-essential gcc autoconf automake git zip unzip mcrypt memcached libmariadb-dev tini libmemcached-dev libtool \ && echo "[mysqld]\nevent_scheduler=on" >/etc/mysql/mariadb.conf.d/50-mysqld.cnf \ From 61650ad4099b5bd4cdfe0154dd7ac1bacedfb8df Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 12:30:15 +0200 Subject: [PATCH 15/38] update the API docs --- docs/API.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/API.md b/docs/API.md index a70061676..27c4d24f7 100644 --- a/docs/API.md +++ b/docs/API.md @@ -54,3 +54,73 @@ curl -i -H "Accept:application/json" "https://echoctf.red/api/headshots?filter[p ```sh curl -i -H "Accept:application/json" "https://echoctf.red/api/headshots?filter[profile_id]=31337&fields=target_name" ``` + +## Bearer Operations +For the following endpoints you will need to have a bearer token to be able to access them + +* `api/target/claim`: Submit a flag for validation +* `api/target/instances`: List of instances (if any) +* `api/target/`: Get details for a given target +* `api/target//spin`: spin a machine +* `api/target//spawn`: Spawn a private instance (if allowed) +* `api/target//shut`: Shutdown a private instance + +### Claim Flag +URL: `POST /api/target/claim` \ +POST: `{ "hash":"flag" }` + +```sh +curl "https://echoctf.red/api/target/claim" \ + -H "Authorization: Bearer YOURTOKEN" \ + -H "Accept:application/json" \ + -d '{"hash":"MyFlagHere"}' +``` + +### Get instances +URL: `GET /api/target/instances` + +Get a list of instances and depending on the platform setup may include team instances as well. +```sh +curl "https://echoctf.red/api/target/instances" \ + -H "Authorization: Bearer YOURTOKEN" \ + -H "Accept:application/json" +``` +### Get target details +URL: `GET /api/target/` +```sh +curl "https://echoctf.red/api/target/11" \ + -H "Authorization: Bearer YOURTOKEN" \ + -H "Accept:application/json" +``` + +### Spin a target +URL: `GET /api/target//spin` + +Perform a spin operation depending on the type and state of the machine. + * If machine is powered off then power up + * If machine is powered up then schedule a reset +```sh +curl "https://echoctf.red/api/target/11/spin" \ + -H "Authorization: Bearer YOURTOKEN" \ + -H "Accept:application/json" +``` + +### Spawn a private instance +URL: `GET /api/target//spawn` + +Spawn a private instance of a given machine (if player is allowed). +```sh +curl "https://echoctf.red/api/target/11/span" \ + -H "Authorization: Bearer YOURTOKEN" \ + -H "Accept:application/json" +``` + +### Shut a private instance +URL: `GET /api/target//shut` + +Shut a private instance of a given machine (if exists for the given player). +```sh +curl "https://echoctf.red/api/target/11/shut" \ + -H "Authorization: Bearer YOURTOKEN" \ + -H "Accept:application/json" +``` From b1b28ed48ee6db51a74797272579fdc5590535e5 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 13:19:39 +0200 Subject: [PATCH 16/38] add api messages and rate limit timeouts --- .../modules/api/actions/ShutRestAction.php | 14 +++++++++---- .../modules/api/actions/SpawnRestAction.php | 14 +++++++++++-- .../modules/api/actions/SpinRestAction.php | 11 ++++++++-- .../api/controllers/TargetController.php | 21 ++++++++----------- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/frontend/modules/api/actions/ShutRestAction.php b/frontend/modules/api/actions/ShutRestAction.php index 43a555bba..c9843b092 100644 --- a/frontend/modules/api/actions/ShutRestAction.php +++ b/frontend/modules/api/actions/ShutRestAction.php @@ -19,10 +19,16 @@ public function run($id) \Yii::$app->response->statusCode = 200; try { + if (\Yii::$app->cache->memcache->get("api_target_shut:" . \Yii::$app->user->id) !== false) { + \Yii::$app->response->statusCode = 429; + return ["message"=>\Yii::t('app',"Rate-limited, wait a few seconds and try again."),"code"=>0,"status"=>\Yii::$app->response->statusCode]; + } + \Yii::$app->cache->memcache->set("api_target_shut:" . \Yii::$app->user->id, time(), intval(\Yii::$app->sys->api_target_shut_timeout) + 1); + $target=$this->findTarget($id); if($target->status!=='online') { - \Yii::$app->response->statusCode = 222; + \Yii::$app->response->statusCode = 422; throw new UserException(\Yii::t('app',"Target is not online yet!")); } $ti=TargetInstance::findOne(['player_id'=>Yii::$app->user->id,'target_id'=>$id]); @@ -32,18 +38,18 @@ public function run($id) $ti->reboot=2; $ti->save(); \Yii::$app->response->statusCode = 201; + \Yii::$app->response->data=["message"=>\Yii::t('app',"Instance scheduled for shutdown."),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } else { \Yii::$app->response->statusCode = 422; + throw new UserException(\Yii::t('app',"No private instance found for target!")); } - } catch(\Exception $e) { + \Yii::$app->response->data=["message"=>$e->getMessage(),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } - - return []; } private function findTarget($id) diff --git a/frontend/modules/api/actions/SpawnRestAction.php b/frontend/modules/api/actions/SpawnRestAction.php index a8430874a..a0e01dfa8 100644 --- a/frontend/modules/api/actions/SpawnRestAction.php +++ b/frontend/modules/api/actions/SpawnRestAction.php @@ -23,6 +23,12 @@ public function run($id) { \Yii::$app->response->statusCode = 201; $target=$this->findTarget($id); + if (\Yii::$app->cache->memcache->get("api_target_spawn:" . \Yii::$app->user->id) !== false) { + \Yii::$app->response->statusCode = 429; + return ["message"=>\Yii::t('app',"Rate-limited, wait a few seconds and try again."),"code"=>0,"status"=>\Yii::$app->response->statusCode]; + } + \Yii::$app->cache->memcache->set("api_target_spawn:" . \Yii::$app->user->id, time(), intval(\Yii::$app->sys->api_target_spawn_timeout) + 1); + if($this->actionAllowedFor($target->id)) { \Yii::$app->response->statusCode = 403; @@ -49,13 +55,15 @@ public function run($id) \Yii::$app->response->statusCode = 201; $ti->reboot=2; $ti->save(); + \Yii::$app->response->data=["message"=>\Yii::t('app',"Scheduled existing instance to shutdown"),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } else { \Yii::$app->response->statusCode = 200; + \Yii::$app->response->data=["message"=>\Yii::t('app',"Instance already exists for this target."),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } - return []; + return \Yii::$app->response->data; } $ti=new TargetInstance; @@ -68,12 +76,14 @@ public function run($id) \Yii::$app->response->statusCode = 422; throw new UserException(\Yii::t('app','Failed to spawn new target instance for you.')); } + \Yii::$app->response->data=["message"=>\Yii::t('app',"Scheduled spawn of private target instance."),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } catch(\Exception $e) { + \Yii::$app->response->data=["message"=>$e->getMessage(),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } - return []; + //return []; } protected function actionAllowedFor($id) diff --git a/frontend/modules/api/actions/SpinRestAction.php b/frontend/modules/api/actions/SpinRestAction.php index 2b671a582..91f54d92d 100644 --- a/frontend/modules/api/actions/SpinRestAction.php +++ b/frontend/modules/api/actions/SpinRestAction.php @@ -23,6 +23,12 @@ public function run($id) \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; \Yii::$app->response->statusCode = 200; try { + if (\Yii::$app->cache->memcache->get("api_target_spin:" . \Yii::$app->user->id) !== false) { + \Yii::$app->response->statusCode = 429; + return ["message"=>\Yii::t('app',"Rate-limited, wait a few seconds and try again."),"code"=>0,"status"=>\Yii::$app->response->statusCode]; + } + \Yii::$app->cache->memcache->set("api_target_spin:" . \Yii::$app->user->id, time(), intval(\Yii::$app->sys->api_target_spin_timeout) + 1); + $target = $this->findModelProgress($id); $module = \Yii::$app->getModule('target'); @@ -31,7 +37,7 @@ public function run($id) if (Yii::$app->user->identity->instance !== NULL && Yii::$app->user->identity->instance->target_id === $target->id) { Yii::$app->user->identity->instance->updateAttributes(['reboot' => 1]); \Yii::$app->response->statusCode = 201; - return []; + return ["message"=>\Yii::t('app',"Instance scheduled for reboot"),"code"=>0,"status"=>\Yii::$app->response->statusCode];; } $this->checkSpinable($target); @@ -44,12 +50,13 @@ public function run($id) $playerSpin->total = intval($playerSpin->total) + 1; if ($SQ->save() !== false && $playerSpin->save() !== false) { \Yii::$app->response->statusCode = 201; + \Yii::$app->response->data=["message"=>\Yii::t('app',"Queued target for spin"),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } else throw new NotFoundHttpException(\Yii::t('app', 'Failed to queue target for restart.')); } catch (\Exception $e) { \Yii::$app->response->statusCode = 422; + \Yii::$app->response->data=["message"=>$e->getMessage(),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } - return []; } protected function findModelProgress($id) diff --git a/frontend/modules/api/controllers/TargetController.php b/frontend/modules/api/controllers/TargetController.php index 84081b460..b26e45f4f 100644 --- a/frontend/modules/api/controllers/TargetController.php +++ b/frontend/modules/api/controllers/TargetController.php @@ -76,7 +76,7 @@ public function actionInstances() $response = Yii::$app->getResponse(); if (\Yii::$app->cache->memcache->get("api_target_instances:" . \Yii::$app->user->id) !== false) { $response->statusCode = 429; - return []; + return ["message"=>\Yii::t('app',"Rate-limited, wait a few seconds and try again."),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } $teamInstances = \app\modules\api\models\TargetInstance::find()->rest()->where(['target_instance.player_id'=>Yii::$app->user->identity->id]); @@ -87,7 +87,6 @@ public function actionInstances() $teamInstances->leftJoin('team_player', 'target_instance.player_id=team_player.player_id'); if (\Yii::$app->sys->team_visible_instances === true) { $teamInstances->orWhere(['in', 'target_instance.player_id', $teamPlayers]); - } else $teamInstances->orWhere(['team_allowed' => 1,'target_instance.player_id'=>$teamPlayers]); @@ -109,44 +108,44 @@ public function actionClaim() $response = Yii::$app->getResponse(); if (\Yii::$app->cache->memcache->get("api_claim:" . \Yii::$app->user->id) !== false) { $response->statusCode = 429; - return []; + return ["message"=>\Yii::t('app',"Rate-limited, wait a few seconds and try again."),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } \Yii::$app->cache->memcache->set("api_claim:" . \Yii::$app->user->id, time(), intval(\Yii::$app->sys->api_claim_timeout) + 1); $string = Yii::$app->getRequest()->getBodyParam('hash'); if (trim($string) === "" || $string === null) { $response->statusCode = 422; - return [Yii::t('app', 'Need to provide hash value.')]; + return ["message"=>Yii::t('app', 'Need to provide hash value.'),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } $treasure = Treasure::find()->claimable()->byCode($string)->one(); if ($treasure !== null && Treasure::find()->byCode($string)->claimable()->notBy((int) Yii::$app->user->id)->one() === null) { $response->statusCode = 422; - return [\Yii::t('app', 'Flag claimed before')]; + return ["message"=>\Yii::t('app', 'Flag claimed before'),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } elseif ($treasure === null) { Yii::$app->counters->increment('failed_claims'); $response->statusCode = 422; - return [\Yii::t('app', 'Flag does not exist!')]; + return ["message"=>\Yii::t('app', 'Flag does not exist!'),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } $player_progress = TPS::findOne(['id' => $treasure->target_id, 'player_id' => Yii::$app->user->id]); if ((Yii::$app->sys->force_findings_to_claim || $treasure->target->require_findings) && $player_progress === null && intval($treasure->target->getFindings()->count()) > 0) { Yii::$app->counters->increment('claim_no_finding'); $response->statusCode = 422; - return [\Yii::t('app', 'You need to discover at least one service before claiming a flag for this system.')]; + return ["message"=>\Yii::t('app', 'You need to discover at least one service before claiming a flag for this system.'),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } try { $module = \Yii::$app->getModule('target'); $module->checkNetwork($treasure->target); } catch (\Throwable $e) { - \Yii::$app->response->statusCode = 422; - return [\Yii::t('app', "You cannot claim this flag. You don't have access to this network.")]; + \Yii::$app->response->statusCode = 403; + return ["message"=>$e->getMessage(),"code"=>0,"status"=>\Yii::$app->response->statusCode]; } Yii::$app->counters->increment('claims'); $this->doClaim($treasure); \Yii::$app->response->statusCode = 201; - return ['OK']; + return ["message"=>"Flag claimed!","code"=>0,"status"=>\Yii::$app->response->statusCode]; } protected function doClaim($treasure) @@ -164,10 +163,8 @@ protected function doClaim($treasure) $transaction->commit(); $this->doOndemand($treasure->target); $PT->refresh(); - Yii::$app->session->setFlash('success', sprintf(\Yii::t('app', 'Flag [%s] claimed for %s points'), $treasure->name, number_format($PT->points))); } catch (\Exception $e) { $transaction->rollBack(); - Yii::$app->session->setFlash('error', \Yii::t('app', 'Flag failed')); throw $e; } catch (\Throwable $e) { $transaction->rollBack(); From f319d94935e95e180f6c74cb0b7de820da933ab6 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 13:19:46 +0200 Subject: [PATCH 17/38] document the new timeouts --- docs/Sysconfig-Keys.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Sysconfig-Keys.md b/docs/Sysconfig-Keys.md index 26cdb4e80..08dc06e4f 100644 --- a/docs/Sysconfig-Keys.md +++ b/docs/Sysconfig-Keys.md @@ -79,6 +79,10 @@ * `api_bearer_enable` Enable Bearer authorizations API operations * `api_claim_timeout` set the rate limit for the api claim. One request per `api_claim_timeout`+1 seconds * `api_target_instances_timeout` set the rate limit for the target instances endpoint. One request per `api_target_instances_timeout`+1 seconds +* `api_target_spin_timeout` set the rate limit for the given target operation endpoints. One request per `api_target_spin_timeout`+1 seconds +* `api_target_spawn_timeout` set the rate limit for the given target operation endpoints. One request per `api_target_spawn_timeout`+1 seconds +* `api_target_spawn_timeout` set the rate limit for the given target operation endpoints. One request per `api_target_spawn_timeout`+1 seconds + ## mail configuration * `mail_from` Email address used to send registration and password reset mails from * `mail_fromName` The name appeared on the email send for registration and password resets From 0e7f62a04bfa4b9199c13d83a653fc1267a5c945 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 13:20:04 +0200 Subject: [PATCH 18/38] check for status prior to progress --- frontend/modules/target/models/Target.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/modules/target/models/Target.php b/frontend/modules/target/models/Target.php index 5c9ecc9fe..857bee15e 100644 --- a/frontend/modules/target/models/Target.php +++ b/frontend/modules/target/models/Target.php @@ -129,13 +129,15 @@ public function getSpinDenied() { return true; } -// dont check for progress - if(Yii::$app->user->identity->profile->last->vpn_local_address === null && intval(self::find()->player_progress(Yii::$app->user->id)->where(['t.id'=>$this->id])->one()->player_findings)<1 && intval(self::find()->player_progress(Yii::$app->user->id)->where(['t.id'=>$this->id])->one()->player_treasures)<1) - return true; if($this->status!=='online') return true; + // dont check for progress + if(Yii::$app->user->identity->profile->last->vpn_local_address === null && intval(self::find()->player_progress(Yii::$app->user->id)->where(['t.id'=>$this->id])->one()->player_findings)<1 && intval(self::find()->player_progress(Yii::$app->user->id)->where(['t.id'=>$this->id])->one()->player_treasures)<1) + return true; + + return false; } From d32b11e87080035e8d9f855796c6654b90240f4d Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Mon, 4 Nov 2024 20:23:50 +0200 Subject: [PATCH 19/38] indent --- frontend/controllers/SiteController.php | 729 +++++++++++------------- 1 file changed, 345 insertions(+), 384 deletions(-) diff --git a/frontend/controllers/SiteController.php b/frontend/controllers/SiteController.php index 3be8ce644..79434bee2 100644 --- a/frontend/controllers/SiteController.php +++ b/frontend/controllers/SiteController.php @@ -19,416 +19,377 @@ class SiteController extends \app\components\BaseController { - /** - * {@inheritdoc} - */ - public function behaviors() - { - $parent=parent::behaviors(); - unset($parent['access']['rules']['teamsAccess']); - return ArrayHelper::merge($parent,[ - 'access' => [ - 'class' => AccessControl::class, - 'only' => ['index','login','logout', 'changelog', 'register', 'request-password-reset', 'verify-email', 'resend-verification-email', 'captcha'], - 'rules' => [ - 'disabledRegs'=>[ - 'actions'=>['register'], - 'allow'=>false, - 'roles'=>['*'], - 'matchCallback' => function ($rule, $action) { - return Yii::$app->sys->disable_registration!==false; - }, - 'denyCallback' => function ($rule, $action) { - return \Yii::$app->getResponse()->redirect([Yii::$app->sys->default_homepage],303); - }, - ], - 'indexAuth'=>[ - 'actions'=>['index'], - 'allow'=>false, - 'roles'=>['@'], - 'matchCallback' => function ($rule, $action) { - return Yii::$app->sys->default_homepage!==false && Yii::$app->sys->default_homepage!==""; - }, - 'denyCallback' => function ($rule, $action) { - return \Yii::$app->getResponse()->redirect([Yii::$app->sys->default_homepage],303); - }, - ], - 'eventStartEnd'=>[ - 'actions' => [ 'register', ], - ], - 'eventStart'=>[ - 'actions' => [ 'register', ], - ], - 'eventEnd'=>[ - 'actions' => [ 'register', 'request-password-reset', 'verify-email', 'resend-verification-email', ], - ], - 'eventActive'=>[ - 'actions' => ['register', 'verify-email', 'resend-verification-email'], - ], - [ - 'actions' => ['logout','index'], - 'allow' => true, - 'roles' => ['@'], - ], - 'denyAuthAccessToGuest'=>[ - 'actions' => ['register','login','verify-email', 'resend-verification-email','request-password-reset', 'captcha'], - 'allow' => false, - 'roles' => ['@'], - 'denyCallback' => function ($rule, $action) { - \Yii::$app->session->setFlash('warning', \Yii::t('app','Only guests can access this area.')); - return \Yii::$app->getResponse()->redirect([Yii::$app->sys->default_homepage],303); - }, - ], - 'checkDisabledRegs'=>[ - 'actions' => ['register'], - 'allow' => false, - 'matchCallback' => function ($rule, $action) { - return Yii::$app->sys->disable_registration===true; - }, - 'denyCallback' => function ($rule, $action) { - Yii::$app->session->setFlash('info', \Yii::t('app','Registrations are disabled on this competition')); - return \Yii::$app->getResponse()->redirect(['/site/login']); - }, - ], - 'registrationsStart'=>[ - 'actions' => ['register','request-password-reset', 'captcha'], - 'allow' => false, - 'roles' => ['?'], - 'matchCallback' => function ($rule, $action) { - return Yii::$app->sys->registrations_start!==false && time()<=Yii::$app->sys->registrations_start; - }, - 'denyCallback' => function ($rule, $action) { - if(time()<(int)Yii::$app->sys->registrations_start) - Yii::$app->session->setFlash('info', \Yii::t('app',"Registrations haven't started yet.")); - return \Yii::$app->getResponse()->redirect(['/site/login']); - - }, - ], - 'registrationsEnd'=>[ - 'actions' => ['register', 'captcha',], - 'allow' => false, - 'roles' => ['?'], - 'matchCallback' => function ($rule, $action) { - return Yii::$app->sys->registrations_end!==false && time()>=Yii::$app->sys->registrations_end; - }, - 'denyCallback' => function ($rule, $action) { - if(time()<(int)Yii::$app->sys->registrations_start) - Yii::$app->session->setFlash('info', \Yii::t('app','Registrations are no longer accepted ended.')); - return \Yii::$app->getResponse()->redirect(['/site/login']); + /** + * {@inheritdoc} + */ + public function behaviors() + { + $parent = parent::behaviors(); + unset($parent['access']['rules']['teamsAccess']); + return ArrayHelper::merge($parent, [ + 'access' => [ + 'class' => AccessControl::class, + 'only' => ['index', 'login', 'logout', 'changelog', 'register', 'request-password-reset', 'verify-email', 'resend-verification-email', 'captcha'], + 'rules' => [ + 'disabledRegs' => [ + 'actions' => ['register'], + 'allow' => false, + 'roles' => ['*'], + 'matchCallback' => function ($rule, $action) { + return Yii::$app->sys->disable_registration !== false; + }, + 'denyCallback' => function ($rule, $action) { + return \Yii::$app->getResponse()->redirect([Yii::$app->sys->default_homepage], 303); + }, + ], + 'indexAuth' => [ + 'actions' => ['index'], + 'allow' => false, + 'roles' => ['@'], + 'matchCallback' => function ($rule, $action) { + return Yii::$app->sys->default_homepage !== false && Yii::$app->sys->default_homepage !== ""; + }, + 'denyCallback' => function ($rule, $action) { + return \Yii::$app->getResponse()->redirect([Yii::$app->sys->default_homepage], 303); + }, + ], + 'eventStartEnd' => [ + 'actions' => ['register',], + ], + 'eventStart' => [ + 'actions' => ['register',], + ], + 'eventEnd' => [ + 'actions' => ['register', 'request-password-reset', 'verify-email', 'resend-verification-email',], + ], + 'eventActive' => [ + 'actions' => ['register', 'verify-email', 'resend-verification-email'], + ], + [ + 'actions' => ['logout', 'index'], + 'allow' => true, + 'roles' => ['@'], + ], + 'denyAuthAccessToGuest' => [ + 'actions' => ['register', 'login', 'verify-email', 'resend-verification-email', 'request-password-reset', 'captcha'], + 'allow' => false, + 'roles' => ['@'], + 'denyCallback' => function ($rule, $action) { + \Yii::$app->session->setFlash('warning', \Yii::t('app', 'Only guests can access this area.')); + return \Yii::$app->getResponse()->redirect([Yii::$app->sys->default_homepage], 303); + }, + ], + 'checkDisabledRegs' => [ + 'actions' => ['register'], + 'allow' => false, + 'matchCallback' => function ($rule, $action) { + return Yii::$app->sys->disable_registration === true; + }, + 'denyCallback' => function ($rule, $action) { + Yii::$app->session->setFlash('info', \Yii::t('app', 'Registrations are disabled on this competition')); + return \Yii::$app->getResponse()->redirect(['/site/login']); + }, + ], + 'registrationsStart' => [ + 'actions' => ['register', 'request-password-reset', 'captcha'], + 'allow' => false, + 'roles' => ['?'], + 'matchCallback' => function ($rule, $action) { + return Yii::$app->sys->registrations_start !== false && time() <= Yii::$app->sys->registrations_start; + }, + 'denyCallback' => function ($rule, $action) { + if (time() < (int)Yii::$app->sys->registrations_start) + Yii::$app->session->setFlash('info', \Yii::t('app', "Registrations haven't started yet.")); + return \Yii::$app->getResponse()->redirect(['/site/login']); + }, + ], + 'registrationsEnd' => [ + 'actions' => ['register', 'captcha',], + 'allow' => false, + 'roles' => ['?'], + 'matchCallback' => function ($rule, $action) { + return Yii::$app->sys->registrations_end !== false && time() >= Yii::$app->sys->registrations_end; + }, + 'denyCallback' => function ($rule, $action) { + if (time() < (int)Yii::$app->sys->registrations_start) + Yii::$app->session->setFlash('info', \Yii::t('app', 'Registrations are no longer accepted ended.')); + return \Yii::$app->getResponse()->redirect(['/site/login']); + }, + ], + 'allowGuestUsers' => [ + 'actions' => ['login', 'index', 'register', 'verify-email', 'resend-verification-email', 'request-password-reset', 'captcha',], + 'allow' => true, + 'roles' => ['?'] + ], + [ + 'actions' => ['changelog'], + 'allow' => true, + ], + ], + ], + 'verbs' => [ + 'class' => VerbFilter::class, + 'actions' => [ + 'logout' => ['post'], + ], + ], + ]); + } + + /** + * {@inheritdoc} + */ + public function actions() + { + return [ + 'error' => [ + 'class' => 'yii\web\ErrorAction', + ], + 'captcha' => [ + 'class' => 'app\widgets\captcha\CaptchaAction', + 'fixedVerifyCode' => (YII_ENV_TEST || YII_ENV_DEV) ? 'testme' : null, + 'offset' => 2, + 'minLength' => 7, + 'maxLength' => 7, + ], + ]; + } + + /** + * Displays homepage. + * + * @return string + */ + public function actionIndex() + { + if (!Yii::$app->user->isGuest && Yii::$app->sys->default_homepage !== false && Yii::$app->sys->default_homepage !== "") { + return $this->redirect([Yii::$app->sys->default_homepage]); + } - }, - ], - 'allowGuestUsers'=>[ - 'actions' => ['login','index','register','verify-email','resend-verification-email', 'request-password-reset','captcha', ], - 'allow' => true, - 'roles'=>['?'] - ], - [ - 'actions' => ['changelog'], - 'allow' => true, - ], - ], - ], - 'verbs' => [ - 'class' => VerbFilter::class, - 'actions' => [ - 'logout' => ['post'], - ], - ], - ]); + return $this->render('index'); + } + + /** + * Displays Maintenance page. If a file exists at `@app/web/dt.html` render it, + * otherwise render the maintenance view file. + * Both pages are full HTML, no layouts are applied from the application. + * + * @return string + */ + public function actionMaintenance() + { + Yii::$app->response->statusCode = 503; + + if (file_exists(Yii::getAlias('@app/web/dt.html')) !== false) + return \Yii::$app->view->renderFile(Yii::getAlias('@app/web/dt.html')); + + return $this->renderPartial('maintenance'); + } + + /** + * Login action. + * + * @return Response|string + */ + public function actionLogin() + { + if (!Yii::$app->user->isGuest) { + return $this->goHome(); } - /** - * {@inheritdoc} - */ - public function actions() - { - return [ - 'error' => [ - 'class' => 'yii\web\ErrorAction', - ], - 'captcha' => [ - 'class' => 'app\widgets\captcha\CaptchaAction', - 'fixedVerifyCode' => (YII_ENV_TEST || YII_ENV_DEV) ? 'testme' : null, - 'offset' => 2, - 'minLength' => 7, - 'maxLength' => 7, - ], - ]; + $model = new LoginForm(); + if ($model->load(Yii::$app->request->post()) && $model->login()) { + return $this->goBack(); } - /** - * Displays homepage. - * - * @return string - */ - public function actionIndex() - { - if(!Yii::$app->user->isGuest && Yii::$app->sys->default_homepage!==false && Yii::$app->sys->default_homepage!=="") - { - return $this->redirect([Yii::$app->sys->default_homepage]); + $model->password = ''; + return $this->render('login', [ + 'model' => $model, + ]); + } + + /** + * Signs user up. + * + * @return mixed + */ + public function actionRegister() + { + + $model = new SignupForm(); + $transaction = Yii::$app->db->beginTransaction(); + try { + if ($model->load(Yii::$app->request->post())) { + if (($player = $model->signup()) !== null) { + $transaction->commit(); + if (Yii::$app->sys->require_activation === true) { + Yii::$app->session->setFlash('success', \Yii::t('app', 'Thank you for registering. Please check your inbox for the verification email. Make sure you also check the spam or junk folders.')); + } elseif (Yii::$app->user->login($player)) { + Yii::$app->session->setFlash('success', \Yii::t('app', 'Thank you for registering. Your account is activated feel free to login.')); + } + } + return $this->goHome(); } - - return $this->render('index'); + } catch (\Exception $e) { + $transaction->rollBack(); + Yii::error($e->getMessage()); + Yii::$app->session->setFlash('error', \Yii::t('app', 'Registration failed.')); + } catch (\Throwable $e) { + $transaction->rollBack(); + Yii::error($e->getMessage()); + Yii::$app->session->setFlash('error', \Yii::t('app', 'Registration failed.')); } - - /** - * Displays Maintenance page. If a file exists at `@app/web/dt.html` render it, - * otherwise render the maintenance view file. - * Both pages are full HTML, no layouts are applied from the application. - * - * @return string - */ - public function actionMaintenance() - { - Yii::$app->response->statusCode = 503; - - if(file_exists(Yii::getAlias('@app/web/dt.html'))!==false) - return \Yii::$app->view->renderFile(Yii::getAlias('@app/web/dt.html')); - - return $this->renderPartial('maintenance'); + $referred = false; + if (Yii::$app->getSession()->get('referred_by') !== null && \app\models\Player::findOne(Yii::$app->getSession()->get('referred_by'))) { + $referred = \app\models\Player::findOne(Yii::$app->getSession()->get('referred_by')); } - - /** - * Login action. - * - * @return Response|string - */ - public function actionLogin() - { - if(!Yii::$app->user->isGuest) - { - return $this->goHome(); - } - - $model=new LoginForm(); - if($model->load(Yii::$app->request->post()) && $model->login()) - { - return $this->goBack(); - } - - $model->password=''; - return $this->render('login', [ - 'model' => $model, - ]); + return $this->render('signup', [ + 'model' => $model, + 'referred' => $referred, + ]); + } + + /** + * Requests password reset. + * + * @return mixed + */ + public function actionRequestPasswordReset() + { + if (Yii::$app->sys->disable_mailer === true) { + Yii::$app->session->setFlash('warning', \Yii::t('app', 'Mailing operations are disabled. Contact the support to assist you with accessing your account.')); + return $this->goHome(); } - /** - * Signs user up. - * - * @return mixed - */ - public function actionRegister() - { - - $model=new SignupForm(); - $transaction=Yii::$app->db->beginTransaction(); - try - { - if($model->load(Yii::$app->request->post())) - { - if(($player=$model->signup())!==null) - { - $transaction->commit(); - if(Yii::$app->sys->require_activation===true) - { - Yii::$app->session->setFlash('success', \Yii::t('app','Thank you for registering. Please check your inbox for the verification email. Make sure you also check the spam or junk folders.')); - } - elseif (Yii::$app->user->login($player)) { - Yii::$app->session->setFlash('success', \Yii::t('app','Thank you for registering. Your account is activated feel free to login.')); - } - } - return $this->goHome(); - } - } - catch(\Exception $e) - { - $transaction->rollBack(); - Yii::error($e->getMessage()); - Yii::$app->session->setFlash('error', \Yii::t('app','Registration failed.')); - } - catch(\Throwable $e) - { - $transaction->rollBack(); - Yii::error($e->getMessage()); - Yii::$app->session->setFlash('error', \Yii::t('app','Registration failed.')); - } - $referred=false; - if(Yii::$app->getSession()->get('referred_by')!==null && \app\models\Player::findOne(Yii::$app->getSession()->get('referred_by'))) - { - $referred=\app\models\Player::findOne(Yii::$app->getSession()->get('referred_by')); - } - return $this->render('signup', [ - 'model' => $model, - 'referred'=>$referred, - ]); - } + $model = new PasswordResetRequestForm(); + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + if ($model->sendEmail()) { + Yii::$app->session->setFlash('success', \Yii::t('app', 'Check your email for further instructions. Keep in mind that the token will expire after 24 hours.')); - /** - * Requests password reset. - * - * @return mixed - */ - public function actionRequestPasswordReset() - { - if(Yii::$app->sys->disable_mailer===true) - { - Yii::$app->session->setFlash('warning', \Yii::t('app', 'Mailing operations are disabled. Contact the support to assist you with accessing your account.')); return $this->goHome(); + } else { + Yii::$app->session->setFlash('error', \Yii::t('app', 'Sorry, we are unable to reset the password for the provided email address.')); } - - $model=new PasswordResetRequestForm(); - if($model->load(Yii::$app->request->post()) && $model->validate()) - { - if($model->sendEmail()) - { - Yii::$app->session->setFlash('success', \Yii::t('app','Check your email for further instructions. Keep in mind that the token will expire after 24 hours.')); - - return $this->goHome(); - } - else - { - Yii::$app->session->setFlash('error', \Yii::t('app','Sorry, we are unable to reset the password for the provided email address.')); - } - } - - return $this->render('requestPasswordResetToken', [ - 'model' => $model, - ]); } - /** - * Resets password. - * - * @param string $token - * @return mixed - * @throws BadRequestHttpException - */ - public function actionResetPassword($token) - { - try - { - $model=new ResetPasswordForm($token); - } - catch(InvalidArgumentException $e) - { - Yii::$app->session->setFlash('warning', \Yii::t('app','Password reset token not found! If you have changed your password already try to sign-in.')); - return $this->redirect(['/site/login']); - } - - if($model->load(Yii::$app->request->post()) && $model->validate() && $model->resetPassword()) - { - if(Yii::$app->user->login($model->player)) - { - Yii::$app->session->setFlash('success', \Yii::t('app','New password saved.')); - } - else - { - Yii::$app->session->setFlash('warning', Yii::t('app','New password saved but failed to auto sign-in.')); - } - - return $this->redirect(['/']); - } - - return $this->render('resetPassword', [ - 'model' => $model, - ]); + return $this->render('requestPasswordResetToken', [ + 'model' => $model, + ]); + } + + /** + * Resets password. + * + * @param string $token + * @return mixed + * @throws BadRequestHttpException + */ + public function actionResetPassword($token) + { + try { + $model = new ResetPasswordForm($token); + } catch (InvalidArgumentException $e) { + Yii::$app->session->setFlash('warning', \Yii::t('app', 'Password reset token not found! If you have changed your password already try to sign-in.')); + return $this->redirect(['/site/login']); } - /** - * Verify email address - * - * @param string $token - * @return mixed - * @throws BadRequestHttpException - */ - public function actionVerifyEmail($token) - { - try - { - $model=new VerifyEmailForm($token); - } - catch(InvalidArgumentException $e) - { - Yii::$app->session->setFlash('warning', \Yii::t('app','Verification token not found! Try to login if you have verified your email already.')); - return $this->redirect(['/site/login']); - } - $post=Yii::$app->request->post('VerifyEmailForm'); - $value=ArrayHelper::getValue($post, 'token'); - - if($value !== $token) - { - return $this->render('verify-email', ['model'=>$model, 'token'=>$token]); - } - $transaction=Yii::$app->db->beginTransaction(); - try - { - if($user=$model->verifyEmail()) - { - if(Yii::$app->user->login($user)) - { - $transaction->commit(); - Yii::$app->session->setFlash('success', \Yii::t('app','Your email has been confirmed!')); - return $this->redirect(['/profile/me']); - } - } - } - catch(\Exception $e) - { - $transaction->rollBack(); - } + if ($model->load(Yii::$app->request->post()) && $model->validate() && $model->resetPassword()) { + if (Yii::$app->user->login($model->player)) { + Yii::$app->session->setFlash('success', \Yii::t('app', 'New password saved.')); + } else { + Yii::$app->session->setFlash('warning', Yii::t('app', 'New password saved but failed to auto sign-in.')); + } - Yii::$app->session->setFlash('error', \Yii::t('app','Sorry, we are unable to verify an account with the provided token.')); - return $this->redirect(['/']); + return $this->redirect(['/']); } - /** - * Resend verification email - * - * @return mixed - */ - public function actionResendVerificationEmail() - { - if(Yii::$app->sys->disable_mailer===true) - { - Yii::$app->session->setFlash('warning', \Yii::t('app', 'Mailing operations are disabled. Contact the support to assist you with accessing your account.')); - return $this->goHome(); - } + return $this->render('resetPassword', [ + 'model' => $model, + ]); + } + + /** + * Verify email address + * + * @param string $token + * @return mixed + * @throws BadRequestHttpException + */ + public function actionVerifyEmail($token) + { + try { + $model = new VerifyEmailForm($token); + } catch (InvalidArgumentException $e) { + Yii::$app->session->setFlash('warning', \Yii::t('app', 'Verification token not found! Try to login if you have verified your email already.')); + return $this->redirect(['/site/login']); + } + $post = Yii::$app->request->post('VerifyEmailForm'); + $value = ArrayHelper::getValue($post, 'token'); - $model=new ResendVerificationEmailForm(); - if($model->load(Yii::$app->request->post()) && $model->validate()) - { - if($model->sendEmail()) - { - Yii::$app->session->setFlash('success', \Yii::t('app','Check your email for further instructions.')); - return $this->goHome(); - } - Yii::$app->session->setFlash('error', \Yii::t('app','Sorry, we are unable to resend verification email for the provided address.')); + if ($value !== $token) { + return $this->render('verify-email', ['model' => $model, 'token' => $token]); + } + $transaction = Yii::$app->db->beginTransaction(); + try { + if ($user = $model->verifyEmail()) { + if (Yii::$app->user->login($user)) { + $transaction->commit(); + Yii::$app->session->setFlash('success', \Yii::t('app', 'Your email has been confirmed!')); + return $this->redirect(['/profile/me']); } + } + } catch (\Exception $e) { + $transaction->rollBack(); + } - return $this->render('resendVerificationEmail', [ - 'model' => $model - ]); + Yii::$app->session->setFlash('error', \Yii::t('app', 'Sorry, we are unable to verify an account with the provided token.')); + return $this->redirect(['/']); + } + + /** + * Resend verification email + * + * @return mixed + */ + public function actionResendVerificationEmail() + { + if (Yii::$app->sys->disable_mailer === true) { + Yii::$app->session->setFlash('warning', \Yii::t('app', 'Mailing operations are disabled. Contact the support to assist you with accessing your account.')); + return $this->goHome(); } - /** - * Logout action. - * - * @return Response - */ - public function actionLogout() - { - Yii::$app->user->logout(); + $model = new ResendVerificationEmailForm(); + if ($model->load(Yii::$app->request->post()) && $model->validate()) { + if ($model->sendEmail()) { + Yii::$app->session->setFlash('success', \Yii::t('app', 'Check your email for further instructions.')); return $this->goHome(); + } + Yii::$app->session->setFlash('error', \Yii::t('app', 'Sorry, we are unable to resend verification email for the provided address.')); } - public function actionChangelog() - { - $changelog=@file_get_contents('../Changelog.md'); - $todo=@file_get_contents('../TODO.md'); - return $this->render('changelog', [ - 'changelog'=>$changelog, - 'todo'=>$todo - ]); - } + return $this->render('resendVerificationEmail', [ + 'model' => $model + ]); + } + /** + * Logout action. + * + * @return Response + */ + public function actionLogout() + { + Yii::$app->user->logout(); + + return $this->goHome(); + } + + public function actionChangelog() + { + $changelog = @file_get_contents('../Changelog.md'); + $todo = @file_get_contents('../TODO.md'); + return $this->render('changelog', [ + 'changelog' => $changelog, + 'todo' => $todo + ]); + } } From 478816bc5c3a4417d8571faf453a1bc3f53e5031 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:35:59 +0200 Subject: [PATCH 20/38] use the generate email token --- backend/commands/PlayerController.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/commands/PlayerController.php b/backend/commands/PlayerController.php index 898640165..0fe612136 100644 --- a/backend/commands/PlayerController.php +++ b/backend/commands/PlayerController.php @@ -212,15 +212,14 @@ public function actionRegister($username, $email, $fullname, $password = false, $player->active = intval($active); $player->status = 10; - if (!$player->active) { - $player->verification_token = str_replace('_', '-', Yii::$app->security->generateRandomString() . '-' . (time())); - $player->status = 9; - } $player->auth_key = Yii::$app->security->generateRandomString(); if (!$player->saveWithSsl()) { - print_r($player->getErrors()); + if (!$player->active) { + $player->generateEmailVerificationToken(); + $player->status = 9; + } throw new ConsoleException('Failed to save player:' . $player->username, "\n"); } From 280d5f797716b905d870e16c3334d08060856e70 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:37:26 +0200 Subject: [PATCH 21/38] update procedure to avoid deleting rolling expirations --- ...1103_121625_create_expire_player_tokens_procedure.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/migrations/m241103_121625_create_expire_player_tokens_procedure.php b/backend/migrations/m241103_121625_create_expire_player_tokens_procedure.php index e601ef329..069680da0 100644 --- a/backend/migrations/m241103_121625_create_expire_player_tokens_procedure.php +++ b/backend/migrations/m241103_121625_create_expire_player_tokens_procedure.php @@ -12,12 +12,15 @@ class m241103_121625_create_expire_player_tokens_procedure extends Migration BEGIN DECLARE tnow TIMESTAMP; SET tnow=NOW(); - IF (SELECT COUNT(*) FROM player_token WHERE expires_at0 THEN + IF (SELECT COUNT(*) FROM player_token WHERE expires_at0 THEN START TRANSACTION; - INSERT INTO notification (player_id,category,title,body,archived,created_at,updated_at) SELECT player_id,'info','Token expiration',CONCAT(type,' Token [',description,'] expired at ',expires_at),0,tnow,tnow FROM player_token WHERE expires_at0 THEN + DELETE FROM player_token WHERE expires_at Date: Tue, 5 Nov 2024 00:37:41 +0200 Subject: [PATCH 22/38] drop the fields, we no longer need them --- ...4_drop_token_columns_from_player_table.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 backend/migrations/m241104_201004_drop_token_columns_from_player_table.php diff --git a/backend/migrations/m241104_201004_drop_token_columns_from_player_table.php b/backend/migrations/m241104_201004_drop_token_columns_from_player_table.php new file mode 100644 index 000000000..0b2c3b0b6 --- /dev/null +++ b/backend/migrations/m241104_201004_drop_token_columns_from_player_table.php @@ -0,0 +1,27 @@ +dropColumn('player', 'password_reset_token'); + $this->dropColumn('player', 'verification_token'); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->addColumn('player', 'password_reset_token', $this->string()); + $this->addColumn('player', 'verification_token', $this->string()); + } +} From a76d8209825311c0e23d26f4ef457a8903d8ada1 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:38:07 +0200 Subject: [PATCH 23/38] add default validity times for tokens --- ...04_211706_add_default_token_validities.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 backend/migrations-init/m241104_211706_add_default_token_validities.php diff --git a/backend/migrations-init/m241104_211706_add_default_token_validities.php b/backend/migrations-init/m241104_211706_add_default_token_validities.php new file mode 100644 index 000000000..e89d1c93d --- /dev/null +++ b/backend/migrations-init/m241104_211706_add_default_token_validities.php @@ -0,0 +1,26 @@ +upsert('sysconfig',['id'=>'password_reset_token_validity','val'=>'24 hour'],true); + $this->upsert('sysconfig',['id'=>'mail_verification_token_validity','val'=>'10 day'],true); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + echo "m241104_211706_add_default_token_validities cannot be reverted.\n"; + } +} From e48db27feb5e8c8c09370e1b580e968904b15db1 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:38:26 +0200 Subject: [PATCH 24/38] use Yii --- backend/modules/frontend/controllers/PlayerTokenController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/modules/frontend/controllers/PlayerTokenController.php b/backend/modules/frontend/controllers/PlayerTokenController.php index d0dd0ef42..b343b05f9 100644 --- a/backend/modules/frontend/controllers/PlayerTokenController.php +++ b/backend/modules/frontend/controllers/PlayerTokenController.php @@ -1,7 +1,7 @@ Date: Tue, 5 Nov 2024 00:38:44 +0200 Subject: [PATCH 25/38] add generate operations --- backend/modules/frontend/models/Player.php | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/backend/modules/frontend/models/Player.php b/backend/modules/frontend/models/Player.php index d92ac67dc..f8f2c2378 100644 --- a/backend/modules/frontend/models/Player.php +++ b/backend/modules/frontend/models/Player.php @@ -163,5 +163,46 @@ public function getAcademicShort() { return Yii::$app->sys->{"academic_".$this->academic."short"}; } + public function getVerification_token() + { + if (($model = PlayerToken::findOne(['player_id' => $this->id, 'type' => 'email_verification'])) !== null) + return $model->token; + return null; + } + + public function getPassword_reset_token() + { + if (($model = PlayerToken::findOne(['player_id' => $this->id, 'type' => 'password_reset'])) !== null) + return $model->token; + return null; + } + /** + * Generates new password reset token + */ + public function generatePasswordResetToken() + { + if (($model = PlayerToken::findOne(['player_id' => $this->id, 'type' => 'password_reset'])) === null) { + $model = new PlayerToken(); + $validity=(\Yii::$app->sys->password_reset_token_validity===false) ? '10 day':\Yii::$app->sys->password_reset_token_validity; + $model->player_id = $this->id; + $model->type = 'password_reset'; + $model->expires_at = \Yii::$app->formatter->asDatetime(new \DateTime('NOW + '.$validity), 'php:Y-m-d H:i:s'); + $model->token = str_replace('_', '-', Yii::$app->security->generateRandomString(30)); + $model->save(); + } + } + + public function generateEmailVerificationToken() + { + if (($model = PlayerToken::findOne(['player_id' => $this->id, 'type' => 'email_verification'])) === null) { + $model = new PlayerToken(); + $validity=(\Yii::$app->sys->mail_verification_token_validity===false) ? '10 day':\Yii::$app->sys->mail_verification_token_validity; + $model->player_id = $this->id; + $model->type = 'email_verification'; + $model->expires_at = \Yii::$app->formatter->asDatetime(new \DateTime('NOW + '.$validity), 'php:Y-m-d H:i:s'); + $model->token = str_replace('_', '-', Yii::$app->security->generateRandomString(30)); + $model->save(); + } + } } From 1708b2f14f350f750120d20a3302886333f71092 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:42:11 +0200 Subject: [PATCH 26/38] remove player_ip left overs --- backend/commands/PlayerController.php | 1 - backend/commands/SslController.php | 1 - backend/commands/SysconfigController.php | 1 - .../frontend/models/PlayerIpSearch.php | 89 ------------------- 4 files changed, 92 deletions(-) delete mode 100644 backend/modules/frontend/models/PlayerIpSearch.php diff --git a/backend/commands/PlayerController.php b/backend/commands/PlayerController.php index 0fe612136..0533a841c 100644 --- a/backend/commands/PlayerController.php +++ b/backend/commands/PlayerController.php @@ -18,7 +18,6 @@ use app\modules\frontend\models\Profile; use app\modules\frontend\models\Team; use app\modules\frontend\models\TeamPlayer; -use app\modules\frontend\models\PlayerIp; use app\modules\frontend\models\PlayerSsl; use yii\helpers\ArrayHelper; use yii\console\widgets\Table; diff --git a/backend/commands/SslController.php b/backend/commands/SslController.php index 1bf96f8ad..ad4937db6 100644 --- a/backend/commands/SslController.php +++ b/backend/commands/SslController.php @@ -7,7 +7,6 @@ use app\modules\frontend\models\Player; use app\modules\frontend\models\Crl; use app\modules\frontend\models\PlayerSsl; -use app\modules\frontend\models\PlayerIp; use app\modules\gameplay\models\Target; use app\modules\settings\models\Sysconfig; use yii\console\Exception as ConsoleException; diff --git a/backend/commands/SysconfigController.php b/backend/commands/SysconfigController.php index a97e4809d..76915ee24 100644 --- a/backend/commands/SysconfigController.php +++ b/backend/commands/SysconfigController.php @@ -4,7 +4,6 @@ use yii\console\Controller; use yii\console\ExitCode; use app\modules\frontend\models\Player; -use app\modules\frontend\models\PlayerIp; use app\modules\gameplay\models\Target; use app\modules\settings\models\Sysconfig; use yii\console\Exception as ConsoleException; diff --git a/backend/modules/frontend/models/PlayerIpSearch.php b/backend/modules/frontend/models/PlayerIpSearch.php deleted file mode 100644 index b6c375c33..000000000 --- a/backend/modules/frontend/models/PlayerIpSearch.php +++ /dev/null @@ -1,89 +0,0 @@ -joinWith(['player']);; - - // add conditions that should always apply here - - $dataProvider=new ActiveDataProvider([ - 'query' => $query, - ]); - - $this->load($params); - - if(!$this->validate()) - { - // uncomment the following line if you do not want to return any records when validation fails - // $query->where('0=1'); - return $dataProvider; - } - - // grid filtering conditions - $query->andFilterWhere([ - 'player_ip.id' => $this->id, - 'player_ip.player_id' => $this->player_id, - 'player_ip.ip' => $this->ip, - ]); - $query->andFilterWhere(['like', 'player.username', $this->username]); - $query->andFilterWhere(['like', 'INET_NTOA(ip)', $this->ipoctet]); - $dataProvider->setSort([ - 'attributes' => array_merge( - $dataProvider->getSort()->attributes, - [ - 'ipoctet' => [ - 'asc' => ['ip' => SORT_ASC], - 'desc' => ['ip' => SORT_DESC], - ], - 'username' => [ - 'asc' => ['player.username' => SORT_ASC], - 'desc' => ['player.username' => SORT_DESC], - ], - ] - ), - ]); - - return $dataProvider; - } -} From 17b00d541b69c3693000fcdfadba4186cb4d0fe6 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:42:41 +0200 Subject: [PATCH 27/38] remove playerip relations and tokens --- backend/modules/frontend/models/PlayerAR.php | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/backend/modules/frontend/models/PlayerAR.php b/backend/modules/frontend/models/PlayerAR.php index e05395fba..0480117d6 100644 --- a/backend/modules/frontend/models/PlayerAR.php +++ b/backend/modules/frontend/models/PlayerAR.php @@ -42,8 +42,6 @@ * @property Finding[] $findings * @property PlayerHint[] $playerHints * @property Hint[] $hints - * @property PlayerIp $playerIp - * @property PlayerIp[] $playerIps * @property PlayerMac[] $playerMacs * @property PlayerQuestion[] $playerQuestions * @property PlayerTreasure[] $playerTreasures @@ -113,7 +111,6 @@ public function rules() [['type'], 'default', 'value' => 'offense'], [['status'], 'default', 'value' => self::STATUS_ACTIVE], [['activkey'], 'default', 'value' => '', 'on' => 'create'], - [['verification_token'], 'default', 'value' => str_replace('_','-',Yii::$app->security->generateRandomString().'-'.time()), 'on' => 'create'], [['username', 'email', 'new_password', 'activkey'], 'string', 'max' => 255], [['fullname'], 'string', 'max' => 128], [['username'], 'unique'], @@ -130,7 +127,7 @@ public function rules() if(intval($count)!==0) $this->addError($attribute, 'This email is banned.'); },'on'=>['validator','extendedValidator']], - [['activkey','verification_token'], function($attribute, $params){ + [['activkey'], function($attribute, $params){ if($this->{$attribute}!==null && $this->active===1 && $this->status=10) $this->addError($attribute, '{attribute} must be empty when player active.'); },'on'=>['validator','extendedValidator']], @@ -219,13 +216,6 @@ public function getHints() return $this->hasMany(Hint::class, ['id' => 'hint_id'])->viaTable('player_hint', ['player_id' => 'id']); } - /** - * @return \yii\db\ActiveQuery - */ - public function getPlayerIp() - { - return $this->hasOne(PlayerIp::class, ['player_id' => 'id']); - } /** * @return \yii\db\ActiveQuery */ @@ -234,14 +224,6 @@ public function getPlayerLast() return $this->hasOne(\app\modules\activity\models\PlayerLast::class, ['id' => 'id']); } - /** - * @return \yii\db\ActiveQuery - */ - public function getPlayerIps() - { - return $this->hasMany(PlayerIp::class, ['player_id' => 'id']); - } - /** * @return \yii\db\ActiveQuery */ From c270868f2c592d8c1378db560e47dd4b22b11620 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:46:37 +0200 Subject: [PATCH 28/38] improve init and introduce before save --- .../modules/frontend/models/PlayerToken.php | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/backend/modules/frontend/models/PlayerToken.php b/backend/modules/frontend/models/PlayerToken.php index 74864eb6d..b645cc457 100644 --- a/backend/modules/frontend/models/PlayerToken.php +++ b/backend/modules/frontend/models/PlayerToken.php @@ -29,15 +29,15 @@ public static function tableName() return 'player_token'; } - public function init(){ + public function init() + { parent::init(); - if(!method_exists($this,'search') && $this->isNewRecord) //for checking this code is on model search or not + if (!method_exists($this, 'search') && $this->isNewRecord) //for checking this code is on model search or not { - $this->type='API'; - if($this->description===null) - $this->description=(string)$this->description; - $this->token=Yii::$app->security->generateRandomString(30); - $this->expires_at=\Yii::$app->formatter->asDatetime(new \DateTime('NOW + 60 days'), 'php:Y-m-d H:i:s'); + $this->type = 'API'; + if ($this->description === null) + $this->description = (string)$this->description; + $this->token = Yii::$app->security->generateRandomString(30); } } public function behaviors() @@ -64,14 +64,13 @@ public function behaviors() public function rules() { return [ - ['token', 'default', 'value' => Yii::$app->security->generateRandomString(20)], - ['description', 'default', 'skipOnEmpty'=>false, 'skipOnError'=>false,'value' => 'auto-generated by backend'], - [['expires_at', 'created_at'], 'default', 'value' => \Yii::$app->formatter->asDatetime(new \DateTime('NOW + 30 days'), 'php:Y-m-d H:i:s')], + ['token', 'default', 'value' => Yii::$app->security->generateRandomString(30)], + ['description', 'default', 'skipOnEmpty' => false, 'skipOnError' => false, 'value' => 'auto-generated by backend'], [['type'], 'default', 'value' => 'API'], [['player_id', 'type', 'token'], 'required'], [['player_id'], 'integer'], [['expires_at', 'created_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'], - [['expires_at', 'created_at','description'], 'safe'], + [['expires_at', 'created_at', 'description'], 'safe'], [['type'], 'string', 'max' => 32], [['token'], 'string', 'max' => 128], [['description'], 'string'], @@ -117,9 +116,22 @@ public static function find() public function getTypes() { return [ - 'API'=>'API', - 'password_reset'=>'Password Reset', - 'email_verification'=>'Email Verification' + 'API' => 'API', + 'password_reset' => 'Password Reset', + 'email_verification' => 'Email Verification' ]; } + + public function beforeSave($insert) { + if ($insert) { + if ($this->type === 'email_verification') { + $this->expires_at = new Expression('now() + INTERVAL '.\Yii::$app->sys->mail_verification_token_validity); + } else if ($this->type === 'password_reset') { + $this->expires_at = new Expression('now() + INTERVAL '.\Yii::$app->sys->password_reset_token_validity); + } else + $this->expires_at = new Expression('now() + INTERVAL 60 day'); + } + return parent::beforeSave($insert); +} + } From 59647b73856e5f7608afc53efc48baa55b2c265a Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:46:53 +0200 Subject: [PATCH 29/38] on new records dont show expires at --- backend/modules/frontend/views/player-token/_form.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/modules/frontend/views/player-token/_form.php b/backend/modules/frontend/views/player-token/_form.php index 019a48f4f..09b71d555 100644 --- a/backend/modules/frontend/views/player-token/_form.php +++ b/backend/modules/frontend/views/player-token/_form.php @@ -25,7 +25,9 @@
field($model, 'type')->dropDownList($model->types)->hint('The type of this token') ?>
field($model, 'token')->textInput(['maxlength' => true]) ?>
+ isNewRecord):?>
field($model, 'expires_at')->textInput()->hint('Token expiration date (default: in 60 days)') ?>
+
From ec3e54ecb88d81febd2d8987f3f7cdf39149cad0 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:49:36 +0200 Subject: [PATCH 30/38] remove save we dont update player any more --- frontend/models/forms/PasswordResetRequestForm.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/models/forms/PasswordResetRequestForm.php b/frontend/models/forms/PasswordResetRequestForm.php index 0408dbd24..79d2f2da1 100644 --- a/frontend/models/forms/PasswordResetRequestForm.php +++ b/frontend/models/forms/PasswordResetRequestForm.php @@ -61,10 +61,6 @@ public function sendEmail() if(!Player::isPasswordResetTokenValid($player->password_reset_token)) { $player->generatePasswordResetToken(); - if(!$player->save()) - { - return false; - } } $password_reset_ip++; $password_reset_email++; From 74dfdaee5e3e3eb1113aeaf9b8535427ffb8d659 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:50:25 +0200 Subject: [PATCH 31/38] verification mail only on 9 and 8 --- frontend/models/forms/ResendVerificationEmailForm.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/models/forms/ResendVerificationEmailForm.php b/frontend/models/forms/ResendVerificationEmailForm.php index aa13c1e94..b9eb16577 100644 --- a/frontend/models/forms/ResendVerificationEmailForm.php +++ b/frontend/models/forms/ResendVerificationEmailForm.php @@ -21,9 +21,9 @@ class ResendVerificationEmailForm extends Model */ public function rules() { - $filter=['status'=>Player::STATUS_INACTIVE]; + $filter=['status'=>[Player::STATUS_INACTIVE,Player::STATUS_UNVERIFIED]]; if(\Yii::$app->sys->player_require_approval) - $filter=['status'=>Player::STATUS_INACTIVE,'approval'=>[1,2]]; + $filter=['status'=>[Player::STATUS_INACTIVE,Player::STATUS_UNVERIFIED],'approval'=>[1,2]]; return [ ['email', 'trim'], ['email', 'required'], @@ -53,10 +53,15 @@ public function sendEmail() return false; } - if(($player=Player::findOne(['email' => $this->email,'status' => Player::STATUS_INACTIVE]))===null) + if(($player=Player::findOne(['email' => $this->email,'status' => [Player::STATUS_INACTIVE,Player::STATUS_UNVERIFIED]]))===null) { return false; } + + if($player->verification_token===null) + { + $player->generateEmailVerificationToken(); + } $verification_resend_ip++; $verification_resend_email++; Yii::$app->cache->memcache->set('verification_resend_ip:'.\Yii::$app->request->userIp,$verification_resend_ip, Yii::$app->sys->verification_resend_ip_timeout); From 2d07ea402e6e5eb665b39e643af79e00bccccd08 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:51:24 +0200 Subject: [PATCH 32/38] generate verification token after we save the player --- frontend/models/forms/SignupForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/models/forms/SignupForm.php b/frontend/models/forms/SignupForm.php index a70f1e1ed..98011770d 100644 --- a/frontend/models/forms/SignupForm.php +++ b/frontend/models/forms/SignupForm.php @@ -87,11 +87,11 @@ public function signup() if(\Yii::$app->sys->require_activation===true) { $player->active=0; - $player->generateEmailVerificationToken(); if($player->saveNewPlayer()===false) { throw new \Exception(\Yii::t('app',"Error Processing Request"), 1); } + $player->generateEmailVerificationToken(); } else { From 63527f024087fbd86a63e84c42c51073505dc4d2 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:55:56 +0200 Subject: [PATCH 33/38] replacement for mail and password tokens, cleanups and formatting --- frontend/models/Player.php | 82 ++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/frontend/models/Player.php b/frontend/models/Player.php index 3788ef076..4a7dd4597 100644 --- a/frontend/models/Player.php +++ b/frontend/models/Player.php @@ -81,7 +81,7 @@ public static function findIdentity($id) */ public static function findIdentityByAccessToken($token, $type = null) { - if(($model=PlayerToken::findOne(['token' => $token,'type'=>'API']))!==null) + if (($model = PlayerToken::findOne(['token' => $token, 'type' => 'API'])) !== null) return static::findOne($model->player_id); } @@ -116,10 +116,8 @@ public static function findByEmail($email) */ public static function findByVerificationToken($token) { - return static::findOne([ - 'verification_token' => $token, - 'status' => [self::STATUS_INACTIVE, self::STATUS_UNVERIFIED] - ]); + if (($model = PlayerToken::findOne(['token' => $token, 'type' => 'email_verification'])) !== null) + return static::findOne(['id' => $model->player_id, 'status' => [self::STATUS_INACTIVE, self::STATUS_UNVERIFIED]]); } /** @@ -130,6 +128,20 @@ public function getId() return $this->id; } + public function getVerification_token() + { + if (($model = PlayerToken::findOne(['player_id' => $this->id, 'type' => 'email_verification'])) !== null) + return $model->token; + return null; + } + + public function getPassword_reset_token() + { + if (($model = PlayerToken::findOne(['player_id' => $this->id, 'type' => 'password_reset'])) !== null) + return $model->token; + return null; + } + /** * {@inheritdoc} */ @@ -181,12 +193,28 @@ public function generateAuthKey() */ public function generatePasswordResetToken() { - $this->password_reset_token = str_replace('_', '-', Yii::$app->security->generateRandomString() . '-' . time()); + if (($model = PlayerToken::findOne(['player_id' => $this->id, 'type' => 'password_reset'])) === null) { + $model = new PlayerToken(); + $validity=(\Yii::$app->sys->password_reset_token_validity===false) ? '10 day':\Yii::$app->sys->password_reset_token_validity; + $model->player_id = $this->id; + $model->type = 'password_reset'; + $model->expires_at = \Yii::$app->formatter->asDatetime(new \DateTime('NOW + '.$validity), 'php:Y-m-d H:i:s'); + $model->token = str_replace('_', '-', Yii::$app->security->generateRandomString(30)); + $model->save(); + } } public function generateEmailVerificationToken() { - $this->verification_token = str_replace('_', '-', Yii::$app->security->generateRandomString() . '-' . time()); + if (($model = PlayerToken::findOne(['player_id' => $this->id, 'type' => 'email_verification'])) === null) { + $model = new PlayerToken(); + $validity=(\Yii::$app->sys->mail_verification_token_validity===false) ? '10 day':\Yii::$app->sys->mail_verification_token_validity; + $model->player_id = $this->id; + $model->type = 'email_verification'; + $model->expires_at = \Yii::$app->formatter->asDatetime(new \DateTime('NOW + '.$validity), 'php:Y-m-d H:i:s'); + $model->token = str_replace('_', '-', Yii::$app->security->generateRandomString(30)); + $model->save(); + } } /** @@ -194,7 +222,7 @@ public function generateEmailVerificationToken() */ public function removePasswordResetToken() { - $this->password_reset_token = null; + PlayerToken::deleteAll(['player_id' => $this->id, 'type' => 'password_reset']); } /** @@ -273,12 +301,11 @@ public static function find() */ public static function isPasswordResetTokenValid($token, $expire = 86400): bool { - if (empty($token) || trim($token) === "") { + if (trim($token) === '') return false; - } - return true; - $timestamp = (int) substr($token, strrpos($token, '_') + 1); - return $timestamp + $expire >= time(); + if (PlayerToken::findOne(['token' => $token, 'type' => 'password_reset']) !== null) + return true; + return false; } /** * Finds user by password reset token @@ -291,11 +318,8 @@ public static function findByPasswordResetToken($token) if (!static::isPasswordResetTokenValid($token)) { return null; } - - return static::findOne([ - 'password_reset_token' => $token, - 'status' => self::STATUS_ACTIVE, - ]); + if (($model = PlayerToken::findOne(['token' => $token, 'type' => 'password_reset'])) !== null) + return static::findOne(['id' => $model->player_id, 'status' => self::STATUS_ACTIVE]); } public function saveWithSsl($validation = true) @@ -358,30 +382,30 @@ public function genAvatar() public function getAcademicWord() { - return \Yii::$app->sys->{"academic_".$this->academic."long"}; + return \Yii::$app->sys->{"academic_" . $this->academic . "long"}; } public function getAcademicShort() { - return \Yii::$app->sys->{"academic_".$this->academic."short"}; + return \Yii::$app->sys->{"academic_" . $this->academic . "short"}; } public function getAcademicIcon() { - return \Yii::$app->sys->{"academic_".$this->academic."icon"}; + return \Yii::$app->sys->{"academic_" . $this->academic . "icon"}; } /** * Send a notification to current user */ - public function notify($type="info",$title,$body) - { - $n=new \app\models\Notification; - $n->player_id=$this->id; - $n->archived=0; - $n->category=$type; - $n->title=$title; - $n->body=$body; + public function notify($type = "info", $title, $body) + { + $n = new \app\models\Notification; + $n->player_id = $this->id; + $n->archived = 0; + $n->category = $type; + $n->title = $title; + $n->body = $body; return $n->save(); } From 848c9d6d5c24c98d1a6a35d94498646989e10f8a Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:56:11 +0200 Subject: [PATCH 34/38] remove filesystem mails we no longer use them --- frontend/mail/emailChangeVerify-html.php | 22 ---------------------- frontend/mail/emailChangeVerify-text.php | 22 ---------------------- frontend/mail/emailVerify-html.php | 22 ---------------------- frontend/mail/emailVerify-text.php | 18 ------------------ frontend/mail/passwordResetToken-html.php | 21 --------------------- frontend/mail/passwordResetToken-text.php | 17 ----------------- 6 files changed, 122 deletions(-) delete mode 100644 frontend/mail/emailChangeVerify-html.php delete mode 100644 frontend/mail/emailChangeVerify-text.php delete mode 100644 frontend/mail/emailVerify-html.php delete mode 100644 frontend/mail/emailVerify-text.php delete mode 100644 frontend/mail/passwordResetToken-html.php delete mode 100644 frontend/mail/passwordResetToken-text.php diff --git a/frontend/mail/emailChangeVerify-html.php b/frontend/mail/emailChangeVerify-html.php deleted file mode 100644 index 1e5fb8eb3..000000000 --- a/frontend/mail/emailChangeVerify-html.php +++ /dev/null @@ -1,22 +0,0 @@ -urlManager->createAbsoluteUrl(['site/verify-email', 'token' => $user->verification_token]); -?> -
-

Hello,

- -

You just requested that this email address be linked to your sys->event_name)?> account.

- -

To verify that this email is valid follow the link below:

- -

- -

If you have any difficulties, feel free to join our discord server and ask for assistance there.

- -

Best regards,
sys->event_name)?> team

- -
diff --git a/frontend/mail/emailChangeVerify-text.php b/frontend/mail/emailChangeVerify-text.php deleted file mode 100644 index b7647d2ed..000000000 --- a/frontend/mail/emailChangeVerify-text.php +++ /dev/null @@ -1,22 +0,0 @@ -urlManager->createAbsoluteUrl(['site/verify-email', 'token' => $user->verification_token]); -?> -Hello, - -You just requested that this email address be linked to your sys->event_name)?> -account. - -To verify that this email is valid follow the link below: - - - -If you have any difficulties, feel free to join our discord server and ask for -assistance there. - -Best regards, - -sys->event_name)?> team diff --git a/frontend/mail/emailVerify-html.php b/frontend/mail/emailVerify-html.php deleted file mode 100644 index 3c903d241..000000000 --- a/frontend/mail/emailVerify-html.php +++ /dev/null @@ -1,22 +0,0 @@ -urlManager->createAbsoluteUrl(['site/verify-email', 'token' => $user->verification_token]); -?> -
-

Hello and welcome to sys->event_name)?>

- -

You (or possibly someone else), just requested that this email address be used - to create an account on our platform.

- -

To complete this verification process and activate the account on our platform - follow the link below:

- -

- -

If you didn't request the account registration, just ignore this email.

- -
diff --git a/frontend/mail/emailVerify-text.php b/frontend/mail/emailVerify-text.php deleted file mode 100644 index 6cfefb558..000000000 --- a/frontend/mail/emailVerify-text.php +++ /dev/null @@ -1,18 +0,0 @@ -urlManager->createAbsoluteUrl(['site/verify-email', 'token' => $user->verification_token]); -?> -Hello and welcome to sys->event_name?> - -You (or possibly someone else), just requested that this email address be used -to create an account on our platform. - -To complete this verification process and activate the account on our platform -follow the link below: - - - -If you didn't request the account registration, just ignore this email. diff --git a/frontend/mail/passwordResetToken-html.php b/frontend/mail/passwordResetToken-html.php deleted file mode 100644 index 5f2510030..000000000 --- a/frontend/mail/passwordResetToken-html.php +++ /dev/null @@ -1,21 +0,0 @@ -urlManager->createAbsoluteUrl(['site/reset-password', 'token' => $user->password_reset_token]); -?> -
-

Hello username) ?>,

- -

You (or possibly someone else), just requested a password reset operation - to be performed on an account associated with this email address.

- -

Follow the link below to reset your password:

- -

- -

If you didn't request this password reset, just ignore this email.

- -
diff --git a/frontend/mail/passwordResetToken-text.php b/frontend/mail/passwordResetToken-text.php deleted file mode 100644 index 8e2b8d9a5..000000000 --- a/frontend/mail/passwordResetToken-text.php +++ /dev/null @@ -1,17 +0,0 @@ -urlManager->createAbsoluteUrl(['site/reset-password', 'token' => $user->password_reset_token]); -?> -Hello username ?>, - -You (or possibly someone else), just requested a password reset operation -to be performed on an account associated with this email address. - -Follow the link below to reset your password: - - - -If you didn't request this password reset, just ignore this email. From 42d42d43bc4502548900c87fda7130b6c18aa5f5 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 00:56:23 +0200 Subject: [PATCH 35/38] update sysconfig documentation --- docs/Sysconfig-Keys.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Sysconfig-Keys.md b/docs/Sysconfig-Keys.md index 08dc06e4f..0eedab924 100644 --- a/docs/Sysconfig-Keys.md +++ b/docs/Sysconfig-Keys.md @@ -76,6 +76,9 @@ * `event_end_notification_body`: The body that will be used to send a notification to all players when the event ends * `plus_writeups`: Number to add to the headshots to allow for writeup activations (eg. a value of `2` means that the player can have `player_headshots+2` writeups active at most). A value of `0` means that the player can have only as many writeups active as its own number of headshots. +* `mail_verification_token_validity`: How long will the mail verification tokens be active for. Can take intervals supported by php and `INTERVAL`, eg. 10 day, meaning 10 days from now +* `password_reset_token_validity`: How long will the password reset tokens be active for. Can take intervals supported by php and `INTERVAL`, eg. 10 day, meaning 10 days from now + * `api_bearer_enable` Enable Bearer authorizations API operations * `api_claim_timeout` set the rate limit for the api claim. One request per `api_claim_timeout`+1 seconds * `api_target_instances_timeout` set the rate limit for the target instances endpoint. One request per `api_target_instances_timeout`+1 seconds From 31662f712af85efa2064781c97ca491a668feb74 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 01:47:09 +0200 Subject: [PATCH 36/38] delete mail tokens when we change from 8/9 into status 10 --- .../m241030_064326_update_tau_player_add_deleted_status.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/migrations/m241030_064326_update_tau_player_add_deleted_status.php b/backend/migrations/m241030_064326_update_tau_player_add_deleted_status.php index f76fc6464..7580235ba 100644 --- a/backend/migrations/m241030_064326_update_tau_player_add_deleted_status.php +++ b/backend/migrations/m241030_064326_update_tau_player_add_deleted_status.php @@ -29,6 +29,8 @@ class m241030_064326_update_tau_player_add_deleted_status extends Migration ELSEIF NEW.status=10 AND OLD.status=0 THEN INSERT INTO stream SELECT * FROM archived_stream WHERE player_id=NEW.id; DELETE FROM archived_stream WHERE player_id=NEW.id; + ELSEIF NEW.status=10 AND (OLD.status=9 OR OLD.status=8) THEN + DELETE FROM player_token WHERE player_id=NEW.id AND `type`='email_verification'; END IF; END"; From 9b2a57bf7ac01d76bbf66d3b35ffcb0763389d0a Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 01:47:38 +0200 Subject: [PATCH 37/38] populate player_token and drop columns from player --- .../m241104_201004_drop_token_columns_from_player_table.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/migrations/m241104_201004_drop_token_columns_from_player_table.php b/backend/migrations/m241104_201004_drop_token_columns_from_player_table.php index 0b2c3b0b6..bca79d34d 100644 --- a/backend/migrations/m241104_201004_drop_token_columns_from_player_table.php +++ b/backend/migrations/m241104_201004_drop_token_columns_from_player_table.php @@ -12,6 +12,8 @@ class m241104_201004_drop_token_columns_from_player_table extends Migration */ public function safeUp() { + $this->db->createCommand("INSERT INTO player_token (player_id,type,token,expires_at,created_at) SELECT id,'password_reset',substr(password_reset_token,1,30),now()+ INTERVAL 24 HOUR,now() FROM player WHERE password_reset_token is not null and password_reset_token!=''")->execute(); + $this->db->createCommand("INSERT INTO player_token (player_id,type,token,expires_at,created_at) SELECT id,'email_verification',substr(verification_token,1,30),now()+ INTERVAL 24 HOUR,now() FROM player WHERE verification_token is not null and verification_token!=''")->execute(); $this->dropColumn('player', 'password_reset_token'); $this->dropColumn('player', 'verification_token'); } From 6f472f960f422263e3b5e509fff3b1d916379b03 Mon Sep 17 00:00:00 2001 From: Pantelis Roditis Date: Tue, 5 Nov 2024 01:47:46 +0200 Subject: [PATCH 38/38] tbu no longer needed --- ...m241104_231037_drop_tbu_player_trigger.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 backend/migrations/m241104_231037_drop_tbu_player_trigger.php diff --git a/backend/migrations/m241104_231037_drop_tbu_player_trigger.php b/backend/migrations/m241104_231037_drop_tbu_player_trigger.php new file mode 100644 index 000000000..19a46f5d8 --- /dev/null +++ b/backend/migrations/m241104_231037_drop_tbu_player_trigger.php @@ -0,0 +1,21 @@ +db->createCommand($this->DROP_SQL)->execute(); + } + + public function down() + { + echo "Nothing to reverse..."; + } +}