diff --git a/api/config/routes.php b/api/config/routes.php index ca7ede9..c973f56 100644 --- a/api/config/routes.php +++ b/api/config/routes.php @@ -7,6 +7,9 @@ return [ '/accounts/change-email/submit-new-email' => 'accounts/change-email-submit-new-email', '/accounts/change-email/confirm-new-email' => 'accounts/change-email-confirm-new-email', + 'POST /two-factor-auth' => 'two-factor-auth/activate', + 'DELETE /two-factor-auth' => 'two-factor-auth/disable', + '/oauth2/v1/' => 'oauth/', '/account/v1/info' => 'identity-info/index', diff --git a/api/controllers/TwoFactorAuthController.php b/api/controllers/TwoFactorAuthController.php index 514b8cc..b6a50ee 100644 --- a/api/controllers/TwoFactorAuthController.php +++ b/api/controllers/TwoFactorAuthController.php @@ -34,4 +34,34 @@ class TwoFactorAuthController extends Controller { return $model->getCredentials(); } + public function actionActivate() { + $account = Yii::$app->user->identity; + $model = new TwoFactorAuthForm($account, ['scenario' => TwoFactorAuthForm::SCENARIO_ACTIVATE]); + if (!$model->activate()) { + return [ + 'success' => false, + 'errors' => $model->getFirstErrors(), + ]; + } + + return [ + 'success' => true, + ]; + } + + public function actionDisable() { + $account = Yii::$app->user->identity; + $model = new TwoFactorAuthForm($account, ['scenario' => TwoFactorAuthForm::SCENARIO_DISABLE]); + if (!$model->disable()) { + return [ + 'success' => false, + 'errors' => $model->getFirstErrors(), + ]; + } + + return [ + 'success' => true, + ]; + } + } diff --git a/api/models/profile/TwoFactorAuthForm.php b/api/models/profile/TwoFactorAuthForm.php index 9c432e4..398cf33 100644 --- a/api/models/profile/TwoFactorAuthForm.php +++ b/api/models/profile/TwoFactorAuthForm.php @@ -18,7 +18,7 @@ use yii\base\ErrorException; class TwoFactorAuthForm extends ApiForm { - const SCENARIO_ENABLE = 'enable'; + const SCENARIO_ACTIVATE = 'enable'; const SCENARIO_DISABLE = 'disable'; public $token; @@ -36,11 +36,13 @@ class TwoFactorAuthForm extends ApiForm { } public function rules() { - $on = [self::SCENARIO_ENABLE, self::SCENARIO_DISABLE]; + $bothScenarios = [self::SCENARIO_ACTIVATE, self::SCENARIO_DISABLE]; return [ - ['token', 'required', 'message' => E::OTP_TOKEN_REQUIRED, 'on' => $on], - ['token', TotpValidator::class, 'account' => $this->account, 'window' => 30, 'on' => $on], - ['password', PasswordRequiredValidator::class, 'account' => $this->account, 'on' => $on], + ['account', 'validateOtpDisabled', 'on' => self::SCENARIO_ACTIVATE], + ['account', 'validateOtpEnabled', 'on' => self::SCENARIO_DISABLE], + ['token', 'required', 'message' => E::OTP_TOKEN_REQUIRED, 'on' => $bothScenarios], + ['token', TotpValidator::class, 'account' => $this->account, 'window' => 30, 'on' => $bothScenarios], + ['password', PasswordRequiredValidator::class, 'account' => $this->account, 'on' => $bothScenarios], ]; } @@ -58,6 +60,47 @@ class TwoFactorAuthForm extends ApiForm { ]; } + public function activate(): bool { + if (!$this->validate()) { + return false; + } + + $account = $this->account; + $account->is_otp_enabled = true; + if (!$account->save()) { + throw new ErrorException('Cannot enable otp for account'); + } + + return true; + } + + public function disable(): bool { + if (!$this->validate()) { + return false; + } + + $account = $this->account; + $account->is_otp_enabled = false; + $account->otp_secret = null; + if (!$account->save()) { + throw new ErrorException('Cannot disable otp for account'); + } + + return true; + } + + public function validateOtpDisabled($attribute) { + if ($this->account->is_otp_enabled) { + $this->addError($attribute, E::OTP_ALREADY_ENABLED); + } + } + + public function validateOtpEnabled($attribute) { + if (!$this->account->is_otp_enabled) { + $this->addError($attribute, E::OTP_NOT_ENABLED); + } + } + public function getAccount(): Account { return $this->account; } diff --git a/common/helpers/Error.php b/common/helpers/Error.php index 6d2347c..8425256 100644 --- a/common/helpers/Error.php +++ b/common/helpers/Error.php @@ -56,5 +56,7 @@ final class Error { const OTP_TOKEN_REQUIRED = 'error.otp_token_required'; const OTP_TOKEN_INCORRECT = 'error.otp_token_incorrect'; + const OTP_ALREADY_ENABLED = 'error.otp_already_enabled'; + const OTP_NOT_ENABLED = 'error.otp_not_enabled'; }