├── .bowerrc ├── .gitignore ├── LICENSE.md ├── README.md ├── codeception.yml ├── commands └── BuildController.php ├── components ├── JwtAuth.php ├── ModelTrait.php └── UrlManager.php ├── composer.json ├── composer.lock ├── config ├── console-test.php ├── console.php ├── params.php ├── web-test.php └── web.php ├── controllers ├── BaseApiController.php ├── SiteController.php └── v1 │ ├── AuthController.php │ ├── PublicController.php │ └── UserController.php ├── deploy.sh ├── env.php.example ├── functions.php ├── gulpfile.js ├── models ├── Profile.php ├── Role.php ├── User.php ├── UserToken.php └── forms │ ├── ContactForm.php │ ├── ForgotForm.php │ ├── LoginEmailForm.php │ └── LoginForm.php ├── package.json ├── requirements.php ├── runtime └── .gitignore ├── tests ├── _bootstrap.php ├── _data │ ├── .gitignore │ └── dump.sql ├── _output │ └── .gitignore ├── _support │ ├── AcceptanceTester.php │ ├── FunctionalTester.php │ ├── Helper │ │ └── FastDb.php │ └── UnitTester.php ├── acceptance.suite.yml.example ├── acceptance │ ├── AboutCest.php │ ├── ContactCest.php │ ├── HomeCest.php │ ├── LoginCest.php │ └── _bootstrap.php ├── bin │ ├── yii │ └── yii.bat ├── functional.suite.yml ├── functional │ ├── AngularPageCest.php │ └── _bootstrap.php ├── unit.suite.yml └── unit │ ├── _bootstrap.php │ └── models │ ├── User1Test.php │ └── User2Test.php ├── views ├── _mail │ ├── layouts │ │ └── html.php │ └── user │ │ ├── confirmEmail.php │ │ ├── forgotPassword.php │ │ ├── layouts │ │ └── html.php │ │ └── loginToken.php └── site │ └── index.php ├── web ├── assets │ └── .gitignore ├── compiled │ └── .gitignore ├── css │ └── site.css ├── favicon.ico ├── index-test.php ├── index.php ├── js │ ├── app.js │ ├── app │ │ ├── app.config.js │ │ ├── app.controller.contact.js │ │ ├── app.controller.index.js │ │ ├── app.controller.nav.js │ │ └── app.routes.js │ ├── auth │ │ ├── auth.controller.account.js │ │ ├── auth.controller.confirm.js │ │ ├── auth.controller.login.js │ │ ├── auth.controller.loginCallback.js │ │ ├── auth.controller.loginEmail.js │ │ ├── auth.controller.profile.js │ │ ├── auth.controller.register.js │ │ ├── auth.controller.reset.js │ │ ├── auth.init.js │ │ └── auth.routes.js │ └── services │ │ ├── factory.ajaxhelper.js │ │ ├── factory.api.js │ │ └── factory.auth.js ├── robots.txt ├── vendor │ ├── angular-http-auth │ │ └── 50c1fe7 │ │ │ ├── http-auth-interceptor.js │ │ │ └── http-auth-interceptor.min.js │ ├── angular │ │ └── 1.5.8 │ │ │ ├── angular-animate.js │ │ │ ├── angular-animate.min.js │ │ │ ├── angular-animate.min.js.map │ │ │ ├── angular-route.js │ │ │ ├── angular-route.min.js │ │ │ ├── angular-route.min.js.map │ │ │ ├── angular.js │ │ │ ├── angular.min.js │ │ │ └── angular.min.js.map │ ├── bootstrap │ │ └── 3.3.5 │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ └── bootstrap.min.css │ ├── ngStorage │ │ └── 0.3.9 │ │ │ ├── ngStorage.js │ │ │ └── ngStorage.min.js │ └── ui-bootstrap │ │ ├── ui-bootstrap-tpls-0.13.4.js │ │ └── ui-bootstrap-tpls-0.13.4.min.js └── views │ ├── app │ ├── 404.html │ ├── about.html │ ├── contact.html │ └── index.html │ └── auth │ ├── account.html │ ├── confirm.html │ ├── login-callback.html │ ├── login-email.html │ ├── login.html │ ├── profile.html │ ├── register.html │ └── reset.html ├── yii └── yii.bat /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "vendor/bower" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | 29 | tests/_output/* 30 | tests/_support/_generated 31 | 32 | # env file 33 | env.php 34 | 35 | # node 36 | node_modules 37 | 38 | # index cache file 39 | /web/index.html -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Yii framework is free software. It is released under the terms of 2 | the following BSD License. 3 | 4 | Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | * Neither the name of Yii Software LLC nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii 2 Angular 2 | ============================ 3 | 4 | Yii 2 Angular is a boilerplate for Yii 2 + angular. It is based on 5 | [Yii 2 Basic Application](https://github.com/yiisoft/yii2-app-basic) 6 | 7 | DEMO 8 | ------------ 9 | 10 | * [Demo](http://yii2a.amnahdev.com) 11 | 12 | INSTALLATION 13 | ------------ 14 | 15 | * Download/clone this repo ```git clone https://github.com/amnah/yii2-angular.git``` 16 | * Copy *env.php.example* file to *env.php* and modify as needed 17 | * Install packages and run migration 18 | 19 | ``` 20 | php composer.phar global require "fxp/composer-asset-plugin:~1.1.1" --prefer-dist 21 | php composer.phar update --prefer-dist 22 | npm install 23 | php yii migrate --migrationPath=@vendor/amnah/yii2-user/migrations 24 | ``` 25 | 26 | * Build assets 27 | 28 | ``` 29 | gulp build 30 | gulp watch # for development 31 | ``` 32 | 33 | * Set up apache/nginx vhost and visit site 34 | 35 | Yii 2 Vue 36 | ============================ 37 | 38 | The Vue version. This is currently on a separate 39 | [branch](https://github.com/amnah/yii2-angular/tree/vue). 40 | 41 | The instructions for setting it up are exactly the same - just download the 42 | [zip archive](https://github.com/amnah/yii2-angular/archive/vue.zip) and use that 43 | instead 44 | 45 | DEMO 46 | ------------ 47 | 48 | [Demo](http://vue.amnahdev.com) -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | paths: 3 | tests: tests 4 | log: tests/_output 5 | data: tests/_data 6 | helpers: tests/_support 7 | settings: 8 | bootstrap: _bootstrap.php 9 | memory_limit: 1024M 10 | colors: true 11 | modules: 12 | config: 13 | Yii2: 14 | configFile: 'config/web-test.php' 15 | cleanup: false 16 | 17 | # To enable code coverage: 18 | #coverage: 19 | # #c3_url: http://localhost:8080/index-test.php/ 20 | # enabled: true 21 | # #remote: true 22 | # #remote_config: '../tests/codeception.yml' 23 | # whitelist: 24 | # include: 25 | # - models/* 26 | # - controllers/* 27 | # - commands/* 28 | # - mail/* 29 | # blacklist: 30 | # include: 31 | # - assets/* 32 | # - config/* 33 | # - runtime/* 34 | # - vendor/* 35 | # - views/* 36 | # - web/* 37 | # - tests/* 38 | -------------------------------------------------------------------------------- /commands/BuildController.php: -------------------------------------------------------------------------------- 1 | request->isConsoleRequest) { 22 | Yii::setAlias('@web', $webPath); 23 | Yii::setAlias('@webroot', $webPath); 24 | } 25 | 26 | // disable debug module from view and render template 27 | // @link http://stackoverflow.com/questions/23560278/how-can-i-disable-yii-debug-toolbar-on-a-specific-view/28903986#28903986 28 | $debugModule = Yii::$app->getModule("debug"); 29 | if ($debugModule) { 30 | $view = $this->view; 31 | $view->off($view::EVENT_END_BODY, [$debugModule, 'renderToolbar']); 32 | } 33 | $html = $this->render("//site/index", compact("date")); 34 | 35 | // update compiled revision dirs 36 | if ($date) { 37 | // get existing compiled dirs 38 | $existingCompiled = glob("$webPath/compiled-*", GLOB_ONLYDIR); 39 | $existingCompiled = implode(" ", $existingCompiled); 40 | 41 | // copy compiled dir 42 | $cmd = "cp -rf $webPath/compiled $webPath/compiled-$date"; 43 | $this->stdout("Copying new dir [ $cmd ]\n", Console::FG_YELLOW); 44 | shell_exec($cmd); 45 | 46 | // remove old compiled dirs 47 | $cmd = "rm -rf $existingCompiled"; 48 | $this->stdout("Removing old dirs [ $cmd ]\n", Console::FG_YELLOW); 49 | shell_exec($cmd); 50 | } 51 | 52 | // write view file 53 | $filePath = "$webPath/index.html"; 54 | @file_put_contents($filePath, $html); 55 | if (Yii::$app->request->isConsoleRequest) { 56 | $this->stdout("Writing index [ $filePath ]\n", Console::FG_YELLOW); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /components/JwtAuth.php: -------------------------------------------------------------------------------- 1 | key)) { 75 | throw new InvalidConfigException(get_class($this) . "::key must be configured with a secret key."); 76 | } 77 | 78 | $this->request = Yii::$app->request; 79 | $this->response = Yii::$app->response; 80 | } 81 | 82 | /** 83 | * @inheritdoc 84 | */ 85 | public function authenticate($user, $request, $response) 86 | { 87 | if ($request->getIsOptions()) { 88 | return true; 89 | } 90 | 91 | $payload = $this->getTokenPayload(); 92 | if (!$payload) { 93 | return null; 94 | } 95 | 96 | // check for valid auth hash 97 | /** @var User $class */ 98 | $class = Yii::$app->user->identityClass; 99 | $user = $class::findIdentity($payload->user->id); 100 | if (!$this->checkUserAuthHash($user, $payload->auth)) { 101 | return null; 102 | } 103 | 104 | // set identity for this one request 105 | // this is needed for other filters to work properly, eg, \yii\filters\RateLimiter 106 | Yii::$app->user->setIdentity($user); 107 | return $this->authenticatedUser = $user; 108 | } 109 | 110 | /** 111 | * Get the authenticated user 112 | * @return User 113 | */ 114 | public function getAuthenticatedUser() 115 | { 116 | return $this->authenticatedUser; 117 | } 118 | 119 | /** 120 | * Calculate auth hash for "auth" claim in jwt 121 | * This is used to invalidate all existing tokens when the user changes his password or auth_key 122 | * @param User $user 123 | * @return string 124 | */ 125 | protected function calculateAuthHash($user) 126 | { 127 | return sha1($user->password . $user->getAuthKey()); 128 | } 129 | 130 | /** 131 | * Check if auth claim matches hash 132 | * @param User $user 133 | * @param string $hash 134 | * @return bool 135 | */ 136 | public function checkUserAuthHash($user, $hash) 137 | { 138 | return $user && $this->calculateAuthHash($user) == $hash; 139 | } 140 | 141 | /** 142 | * Get token payload from $_GET, header, and cookie (in that order) 143 | * @return object 144 | */ 145 | public function getTokenPayload() 146 | { 147 | if ($this->payload !== null) { 148 | return $this->payload; 149 | } 150 | 151 | // check $_GET, header, and cookie 152 | $token = $this->request->get($this->tokenParam); 153 | if (!$token) { 154 | $authHeader = $this->request->getHeaders()->get("Authorization"); 155 | if ($authHeader !== null && preg_match("/^Bearer\\s+(.*?)$/", $authHeader, $matches)) { 156 | $token = $matches[1]; 157 | } 158 | } 159 | if (!$token) { 160 | $token = $this->request->cookies->getValue($this->tokenParam); 161 | if ($token) { 162 | $this->fromJwtCookie = true; 163 | } 164 | } 165 | 166 | // decode and store payload 167 | $this->payload = $token ? $this->decode($token) : false; 168 | return $this->payload; 169 | } 170 | 171 | /** 172 | * Get refresh token payload from $_GET or cookie 173 | * @return object 174 | */ 175 | public function getRefreshTokenPayload() 176 | { 177 | // check $_GET and then cookie 178 | $refreshToken = $this->request->get($this->refreshTokenParam); 179 | if (!$refreshToken) { 180 | $refreshToken = $this->request->cookies->getValue($this->refreshTokenParam); 181 | if ($refreshToken) { 182 | $this->fromJwtCookie = true; 183 | } 184 | } 185 | 186 | // decode token 187 | return $refreshToken ? $this->decode($refreshToken) : false; 188 | } 189 | 190 | /** 191 | * Add token in cookie 192 | * @param string $cookieName 193 | * @param string $token 194 | * @param int $exp 195 | */ 196 | public function addCookieToken($cookieName, $token, $exp) 197 | { 198 | $this->response->cookies->add(new Cookie([ 199 | "name" => $cookieName, 200 | "value" => $token, 201 | "secure" => $this->request->isSecureConnection, 202 | "expire" => $exp, 203 | ])); 204 | } 205 | 206 | /** 207 | * Remove token cookie 208 | */ 209 | public function removeCookieToken() 210 | { 211 | $this->response->cookies->remove($this->tokenParam); 212 | return $this; 213 | } 214 | 215 | /** 216 | * Remove refresh token cookie 217 | */ 218 | public function removeRefreshCookieToken() 219 | { 220 | $this->response->cookies->remove($this->refreshTokenParam); 221 | return $this; 222 | } 223 | 224 | /** 225 | * Encode data into jwt token string 226 | * @param array|object $data 227 | * @return string 228 | */ 229 | public function encode($data) 230 | { 231 | $data = (array) $data; 232 | $data = array_merge($this->getTokenDefaults(), $data); 233 | return JWT::encode($data, $this->key, $this->algorithm); 234 | } 235 | 236 | /** 237 | * Decode jwt token string 238 | * @param string $token 239 | * @return object|bool 240 | * @throws Exception 241 | */ 242 | public function decode($token) 243 | { 244 | JWT::$leeway = $this->leeway; 245 | try { 246 | $payload = JWT::decode($token, $this->key, [$this->algorithm]); 247 | } catch (Exception $e) { 248 | return false; 249 | } 250 | 251 | // ensure that iss, aud, and csrf are good 252 | $tokenDefaults = $this->getTokenDefaults(); 253 | if ($payload->iss != $tokenDefaults["iss"] || $payload->aud != $tokenDefaults["aud"]) { 254 | return false; 255 | } 256 | if (!empty($payload->csrf) && !$this->request->validateCsrfToken($payload->csrf)) { 257 | return false; 258 | } 259 | return $payload; 260 | } 261 | 262 | /** 263 | * Get token defaults 264 | * @return array 265 | * @link http://websec.io/2014/08/04/Securing-Requests-with-JWT.html 266 | */ 267 | protected function getTokenDefaults() 268 | { 269 | $hostInfo = parse_url($this->request->getHostInfo(), PHP_URL_HOST); 270 | $referrerInfo = parse_url($this->request->getReferrer(), PHP_URL_HOST); 271 | return [ 272 | "iss" => $hostInfo, 273 | "aud" => $referrerInfo ?: $hostInfo, 274 | "iat" => time(), 275 | ]; 276 | } 277 | 278 | /** 279 | * Generate a jwt token for user 280 | * @param User $user 281 | * @param bool $rememberMe 282 | * @param bool $jwtCookie 283 | * @return string 284 | */ 285 | public function generateUserToken($user, $rememberMe = false, $jwtCookie = false) 286 | { 287 | $data = [ 288 | "sub" => (int) $user->id, 289 | "user" => $user->toArray(), 290 | "rememberMe" => (int) $rememberMe, 291 | "auth" => $this->calculateAuthHash($user), 292 | ]; 293 | 294 | // compute exp 295 | $ttl = $rememberMe ? $this->ttlRememberMe : $this->ttl; 296 | $data["exp"] = is_string($ttl) ? strtotime($ttl) : time() + $ttl; 297 | 298 | // compute csrf if using cookie 299 | if ($jwtCookie) { 300 | $data["csrf"] = $this->request->getCsrfToken(); 301 | } 302 | 303 | $token = $this->encode($data); 304 | if ($jwtCookie) { 305 | $this->addCookieToken($this->tokenParam, $token, $data["exp"]); 306 | } 307 | return $token; 308 | } 309 | 310 | /** 311 | * Generate a jwt token for user based on access token 312 | * Note: this token does NOT expire, so you should have some way to revoke the access token 313 | * @param User $user 314 | * @param string $accessToken 315 | * @param bool $jwtCookie 316 | * @return string 317 | */ 318 | public function generateRefreshToken($user, $accessToken, $jwtCookie = false) 319 | { 320 | $data = [ 321 | "sub" => (int) $user->id, 322 | "auth" => $this->calculateAuthHash($user), 323 | "accessToken" => $accessToken, 324 | ]; 325 | 326 | $refreshToken = $this->encode($data); 327 | if ($jwtCookie) { 328 | $this->addCookieToken($this->refreshTokenParam, $refreshToken, strtotime("2037-12-31")); // far, far future 329 | } 330 | return $refreshToken; 331 | } 332 | 333 | /** 334 | * Renew token 335 | * @param object $payload 336 | * @return bool|string 337 | */ 338 | public function renewToken($payload) 339 | { 340 | // update exp and csrf 341 | // iat will be handled in [[getTokenDefaults()]] 342 | if (!empty($payload->exp)) { 343 | $duration = $payload->exp - $payload->iat; 344 | $payload->exp = time() + $duration; 345 | } 346 | if (!empty($payload->csrf)) { 347 | $payload->csrf = $this->request->getCsrfToken(); 348 | } 349 | 350 | $token = $this->encode($payload); 351 | if (!empty($payload->csrf)) { 352 | $this->addCookieToken($this->tokenParam, $token, $payload->exp); 353 | } 354 | return $token; 355 | } 356 | } -------------------------------------------------------------------------------- /components/ModelTrait.php: -------------------------------------------------------------------------------- 1 | load(Yii::$app->request->post(), $formName); 22 | } 23 | 24 | /** 25 | * Load post data into model and validate 26 | * Returns null if no post data is loaded. Otherwise returns validation result 27 | * @param string $formName 28 | * @param array $attributeNames 29 | * @return bool|null 30 | */ 31 | public function loadPostAndValidate($formName = "", $attributeNames = null) 32 | { 33 | if (!$this->loadPost($formName)) { 34 | return null; 35 | } 36 | return $this->validate($attributeNames); 37 | } 38 | 39 | /** 40 | * Load post data into model and save (with validation) 41 | * Returns null if no post data is loaded. Otherwise returns save result 42 | * @param string $formName 43 | * @param array $attributeNames 44 | * @return bool 45 | */ 46 | public function loadPostAndSave($formName = "", $attributeNames = null) 47 | { 48 | if (!$this->loadPost($formName)) { 49 | return null; 50 | } 51 | return $this->save(true, $attributeNames); 52 | } 53 | } -------------------------------------------------------------------------------- /components/UrlManager.php: -------------------------------------------------------------------------------- 1 | getPathInfo(); 29 | $params = $request->getQueryParams(); 30 | 31 | // check if we're calling a route that should be processed by yii (and not angular) 32 | foreach ($this->yiiRoutes as $yiiRoute) { 33 | if (strpos($pathInfo, $yiiRoute) === 0) { 34 | return [$pathInfo, $params]; 35 | } 36 | } 37 | 38 | // use default route 39 | return [$this->defaultRoute, []]; 40 | } 41 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/yii2-app-basic", 3 | "description": "Yii 2 Basic Project Template", 4 | "keywords": ["yii2", "framework", "basic", "project template"], 5 | "homepage": "http://www.yiiframework.com/", 6 | "type": "project", 7 | "license": "BSD-3-Clause", 8 | "support": { 9 | "issues": "https://github.com/yiisoft/yii2/issues?state=open", 10 | "forum": "http://www.yiiframework.com/forum/", 11 | "wiki": "http://www.yiiframework.com/wiki/", 12 | "irc": "irc://irc.freenode.net/yii", 13 | "source": "https://github.com/yiisoft/yii2" 14 | }, 15 | "minimum-stability": "dev", 16 | "require": { 17 | "php": ">=5.4.0", 18 | "yiisoft/yii2": "*", 19 | "yiisoft/yii2-bootstrap": "*", 20 | "yiisoft/yii2-swiftmailer": "*", 21 | "yiisoft/yii2-redis": "*", 22 | "himiklab/yii2-recaptcha-widget": "*", 23 | "firebase/php-jwt": "*", 24 | "amnah/yii2-user": "*", 25 | "amnah/yii2-debug": "*" 26 | }, 27 | "require-dev": { 28 | "yiisoft/yii2-gii": "*", 29 | "codeception/base": "^2.2.3", 30 | "codeception/verify": "~0.3.1", 31 | "codeception/specify": "~0.4.3" 32 | }, 33 | "config": { 34 | "process-timeout": 1800 35 | }, 36 | "scripts": { 37 | "post-create-project-cmd": [ 38 | "yii\\composer\\Installer::postCreateProject" 39 | ] 40 | }, 41 | "extra": { 42 | "yii\\composer\\Installer::postCreateProject": { 43 | "setPermission": [ 44 | { 45 | "runtime": "0777", 46 | "web/assets": "0777", 47 | "yii": "0755" 48 | } 49 | ], 50 | "generateCookieValidationKey": [ 51 | "config/web.php" 52 | ] 53 | }, 54 | "asset-installer-paths": { 55 | "npm-asset-library": "vendor/npm", 56 | "bower-asset-library": "vendor/bower" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/console-test.php: -------------------------------------------------------------------------------- 1 | [ // Fixture generation command line. 12 | 'class' => 'yii\faker\FixtureController', 13 | ], 14 | ]; 15 | */ 16 | 17 | if (YII_ENV_DEV) { 18 | $config['bootstrap'][] = 'gii'; 19 | $config['modules']['gii'] = [ 20 | 'class' => 'yii\gii\Module', 21 | ]; 22 | } 23 | 24 | unset($config['components']['request']); 25 | unset($config['components']['log']['traceLevel']); 26 | unset($config['components']['user']); 27 | unset($config['components']['errorHandler']); 28 | 29 | return $config; 30 | -------------------------------------------------------------------------------- /config/params.php: -------------------------------------------------------------------------------- 1 | 'admin@example.com', 5 | ]; 6 | -------------------------------------------------------------------------------- /config/web-test.php: -------------------------------------------------------------------------------- 1 | 'yii2angular', 8 | 'name' => 'Yii 2 Angular', 9 | 'basePath' => dirname(__DIR__), 10 | 'timeZone' => 'UTC', 11 | 'language' => 'en-US', 12 | 'params' => require __DIR__ . '/params.php', 13 | 'bootstrap' => ['log'], 14 | 'components' => [ 15 | 'request' => [ 16 | 'cookieValidationKey' => env('YII_KEY'), 17 | 'parsers' => [ 18 | 'application/json' => 'yii\web\JsonParser', // required for POST input via `php://input` 19 | ] 20 | ], 21 | 'jwtAuth' => [ 22 | 'class' => 'app\components\JwtAuth', 23 | 'key' => env('YII_KEY'), 24 | ], 25 | 'redis' => [ 26 | 'class' => 'yii\redis\Connection', 27 | ], 28 | 'cache' => [ 29 | 'class' => 'yii\redis\Cache', 30 | ], 31 | 'user' => [ 32 | 'class' => 'amnah\yii2\user\components\User', 33 | 'identityClass' => 'app\models\User', 34 | 'enableSession' => false, 35 | 'enableAutoLogin' => false, 36 | 'loginUrl' => null, 37 | ], 38 | 'mailer' => [ 39 | 'class' => 'yii\swiftmailer\Mailer', 40 | 'viewPath' => '@app/views/_mail', 41 | 'useFileTransport' => env('MAIL_FILE_TRANSPORT'), 42 | 'transport' => [ 43 | 'class' => 'Swift_SmtpTransport', 44 | 'host' => env('MAIL_HOST'), 45 | 'port' => env('MAIL_PORT'), 46 | 'username' => env('MAIL_USER'), 47 | 'password' => env('MAIL_PASS'), 48 | 'encryption' => env('MAIL_ENCRYPTION'), 49 | ], 50 | ], 51 | 'log' => [ 52 | 'traceLevel' => YII_DEBUG ? 3 : 0, 53 | 'targets' => [ 54 | [ 55 | 'class' => 'yii\log\FileTarget', 56 | 'levels' => ['error', 'warning'], 57 | ], 58 | ], 59 | ], 60 | 'db' => [ 61 | 'class' => 'yii\db\Connection', 62 | 'dsn' => env('DB_DSN'), 63 | 'username' => env('DB_USER'), 64 | 'password' => env('DB_PASS'), 65 | 'tablePrefix' => env('DB_PREFIX'), 66 | 'charset' => 'utf8', 67 | 'enableSchemaCache' => YII_ENV_PROD, 68 | ], 69 | 'urlManager' => [ 70 | 'class' => 'app\components\UrlManager', 71 | 'enablePrettyUrl' => true, 72 | 'showScriptName' => false, 73 | 'rules' => [], 74 | ], 75 | ], 76 | 'modules' => [ 77 | 'user' => [ 78 | 'class' => 'amnah\yii2\user\Module', 79 | 'emailViewPath' => '@app/views/_mail/user', // eg, @app/views/_mail/user/confirmEmail.php 80 | ], 81 | ], 82 | ]; 83 | 84 | // ------------------------------------------------------------------------ 85 | // Dev 86 | // ------------------------------------------------------------------------ 87 | $debugModule = 'amnah\yii2\debug\Module'; 88 | if (YII_ENV_DEV) { 89 | $config['bootstrap'][] = 'debug'; 90 | $config['modules']['debug'] = [ 91 | 'class' => $debugModule, 92 | 'allowedIPs' => ['*'], 93 | 'limitToCurrentRequest' => false, 94 | ]; 95 | 96 | $config['bootstrap'][] = 'gii'; 97 | $config['modules']['gii'] = [ 98 | 'class' => 'yii\gii\Module', 99 | 'allowedIPs' => ['*'], 100 | ]; 101 | } 102 | 103 | // ------------------------------------------------------------------------ 104 | // Prod 105 | // ------------------------------------------------------------------------ 106 | if (YII_ENV_PROD) { 107 | if (isForceDebug()) { 108 | // enable debug for current ip 109 | $userIp = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; 110 | $config['bootstrap'][] = 'debug'; 111 | $config['modules']['debug'] = [ 112 | 'class' => $debugModule, 113 | 'allowedIPs' => [$userIp], 114 | 'limitToCurrentRequest' => false, 115 | ]; 116 | } 117 | } 118 | 119 | 120 | 121 | 122 | return $config; 123 | -------------------------------------------------------------------------------- /controllers/BaseApiController.php: -------------------------------------------------------------------------------- 1 | jwtAuth = Yii::$app->get("jwtAuth"); 26 | $this->response = Yii::$app->get("response"); 27 | 28 | // set json output and use "pretty" output in debug mode 29 | $this->response->format = 'json'; 30 | $this->response->formatters['json'] = [ 31 | 'class' => 'yii\web\JsonResponseFormatter', 32 | 'prettyPrint' => YII_DEBUG, // use "pretty" output in debug mode 33 | ]; 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | public function beforeAction($action) 40 | { 41 | if (!parent::beforeAction($action)) { 42 | return false; 43 | } 44 | 45 | // check for CORS preflight OPTIONS. if so, then return false so that it doesn't run 46 | // the controller action 47 | // @link https://github.com/yiisoft/yii2/pull/8626/files 48 | // @link https://github.com/yiisoft/yii2/issues/6254 49 | if (Yii::$app->request->isOptions) { 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | */ 59 | public function behaviors() 60 | { 61 | return [ 62 | 63 | // cors filter - should be before authentication 64 | /* 65 | 'corsFilter' => [ 66 | "class" => Cors::className(), 67 | "cors" => [ 68 | 'Origin' => ['*'], 69 | 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], 70 | 'Access-Control-Request-Headers' => ['*'], 71 | 'Access-Control-Allow-Credentials' => true, // allow cookies 72 | 'Access-Control-Max-Age' => 1800, // 30 minutes 73 | 'Access-Control-Expose-Headers' => [], 74 | ], 75 | ], 76 | */ 77 | 78 | 'jwtAuth' => $this->jwtAuth, 79 | 80 | // rate limiter - should be after authentication 81 | /* 82 | 'rateLimiter' => [ 83 | 'class' => RateLimiter::className(), 84 | ], 85 | */ 86 | ]; 87 | } 88 | } -------------------------------------------------------------------------------- /controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | render("index", compact("date")); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /controllers/v1/AuthController.php: -------------------------------------------------------------------------------- 1 | request; 24 | $model = new LoginForm(); 25 | if ($model->loadPostAndValidate()) { 26 | $user = $model->getUser(); 27 | $rememberMe = $request->post("rememberMe", true); 28 | $jwtCookie = $request->post("jwtCookie", true); 29 | return ["success" => $this->generateAuthSuccess($user, $rememberMe, $jwtCookie)]; 30 | } 31 | return ["errors" => $model->errors]; 32 | } 33 | 34 | /** 35 | * Logout 36 | */ 37 | public function actionLogout() 38 | { 39 | $this->jwtAuth->removeCookieToken()->removeRefreshCookieToken(); 40 | return ["success" => true]; 41 | } 42 | 43 | /** 44 | * Register 45 | */ 46 | public function actionRegister() 47 | { 48 | $user = new User(["scenario" => "register"]); 49 | $profile = new Profile(); 50 | 51 | // ensure that both models get validated for errors 52 | $userValidate = $user->loadPostAndValidate(); 53 | $profileValidate = $profile->loadPostAndValidate(); 54 | if (!$userValidate || !$profileValidate) { 55 | return ["errors" => array_merge($user->errors, $profile->errors)]; 56 | } 57 | 58 | // create user/profile 59 | $user->setRegisterAttributes(Role::ROLE_USER)->save(false); 60 | $profile->setUser($user->id)->save(false); 61 | 62 | // determine userToken type to see if we need to send email 63 | $userTokenType = null; 64 | if ($user->status == $user::STATUS_INACTIVE) { 65 | $userTokenType = UserToken::TYPE_EMAIL_ACTIVATE; 66 | } elseif ($user->status == $user::STATUS_UNCONFIRMED_EMAIL) { 67 | $userTokenType = UserToken::TYPE_EMAIL_CHANGE; 68 | } 69 | 70 | // check if we have a userToken type to process, or just generate jwt data 71 | if ($userTokenType) { 72 | $userToken = UserToken::generate($user->id, $userTokenType); 73 | $user->sendEmailConfirmation($userToken); 74 | return ["success" => ["userToken" => 1]]; 75 | } else { 76 | $request = Yii::$app->request; 77 | $rememberMe = $request->post("rememberMe", true); 78 | $jwtCookie = $request->post("jwtCookie", true); 79 | return ["success" => $this->generateAuthSuccess($user, $rememberMe, $jwtCookie)]; 80 | } 81 | } 82 | 83 | /** 84 | * Confirm email 85 | */ 86 | public function actionConfirm() 87 | { 88 | /** @var User $user */ 89 | 90 | // search for userToken 91 | $success = false; 92 | $email = ""; 93 | $token = Yii::$app->request->get("token"); 94 | $userToken = UserToken::findByToken($token, [UserToken::TYPE_EMAIL_ACTIVATE, UserToken::TYPE_EMAIL_CHANGE]); 95 | if ($userToken) { 96 | 97 | // find user and ensure that another user doesn't have that email 98 | // for example, user registered another account before confirming change of email 99 | $user = User::findOne($userToken->user_id); 100 | $newEmail = $userToken->data; 101 | if ($user->confirm($newEmail)) { 102 | $success = true; 103 | } 104 | 105 | // set email and delete token 106 | $email = $newEmail ?: $user->email; 107 | $userToken->delete(); 108 | } 109 | 110 | if ($success) { 111 | return ["success" => $email]; 112 | } elseif ($email) { 113 | return ["error" => "Email is already active"]; 114 | } else { 115 | return ["error" => "Invalid token"]; 116 | } 117 | } 118 | 119 | /** 120 | * Renew token 121 | */ 122 | public function actionRenewToken($refreshDb = 0) 123 | { 124 | /** @var User $user */ 125 | 126 | $user = null; 127 | $jwtAuth = $this->jwtAuth; 128 | $payload = $jwtAuth->getTokenPayload(); 129 | 130 | // renew token directly or generate fresh from db 131 | if ($payload && !$refreshDb) { 132 | $token = $jwtAuth->renewToken($payload); 133 | return ["success" => ["user" => $payload->user, "token" => $token]]; 134 | } elseif ($payload && $refreshDb) { 135 | $user = Yii::$app->user->identityClass; 136 | $user = $user::findIdentity($payload->user->id); 137 | return ["success" => $this->generateAuthSuccess($user, $payload->rememberMe, $jwtAuth->fromJwtCookie)]; 138 | } 139 | 140 | // attempt to renew token using refresh token 141 | return $this->actionUseRefreshToken(); 142 | } 143 | 144 | /** 145 | * Get refresh token 146 | * Note: PERMANENT. You should have some way to revoke these access tokens 147 | */ 148 | public function actionRequestRefreshToken() 149 | { 150 | /** @var User $user */ 151 | 152 | $jwtAuth = $this->jwtAuth; 153 | $payload = $jwtAuth->getTokenPayload(); 154 | if (!$payload) { 155 | return ["error" => Yii::t("app", "Invalid token")]; 156 | } 157 | 158 | // get user based off of id and get access token 159 | $user = Yii::$app->user->identityClass; 160 | $user = $user::findIdentity($payload->user->id); 161 | 162 | // generate refresh token 163 | // you can change this, eg, UserToken.token instead of User.access_token 164 | $token = $user->access_token; 165 | return ["success" => $jwtAuth->generateRefreshToken($user, $token, $jwtAuth->fromJwtCookie)]; 166 | } 167 | 168 | /** 169 | * Use refreshToken to refresh the regular token 170 | */ 171 | public function actionUseRefreshToken() 172 | { 173 | /** @var User $user */ 174 | 175 | // get token/payload 176 | $jwtAuth = $this->jwtAuth; 177 | $payload = $jwtAuth->getRefreshTokenPayload(); 178 | $returnError = ["error" => Yii::t("app", "Invalid token")]; 179 | if (!$payload) { 180 | return $returnError; 181 | } 182 | 183 | // find user and check data 184 | $user = Yii::$app->user->identityClass; 185 | $user = $user::findIdentityByAccessToken($payload->accessToken); 186 | if (!$jwtAuth->checkUserAuthHash($user, $payload->auth)) { 187 | return $returnError; 188 | } 189 | 190 | // use $rememberMe = false for refresh tokens. faster expiration = more security 191 | $rememberMe = false; 192 | return ["success" => $this->generateAuthSuccess($user, $rememberMe, $jwtAuth->fromJwtCookie)]; 193 | } 194 | 195 | /** 196 | * Remove refresh token 197 | */ 198 | public function actionRemoveRefreshToken() 199 | { 200 | $this->jwtAuth->removeRefreshCookieToken(); 201 | return ["success" => true]; 202 | } 203 | 204 | /** 205 | * Login via email 206 | */ 207 | public function actionLoginEmail() 208 | { 209 | $loginEmailForm = new LoginEmailForm(); 210 | if ($loginEmailForm->loadPost() && $loginEmailForm->sendEmail()) { 211 | return ["success" => ["user" => $loginEmailForm->getUser()]]; 212 | } 213 | 214 | return ["errors" => $loginEmailForm->errors]; 215 | } 216 | 217 | /** 218 | * Login/register callback via email 219 | */ 220 | public function actionLoginCallback($token, $jwtCookie = true) 221 | { 222 | /** @var User $user */ 223 | 224 | // check token and log user in directly 225 | $userToken = UserToken::findByToken($token, UserToken::TYPE_EMAIL_LOGIN); 226 | if (!$userToken) { 227 | return ["error" => "Invalid token"]; 228 | } 229 | 230 | // log user in directly 231 | $rememberMe = $userToken->data; 232 | $user = $userToken->user; 233 | if ($user) { 234 | $userToken->delete(); 235 | return ["success" => $this->generateAuthSuccess($user, $rememberMe, $jwtCookie)]; 236 | } 237 | 238 | // check for post data (for registering) 239 | $user = new User(); 240 | $profile = new Profile(); 241 | if (!$user->loadPost()) { 242 | return ["success" => true, "email" => $userToken->data]; 243 | } 244 | 245 | // ensure that email is taken from the $userToken (NOT from user input) 246 | $user->email = $userToken->data; 247 | $rememberMe = 1; 248 | 249 | // load profile, validate, and register 250 | $userValidate = $user->validate(); 251 | $profileValidate = $profile->loadPostAndValidate(); 252 | if ($userValidate && $profileValidate) { 253 | $user->setRegisterAttributes(Role::ROLE_USER, User::STATUS_ACTIVE)->save(); 254 | $profile->setUser($user->id)->save(); 255 | $userToken->delete(); 256 | return ["success" => $this->generateAuthSuccess($user, $rememberMe, $jwtCookie)]; 257 | } else { 258 | $errors = array_merge($user->errors, $profile->errors); 259 | return ["errors" => $errors]; 260 | } 261 | } 262 | 263 | /** 264 | * Forgot 265 | */ 266 | public function actionForgot() 267 | { 268 | $model = new ForgotForm(); 269 | if ($model->loadPost() && $model->sendForgotEmail()) { 270 | return ["success" => true]; 271 | } 272 | return ["errors" => $model->errors]; 273 | 274 | } 275 | 276 | /** 277 | * Reset 278 | */ 279 | public function actionReset($token) 280 | { 281 | /** @var User $user */ 282 | 283 | // get user token and check expiration 284 | $userToken = UserToken::findByToken($token, UserToken::TYPE_PASSWORD_RESET); 285 | if (!$userToken) { 286 | return ["error" => "Invalid token"]; 287 | } 288 | 289 | // get user and load post 290 | // return user email if user hasn't submitted yet 291 | $user = User::findOne($userToken->user_id); 292 | if (!$user->loadPost()) { 293 | return ["success" => $user->email]; 294 | } 295 | 296 | // set scenario and save new password 297 | $user->setScenario("reset"); 298 | if ($user->save(true, ["password", "newPassword", "newPasswordConfirm"])) { 299 | $userToken->delete(); 300 | return ["success" => true]; 301 | } 302 | return ["errors" => $user->errors]; 303 | } 304 | 305 | /** 306 | * Generate auth success (for sending back to client) 307 | * @param User $user 308 | * @param bool $rememberMe 309 | * @param bool $jwtCookie 310 | * @return array 311 | */ 312 | protected function generateAuthSuccess($user, $rememberMe, $jwtCookie) 313 | { 314 | $token = $this->jwtAuth->generateUserToken($user, $rememberMe, $jwtCookie); 315 | return [ 316 | "user" => $user->toArray(), 317 | "token" => $token, 318 | ]; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /controllers/v1/PublicController.php: -------------------------------------------------------------------------------- 1 | params["adminEmail"]; 28 | $model->load(Yii::$app->request->post(), ""); 29 | if ($model->contact($toEmail)) { 30 | return ["success" => true]; 31 | } 32 | return ["errors" => $model->errors]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /controllers/v1/UserController.php: -------------------------------------------------------------------------------- 1 | jwtAuth->getAuthenticatedUser(); 22 | $user->setScenario("account"); 23 | 24 | // check for post input errors 25 | $loadedAndValidated = $user->loadPostAndValidate(); 26 | if ($loadedAndValidated === false) { 27 | return ["errors" => $user->errors]; 28 | } 29 | 30 | // process account update or find a $userToken (for pending email confirmation) 31 | $userToken = null; 32 | if ($loadedAndValidated) { 33 | 34 | // check if user changed his email 35 | $newEmail = $user->checkEmailChange(); 36 | if ($newEmail) { 37 | $userToken = UserToken::generate($user->id, UserToken::TYPE_EMAIL_CHANGE, $newEmail); 38 | $user->sendEmailConfirmation($userToken); 39 | } 40 | $user->save(false); 41 | } else { 42 | $userToken = UserToken::findByUser($user->id, UserToken::TYPE_EMAIL_CHANGE); 43 | } 44 | 45 | $hasPassword = (bool) $user->password; 46 | return ["success" => ["user" => $user, "userToken" => $userToken, "hasPassword" => $hasPassword]]; 47 | } 48 | 49 | /** 50 | * Resend email change 51 | */ 52 | public function actionChangeResend() 53 | { 54 | /** @var User $user */ 55 | 56 | $user = $this->jwtAuth->getAuthenticatedUser(); 57 | $userToken = UserToken::findByUser($user->id, UserToken::TYPE_EMAIL_CHANGE); 58 | if ($userToken) { 59 | $user->sendEmailConfirmation($userToken); 60 | return ["success" => true]; 61 | } 62 | return ["error" => true]; 63 | } 64 | 65 | /** 66 | * Cancel email change 67 | */ 68 | public function actionChangeCancel() 69 | { 70 | /** @var User $user */ 71 | 72 | $user = $this->jwtAuth->getAuthenticatedUser(); 73 | $userToken = UserToken::findByUser($user->id, UserToken::TYPE_EMAIL_CHANGE); 74 | if ($userToken) { 75 | $userToken->delete(); 76 | return ["success" => true]; 77 | } 78 | return ["error" => true]; 79 | } 80 | 81 | /** 82 | * Profile 83 | */ 84 | public function actionProfile() 85 | { 86 | /** @var User $user */ 87 | /** @var Profile $profile */ 88 | 89 | // get user and profile 90 | $user = $this->jwtAuth->getAuthenticatedUser(); 91 | $profile = Profile::findOne(["user_id" => $user->id]); 92 | 93 | // update profile 94 | if ($profile->loadPostAndSave() === false) { 95 | return ["errors" => $profile->errors]; 96 | } 97 | 98 | return ["success" => ["profile" => $profile]]; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | date=$(date "+%Y%m%d-%H%M%S") 4 | echo "----------------------------------------------" 5 | echo "-> Start: ${date}" 6 | echo "----------------------------------------------" 7 | echo "-> Git branch:" 8 | git rev-parse --abbrev-ref HEAD 9 | git pull 10 | echo "----------------------------------------------" 11 | echo "-> Gulp:" 12 | gulp build 13 | echo "----------------------------------------------" 14 | echo "-> Yii build:" 15 | php yii build ${date} 16 | echo "----------------------------------------------" 17 | echo "-> Done: $(date "+%Y%m%d-%H%M%S")" 18 | echo "----------------------------------------------" -------------------------------------------------------------------------------- /env.php.example: -------------------------------------------------------------------------------- 1 | "dev", 7 | "YII_DEBUG" => true, 8 | "YII_KEY" => "RANDOM_STRING_HERE", // cookie validation key 9 | 10 | // force debug module using this $_GET param (for prod environment) 11 | // leave empty to disable 12 | "DEBUG_PASSWORD" => "", 13 | 14 | // api 15 | "API_URL" => "/v1/", 16 | "JWT_COOKIE" => true, // store jwt tokens in cookie. if false, it will use local storage instead 17 | 18 | // database 19 | "DB_DSN" => "mysql:host=localhost;dbname=basic", 20 | "DB_USER" => "", 21 | "DB_PASS" => "", 22 | "DB_PREFIX" => "", 23 | 24 | // mail 25 | "MAIL_FILE_TRANSPORT" => true, 26 | "MAIL_ENCRYPTION" => "tls", 27 | "MAIL_HOST" => "smtp.mandrillapp.com", 28 | "MAIL_PORT" => "587", 29 | "MAIL_USER" => "", 30 | "MAIL_PASS" => "", 31 | 32 | // recaptcha 33 | // leave empty to disable 34 | "RECAPTCHA_SITEKEY" => "", 35 | "RECAPTCHA_SECRET" => "", 36 | ]; -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 | $value) { 10 | if (!$overwrite && getenv($key) !== false) { 11 | continue; 12 | } 13 | 14 | // set bool/null explicitly, otherwise they get computed as 0 or 1 15 | if ($value === true) { 16 | $value = "true"; 17 | } elseif ($value === false) { 18 | $value = "false"; 19 | } elseif ($value === null) { 20 | $value = "null"; 21 | } 22 | putenv("$key=$value"); 23 | } 24 | } 25 | 26 | /** 27 | * Get env 28 | * @param string $key 29 | * @param mixed $default 30 | * @return mixed 31 | */ 32 | function env($key, $default = null) 33 | { 34 | // check if $key is not set 35 | $value = getenv($key); 36 | if ($value === false) { 37 | return $default; 38 | } 39 | 40 | // return bool/null/value 41 | if ($value == "true") { 42 | return true; 43 | } elseif ($value == "false") { 44 | return false; 45 | } elseif ($value == "null") { 46 | return null; 47 | } else { 48 | return $value; 49 | } 50 | } 51 | 52 | /** 53 | * Check if we force enable yii debug module 54 | * @return bool 55 | */ 56 | function isForceDebug() 57 | { 58 | // store/return result 59 | static $result; 60 | if ($result !== null) { 61 | return $result; 62 | } 63 | 64 | // force debug module using $_GET param 65 | // enable this by manually entering the url "http://example.com?qwe" 66 | $debugPassword = env('DEBUG_PASSWORD'); 67 | $cookieName = '_forceDebug'; 68 | $cookieExpire = 60*5; // 5 minutes 69 | 70 | // check $_GET and $_COOKIE 71 | $isGetSet = isset($_GET[$debugPassword]); 72 | $isCookieSet = (isset($_COOKIE[$cookieName]) && $_COOKIE[$cookieName] === $debugPassword); 73 | if ($debugPassword && ($isGetSet || $isCookieSet)) { 74 | // set/refresh cookie 75 | setcookie($cookieName, $debugPassword, time() + $cookieExpire); 76 | $result = true; 77 | } else { 78 | $result = false; 79 | } 80 | return $result; 81 | } 82 | 83 | /** 84 | * Get url 85 | * @param array|string $url 86 | * @param bool|string $scheme 87 | * @return string 88 | */ 89 | function url($url = '', $scheme = false) 90 | { 91 | return \yii\helpers\Url::to($url, $scheme); 92 | } 93 | 94 | /** 95 | * Get param 96 | * @param string $name 97 | * @param mixed $default 98 | * @return mixed 99 | */ 100 | function param($name, $default = null) 101 | { 102 | return array_key_exists($name, Yii::$app->params) ? Yii::$app->params[$name] : $default; 103 | } 104 | 105 | /** 106 | * Get/set cache 107 | * @param string $key 108 | * @param mixed $value 109 | * @param int $duration 110 | * @param yii\caching\Dependency $dependency 111 | * @return mixed 112 | */ 113 | function cache($key, $value = false, $duration = 0, $dependency = null) 114 | { 115 | if ($value === false) { 116 | return Yii::$app->cache->get($key); 117 | } 118 | return Yii::$app->cache->set($key, $value, $duration, $dependency); 119 | } 120 | 121 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 2 | var gulp = require('gulp'); 3 | var concat = require('gulp-concat'); 4 | var uglify = require('gulp-uglify'); 5 | var ngAnnotate = require('gulp-ng-annotate'); 6 | var cssnano = require('gulp-cssnano'); 7 | var flatten = require('gulp-flatten'); 8 | var del = require('del'); 9 | 10 | // ------------------------------------------------------------- 11 | // Variables 12 | // ------------------------------------------------------------- 13 | var dest = 'web/compiled'; 14 | var cssDir = 'web/css/**/*.css'; 15 | var jsDir = 'web/js/**/*.js'; 16 | var vendorDir = 'web/vendor'; 17 | 18 | // ------------------------------------------------------------- 19 | // Default task 20 | // ------------------------------------------------------------- 21 | gulp.task('default', ['watch', 'build']); 22 | 23 | // ------------------------------------------------------------- 24 | // Build task 25 | // ------------------------------------------------------------- 26 | gulp.task('build', ['clean'], function() { 27 | 28 | // build asset files 29 | gulp.src(cssDir) 30 | // compiled 31 | .pipe(concat(`site.compiled.css`)) 32 | .pipe(gulp.dest(dest)) 33 | // compiled min 34 | .pipe(concat(`site.compiled.min.css`)) 35 | .pipe(cssnano()) 36 | .pipe(gulp.dest(dest)); 37 | 38 | gulp.src(jsDir) 39 | // compiled 40 | .pipe(concat(`app.compiled.js`)) 41 | .pipe(ngAnnotate()) 42 | .pipe(gulp.dest(dest)) 43 | // compiled.min 44 | .pipe(concat(`app.compiled.min.js`)) 45 | .pipe(uglify()) 46 | .pipe(gulp.dest(dest)); 47 | 48 | // build vendor files 49 | gulp.src([`${vendorDir}/**/*.css`, `!${vendorDir}/**/*.min.css`]) 50 | .pipe(concat(`vendor.compiled.css`)) 51 | .pipe(gulp.dest(dest)); 52 | gulp.src([`${vendorDir}/**/*.min.css`]) 53 | .pipe(concat(`vendor.compiled.min.css`)) 54 | .pipe(gulp.dest(dest)); 55 | gulp.src([`${vendorDir}/**/angular.js`, `${vendorDir}/**/*.js`, `!${vendorDir}/**/*.min.js`]) 56 | .pipe(concat(`vendor.compiled.js`)) 57 | .pipe(gulp.dest(dest)); 58 | gulp.src([`${vendorDir}/**/angular.min.js`, `${vendorDir}/**/*.min.js`]) 59 | .pipe(concat(`vendor.compiled.min.js`)) 60 | .pipe(gulp.dest(dest)); 61 | 62 | // copy map files 63 | gulp.src(`${vendorDir}/**/*.map`) 64 | .pipe(flatten()) 65 | .pipe(gulp.dest(dest)); 66 | }); 67 | 68 | gulp.task('clean', function() { 69 | del([`${dest}/*`]); 70 | }); 71 | 72 | // ------------------------------------------------------------- 73 | // Watch task 74 | // ------------------------------------------------------------- 75 | gulp.task('watch', function () { 76 | gulp.watch([cssDir, jsDir], ['build']) 77 | }); -------------------------------------------------------------------------------- /models/Profile.php: -------------------------------------------------------------------------------- 1 | $recaptchaSecret, 'message' => Yii::t('app', 'Invalid captcha'), 37 | 'when' => function($model) { return !$model->hasErrors(); } 38 | ]; 39 | } 40 | 41 | return $rules; 42 | } 43 | 44 | /** 45 | * @return array customized attribute labels 46 | */ 47 | public function attributeLabels() 48 | { 49 | return [ 50 | 'captcha' => 'Captcha', 51 | ]; 52 | } 53 | 54 | /** 55 | * Sends an email to the specified email address using the information collected by this model. 56 | * @param string $email the target email address 57 | * @return boolean whether the model passes validation 58 | */ 59 | public function contact($email) 60 | { 61 | if ($this->validate()) { 62 | Yii::$app->mailer->compose() 63 | ->setTo($email) 64 | ->setFrom([$this->email => $this->name]) 65 | ->setSubject($this->subject) 66 | ->setTextBody($this->body) 67 | ->send(); 68 | 69 | return true; 70 | } 71 | return false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /models/forms/ForgotForm.php: -------------------------------------------------------------------------------- 1 | Error'; 18 | echo '
The path to yii framework seems to be incorrect.
'; 19 | echo 'You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . '.
'; 20 | echo 'Please refer to the README on how to install Yii.
'; 21 | } 22 | 23 | require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); 24 | $requirementsChecker = new YiiRequirementChecker(); 25 | 26 | $gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.'; 27 | $gdOK = $imagickOK = false; 28 | 29 | if (extension_loaded('imagick')) { 30 | $imagick = new Imagick(); 31 | $imagickFormats = $imagick->queryFormats('PNG'); 32 | if (in_array('PNG', $imagickFormats)) { 33 | $imagickOK = true; 34 | } else { 35 | $imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.'; 36 | } 37 | } 38 | 39 | if (extension_loaded('gd')) { 40 | $gdInfo = gd_info(); 41 | if (!empty($gdInfo['FreeType Support'])) { 42 | $gdOK = true; 43 | } else { 44 | $gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.'; 45 | } 46 | } 47 | 48 | /** 49 | * Adjust requirements according to your application specifics. 50 | */ 51 | $requirements = array( 52 | // Database : 53 | array( 54 | 'name' => 'PDO extension', 55 | 'mandatory' => true, 56 | 'condition' => extension_loaded('pdo'), 57 | 'by' => 'All DB-related classes', 58 | ), 59 | array( 60 | 'name' => 'PDO SQLite extension', 61 | 'mandatory' => false, 62 | 'condition' => extension_loaded('pdo_sqlite'), 63 | 'by' => 'All DB-related classes', 64 | 'memo' => 'Required for SQLite database.', 65 | ), 66 | array( 67 | 'name' => 'PDO MySQL extension', 68 | 'mandatory' => false, 69 | 'condition' => extension_loaded('pdo_mysql'), 70 | 'by' => 'All DB-related classes', 71 | 'memo' => 'Required for MySQL database.', 72 | ), 73 | array( 74 | 'name' => 'PDO PostgreSQL extension', 75 | 'mandatory' => false, 76 | 'condition' => extension_loaded('pdo_pgsql'), 77 | 'by' => 'All DB-related classes', 78 | 'memo' => 'Required for PostgreSQL database.', 79 | ), 80 | // Cache : 81 | array( 82 | 'name' => 'Memcache extension', 83 | 'mandatory' => false, 84 | 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), 85 | 'by' => 'MemCache', 86 | 'memo' => extension_loaded('memcached') ? 'To use memcached set MemCache::useMemcached totrue
.' : ''
87 | ),
88 | // CAPTCHA:
89 | array(
90 | 'name' => 'GD PHP extension with FreeType support',
91 | 'mandatory' => false,
92 | 'condition' => $gdOK,
93 | 'by' => 'Captcha',
94 | 'memo' => $gdMemo,
95 | ),
96 | array(
97 | 'name' => 'ImageMagick PHP extension with PNG support',
98 | 'mandatory' => false,
99 | 'condition' => $imagickOK,
100 | 'by' => 'Captcha',
101 | 'memo' => $imagickMemo,
102 | ),
103 | // PHP ini :
104 | 'phpExposePhp' => array(
105 | 'name' => 'Expose PHP',
106 | 'mandatory' => false,
107 | 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"),
108 | 'by' => 'Security reasons',
109 | 'memo' => '"expose_php" should be disabled at php.ini',
110 | ),
111 | 'phpAllowUrlInclude' => array(
112 | 'name' => 'PHP allow url include',
113 | 'mandatory' => false,
114 | 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"),
115 | 'by' => 'Security reasons',
116 | 'memo' => '"allow_url_include" should be disabled at php.ini',
117 | ),
118 | 'phpSmtp' => array(
119 | 'name' => 'PHP mail SMTP',
120 | 'mandatory' => false,
121 | 'condition' => strlen(ini_get('SMTP')) > 0,
122 | 'by' => 'Email sending',
123 | 'memo' => 'PHP mail SMTP server required',
124 | ),
125 | );
126 |
127 | // OPcache check
128 | if (!version_compare(phpversion(), '5.5', '>=')) {
129 | $requirements[] = array(
130 | 'name' => 'APC extension',
131 | 'mandatory' => false,
132 | 'condition' => extension_loaded('apc'),
133 | 'by' => 'ApcCache',
134 | );
135 | }
136 |
137 | $requirementsChecker->checkYii()->check($requirements)->render();
138 |
--------------------------------------------------------------------------------
/runtime/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/tests/_bootstrap.php:
--------------------------------------------------------------------------------
1 | null,
19 | ];
20 |
21 | /**
22 | * @inheritdoc
23 | */
24 | public function _initialize()
25 | {
26 | // compute database info
27 | $match = preg_match("/host=(.*);dbname=(.*)/", env("DB_DSN"), $matches);
28 | if (!$match) {
29 | return;
30 | }
31 | $host = $matches[1];
32 | $name = $matches[2] . "_test";
33 | $user = env("DB_USER");
34 | $pass = env("DB_PASS");
35 |
36 | // compute dump file
37 | $dumpFile = $this->config['dump'] ?: "tests/_data/dump.sql";
38 | $dumpFile = Configuration::projectDir() . $dumpFile;
39 | if (!file_exists($dumpFile)) {
40 | throw new ModuleException(__CLASS__, "Dump file does not exist [ $dumpFile ]");
41 | }
42 |
43 | // dump
44 | $cmd = "mysql -h $host -u $user -p$pass $name < $dumpFile";
45 | $start = microtime(true);
46 | $output = shell_exec($cmd);
47 | $end = microtime(true);
48 | $diff = round(($end - $start) * 1000, 2);
49 |
50 | // output debug info
51 | $className = get_called_class();
52 | codecept_debug("$className - Importing db [ $name ] [ $diff ms ]");
53 |
54 | // check for error
55 | if ($output) {
56 | throw new ModuleException(__CLASS__, "Failed to import db [ $cmd ]");
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/_support/UnitTester.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/site/about'));
9 | $I->see('About', 'h1');
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/tests/acceptance/ContactCest.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/site/contact'));
10 | }
11 |
12 | public function contactPageWorks(AcceptanceTester $I)
13 | {
14 | $I->wantTo('ensure that contact page works');
15 | $I->see('Contact', 'h1');
16 | }
17 |
18 | public function contactFormCanBeSubmitted(AcceptanceTester $I)
19 | {
20 | $I->amGoingTo('submit contact form with correct data');
21 | $I->fillField('#contactform-name', 'tester');
22 | $I->fillField('#contactform-email', 'tester@example.com');
23 | $I->fillField('#contactform-subject', 'test subject');
24 | $I->fillField('#contactform-body', 'test content');
25 | $I->fillField('#contactform-verifycode', 'testme');
26 |
27 | $I->click('contact-button');
28 |
29 | $I->wait(2); // wait for button to be clicked
30 |
31 | $I->dontSeeElement('#contact-form');
32 | $I->see('Thank you for contacting us. We will respond to you as soon as possible.');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/acceptance/HomeCest.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/site/index'));
9 | $I->see('My Company');
10 |
11 | $I->seeLink('About');
12 | $I->click('About');
13 | $I->wait(2); // wait for page to be opened
14 |
15 | $I->see('This is the About page.');
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/acceptance/LoginCest.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/site/login'));
9 | $I->see('Login', 'h1');
10 |
11 | $I->amGoingTo('try to login with correct credentials');
12 | $I->fillField('input[name="LoginForm[username]"]', 'admin');
13 | $I->fillField('input[name="LoginForm[password]"]', 'admin');
14 | $I->click('login-button');
15 | $I->wait(2); // wait for button to be clicked
16 |
17 | $I->expectTo('see user info');
18 | $I->see('Logout');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/acceptance/_bootstrap.php:
--------------------------------------------------------------------------------
1 | run();
23 | exit($exitCode);
--------------------------------------------------------------------------------
/tests/bin/yii.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem -------------------------------------------------------------
4 | rem Yii command line bootstrap script for Windows.
5 | rem
6 | rem @author Qiang Xue = Yii::t("user", "Please confirm your email address by clicking the link below:") ?>
16 | 17 |= Url::toRoute(["/confirm", "token" => $userToken->token], true); ?>
-------------------------------------------------------------------------------- /views/_mail/user/forgotPassword.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |= Yii::t("user", "Please use this link to reset your password:") ?>
15 | 16 |= Url::toRoute(["/reset", "token" => $userToken->token], true); ?>
17 | -------------------------------------------------------------------------------- /views/_mail/user/layouts/html.php: -------------------------------------------------------------------------------- 1 | 10 | beginPage() ?> 11 | 12 | 13 | 14 | 15 |= Url::toRoute(["/login-callback", "token" => $userToken->token], true); ?>
-------------------------------------------------------------------------------- /views/site/index.php: -------------------------------------------------------------------------------- 1 | name; 7 | 8 | $date = !empty($date) ? $date : null; 9 | $assetPath = $date ? "/compiled-$date" : "/compiled"; 10 | $min = $date ? ".min" : ""; 11 | 12 | $html5Mode = isset($html5Mode) ? $html5Mode : true; // default to true unless explicitly disabled 13 | $linkPrefix = $html5Mode ? "/" : "#/"; 14 | 15 | ?> 16 | 17 | 18 | 19 | 20 | 21 |Page not found
6 |6 | This is the About page. 7 |
8 |11 | Note that if you turn on the Yii debugger, you should be able 12 | to view the mail message on the mail panel of the debugger. 13 |
14 | 15 |
16 | If the application is in development mode, the email is not sent but saved as
17 | a file under Yii::$app->mailer->fileTransportPath
.
18 | Please configure the useFileTransport
property of the mail
19 | application component to be false to enable email sending.
20 |
26 | If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. 27 |
28 | 29 |Yii 2 REST server + AngularJS client
8 | 9 | 10 |Status: {{ vm.message }}
18 |Refresh Token: {{ vm.refreshToken.substr(-23) }}
19 |Note: Refresh tokens are PERMANENT and typically used for mobile apps - 20 | NOT web apps.
21 | 22 |{{ vm.successMsg }}
11 |Please fill out the following fields to register:
10 | 11 | 47 |Login link sent - Please check your email
7 |Registration link sent - Please check your email
8 |This will send a link to the email address to log in or register
13 | 14 |These links expire in 15 minutes
15 | 16 | 40 | 41 |Please fill out the following fields to login:
6 | 7 |After logging in, you will be redirected to {{ vm.loginUrl }}
8 | 9 | 47 | 48 |Profile saved
11 |This is the Profile page. Note that you need to be logged in to view this.
15 | 16 | 34 | 35 |User [ {{ vm.User.email }} ] registered
8 |Please check your email for an activation link
9 |User [ {{ vm.User.email }} ] registered
14 | 15 |Please fill out the following fields to register:
20 | 21 | 55 |Please check your email for a reset password link
6 |Password updated
9 | 10 |