├── .travis.yml ├── README.md ├── app ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── browserslist ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── auth │ │ │ ├── auth-routing.module.ts │ │ │ ├── auth.module.ts │ │ │ ├── guards │ │ │ │ ├── auth.guard.spec.ts │ │ │ │ └── auth.guard.ts │ │ │ ├── interceptors │ │ │ │ ├── jwt.interceptor.spec.ts │ │ │ │ └── jwt.interceptor.ts │ │ │ ├── pages │ │ │ │ ├── login-page │ │ │ │ │ ├── login-page.component.html │ │ │ │ │ ├── login-page.component.scss │ │ │ │ │ ├── login-page.component.spec.ts │ │ │ │ │ └── login-page.component.ts │ │ │ │ └── profile-page │ │ │ │ │ ├── profile-page.component.html │ │ │ │ │ ├── profile-page.component.scss │ │ │ │ │ ├── profile-page.component.spec.ts │ │ │ │ │ └── profile-page.component.ts │ │ │ └── services │ │ │ │ ├── auth.service.spec.ts │ │ │ │ └── auth.service.ts │ │ ├── dashboard │ │ │ ├── dashboard-routing.module.ts │ │ │ ├── dashboard.module.ts │ │ │ └── pages │ │ │ │ └── dashboard-page │ │ │ │ ├── dashboard-page.component.html │ │ │ │ ├── dashboard-page.component.scss │ │ │ │ ├── dashboard-page.component.spec.ts │ │ │ │ └── dashboard-page.component.ts │ │ ├── material.module.ts │ │ ├── shared │ │ │ ├── models │ │ │ │ ├── deserializable.model.ts │ │ │ │ └── user.model.ts │ │ │ ├── services │ │ │ │ ├── alert.service.spec.ts │ │ │ │ └── alert.service.ts │ │ │ └── shared.module.ts │ │ └── user │ │ │ ├── components │ │ │ └── user-add │ │ │ │ ├── user-add.component.html │ │ │ │ ├── user-add.component.scss │ │ │ │ ├── user-add.component.spec.ts │ │ │ │ └── user-add.component.ts │ │ │ ├── datasource │ │ │ └── user-data-source.ts │ │ │ ├── pages │ │ │ └── user-list-page │ │ │ │ ├── user-list-page.component.html │ │ │ │ ├── user-list-page.component.scss │ │ │ │ ├── user-list-page.component.spec.ts │ │ │ │ └── user-list-page.component.ts │ │ │ ├── services │ │ │ ├── user.service.spec.ts │ │ │ └── user.service.ts │ │ │ ├── user-routing.module.ts │ │ │ └── user.module.ts │ ├── assets │ │ ├── .gitkeep │ │ └── logo.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── backend ├── .bowerrc ├── .gitignore ├── LICENSE.md ├── README.md ├── Vagrantfile ├── assets │ └── AppAsset.php ├── codeception.yml ├── commands │ └── UserController.php ├── composer.json ├── composer.lock ├── config │ ├── console.php │ ├── db.php │ ├── params.php │ ├── test.php │ ├── test_db.php │ └── web.php ├── controllers │ └── CorsCustom.php ├── docker-compose.yml ├── mail │ └── layouts │ │ └── html.php ├── migrations │ ├── m191021_165358_user.php │ └── m200316_065802_refresh_token.php ├── models │ ├── RefreshToken.php │ ├── User.php │ └── UserSearch.php ├── modules │ └── v1 │ │ ├── Module.php │ │ └── controllers │ │ ├── ApiController.php │ │ ├── AuthController.php │ │ ├── RestController.php │ │ └── UserController.php ├── requirements.php ├── runtime │ └── .gitignore ├── tests │ ├── _bootstrap.php │ ├── _data │ │ └── .gitkeep │ ├── _output │ │ └── .gitignore │ ├── _support │ │ ├── AcceptanceTester.php │ │ ├── FunctionalTester.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 │ │ ├── ContactFormCest.php │ │ ├── LoginFormCest.php │ │ └── _bootstrap.php │ ├── unit.suite.yml │ └── unit │ │ ├── _bootstrap.php │ │ └── models │ │ ├── ContactFormTest.php │ │ ├── LoginFormTest.php │ │ └── UserTest.php ├── vagrant │ ├── config │ │ ├── .gitignore │ │ └── vagrant-local.example.yml │ ├── nginx │ │ ├── app.conf │ │ └── log │ │ │ └── .gitignore │ └── provision │ │ ├── always-as-root.sh │ │ ├── once-as-root.sh │ │ └── once-as-vagrant.sh ├── views │ └── layouts │ │ └── main.php ├── web │ ├── .htaccess │ ├── assets │ │ └── .gitignore │ ├── css │ │ └── site.css │ ├── favicon.ico │ ├── index-test.php │ ├── index.php │ └── robots.txt ├── widgets │ └── Alert.php ├── yii └── yii.bat └── screenshots ├── Create User.png ├── Dashboard.png ├── Login.png └── View Users.png /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | dist: bionic 3 | os: 4 | - linux 5 | services: 6 | - docker 7 | - xvfb 8 | language: node_js 9 | node_js: 10 | - "12.16.1" 11 | addons: 12 | apt: 13 | packages: 14 | - dpkg 15 | chrome: stable 16 | cache: 17 | directories: 18 | - node_modules 19 | before_install: 20 | - npm install -g @angular/cli 21 | - cd app 22 | install: 23 | - npm ci 24 | script: 25 | - ng test --watch=false 26 | # - ng e2e 27 | notifications: 28 | webhooks: 29 | on_success: change 30 | on_failure: always 31 | on_start: never -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Yii2 Angular Template

6 |
7 |

8 | 9 | [![Build Status](https://travis-ci.org/databoxtech/yii2-angular-template.svg?branch=master)](https://travis-ci.org/databoxtech/yii2-angular-template) 10 | 11 | 12 | Yii2 Angular Template is a skelaton project with a Yii2 Rest API and a Angular (v9) client. 13 | The template contains the basic features including, 14 | ------------------------------ 15 | JWT authentication 16 | User Management (via angular app) 17 | Role based access control (Yii2 RBAC) 18 | 19 | You can keep adding more functionality to following same pattern. Refer documentations to find out more. 20 | 21 | DIRECTORY STRUCTURE 22 | ------------------- 23 | 24 | app/ Angular v9 frontend application 25 | backend/ Yii2 backend application 26 | 27 | SETUP INSTRUCTIONS 28 | ------------------ 29 | 1. Clone the repo (git clone https://github.com/databoxtech/yii2-angular-template) 30 | 2. Install yii2 dependecies using composer (cd backend && composer install) 31 | 3. Configure database by editing config/db.php 32 | 4. Initialize database (./yii migrate) 33 | 5. Initialize yii2 rbac by running `./yii migrate --migrationPath=@yii/rbac/migrations` 34 | 6. Initialize basic permissions/role and admin account by running `./yii user/permissions` 35 | 7. Run backend api by running `./yii serve` 36 | 8. Install angular dependencies using npm (cd app && npm install) 37 | 9. Run frontend application by running `ng serve` 38 | 10. Open http://localhost:4200 and login using below credentials, 39 | Username (email): admin@template.com 40 | Passowrd: test@123 41 | 42 | 43 | 44 | SCREENSHOTS 45 | ----------- 46 | 47 | ![Login](https://raw.githubusercontent.com/databoxtech/yii2-angular-template/master/screenshots/Login.png) 48 | 49 | ![Dashboard](https://raw.githubusercontent.com/databoxtech/yii2-angular-template/master/screenshots/Dashboard.png) 50 | 51 | ![Users](https://raw.githubusercontent.com/databoxtech/yii2-angular-template/master/screenshots/View%20Users.png) 52 | 53 | ![Create User](https://raw.githubusercontent.com/databoxtech/yii2-angular-template/master/screenshots/Create%20User.png) -------------------------------------------------------------------------------- /app/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Yii2AngularTemplate 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /app/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "crm-app": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/crm-app", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 32 | "src/styles.scss" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "extractCss": true, 48 | "namedChunks": false, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | }, 58 | { 59 | "type": "anyComponentStyle", 60 | "maximumWarning": "6kb", 61 | "maximumError": "10kb" 62 | } 63 | ] 64 | } 65 | } 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "options": { 70 | "browserTarget": "crm-app:build" 71 | }, 72 | "configurations": { 73 | "production": { 74 | "browserTarget": "crm-app:build:production" 75 | } 76 | } 77 | }, 78 | "extract-i18n": { 79 | "builder": "@angular-devkit/build-angular:extract-i18n", 80 | "options": { 81 | "browserTarget": "crm-app:build" 82 | } 83 | }, 84 | "test": { 85 | "builder": "@angular-devkit/build-angular:karma", 86 | "options": { 87 | "main": "src/test.ts", 88 | "polyfills": "src/polyfills.ts", 89 | "tsConfig": "tsconfig.spec.json", 90 | "karmaConfig": "karma.conf.js", 91 | "assets": [ 92 | "src/favicon.ico", 93 | "src/assets" 94 | ], 95 | "styles": [ 96 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 97 | "src/styles.scss" 98 | ], 99 | "scripts": [] 100 | } 101 | }, 102 | "lint": { 103 | "builder": "@angular-devkit/build-angular:tslint", 104 | "options": { 105 | "tsConfig": [ 106 | "tsconfig.app.json", 107 | "tsconfig.spec.json", 108 | "e2e/tsconfig.json" 109 | ], 110 | "exclude": [ 111 | "**/node_modules/**" 112 | ] 113 | } 114 | }, 115 | "e2e": { 116 | "builder": "@angular-devkit/build-angular:protractor", 117 | "options": { 118 | "protractorConfig": "e2e/protractor.conf.js", 119 | "devServerTarget": "crm-app:serve" 120 | }, 121 | "configurations": { 122 | "production": { 123 | "devServerTarget": "crm-app:serve:production" 124 | } 125 | } 126 | } 127 | } 128 | }, 129 | }, 130 | "defaultProject": "crm-app" 131 | } -------------------------------------------------------------------------------- /app/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /app/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /app/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('crm-app app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /app/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/crm-app'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crm-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular-material-extensions/password-strength": "^6.0.0", 15 | "@angular/animations": "~9.0.1", 16 | "@angular/cdk": "^9.0.0", 17 | "@angular/common": "~9.0.1", 18 | "@angular/compiler": "~9.0.1", 19 | "@angular/core": "~9.0.1", 20 | "@angular/flex-layout": "^9.0.0-beta.29", 21 | "@angular/forms": "~9.0.1", 22 | "@angular/material": "^9.0.0", 23 | "@angular/platform-browser": "~9.0.1", 24 | "@angular/platform-browser-dynamic": "~9.0.1", 25 | "@angular/router": "~9.0.1", 26 | "@sweetalert2/ngx-sweetalert2": "^8.0.0", 27 | "ngx-spinner": "^8.1.0", 28 | "rxjs": "~6.5.4", 29 | "sweetalert2": "^9.7.2", 30 | "tslib": "^1.10.0", 31 | "zone.js": "~0.10.2" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "~0.900.2", 35 | "@angular-devkit/build-ng-packagr": "~0.900.2", 36 | "@angular/cli": "~9.0.2", 37 | "@angular/compiler-cli": "~9.0.1", 38 | "@angular/language-service": "~9.0.1", 39 | "@types/node": "^12.11.1", 40 | "@types/jasmine": "~3.5.0", 41 | "@types/jasminewd2": "~2.0.3", 42 | "codelyzer": "^5.1.2", 43 | "jasmine-core": "~3.5.0", 44 | "jasmine-spec-reporter": "~4.2.1", 45 | "karma": "~4.3.0", 46 | "karma-chrome-launcher": "~3.1.0", 47 | "karma-coverage-istanbul-reporter": "~2.1.0", 48 | "karma-jasmine": "~2.0.1", 49 | "karma-jasmine-html-reporter": "^1.4.2", 50 | "ng-packagr": "^9.0.0", 51 | "protractor": "~5.4.3", 52 | "ts-node": "~8.3.0", 53 | "tslint": "~5.18.0", 54 | "typescript": "~3.7.5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { DashboardModule } from './dashboard/dashboard.module'; 4 | import { AuthGuard } from './auth/guards/auth.guard'; 5 | import { AuthModule } from './auth/auth.module'; 6 | 7 | 8 | const routes: Routes = [ 9 | { 10 | path: 'auth', 11 | loadChildren: () => import('./auth/auth.module').then(m => m.AuthModule), 12 | }, 13 | { 14 | path: 'user', 15 | loadChildren: () => import('./user/user.module').then(m => m.UserModule), 16 | }, 17 | { 18 | path: '**', 19 | loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), 20 | canActivate: [AuthGuard] 21 | } 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [RouterModule.forRoot(routes)], 26 | exports: [RouterModule] 27 | }) 28 | export class AppRoutingModule { } 29 | -------------------------------------------------------------------------------- /app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yii2 Angular Template 6 | 7 | 8 | 9 | 10 | dashboardDashboard 11 | 12 | 13 | peopleUsers 14 | 15 | 16 |

From Databox with Love

17 |
18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 |
28 | Logout 29 |
30 |
31 | 32 | 33 | 39 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /app/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .filler { 2 | flex: 1 1 auto; 3 | } 4 | 5 | .active-link{ 6 | color: #3F51B5 !important; /* Note: You could also use a custom theme */ 7 | } 8 | 9 | .love{ 10 | position: absolute; 11 | bottom: 0px; 12 | left: 10px; 13 | } 14 | 15 | 16 | .sidenav-container { 17 | height: 100vh; 18 | .sidenav { 19 | width: 300px; 20 | background-color: #fff; 21 | box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); 22 | } 23 | .sidenav .home-button { 24 | width: 100%; 25 | padding-top: 50px; 26 | margin-bottom: 30px; 27 | display: inline-block; 28 | a{ 29 | text-decoration: none; 30 | } 31 | span{ 32 | line-height: 60px; 33 | vertical-align: top; 34 | height: 50px; 35 | display: inline-block; 36 | } 37 | } 38 | .sidenav .home-button .app-logo { 39 | width: 50px; 40 | display: inline-block; 41 | } 42 | .sidenav .mat-toolbar { 43 | background: inherit; 44 | } 45 | .sidenav .navigation-list { 46 | padding-top: 30px; 47 | } 48 | .sidenav .navigation-list .navigation-item.active { 49 | background: #dcf0fb; 50 | color: #062639; 51 | } 52 | .sidenav .navigation-list .navigation-item .mat-icon { 53 | margin-right: 5px; 54 | } 55 | .sidenav .navigation-list .navigation-item .navigation-item-label { 56 | padding-top: 2px; 57 | } 58 | .sidenav .navigation-list .navigation-divider { 59 | color: #dcf0fb; 60 | border-color: #dcf0fb; 61 | } 62 | .sidenav-content{ 63 | margin-left: 60px; 64 | .sidenav-content-toolbar { 65 | background: #062639; 66 | color: #fff; 67 | padding-left: 15px; 68 | } 69 | } 70 | .sidenav-content .sidenav-content-toolbar .menu-button { 71 | color: #fff; 72 | background: #062639; 73 | } 74 | .sidenav-content .sidenav-content-toolbar .menu-button:focus { 75 | outline: none; 76 | } 77 | .sidenav-content .sidenav-content-toolbar .navbar-link { 78 | color: #fff; 79 | padding-right: 10px; 80 | cursor: pointer; 81 | } 82 | .sidenav-content .sidenav-content-toolbar .navbar-link:hover { 83 | color: #fff; 84 | } 85 | .sidenav-content .sidenav-content-toolbar .navbar-link:focus { 86 | outline: none; 87 | } 88 | .sidenav-content .sidenav-content-toolbar .gifts a { 89 | color: #fff; 90 | display: inline-block; 91 | padding-top: 7px; 92 | padding-right: 20px; 93 | position: relative; 94 | } 95 | .sidenav-content .sidenav-content-toolbar .gifts a .mat-badge-content { 96 | background: #d3544d; 97 | } 98 | .sidenav-content .sidenav-content-toolbar .gifts:focus { 99 | outline: none; 100 | } 101 | 102 | .mat-toolbar.mat-primary { 103 | position: sticky; 104 | top: 0; 105 | z-index: 1; 106 | } 107 | } 108 | /*left sidenav style override*/ 109 | mat-sidenav:not(.mat-drawer-opened) { 110 | transform: translate3d(0, 0, 0) !important; 111 | visibility: visible !important; 112 | width: 60px !important; 113 | overflow: hidden; 114 | } 115 | mat-sidenav:not(.mat-drawer-opened) { 116 | 117 | .navigation-item-label, .home-button, .love { 118 | display: none !important; 119 | } 120 | } -------------------------------------------------------------------------------- /app/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { MaterialModule } from './material.module'; 5 | import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | import { NgxSpinnerModule } from 'ngx-spinner'; 9 | 10 | describe('AppComponent', () => { 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | RouterTestingModule, 15 | MaterialModule,SweetAlert2Module.forChild(), 16 | HttpClientModule, 17 | BrowserAnimationsModule, 18 | NgxSpinnerModule, 19 | SweetAlert2Module.forRoot() 20 | ], 21 | declarations: [ 22 | AppComponent 23 | ], 24 | }).compileComponents(); 25 | })); 26 | 27 | it('should create the app', () => { 28 | const fixture = TestBed.createComponent(AppComponent); 29 | const app = fixture.componentInstance; 30 | expect(app).toBeTruthy(); 31 | }); 32 | 33 | // it(`should have as title 'crm-app'`, () => { 34 | // const fixture = TestBed.createComponent(AppComponent); 35 | // const app = fixture.componentInstance; 36 | // expect(app.title).toEqual('crm-app'); 37 | // }); 38 | 39 | // it('should render title', () => { 40 | // const fixture = TestBed.createComponent(AppComponent); 41 | // fixture.detectChanges(); 42 | // const compiled = fixture.nativeElement; 43 | // expect(compiled.querySelector('.content span').textContent).toContain('crm-app app is running!'); 44 | // }); 45 | }); 46 | -------------------------------------------------------------------------------- /app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { AuthService } from './auth/services/auth.service'; 3 | import { Router } from '@angular/router'; 4 | import { SwalComponent } from '@sweetalert2/ngx-sweetalert2'; 5 | import { AlertService, Alert } from './shared/services/alert.service'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.scss'] 11 | }) 12 | export class AppComponent { 13 | 14 | drawer; 15 | 16 | @ViewChild('alertSwal') private alertSwal: SwalComponent; 17 | alert = { 18 | title: '', 19 | text: '' 20 | }; 21 | 22 | constructor(public auth: AuthService, private router: Router, private alertSrv: AlertService){ 23 | this.alertSrv.subject.subscribe(alert => { 24 | if(alert === null){ 25 | this.alertSwal.dismiss(); 26 | }else{ 27 | this.showAlert(alert); 28 | } 29 | }); 30 | } 31 | 32 | showAlert(alert: Alert){ 33 | this.alertSwal.title = alert.title; 34 | this.alertSwal.text = alert.text; 35 | this.alertSwal.update(); 36 | this.alertSwal.fire(); 37 | } 38 | 39 | logout(){ 40 | this.auth.logout(); 41 | this.router.navigate(['/login']); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | 9 | import { HttpClientModule } from '@angular/common/http'; 10 | 11 | import { SharedModule } from './shared/shared.module'; 12 | 13 | import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; 14 | import { AuthModule } from './auth/auth.module'; 15 | import { MaterialModule } from './material.module'; 16 | 17 | import { NgxSpinnerModule } from "ngx-spinner"; 18 | import { MatPasswordStrengthModule } from '@angular-material-extensions/password-strength'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | AppComponent, 23 | ], 24 | imports: [ 25 | BrowserModule, 26 | AppRoutingModule, 27 | HttpClientModule, 28 | BrowserAnimationsModule, 29 | MaterialModule, 30 | SharedModule, 31 | 32 | AuthModule, 33 | NgxSpinnerModule, 34 | 35 | SweetAlert2Module.forRoot(), 36 | 37 | // MatPasswordStrengthModule 38 | ], 39 | providers: [], 40 | bootstrap: [AppComponent], 41 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 42 | }) 43 | export class AppModule { } 44 | -------------------------------------------------------------------------------- /app/src/app/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LoginPageComponent } from './pages/login-page/login-page.component'; 4 | 5 | 6 | const routes: Routes = [ 7 | { path: 'login', component: LoginPageComponent}, 8 | { path: '**', component: LoginPageComponent} 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class AuthRoutingModule { } 16 | -------------------------------------------------------------------------------- /app/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AuthRoutingModule } from './auth-routing.module'; 5 | import { ProfilePageComponent } from './pages/profile-page/profile-page.component'; 6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 7 | 8 | import { LoginPageComponent } from './pages/login-page/login-page.component'; 9 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 10 | import { JwtInterceptor } from './interceptors/jwt.interceptor'; 11 | import { MaterialModule } from '../material.module'; 12 | 13 | 14 | 15 | 16 | @NgModule({ 17 | declarations: [ 18 | ProfilePageComponent, 19 | LoginPageComponent 20 | ], 21 | imports: [ 22 | CommonModule, 23 | AuthRoutingModule, 24 | FormsModule, 25 | ReactiveFormsModule, 26 | MaterialModule, 27 | ], 28 | providers: [ 29 | { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, 30 | ] 31 | }) 32 | export class AuthModule { } 33 | -------------------------------------------------------------------------------- /app/src/app/auth/guards/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGuard } from './auth.guard'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { HttpClientModule } from '@angular/common/http'; 6 | 7 | describe('AuthGuard', () => { 8 | let guard: AuthGuard; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [ 13 | RouterTestingModule, 14 | HttpClientModule 15 | ] 16 | }); 17 | guard = TestBed.inject(AuthGuard); 18 | }); 19 | 20 | it('should be created', () => { 21 | expect(guard).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /app/src/app/auth/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from '../services/auth.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthGuard implements CanActivate { 10 | constructor( 11 | private router: Router, 12 | private authService: AuthService 13 | ) {} 14 | 15 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 16 | if (this.authService.isLoggedIn) { 17 | if(route.data.permission){ 18 | return this.authService.can(route.data.permission); 19 | }else{ 20 | return true; 21 | } 22 | } 23 | // not logged in so redirect to login page with the return url 24 | this.router.navigate(['/auth/login'], { queryParams: { returnUrl: state.url }}); 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/app/auth/interceptors/jwt.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { JwtInterceptor } from './jwt.interceptor'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | 6 | describe('JwtInterceptor', () => { 7 | beforeEach(() => TestBed.configureTestingModule({ 8 | providers: [ 9 | JwtInterceptor 10 | ], 11 | imports: [ 12 | HttpClientModule 13 | ] 14 | })); 15 | 16 | it('should be created', () => { 17 | const interceptor: JwtInterceptor = TestBed.inject(JwtInterceptor); 18 | expect(interceptor).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/src/app/auth/interceptors/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; 3 | import { Observable, throwError, BehaviorSubject } from 'rxjs'; 4 | import { AuthService } from '../services/auth.service'; 5 | import { catchError, switchMap, filter, take } from 'rxjs/operators'; 6 | import { User } from 'src/app/shared/models/user.model'; 7 | 8 | @Injectable() 9 | export class JwtInterceptor implements HttpInterceptor { 10 | 11 | private isRefreshing = false; 12 | private refreshTokenSubject: BehaviorSubject = new BehaviorSubject(null); 13 | 14 | constructor(private auth: AuthService) {} 15 | 16 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 17 | 18 | const jwt = this.auth.getJwtToken(); 19 | 20 | if (jwt) { 21 | request = this.addToken(request, jwt); 22 | } 23 | 24 | return next.handle(request).pipe(catchError(error => { 25 | console.log('handle', error); 26 | if (error instanceof HttpErrorResponse && error.status === 401) { 27 | return this.handle401Error(request, next); 28 | } else { 29 | return throwError(error); 30 | } 31 | })); 32 | 33 | } 34 | 35 | private handle401Error(request: HttpRequest, next: HttpHandler): Observable> { 36 | if (!this.isRefreshing) { 37 | this.isRefreshing = true; 38 | this.refreshTokenSubject.next(null); 39 | 40 | return this.auth.refreshJwt().pipe( 41 | switchMap((user: User) => { 42 | this.isRefreshing = false; 43 | this.refreshTokenSubject.next(user.jwt); 44 | return next.handle(this.addToken(request, user.jwt)); 45 | }), 46 | catchError((error) => { 47 | //logout upon error 48 | this.auth.logout(); 49 | location.reload(true); 50 | return throwError(error); 51 | })); 52 | 53 | } else { 54 | return this.refreshTokenSubject.pipe( 55 | filter(user => user != null), 56 | take(1), 57 | switchMap(user => { 58 | return next.handle(this.addToken(request, user.jwt)); 59 | })); 60 | } 61 | } 62 | 63 | private addToken(request: HttpRequest, token: string) { 64 | return request.clone({ 65 | setHeaders: { 66 | 'Authorization': `Bearer ${token}` 67 | } 68 | }); 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /app/src/app/auth/pages/login-page/login-page.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/app/auth/pages/login-page/login-page.component.scss: -------------------------------------------------------------------------------- 1 | .login-wrapper .crm-form { 2 | min-width: 100%; 3 | max-width: 300px; 4 | width: 100%; 5 | } 6 | 7 | .login-wrapper .crm-full-width, 8 | .login-wrapper .btn-block { 9 | width: 100%; 10 | } 11 | 12 | .login-wrapper .mat-card-header .mat-card-title { 13 | font-size: 30px; 14 | } 15 | 16 | .login-wrapper .mat-card { 17 | padding: 40px 70px 50px; 18 | } 19 | 20 | .login-wrapper .mat-card-header-text { 21 | margin: 0; 22 | width: 100%; 23 | text-align: center; 24 | } 25 | 26 | .login-wrapper .mat-stroked-button { 27 | border: 1px solid currentColor; 28 | line-height: 54px; 29 | background: #FFF7FA; 30 | } 31 | 32 | .login-wrapper .mat-form-field-appearance-legacy .mat-form-field-infix { 33 | padding: 0.8375em 0; 34 | } 35 | 36 | 37 | .box { 38 | position: relative; 39 | top: 0; 40 | opacity: 1; 41 | float: left; 42 | padding: 60px 50px 40px 50px; 43 | width: 100%; 44 | background: #fff; 45 | border-radius: 10px; 46 | transform: scale(1); 47 | -webkit-transform: scale(1); 48 | -ms-transform: scale(1); 49 | z-index: 5; 50 | max-width: 330px; 51 | } 52 | 53 | .box.back { 54 | transform: scale(.95); 55 | -webkit-transform: scale(.95); 56 | -ms-transform: scale(.95); 57 | top: -20px; 58 | opacity: .8; 59 | z-index: -1; 60 | } 61 | 62 | .box:before { 63 | content: ""; 64 | width: 100%; 65 | height: 30px; 66 | border-radius: 10px; 67 | position: absolute; 68 | top: -10px; 69 | background: rgba(255, 255, 255, .6); 70 | left: 0; 71 | transform: scale(.95); 72 | -webkit-transform: scale(.95); 73 | -ms-transform: scale(.95); 74 | z-index: -1; 75 | } -------------------------------------------------------------------------------- /app/src/app/auth/pages/login-page/login-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginPageComponent } from './login-page.component'; 4 | import { MaterialModule } from 'src/app/material.module'; 5 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | import { RouterTestingModule } from '@angular/router/testing'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | 10 | describe('LoginPageComponent', () => { 11 | let component: LoginPageComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [ LoginPageComponent ], 17 | imports: [ 18 | MaterialModule, 19 | FormsModule, 20 | ReactiveFormsModule, 21 | HttpClientModule, 22 | RouterTestingModule, 23 | BrowserAnimationsModule 24 | ] 25 | }) 26 | .compileComponents(); 27 | })); 28 | 29 | beforeEach(() => { 30 | fixture = TestBed.createComponent(LoginPageComponent); 31 | component = fixture.componentInstance; 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should create', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /app/src/app/auth/pages/login-page/login-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../../services/auth.service'; 3 | import { Router, ActivatedRoute } from '@angular/router'; 4 | import { first } from 'rxjs/operators'; 5 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 6 | 7 | @Component({ 8 | selector: 'app-login-page', 9 | templateUrl: './login-page.component.html', 10 | styleUrls: ['./login-page.component.scss'] 11 | }) 12 | export class LoginPageComponent implements OnInit { 13 | 14 | loginForm: FormGroup; 15 | returnUrl = '/'; 16 | 17 | constructor(private atuhService: AuthService, private router: Router, private route: ActivatedRoute, private formBuilder: FormBuilder){ 18 | if (this.atuhService.currentUserValue) { 19 | this.router.navigate(['/']); 20 | } 21 | } 22 | 23 | ngOnInit(): void { 24 | this.loginForm = this.formBuilder.group({ 25 | email: ['', Validators.required], 26 | password: ['', Validators.required] 27 | }); 28 | 29 | this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; 30 | } 31 | 32 | get f() { return this.loginForm.controls; } 33 | 34 | onSubmit(){ 35 | if (this.loginForm.invalid) { 36 | return; 37 | } 38 | 39 | this.atuhService.login(this.f.email.value, this.f.password.value) 40 | .pipe(first()) 41 | .subscribe( 42 | data => { 43 | this.router.navigate([this.returnUrl]); 44 | }, 45 | error => { 46 | console.log('Error'); 47 | // this.alertService.error(error); 48 | // this.loading = false; 49 | }); 50 | } 51 | 52 | isEmpty(str){ 53 | return !(str && str!=''); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/app/auth/pages/profile-page/profile-page.component.html: -------------------------------------------------------------------------------- 1 |

profile-page works!

2 | -------------------------------------------------------------------------------- /app/src/app/auth/pages/profile-page/profile-page.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/app/src/app/auth/pages/profile-page/profile-page.component.scss -------------------------------------------------------------------------------- /app/src/app/auth/pages/profile-page/profile-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProfilePageComponent } from './profile-page.component'; 4 | 5 | describe('ProfilePageComponent', () => { 6 | let component: ProfilePageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProfilePageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProfilePageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /app/src/app/auth/pages/profile-page/profile-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-profile-page', 5 | templateUrl: './profile-page.component.html', 6 | styleUrls: ['./profile-page.component.scss'] 7 | }) 8 | export class ProfilePageComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/app/auth/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | import { 6 | HttpClientTestingModule, 7 | HttpTestingController 8 | } from '@angular/common/http/testing'; 9 | import { PassThrough } from 'stream'; 10 | import { HttpEvent, HttpEventType } from '@angular/common/http'; 11 | 12 | 13 | const mockUser = { email: 'admin@crm.lk', password: '1234', displayName: 'Prabath P', phone: '0775831176', jwt: 'sdzcasidinawdaasdasdas' }; 14 | 15 | 16 | describe('AuthService', () => { 17 | let service: AuthService; 18 | 19 | beforeEach(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [ 22 | HttpClientTestingModule, 23 | ], 24 | providers: [ AuthService ] 25 | }); 26 | service = TestBed.inject(AuthService); 27 | }); 28 | 29 | it('should be created', () => { 30 | expect(service).toBeTruthy(); 31 | }); 32 | 33 | it('should login', 34 | inject( 35 | [HttpTestingController, AuthService], 36 | ( 37 | httpMock: HttpTestingController, 38 | authService: AuthService 39 | ) => { 40 | authService.login(mockUser.email, mockUser.password).subscribe((event: HttpEvent) => { 41 | switch (event.type) { 42 | case HttpEventType.Response: 43 | expect(event.body).toEqual(mockUser); 44 | } 45 | }); 46 | const mockReq = httpMock.expectOne(authService.authUrl); 47 | expect(mockReq.cancelled).toBeFalsy(); 48 | expect(mockReq.request.responseType).toEqual('json'); 49 | mockReq.flush(mockUser); 50 | httpMock.verify(); 51 | } 52 | ) 53 | ); 54 | }); 55 | -------------------------------------------------------------------------------- /app/src/app/auth/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | import { User } from 'src/app/shared/models/user.model'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { map } from 'rxjs/operators'; 6 | import { environment } from 'src/environments/environment'; 7 | import { isArray } from 'util'; 8 | 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class AuthService { 14 | 15 | public isLoggedIn: boolean = false; 16 | private currentUserSubject: BehaviorSubject; 17 | public currentUser: Observable; 18 | 19 | private permissions; 20 | 21 | public authUrl = `${environment.apiBaseUrl}auth/token`; 22 | 23 | constructor(private http: HttpClient) { 24 | this.currentUserSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('__yat-user'))); 25 | this.currentUser = this.currentUserSubject.asObservable(); 26 | this.currentUser.subscribe(user => { 27 | if(user && user.jwt){ 28 | this.isLoggedIn = true; 29 | }else{ 30 | this.isLoggedIn = false; 31 | } 32 | }) 33 | } 34 | 35 | public get currentUserValue(): User { 36 | return this.currentUserSubject.value; 37 | } 38 | 39 | public getJwtToken(): string{ 40 | return this.currentUserValue ? this.currentUserValue.jwt : null; 41 | } 42 | 43 | login(email, password) { 44 | return this.http.post(this.authUrl, { 45 | "email": email, 46 | "password" : password, 47 | "grant_type": "password" 48 | }).pipe(map(resp => { 49 | return this.processAuthResponse(resp); 50 | })); 51 | } 52 | 53 | refreshJwt(){ 54 | return this.http.post(this.authUrl, { 55 | "grant_type": "refresh_token", 56 | "refresh_token": this.currentUserValue.refresh_token 57 | }).pipe(map(resp => { 58 | return this.processAuthResponse(resp); 59 | })); 60 | } 61 | 62 | private processAuthResponse(response){ 63 | if(response && response.jwt){ 64 | const user = (new User()).deserialize(response.user); 65 | user.jwt = response.jwt; 66 | user.refresh_token = response.refresh_token; 67 | user.permissions = response.permissions; 68 | localStorage.setItem('__yat-user', JSON.stringify(user)); 69 | 70 | this.permissions = user.permissions; 71 | this.currentUserSubject.next(user); 72 | return user; 73 | } 74 | return null; 75 | } 76 | 77 | logout() { 78 | localStorage.removeItem('__yat-user'); 79 | this.currentUserSubject.next(null); 80 | } 81 | 82 | can(permission){ 83 | const can = this.currentUserValue && isArray(this.currentUserValue.permissions) && (this.currentUserValue.permissions.includes(permission)); 84 | console.log(`Can user, ${permission} => ${can}`); 85 | return can; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/app/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { DashboardPageComponent } from './pages/dashboard-page/dashboard-page.component'; 4 | 5 | 6 | const routes: Routes = [ 7 | { path: '**', component: DashboardPageComponent} 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class DashboardRoutingModule { } 15 | -------------------------------------------------------------------------------- /app/src/app/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { DashboardRoutingModule } from './dashboard-routing.module'; 5 | import { DashboardPageComponent } from './pages/dashboard-page/dashboard-page.component'; 6 | 7 | 8 | @NgModule({ 9 | declarations: [ 10 | DashboardPageComponent 11 | ], 12 | imports: [ 13 | CommonModule, 14 | DashboardRoutingModule, 15 | ], 16 | }) 17 | export class DashboardModule { } 18 | -------------------------------------------------------------------------------- /app/src/app/dashboard/pages/dashboard-page/dashboard-page.component.html: -------------------------------------------------------------------------------- 1 |

dashboard-page works!

2 | -------------------------------------------------------------------------------- /app/src/app/dashboard/pages/dashboard-page/dashboard-page.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/app/src/app/dashboard/pages/dashboard-page/dashboard-page.component.scss -------------------------------------------------------------------------------- /app/src/app/dashboard/pages/dashboard-page/dashboard-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardPageComponent } from './dashboard-page.component'; 4 | 5 | describe('DashboardPageComponent', () => { 6 | let component: DashboardPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /app/src/app/dashboard/pages/dashboard-page/dashboard-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dashboard-page', 5 | templateUrl: './dashboard-page.component.html', 6 | styleUrls: ['./dashboard-page.component.scss'] 7 | }) 8 | export class DashboardPageComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatFormFieldModule } from '@angular/material/form-field'; 7 | import { MatSliderModule } from '@angular/material/slider'; 8 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 9 | import { MatInputModule } from '@angular/material/input'; 10 | import { MatToolbarModule } from '@angular/material/toolbar'; 11 | import { MatSidenavModule } from '@angular/material/sidenav'; 12 | import { MatListModule } from '@angular/material/list'; 13 | import { MatButtonModule } from '@angular/material/button'; 14 | import { MatIconModule } from '@angular/material/icon'; 15 | import { MatTableModule } from '@angular/material/table'; 16 | import { MatPaginatorModule } from '@angular/material/paginator'; 17 | import { MatSortModule } from '@angular/material/sort'; 18 | import { MatDialogModule } from '@angular/material/dialog'; 19 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 20 | import { MatCheckboxModule } from '@angular/material/checkbox'; 21 | import { MatSelectModule } from '@angular/material/select'; 22 | 23 | 24 | const matModules = [ 25 | FlexLayoutModule, 26 | 27 | MatCardModule, 28 | MatFormFieldModule, 29 | MatSliderModule, 30 | MatProgressSpinnerModule, 31 | MatInputModule, 32 | MatToolbarModule, 33 | MatSidenavModule, 34 | MatListModule, 35 | MatButtonModule, 36 | MatIconModule, 37 | MatTableModule, 38 | MatPaginatorModule, 39 | MatSortModule, 40 | MatDialogModule, 41 | MatAutocompleteModule, 42 | MatCheckboxModule, 43 | MatSelectModule, 44 | ] 45 | 46 | @NgModule({ 47 | imports: [matModules], 48 | exports: [matModules], 49 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 50 | }) 51 | export class MaterialModule { } 52 | -------------------------------------------------------------------------------- /app/src/app/shared/models/deserializable.model.ts: -------------------------------------------------------------------------------- 1 | export interface Deserializable { 2 | deserialize(input: any): this; 3 | } -------------------------------------------------------------------------------- /app/src/app/shared/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Deserializable } from './deserializable.model'; 2 | 3 | export class User implements Deserializable{ 4 | id: number; 5 | displayName: string; 6 | email: string; 7 | phone: string; 8 | permissions: string[]; 9 | jwt: string; 10 | refresh_token: string; 11 | role: string; 12 | 13 | deserialize(input: any) { 14 | Object.assign(this, input); 15 | return this; 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/app/shared/services/alert.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AlertService } from './alert.service'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | describe('AlertService', () => { 7 | let service: AlertService; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [ RouterTestingModule] 12 | }); 13 | service = TestBed.inject(AlertService); 14 | }); 15 | 16 | it('should be created', () => { 17 | expect(service).toBeTruthy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /app/src/app/shared/services/alert.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { Router, NavigationStart } from '@angular/router'; 4 | 5 | export class Alert{ 6 | title: string = ''; 7 | text: string = '' 8 | } 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class AlertService { 14 | 15 | public subject = new Subject(); 16 | 17 | constructor(private router: Router) { 18 | this.router.events.subscribe(event => { 19 | if (event instanceof NavigationStart) { 20 | this.clear(); 21 | } 22 | }); 23 | } 24 | 25 | notify(title, message){ 26 | this.subject.next({title: title, text: message}); 27 | } 28 | 29 | alert(alert: Alert) { 30 | this.subject.next(alert); 31 | } 32 | 33 | clear(){ 34 | this.subject.next(null); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { MatToolbarModule } from '@angular/material/toolbar'; 5 | import { MatIconModule } from '@angular/material/icon'; 6 | 7 | 8 | @NgModule({ 9 | declarations: [], 10 | imports: [ 11 | CommonModule, 12 | MatToolbarModule, 13 | MatIconModule, 14 | ] 15 | }) 16 | export class SharedModule { } 17 | -------------------------------------------------------------------------------- /app/src/app/user/components/user-add/user-add.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Add New User

3 |

Update User ({{user.displayName}})

4 |
5 |
6 | 7 | 8 | 12 | 22 | 23 | 24 | 28 | 32 | 33 | 34 | 43 | 46 | 47 | 48 | 64 | 65 |
9 | Display Name* 10 | 11 | 13 | 14 | Role 15 | 16 | 17 | {{role.name}} 18 | 19 | 20 | 21 |
25 | Phone* 26 | 27 | 29 | Email* 30 | 31 |
35 | 36 | Password 37 | 41 | 42 | 44 | Send password via email 45 |
49 | 58 | 59 | 60 | 62 | 63 |
66 | 69 | 70 | 73 | 74 | 77 |
-------------------------------------------------------------------------------- /app/src/app/user/components/user-add/user-add.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/app/src/app/user/components/user-add/user-add.component.scss -------------------------------------------------------------------------------- /app/src/app/user/components/user-add/user-add.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserAddComponent } from './user-add.component'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | import { CommonModule } from '@angular/common'; 6 | import { MaterialModule } from 'src/app/material.module'; 7 | import { MatPasswordStrengthModule } from '@angular-material-extensions/password-strength'; 8 | import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; 9 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 10 | import { RouterTestingModule } from '@angular/router/testing'; 11 | import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 12 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 13 | 14 | describe('UserAddComponent', () => { 15 | let component: UserAddComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [ 21 | FormsModule, 22 | CommonModule, 23 | MaterialModule, 24 | ReactiveFormsModule, 25 | MatPasswordStrengthModule, 26 | SweetAlert2Module.forRoot(), 27 | HttpClientTestingModule, 28 | RouterTestingModule, 29 | BrowserAnimationsModule 30 | ], 31 | declarations: [ UserAddComponent ], 32 | providers: [ 33 | { 34 | provide: MatDialogRef, 35 | useValue: {} 36 | }, 37 | { 38 | provide: MAT_DIALOG_DATA, 39 | useValue: {} 40 | }, 41 | ], 42 | }) 43 | .compileComponents(); 44 | })); 45 | 46 | beforeEach(() => { 47 | fixture = TestBed.createComponent(UserAddComponent); 48 | component = fixture.componentInstance; 49 | fixture.detectChanges(); 50 | }); 51 | 52 | it('should create', () => { 53 | expect(component).toBeTruthy(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /app/src/app/user/components/user-add/user-add.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject } from '@angular/core'; 2 | import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { AlertService } from 'src/app/shared/services/alert.service'; 4 | import { Router } from '@angular/router'; 5 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 6 | 7 | import { Observable } from 'rxjs'; 8 | import { User } from 'src/app/shared/models/user.model'; 9 | import { UserService } from '../../services/user.service'; 10 | 11 | export interface DialogData { 12 | modal: boolean; 13 | user: User; 14 | } 15 | 16 | @Component({ 17 | selector: 'app-user-add', 18 | templateUrl: './user-add.component.html', 19 | styleUrls: ['./user-add.component.scss'] 20 | }) 21 | export class UserAddComponent implements OnInit { 22 | 23 | userForm: FormGroup; 24 | user: User = null; 25 | showStrengthInfo: boolean = false; 26 | roles: Observable; 27 | passwordWithValidation; 28 | 29 | 30 | constructor(private api: UserService, 31 | private formBuilder: FormBuilder, 32 | private router: Router, 33 | private alert: AlertService, 34 | public dialogRef: MatDialogRef, 35 | @Inject(MAT_DIALOG_DATA) public data: DialogData) { 36 | 37 | if(data.user){ 38 | this.user = data.user; 39 | }else{ 40 | this.user = null; 41 | } 42 | this.roles = this.api.getAvailableRoles(); 43 | this.userForm = this.createForm(); 44 | } 45 | 46 | ngOnInit(): void { 47 | 48 | } 49 | 50 | 51 | createForm(){ 52 | let psswdValidator = []; 53 | if(this.user == null){ 54 | psswdValidator = [Validators.required]; 55 | } 56 | 57 | return this.formBuilder.group({ 58 | displayName: new FormControl(this.user ? this.user.displayName : '', Validators.required), 59 | phone: new FormControl(this.user ? this.user.phone : '', Validators.required), 60 | email: new FormControl(this.user ? this.user.email : '', Validators.required), 61 | password: new FormControl('', psswdValidator), 62 | sendPassword: new FormControl(false), 63 | role: new FormControl(this.user ? this.user.role : '', Validators.required) 64 | }); 65 | } 66 | 67 | onStrengthChanged(event){ 68 | console.log(event); 69 | this.showStrengthInfo = true; 70 | } 71 | 72 | 73 | onSubmit(): void{ 74 | console.log(this.userForm.value); 75 | const usr = this.userForm.value as User; 76 | let obs: Observable; 77 | 78 | let action = 'created'; 79 | if(this.user){ 80 | usr.id = this.user.id; 81 | obs = this.api.update(usr); 82 | action = 'updated'; 83 | }else{ 84 | obs = this.api.create(usr); 85 | } 86 | obs.subscribe(resp => { 87 | console.log('Success'); 88 | this.alert.notify('Created', `User "${usr.displayName}" ${action} succesfully.`); 89 | this.dialogRef.close(true); 90 | }, error => { 91 | this.alert.notify('Error', error); 92 | console.log('Error', error); 93 | }) 94 | } 95 | 96 | reset(){ 97 | if(this.user){ 98 | this.userForm.reset(this.user); 99 | }else{ 100 | this.userForm.reset(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/app/user/datasource/user-data-source.ts: -------------------------------------------------------------------------------- 1 | import {CollectionViewer, DataSource} from "@angular/cdk/collections"; 2 | import { BehaviorSubject, Observable, of } from 'rxjs'; 3 | import { User } from 'src/app/shared/models/user.model'; 4 | import { catchError, finalize, map } from 'rxjs/operators'; 5 | import { NgxSpinnerService } from 'ngx-spinner'; 6 | import { UserService } from '../services/user.service'; 7 | 8 | export class UserDataSource implements DataSource { 9 | 10 | private usersSubject = new BehaviorSubject([]); 11 | private loadingSubject = new BehaviorSubject(false); 12 | 13 | public totalUsers = 0; 14 | 15 | constructor(private userService: UserService, private spinner: NgxSpinnerService) { 16 | this.loadingSubject.subscribe(status => { 17 | if(status){ 18 | this.spinner.show(); 19 | }else{ 20 | this.spinner.hide(); 21 | } 22 | }) 23 | } 24 | 25 | connect(collectionViewer: CollectionViewer): Observable { 26 | return this.usersSubject.asObservable(); 27 | } 28 | 29 | disconnect(collectionViewer: CollectionViewer): void { 30 | this.usersSubject.complete(); 31 | this.loadingSubject.complete(); 32 | } 33 | 34 | loadUsers(filter: string, sortField: string, sortDirection: string, pageIndex: number, pageSize: number) { 35 | 36 | console.log('Load Users', [ 37 | filter, sortField, sortDirection, pageIndex, pageSize 38 | ]); 39 | this.loadingSubject.next(true); 40 | 41 | this.userService.getUsers(filter, sortField, sortDirection, pageIndex, pageSize).pipe( 42 | map(data => { 43 | console.log(data.headers.get('X-Pagination-Total-Count')); 44 | console.log(data.headers); 45 | this.totalUsers = Number(data.headers.get('X-Pagination-Total-Count')); 46 | //console.log('resultsLength', this.resultsLength); 47 | return data.body as User[]; 48 | }), 49 | catchError(() => of([])), 50 | finalize(() => this.loadingSubject.next(false)), 51 | ) 52 | .subscribe(Users => this.usersSubject.next(Users)); 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/app/user/pages/user-list-page/user-list-page.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | Filter 8 | 9 | search 10 | 13 | 14 | 17 |
18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 57 | 58 | 59 |
ID {{element.id}} Name {{element.displayName}} Email {{element.email}} Phone {{element.phone}} Actions 46 | 49 | 54 |
60 |
61 | 62 | 63 |
64 |
65 |
-------------------------------------------------------------------------------- /app/src/app/user/pages/user-list-page/user-list-page.component.scss: -------------------------------------------------------------------------------- 1 | .container{ 2 | padding: 15px; 3 | } 4 | 5 | table { 6 | width: 100%; 7 | } 8 | 9 | .filter{ 10 | width: 30%; 11 | min-width: 150px; 12 | } 13 | 14 | .header-container{ 15 | justify-content: space-between; 16 | width: 100%; 17 | display: flex; 18 | } -------------------------------------------------------------------------------- /app/src/app/user/pages/user-list-page/user-list-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { UserListPageComponent } from './user-list-page.component'; 3 | import { MaterialModule } from 'src/app/material.module'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; 6 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | 9 | 10 | describe('UserListPageComponent', () => { 11 | let component: UserListPageComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [ UserListPageComponent ], 17 | imports: [ 18 | MaterialModule, 19 | FormsModule, 20 | SweetAlert2Module.forRoot(), 21 | HttpClientTestingModule, 22 | BrowserAnimationsModule 23 | ] 24 | }) 25 | .compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(UserListPageComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /app/src/app/user/pages/user-list-page/user-list-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, AfterViewInit, ElementRef } from '@angular/core'; 2 | import { MatPaginator } from '@angular/material/paginator'; 3 | import { MatSort } from '@angular/material/sort'; 4 | 5 | import {merge, Observable, of as observableOf, fromEvent} from 'rxjs'; 6 | import {catchError, map, startWith, switchMap, debounceTime, distinctUntilChanged, tap} from 'rxjs/operators'; 7 | import { NgxSpinnerService } from 'ngx-spinner'; 8 | import { SwalProvider } from '@sweetalert2/ngx-sweetalert2/lib/sweetalert2-loader.service'; 9 | import { AuthService } from 'src/app/auth/services/auth.service'; 10 | import { MatDialog } from '@angular/material/dialog'; 11 | import { UserDataSource } from '../../datasource/user-data-source'; 12 | import { UserService } from '../../services/user.service'; 13 | import { UserAddComponent } from '../../components/user-add/user-add.component'; 14 | 15 | @Component({ 16 | selector: 'app-user-list-page', 17 | templateUrl: './user-list-page.component.html', 18 | styleUrls: ['./user-list-page.component.scss'] 19 | }) 20 | export class UserListPageComponent implements OnInit { 21 | 22 | PER_PAGE = Math.floor((window.innerHeight - 105)/ 48); 23 | 24 | isLoadingResults = true; 25 | displayedColumns : string[] = ['id', 'name', 'email', 'phone1', 'actions']; 26 | dataSource: UserDataSource; 27 | resultsLength = 0; 28 | 29 | filterValue = ''; 30 | 31 | canCreate = false; 32 | canDelete = false; 33 | 34 | 35 | @ViewChild(MatPaginator) paginator: MatPaginator; 36 | @ViewChild(MatSort, {static: true}) sort: MatSort; 37 | @ViewChild('filterInput') input: ElementRef; 38 | 39 | 40 | constructor(private api: UserService, 41 | private auth: AuthService, 42 | private spinner: NgxSpinnerService, 43 | public dialog: MatDialog) { } 44 | 45 | ngOnInit() { 46 | this.canCreate = this.auth.can('user:create'); 47 | this.canDelete = this.auth.can('user:delete'); 48 | this.dataSource = new UserDataSource(this.api, this.spinner); 49 | this.dataSource.loadUsers('', 'id', 'asc', 0, this.PER_PAGE); 50 | } 51 | 52 | ngAfterViewInit() { 53 | 54 | fromEvent(this.input.nativeElement,'keyup') 55 | .pipe( 56 | debounceTime(150), 57 | distinctUntilChanged(), 58 | tap(() => { 59 | this.paginator.pageIndex = 0; 60 | this.loadUsers(); 61 | }) 62 | ) 63 | .subscribe(); 64 | 65 | // reset the paginator after sorting 66 | this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); 67 | 68 | // on sort or paginate events, load a new page 69 | merge(this.sort.sortChange, this.paginator.page) 70 | .pipe( 71 | tap(() => this.loadUsers()) 72 | ) 73 | .subscribe(); 74 | } 75 | 76 | clearFilter(){ 77 | this.filterValue = ''; 78 | this.loadUsers(); 79 | } 80 | 81 | loadUsers(){ 82 | this.dataSource.loadUsers( 83 | this.filterValue, 84 | this.sort.active, 85 | this.sort.direction, 86 | this.paginator.pageIndex, 87 | this.PER_PAGE 88 | ); 89 | } 90 | 91 | delete(user){ 92 | console.log(user); 93 | this.api.delete(user.id).subscribe( 94 | resp =>{ 95 | console.log('Sucess', resp); 96 | this.loadUsers(); 97 | }, 98 | error => { 99 | console.log('Error', error); 100 | }) 101 | } 102 | 103 | addOrEditDialog(user=null): void{ 104 | const dialogRef = this.dialog.open(UserAddComponent, { 105 | width: '500px', 106 | data: {modal: true, user: user}, 107 | disableClose: true 108 | }); 109 | 110 | dialogRef.afterClosed().subscribe(result => { 111 | this.loadUsers(); 112 | }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/app/user/services/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserService } from './user.service'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | 6 | describe('UserService', () => { 7 | let service: UserService; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule] 12 | }); 13 | service = TestBed.inject(UserService); 14 | }); 15 | 16 | it('should be created', () => { 17 | expect(service).toBeTruthy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /app/src/app/user/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpParams, HttpResponse, HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { User } from 'src/app/shared/models/user.model'; 5 | import { environment } from 'src/environments/environment'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class UserService { 11 | 12 | url = `${environment.apiBaseUrl}users`; 13 | 14 | constructor(private http: HttpClient) { } 15 | 16 | 17 | getUsers(filter: string, sortFiled: string, sortDirection, pageIndex, perPage): Observable> { 18 | console.log([sortFiled, sortDirection, pageIndex]); 19 | 20 | let params = new HttpParams() 21 | .set('per-page', `${perPage}`) 22 | .set('page', `${pageIndex}`) 23 | .set('search', filter); 24 | 25 | if(sortDirection == 'asc'){ 26 | params = params.append('sort', `-${sortFiled}`); 27 | }else if(sortDirection == 'desc'){ 28 | params = params.append('sort', `${sortFiled}`); 29 | } 30 | 31 | return this.http.get(this.url, { observe: 'response' as 'response', params: params}); 32 | } 33 | 34 | getAvailableRoles(){ 35 | return this.http.get(`${this.url}/available-roles`); 36 | } 37 | 38 | delete(id){ 39 | return this.http.delete(`${this.url}/${id}`); 40 | } 41 | 42 | create(user: User){ 43 | return this.http.post(this.url, user); 44 | } 45 | 46 | update(user: User){ 47 | return this.http.put(`${this.url}/${user.id}`, user); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/app/user/user-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { AuthGuard } from '../auth/guards/auth.guard'; 4 | import { UserListPageComponent } from './pages/user-list-page/user-list-page.component'; 5 | 6 | 7 | const routes: Routes = [ 8 | { path: '**', component: UserListPageComponent, canActivate: [AuthGuard], data: {permission: 'user:view'}} 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class UserRoutingModule { } 16 | -------------------------------------------------------------------------------- /app/src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { UserListPageComponent } from './pages/user-list-page/user-list-page.component'; 4 | import { UserAddComponent } from './components/user-add/user-add.component'; 5 | import { UserRoutingModule } from './user-routing.module'; 6 | import { MaterialModule } from '../material.module'; 7 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 8 | import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; 9 | import { MatPasswordStrengthModule } from '@angular-material-extensions/password-strength'; 10 | 11 | 12 | 13 | @NgModule({ 14 | declarations: [UserListPageComponent, UserAddComponent], 15 | imports: [ 16 | CommonModule, 17 | UserRoutingModule, 18 | MaterialModule, 19 | FormsModule, 20 | ReactiveFormsModule, 21 | SweetAlert2Module, 22 | MatPasswordStrengthModule 23 | ], 24 | entryComponents: [UserAddComponent] 25 | }) 26 | export class UserModule { } 27 | -------------------------------------------------------------------------------- /app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/app/src/assets/.gitkeep -------------------------------------------------------------------------------- /app/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/app/src/assets/logo.png -------------------------------------------------------------------------------- /app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /app/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | apiBaseUrl: 'http://localhost:8080/v1/' 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/app/src/favicon.ico -------------------------------------------------------------------------------- /app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CrmApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /app/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /app/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '@angular/material/prebuilt-themes/deeppurple-amber.css'; 3 | 4 | html, body { height: 100%; } 5 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 6 | 7 | body { 8 | background-image: url(https://lh4.googleusercontent.com/-XplyTa1Za-I/VMSgIyAYkHI/AAAAAAAADxM/oL-rD6VP4ts/w1184-h666/Android-Lollipop-wallpapers-Google-Now-Wallpaper-2.png); 9 | background-position: center; 10 | background-size: cover; 11 | background-repeat: no-repeat; 12 | min-height: 100vh; 13 | font-family: 'Roboto', sans-serif; 14 | } 15 | 16 | .app-header { 17 | justify-content: space-between; 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | z-index: 2; 23 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12); 24 | } 25 | 26 | .login-wrapper { 27 | height: 100%; 28 | } 29 | 30 | .positronx { 31 | text-decoration: none; 32 | color: #ffffff; 33 | } -------------------------------------------------------------------------------- /app/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | 23 | } 24 | }, 25 | "angularCompilerOptions": { 26 | "fullTemplateTypeCheck": true, 27 | "strictInjectionParameters": true, 28 | "enableIvy": false 29 | } 30 | } -------------------------------------------------------------------------------- /app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /app/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-var-requires": false, 64 | "object-literal-key-quotes": [ 65 | true, 66 | "as-needed" 67 | ], 68 | "object-literal-sort-keys": false, 69 | "ordered-imports": false, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "trailing-comma": false, 75 | "no-conflicting-lifecycle": true, 76 | "no-host-metadata-property": true, 77 | "no-input-rename": true, 78 | "no-inputs-metadata-property": true, 79 | "no-output-native": true, 80 | "no-output-on-prefix": true, 81 | "no-output-rename": true, 82 | "no-outputs-metadata-property": true, 83 | "template-banana-in-box": true, 84 | "template-no-negated-async": true, 85 | "use-lifecycle-interface": true, 86 | "use-pipe-transform-interface": true 87 | }, 88 | "rulesDirectory": [ 89 | "codelyzer" 90 | ] 91 | } -------------------------------------------------------------------------------- /backend/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "vendor/bower-asset" 3 | } 4 | -------------------------------------------------------------------------------- /backend/.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 | #vagrant folder 33 | /.vagrant -------------------------------------------------------------------------------- /backend/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software LLC nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | Directory Structure 2 | ------------------- 3 | commands/ Console commands for permission generation and user creation 4 | migrations/ Database migrations 5 | models/ Shared model classes 6 | modules/v1/ API version 1 module 7 | modules/v1/controllers/ Controllers for api v1 8 | 9 | Special Classes 10 | --------------- 11 | Below classes are base classes for certain classes, 12 | 13 | app\modules\v1\controllers\ApiController Used as base class for non ActiveControllers. Extending from this will automatically configure authentication & authorization, CORS and optionally rate limiting 14 | app\modules\v1\controllers\RestController Used as ActiveController base class. Extending from this will automatically configure authentication & authorization, CORS and optionally rate limiting 15 | 16 | API Endpoints 17 | ------------ 18 | * POST auth/token 19 | Retrieve JWT token & Refresh token by either providing username & password or refresh token. For email/password set grant_type=password, for resfresh token set grant_type=refresh_token 20 | * GET users?search={query}&per-page=10&page=2&sort=-id 21 | Get list of users. search,per-page,page,sort params are optional. If search param is provided will search against displayName, email & phone attributes. Result can be paged by providing `per-page` and `page` attributes. Paging information will be available in the response header. Sort param will sort the result per specified attribute (ASC id => id, DESC id => -id) 22 | * GET users/{id} 23 | Get user specified by id 24 | * POST users 25 | Create new user based on the provided attributes 26 | * PUT users/{id} 27 | Update user 28 | * DELETE users/{id} 29 | Delete specified user 30 | 31 | Authentications 32 | -------------- 33 | Authentication handled via JWT tokens. JWT generation code can be found in models/User 34 | 35 | $token = $jwt->getBuilder() 36 | ->setIssuer('https://github.com/databoxtech/yii2-angular-template')// Configures the issuer (iss claim) 37 | ->setAudience('yii2-angular-template')// Configures the audience (aud claim) 38 | ->setId('6O5457V2RW', true)// Configures the id (jti claim), replicating as a header item 39 | ->setIssuedAt(time())// Configures the time that the token was issue (iat claim) 40 | ->setExpiration(time() + 5184000)// Configures the expiration time (60 days) of the token (exp claim) 41 | ->set('uid', $user->id)// Configures a new claim, called "id" 42 | ->set('displayName', $user->displayName) 43 | ->set('permission', $permissions) 44 | ->sign($signer, $jwt->key)// creates a signature using [[Jwt::$key]] 45 | ->getToken(); 46 | 47 | Authorizations 48 | -------------- 49 | Yii2 Database RBAC is used. However in controllers instead of restricting access based on roles, permissions are used to manage access. Permissions has a special format, 50 | 51 | controller:action 52 | Ex: user:create, user:view 53 | Base class rest controller is configured to check access based on this permission definition format, 54 | 55 | $behaviors['access'] = [ 56 | 'class' => AccessControl::className(), 57 | 'rules' => [ 58 | [ 59 | 'allow' => true, 60 | 'roles' => ['@'], 61 | 'matchCallback' => function ($rule, $action) { 62 | return Yii::$app->user->can(Yii::$app->controller->id.':'.$action->id); 63 | }, 64 | ], 65 | ], 66 | 'denyCallback' => function ($rule, $action) { 67 | throw new \yii\web\ForbiddenHttpException('You are not allowed to access this page: '. json_encode($action)); 68 | } 69 | ]; 70 | 71 | 72 | Available Console Commands 73 | -------------------------- 74 | Apart from basic yii console commands, below two commands are available 75 | * user/create {email} {password} : create new user account by providing email/ password 76 | * user/permissions : Generate permissions and roles. Modify this command to include required permissions. -------------------------------------------------------------------------------- /backend/Vagrantfile: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'fileutils' 3 | 4 | required_plugins = %w( vagrant-hostmanager vagrant-vbguest ) 5 | required_plugins.each do |plugin| 6 | exec "vagrant plugin install #{plugin}" unless Vagrant.has_plugin? plugin 7 | end 8 | 9 | domains = { 10 | app: 'yii2basic.test' 11 | } 12 | 13 | vagrantfile_dir_path = File.dirname(__FILE__) 14 | 15 | config = { 16 | local: vagrantfile_dir_path + '/vagrant/config/vagrant-local.yml', 17 | example: vagrantfile_dir_path + '/vagrant/config/vagrant-local.example.yml' 18 | } 19 | 20 | # copy config from example if local config not exists 21 | FileUtils.cp config[:example], config[:local] unless File.exist?(config[:local]) 22 | # read config 23 | options = YAML.load_file config[:local] 24 | 25 | # check github token 26 | if options['github_token'].nil? || options['github_token'].to_s.length != 40 27 | puts "You must place REAL GitHub token into configuration:\n/yii2-app-basic/vagrant/config/vagrant-local.yml" 28 | exit 29 | end 30 | 31 | # vagrant configurate 32 | Vagrant.configure(2) do |config| 33 | # select the box 34 | config.vm.box = 'bento/ubuntu-16.04' 35 | 36 | # should we ask about box updates? 37 | config.vm.box_check_update = options['box_check_update'] 38 | 39 | config.vm.provider 'virtualbox' do |vb| 40 | # machine cpus count 41 | vb.cpus = options['cpus'] 42 | # machine memory size 43 | vb.memory = options['memory'] 44 | # machine name (for VirtualBox UI) 45 | vb.name = options['machine_name'] 46 | end 47 | 48 | # machine name (for vagrant console) 49 | config.vm.define options['machine_name'] 50 | 51 | # machine name (for guest machine console) 52 | config.vm.hostname = options['machine_name'] 53 | 54 | # network settings 55 | config.vm.network 'private_network', ip: options['ip'] 56 | 57 | # sync: folder 'yii2-app-advanced' (host machine) -> folder '/app' (guest machine) 58 | config.vm.synced_folder './', '/app', owner: 'vagrant', group: 'vagrant' 59 | 60 | # disable folder '/vagrant' (guest machine) 61 | config.vm.synced_folder '.', '/vagrant', disabled: true 62 | 63 | # hosts settings (host machine) 64 | config.vm.provision :hostmanager 65 | config.hostmanager.enabled = true 66 | config.hostmanager.manage_host = true 67 | config.hostmanager.ignore_private_ip = false 68 | config.hostmanager.include_offline = true 69 | config.hostmanager.aliases = domains.values 70 | 71 | # quick fix for failed guest additions installations 72 | # config.vbguest.auto_update = false 73 | 74 | # provisioners 75 | config.vm.provision 'shell', path: './vagrant/provision/once-as-root.sh', args: [options['timezone']] 76 | config.vm.provision 'shell', path: './vagrant/provision/once-as-vagrant.sh', args: [options['github_token']], privileged: false 77 | config.vm.provision 'shell', path: './vagrant/provision/always-as-root.sh', run: 'always' 78 | 79 | # post-install message (vagrant console) 80 | config.vm.post_up_message = "App URL: http://#{domains[:app]}" 81 | end 82 | -------------------------------------------------------------------------------- /backend/assets/AppAsset.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0 17 | */ 18 | class AppAsset extends AssetBundle 19 | { 20 | public $basePath = '@webroot'; 21 | public $baseUrl = '@web'; 22 | public $css = [ 23 | 'css/site.css', 24 | ]; 25 | public $js = [ 26 | ]; 27 | public $depends = [ 28 | 'yii\web\YiiAsset', 29 | 'yii\bootstrap\BootstrapAsset', 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /backend/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/test.php' 15 | 16 | # To enable code coverage: 17 | #coverage: 18 | # #c3_url: http://localhost:8080/index-test.php/ 19 | # enabled: true 20 | # #remote: true 21 | # #remote_config: '../codeception.yml' 22 | # whitelist: 23 | # include: 24 | # - models/* 25 | # - controllers/* 26 | # - commands/* 27 | # - mail/* 28 | # blacklist: 29 | # include: 30 | # - assets/* 31 | # - config/* 32 | # - runtime/* 33 | # - vendor/* 34 | # - views/* 35 | # - web/* 36 | # - tests/* 37 | -------------------------------------------------------------------------------- /backend/commands/UserController.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 2.0 22 | */ 23 | class UserController extends Controller 24 | { 25 | /** 26 | * This command echoes what you have entered as the message. 27 | * @param string $message the message to be echoed. 28 | * @return int Exit code 29 | */ 30 | public function actionCreate($username, $password) 31 | { 32 | $user = new User(); 33 | $user->email = $username; 34 | $user->setPassword($password); 35 | $user->save(false); 36 | 37 | echo "User created, $username => $password => {$user->id}\n"; 38 | 39 | } 40 | 41 | 42 | public function actionPermissions(){ 43 | $auth = Yii::$app->authManager; 44 | $auth->removeAll(); 45 | 46 | $permissionsArray = [ 47 | 'user:me' => 'Get my profile', 48 | 'user:index' => 'View users', 49 | 'user:view' => 'View user', 50 | 'user:create' => 'Add users', 51 | 'user:update' => 'Edit users', 52 | 'user:delete' => 'Delete users', 53 | 'user:available-roles' => 'Get available roles', 54 | 'user:assign-role' => 'Assign role to a specified user', 55 | ]; 56 | 57 | $admin = $auth->createRole('admin'); 58 | $auth->add($admin); 59 | 60 | $permissions = []; 61 | foreach ($permissionsArray as $perm => $desc){ 62 | $permissions[$perm] = $auth->createPermission($perm); 63 | $permissions[$perm]->description = $desc; 64 | $auth->add($permissions[$perm]); 65 | $auth->addChild($admin, $permissions[$perm]); 66 | } 67 | 68 | $user =$auth->createRole('user'); 69 | $auth->add($user); 70 | $auth->addChild($user, $permissions['user:index']); 71 | $auth->addChild($user, $permissions['user:view']); 72 | 73 | User::deleteAll([]); 74 | 75 | $user = new User(); 76 | $user->displayName = 'Test Admin'; 77 | $user->email = 'admin@template.com'; 78 | $user->setPassword('test@123'); 79 | $user->save(false); 80 | 81 | $auth->assign($admin, $user->id); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /backend/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": "stable", 16 | "require": { 17 | "php": ">=7.0", 18 | "yiisoft/yii2": "~2.0.14", 19 | "yiisoft/yii2-bootstrap": "~2.0.0", 20 | "yiisoft/yii2-swiftmailer": "~2.0.0 || ~2.1.0", 21 | "sizeg/yii2-jwt": "^2.0" 22 | }, 23 | "require-dev": { 24 | "yiisoft/yii2-debug": "~2.1.0", 25 | "yiisoft/yii2-gii": "~2.1.0", 26 | "yiisoft/yii2-faker": "~2.0.0", 27 | 28 | "codeception/base": "~2.3.0", 29 | "codeception/verify": "~0.4.0", 30 | "codeception/specify": "~0.4.6", 31 | "symfony/browser-kit": ">=2.7 <=4.2.4" 32 | }, 33 | "config": { 34 | "process-timeout": 1800, 35 | "fxp-asset": { 36 | "enabled": false 37 | } 38 | }, 39 | "scripts": { 40 | "post-install-cmd": [ 41 | "yii\\composer\\Installer::postInstall" 42 | ], 43 | "post-create-project-cmd": [ 44 | "yii\\composer\\Installer::postCreateProject", 45 | "yii\\composer\\Installer::postInstall" 46 | ] 47 | }, 48 | "extra": { 49 | "yii\\composer\\Installer::postCreateProject": { 50 | "setPermission": [ 51 | { 52 | "runtime": "0777", 53 | "web/assets": "0777", 54 | "yii": "0755" 55 | } 56 | ] 57 | }, 58 | "yii\\composer\\Installer::postInstall": { 59 | "generateCookieValidationKey": [ 60 | "config/web.php" 61 | ] 62 | } 63 | }, 64 | "repositories": [ 65 | { 66 | "type": "composer", 67 | "url": "https://asset-packagist.org" 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /backend/config/console.php: -------------------------------------------------------------------------------- 1 | 'basic-console', 8 | 'basePath' => dirname(__DIR__), 9 | 'bootstrap' => ['log'], 10 | 'controllerNamespace' => 'app\commands', 11 | 'aliases' => [ 12 | '@bower' => '@vendor/bower-asset', 13 | '@npm' => '@vendor/npm-asset', 14 | '@tests' => '@app/tests', 15 | ], 16 | 'components' => [ 17 | 'cache' => [ 18 | 'class' => 'yii\caching\FileCache', 19 | ], 20 | 'authManager' => [ 21 | 'class' => 'yii\rbac\DbManager', 22 | // uncomment if you want to cache RBAC items hierarchy 23 | // 'cache' => 'cache', 24 | ], 25 | 'log' => [ 26 | 'targets' => [ 27 | [ 28 | 'class' => 'yii\log\FileTarget', 29 | 'levels' => ['error', 'warning'], 30 | ], 31 | ], 32 | ], 33 | 'db' => $db, 34 | ], 35 | 'params' => $params, 36 | /* 37 | 'controllerMap' => [ 38 | 'fixture' => [ // Fixture generation command line. 39 | 'class' => 'yii\faker\FixtureController', 40 | ], 41 | ], 42 | */ 43 | ]; 44 | 45 | if (YII_ENV_DEV) { 46 | // configuration adjustments for 'dev' environment 47 | $config['bootstrap'][] = 'gii'; 48 | $config['modules']['gii'] = [ 49 | 'class' => 'yii\gii\Module', 50 | ]; 51 | } 52 | 53 | return $config; 54 | -------------------------------------------------------------------------------- /backend/config/db.php: -------------------------------------------------------------------------------- 1 | 'yii\db\Connection', 5 | 'dsn' => 'mysql:host=localhost;dbname=template', 6 | 'username' => 'root', 7 | 'password' => '', 8 | 'charset' => 'utf8', 9 | 10 | // Schema cache options (for production environment) 11 | //'enableSchemaCache' => true, 12 | //'schemaCacheDuration' => 60, 13 | //'schemaCache' => 'cache', 14 | ]; 15 | -------------------------------------------------------------------------------- /backend/config/params.php: -------------------------------------------------------------------------------- 1 | 'admin@example.com', 5 | 'senderEmail' => 'noreply@example.com', 6 | 'senderName' => 'Example.com mailer', 7 | 8 | 'jwt.expire.duration' => 30, // 900 seconds 9 | 'refresh_token.expire.duration' => 90, // 900 seconds 10 | ]; 11 | -------------------------------------------------------------------------------- /backend/config/test.php: -------------------------------------------------------------------------------- 1 | 'basic-tests', 10 | 'basePath' => dirname(__DIR__), 11 | 'aliases' => [ 12 | '@bower' => '@vendor/bower-asset', 13 | '@npm' => '@vendor/npm-asset', 14 | ], 15 | 'language' => 'en-US', 16 | 'components' => [ 17 | 'db' => $db, 18 | 'mailer' => [ 19 | 'useFileTransport' => true, 20 | ], 21 | 'assetManager' => [ 22 | 'basePath' => __DIR__ . '/../web/assets', 23 | ], 24 | 'urlManager' => [ 25 | 'showScriptName' => true, 26 | ], 27 | 'user' => [ 28 | 'identityClass' => 'app\models\User', 29 | ], 30 | 'request' => [ 31 | 'cookieValidationKey' => 'test', 32 | 'enableCsrfValidation' => false, 33 | // but if you absolutely need it set cookie domain to localhost 34 | /* 35 | 'csrfCookie' => [ 36 | 'domain' => 'localhost', 37 | ], 38 | */ 39 | ], 40 | ], 41 | 'params' => $params, 42 | ]; 43 | -------------------------------------------------------------------------------- /backend/config/test_db.php: -------------------------------------------------------------------------------- 1 | 'basic', 8 | 'basePath' => dirname(__DIR__), 9 | 'bootstrap' => ['log'], 10 | 'aliases' => [ 11 | '@bower' => '@vendor/bower-asset', 12 | '@npm' => '@vendor/npm-asset', 13 | ], 14 | 'modules' => [ 15 | 'v1' => [ 16 | 'class' => 'app\modules\v1\Module', 17 | ], 18 | ], 19 | 'components' => [ 20 | 'request' => [ 21 | // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation 22 | 'cookieValidationKey' => 'phFySSh6w-F6VXYnvtPKhpWP0PTHY_em', 23 | 'parsers' => [ 24 | 'application/json' => 'yii\web\JsonParser', 25 | ] 26 | ], 27 | 'authManager' => [ 28 | 'class' => 'yii\rbac\DbManager', 29 | // uncomment if you want to cache RBAC items hierarchy 30 | // 'cache' => 'cache', 31 | ], 32 | 'cache' => [ 33 | 'class' => 'yii\caching\FileCache', 34 | ], 35 | 'user' => [ 36 | 'identityClass' => 'app\models\User', 37 | 'enableAutoLogin' => false, 38 | ], 39 | 'jwt' => [ 40 | 'class' => 'sizeg\jwt\Jwt', 41 | 'key' => '9864rwu1241m39i3', 42 | ], 43 | 'errorHandler' => [ 44 | 'errorAction' => 'site/error', 45 | ], 46 | 'mailer' => [ 47 | 'class' => 'yii\swiftmailer\Mailer', 48 | // send all mails to a file by default. You have to set 49 | // 'useFileTransport' to false and configure a transport 50 | // for the mailer to send real emails. 51 | 'useFileTransport' => true, 52 | ], 53 | 'log' => [ 54 | 'traceLevel' => YII_DEBUG ? 3 : 0, 55 | 'targets' => [ 56 | [ 57 | 'class' => 'yii\log\FileTarget', 58 | 'levels' => ['error', 'warning'], 59 | ], 60 | ], 61 | ], 62 | 'db' => $db, 63 | 'urlManager' => [ 64 | 'enablePrettyUrl' => true, 65 | 'showScriptName' => false, 66 | 'rules' => [ 67 | 'OPTIONS /v1/' => '/v1/api/options', 68 | [ 69 | 'class' => 'yii\rest\UrlRule', 70 | 'controller' => ['v1/user'], 71 | 'extraPatterns' => [ 72 | 'GET available-roles' => 'available-roles' 73 | ], 74 | ] 75 | ], 76 | ], 77 | ], 78 | 'params' => $params, 79 | ]; 80 | 81 | if (YII_ENV_DEV) { 82 | // configuration adjustments for 'dev' environment 83 | $config['bootstrap'][] = 'debug'; 84 | $config['modules']['debug'] = [ 85 | 'class' => 'yii\debug\Module', 86 | // uncomment the following to add your IP if you are not connecting from localhost. 87 | //'allowedIPs' => ['127.0.0.1', '::1'], 88 | ]; 89 | 90 | $config['bootstrap'][] = 'gii'; 91 | $config['modules']['gii'] = [ 92 | 'class' => 'yii\gii\Module', 93 | // uncomment the following to add your IP if you are not connecting from localhost. 94 | //'allowedIPs' => ['127.0.0.1', '::1'], 95 | ]; 96 | } 97 | 98 | return $config; 99 | -------------------------------------------------------------------------------- /backend/controllers/CorsCustom.php: -------------------------------------------------------------------------------- 1 | getRequest()->getMethod() === 'OPTIONS') { 21 | Yii::$app->getResponse()->getHeaders()->set('Allow', 'POST GET PUT'); 22 | Yii::$app->end(); 23 | } 24 | 25 | return true; 26 | } 27 | } -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | php: 4 | image: yiisoftware/yii2-php:7.1-apache 5 | volumes: 6 | - ~/.composer-docker/cache:/root/.composer/cache:delegated 7 | - ./:/app:delegated 8 | ports: 9 | - '8000:80' -------------------------------------------------------------------------------- /backend/mail/layouts/html.php: -------------------------------------------------------------------------------- 1 | 8 | beginPage() ?> 9 | 10 | 11 | 12 | 13 | <?= Html::encode($this->title) ?> 14 | head() ?> 15 | 16 | 17 | beginBody() ?> 18 | 19 | endBody() ?> 20 | 21 | 22 | endPage() ?> 23 | -------------------------------------------------------------------------------- /backend/migrations/m191021_165358_user.php: -------------------------------------------------------------------------------- 1 | createTable('users', [ 17 | 'id' => $this->primaryKey(11), 18 | 'password' => $this->char(128), 19 | 'displayName' => $this->char(128), 20 | 'email' => $this->char(128)->notNull()->unique(), 21 | 'phone' => $this->char(15), 22 | 'deleted' => $this->boolean()->defaultValue(false), 23 | 'blocked' => $this->boolean()->defaultValue(false), 24 | 'created_at' => $this->dateTime()->defaultExpression('NOW()'), 25 | 'updated_at' => $this->dateTime()->defaultExpression('NOW()')->append('ON UPDATE NOW()'), 26 | ]); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function safeDown() 33 | { 34 | return $this->dropTable('users');; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/migrations/m200316_065802_refresh_token.php: -------------------------------------------------------------------------------- 1 | createTable('refresh_tokens', [ 16 | 'id' => $this->primaryKey(11), 17 | 'user' => $this->integer(11), 18 | 'token' => $this->char(192), 19 | 'created_at' => $this->dateTime()->defaultExpression('NOW()'), 20 | 'expires_at' => $this->dateTime(), 21 | ]); 22 | 23 | $this->addForeignKey('ifx-refresh_token-user', 'refresh_tokens', 'user', 'users', 'id'); 24 | return true; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function safeDown() 31 | { 32 | $this->dropForeignKey('ifx-refresh_token-user', 'refresh_tokens'); 33 | return $this->dropTable('refresh_tokens');; 34 | } 35 | 36 | /* 37 | // Use up()/down() to run migration code without a transaction. 38 | public function up() 39 | { 40 | 41 | } 42 | 43 | public function down() 44 | { 45 | echo "m200316_065802_refresh_token cannot be reverted.\n"; 46 | 47 | return false; 48 | } 49 | */ 50 | } 51 | -------------------------------------------------------------------------------- /backend/models/RefreshToken.php: -------------------------------------------------------------------------------- 1 | 192], 37 | [['user'], 'exist', 'skipOnError' => true, 'targetClass' => User::className(), 'targetAttribute' => ['user' => 'id']], 38 | ]; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function attributeLabels() 45 | { 46 | return [ 47 | 'id' => 'ID', 48 | 'user' => 'User', 49 | 'token' => 'Token', 50 | 'created_at' => 'Created At', 51 | 'expires_at' => 'Expires At', 52 | ]; 53 | } 54 | 55 | /** 56 | * @return \yii\db\ActiveQuery 57 | */ 58 | public function getUser0() 59 | { 60 | return $this->hasOne(User::className(), ['id' => 'user']); 61 | } 62 | 63 | public function isExpired(){ 64 | return strtotime($this->expires_at) < time(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/models/User.php: -------------------------------------------------------------------------------- 1 | $username]); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function rules() 42 | { 43 | return [ 44 | [['deleted', 'blocked'], 'integer'], 45 | [['created_at', 'updated_at'], 'safe'], 46 | [['password', 'displayName', 'email'], 'string', 'max' => 128], 47 | [['phone'], 'string', 'max' => 15], 48 | [['email'], 'unique'], 49 | [['email'], 'required'], 50 | 51 | //generate password hash, if password field is dirty (updated) 52 | ['password', 'filter', 'skipOnEmpty' => true, 'filter' => function ($plain_password) { 53 | return Yii::$app->getSecurity()->generatePasswordHash($plain_password); 54 | }, 'when' => function ($model, $attribute) { 55 | return isset($model->dirtyAttributes['password']); 56 | }], 57 | ]; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function attributeLabels() 64 | { 65 | return [ 66 | 'id' => 'ID', 67 | 'password' => 'Password', 68 | 'displayName' => 'Display Name', 69 | 'email' => 'Email', 70 | 'phone' => 'Phone', 71 | 'deleted' => 'Deleted', 72 | 'blocked' => 'Blocked', 73 | 'created_at' => 'Created At', 74 | 'updated_at' => 'Updated At', 75 | ]; 76 | } 77 | 78 | public function fields() { 79 | return [ 80 | 'id', 81 | 'displayName', 82 | 'email', 83 | 'phone', 84 | 'deleted', 85 | 'blocked', 86 | 'role' 87 | ]; 88 | } 89 | 90 | /* 91 | * @return array 92 | */ 93 | public function getPublic(){ 94 | return [ 95 | 'id' => $this->id, 96 | 'displayName' => $this->displayName, 97 | 'email' => $this->email, 98 | 'phone' => $this->phone, 99 | ]; 100 | } 101 | 102 | /** 103 | * Finds an identity by the given ID. 104 | * @param string|int $id the ID to be looked for 105 | * @return IdentityInterface|null the identity object that matches the given ID. 106 | * Null should be returned if such an identity cannot be found 107 | * or the identity is not in an active state (disabled, deleted, etc.) 108 | */ 109 | public static function findIdentity($id) 110 | { 111 | return User::findOne(['id' => $id]); 112 | } 113 | 114 | /** 115 | * Finds an identity by the given token. 116 | * @param mixed $token the token to be looked for 117 | * @param mixed $type the type of the token. The value of this parameter depends on the implementation. 118 | * For example, [[\yii\filters\auth\HttpBearerAuth]] will set this parameter to be `yii\filters\auth\HttpBearerAuth`. 119 | * @return IdentityInterface|null the identity object that matches the given token. 120 | * Null should be returned if such an identity cannot be found 121 | * or the identity is not in an active state (disabled, deleted, etc.) 122 | */ 123 | public static function findIdentityByAccessToken($token, $type = null) 124 | { 125 | $u = new User(); 126 | $u->id = $token->getClaim('uid'); 127 | return $u; 128 | } 129 | 130 | /** 131 | * Returns an ID that can uniquely identify a user identity. 132 | * @return string|int an ID that uniquely identifies a user identity. 133 | */ 134 | public function getId() 135 | { 136 | return $this->id; 137 | } 138 | 139 | /** 140 | * Returns a key that can be used to check the validity of a given identity ID. 141 | * 142 | * The key should be unique for each individual user, and should be persistent 143 | * so that it can be used to check the validity of the user identity. 144 | * 145 | * The space of such keys should be big enough to defeat potential identity attacks. 146 | * 147 | * This is required if [[User::enableAutoLogin]] is enabled. The returned key will be stored on the 148 | * client side as a cookie and will be used to authenticate user even if PHP session has been expired. 149 | * 150 | * Make sure to invalidate earlier issued authKeys when you implement force user logout, password change and 151 | * other scenarios, that require forceful access revocation for old sessions. 152 | * 153 | * @return string a key that is used to check the validity of a given identity ID. 154 | * @see validateAuthKey() 155 | */ 156 | public function getAuthKey() 157 | { 158 | // TODO: Implement getAuthKey() method. 159 | } 160 | 161 | /** 162 | * Validates the given auth key. 163 | * 164 | * This is required if [[User::enableAutoLogin]] is enabled. 165 | * @param string $authKey the given auth key 166 | * @return bool whether the given auth key is valid. 167 | * @see getAuthKey() 168 | */ 169 | public function validateAuthKey($authKey) 170 | { 171 | // TODO: Implement validateAuthKey() method. 172 | } 173 | 174 | /** 175 | * Validates password 176 | * 177 | * @param string $password password to validate 178 | * @return bool if password provided is valid for current user 179 | */ 180 | public function validatePassword($password) 181 | { 182 | return Yii::$app->getSecurity()->validatePassword($password, $this->password); 183 | } 184 | 185 | public function setPassword($plain_password){ 186 | $this->password = Yii::$app->getSecurity()->generatePasswordHash($plain_password); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /backend/models/UserSearch.php: -------------------------------------------------------------------------------- 1 | select([ 44 | 'users.id', 45 | 'users.deleted', 46 | 'users.blocked', 47 | 'users.displayName', 48 | 'users.email', 49 | 'users.phone', 50 | 'users.created_at', 51 | 'users.updated_at', 52 | 'auth_assignment.item_name as role' 53 | ]); 54 | 55 | $query->join('LEFT OUTER JOIN','auth_assignment','auth_assignment.user_id = users.id'); 56 | 57 | $dataProvider = new ActiveDataProvider([ 58 | 'query' => $query, 59 | ]); 60 | 61 | if(isset($params['search'])){ 62 | $search = $params['search']; 63 | $query->andFilterWhere(['like', 'displayName', $search]) 64 | ->orFilterWhere(['like', 'email', $search]) 65 | ->orFilterWhere(['like', 'phone', $search]); 66 | } 67 | 68 | return $dataProvider; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /backend/modules/v1/Module.php: -------------------------------------------------------------------------------- 1 | user->enableSession = false; 27 | \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; 28 | \Yii::$app->response->charset = 'UTF-8'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/modules/v1/controllers/ApiController.php: -------------------------------------------------------------------------------- 1 | JwtHttpBearerAuth::class, 31 | 'except' => ['options'], 32 | ]; 33 | 34 | $behaviors['authenticator']['except'] = ['options']; 35 | 36 | 37 | $behaviors['corsFilter'] = [ 38 | 'class' => Cors::className(), 39 | 'cors' => [ 40 | 'Origin' => ['*'], 41 | 'Access-Control-Allow-Origin' => ['*'], 42 | 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], 43 | 'Access-Control-Allow-Headers' => ['Content-Type', 'Authorization'], 44 | 'Access-Control-Allow-Credentials' => true, 45 | 'Access-Control-Max-Age' => 86400, 46 | 'Access-Control-Expose-Headers' => [] 47 | ] 48 | 49 | ]; 50 | 51 | $behaviors['access'] = [ 52 | 'class' => AccessControl::className(), 53 | 'rules' => [ 54 | [ 55 | 'allow' => true, 56 | 'roles' => ['@'], 57 | 'matchCallback' => function ($rule, $action) { 58 | return Yii::$app->user->can(Yii::$app->controller->id.':'.$action->id); 59 | }, 60 | ], 61 | ], 62 | 'denyCallback' => function ($rule, $action) { 63 | throw new \yii\web\ForbiddenHttpException('You are not allowed to access this page: '. json_encode($action)); 64 | } 65 | ]; 66 | 67 | return $behaviors; 68 | } 69 | 70 | public function getParam($name){ 71 | return isset($this->params[$name]) ? $this->params[$name] : false; 72 | } 73 | 74 | public function actionOptions(){ 75 | return ''; 76 | } 77 | 78 | public function init() 79 | { 80 | $this->request = json_decode(file_get_contents('php://input'), true); 81 | $params = Yii::$app->request->bodyParams; 82 | 83 | $this->params = array_merge($this->request ? $this->request : [], $params); 84 | } 85 | 86 | 87 | public function beforeAction($action) 88 | { 89 | Yii::$app->getResponse()->getHeaders()->set('Access-Control-Allow-Origin', '*'); 90 | Yii::$app->getResponse()->getHeaders()->set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 91 | 92 | if (Yii::$app->getRequest()->getMethod() === 'OPTIONS') { 93 | Yii::$app->getResponse()->getHeaders()->set('Access-Control-Allow-Credentials', 'true'); 94 | Yii::$app->end(); 95 | } 96 | return parent::beforeAction($action); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /backend/modules/v1/controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | request->getBodyParam('grant_type', 'password'); 29 | $user = null; 30 | 31 | if($grant_type == 'password') { 32 | if (!$username = $this->getParam("email")) { 33 | throw new HttpException(500, 'Email cannot be empty'); 34 | } 35 | if (!$password = $this->getParam("password")) { 36 | throw new HttpException(500, 'password cannot be empty'); 37 | } 38 | 39 | if (!$user = User::findByUsername($username)) { 40 | throw new HttpException(404, 'User not found'); 41 | } 42 | 43 | if ($user->validatePassword($password)) { 44 | if ($user->deleted || $user->blocked) { 45 | throw new HttpException(401, "Unauthorized user."); 46 | } 47 | 48 | } else { 49 | throw new HttpException(403, "Invalid Credentials {$username} {$password}"); 50 | } 51 | }else if($grant_type == 'refresh_token'){ 52 | if (!$refresh_token = $this->getParam("refresh_token")) { 53 | throw new HttpException(500, 'refresh_token cannot be empty'); 54 | } 55 | if(!$rt = RefreshToken::findOne(['token' => $refresh_token])){ 56 | throw new HttpException(500, 'Invalid refresh_token'); 57 | } 58 | 59 | if($rt->isExpired()){ 60 | $rt->delete(); 61 | throw new HttpException(500, 'Refresh token expired.'); 62 | } 63 | $user = $rt->user0; 64 | 65 | $rt->delete(); 66 | } 67 | 68 | $user_permissions = Yii::$app->authManager->getPermissionsByUser($user->id); 69 | $permissions = []; 70 | foreach ($user_permissions as $key => $user_permission) { 71 | array_push($permissions, $key); 72 | } 73 | 74 | $refresh_token = $this->createRefreshToken($user); 75 | $jwt = $this->createJwt($user, $permissions); 76 | 77 | return [ 78 | 'refresh_token' => $refresh_token, 79 | 'jwt' => $jwt, 80 | 'expires_in' => Yii::$app->params['jwt.expire.duration'], 81 | 'user' => $user, 82 | 'permissions' => $permissions 83 | ]; 84 | } 85 | 86 | 87 | protected function createJwt($user, $permissions) 88 | { 89 | $signer = new \Lcobucci\JWT\Signer\Hmac\Sha256(); 90 | /** @var Jwt $jwt */ 91 | $jwt = Yii::$app->jwt; 92 | 93 | $token = $jwt->getBuilder() 94 | ->setIssuer('https://github.com/databoxtech/yii2-angular-template')// Configures the issuer (iss claim) 95 | ->setAudience('yii2-angular-template')// Configures the audience (aud claim) 96 | ->setId('6O5457V2RW', true)// Configures the id (jti claim), replicating as a header item 97 | ->setIssuedAt(time())// Configures the time that the token was issue (iat claim) 98 | ->setExpiration(time() + Yii::$app->params['jwt.expire.duration'])// Configures the expiration time (60 days) of the token (exp claim) 99 | ->set('uid', $user->id)// Configures a new claim, called "id" 100 | ->set('displayName', $user->displayName) 101 | ->set('permission', $permissions) 102 | ->sign($signer, $jwt->key)// creates a signature using [[Jwt::$key]] 103 | ->getToken(); 104 | 105 | return (string)$token; 106 | } 107 | 108 | protected function createRefreshToken($user){ 109 | $rt = new RefreshToken(); 110 | $rt->user = $user->id; 111 | $rt->token = $this->random_str(192); 112 | $rt->expires_at = date('Y-m-d H:i:s', (time() + Yii::$app->params['refresh_token.expire.duration'])); 113 | if(!$rt->save()){ 114 | throw new ServerErrorHttpException('Internal error occurred'); 115 | } 116 | return $rt->token; 117 | } 118 | 119 | /** 120 | * Ref: https://stackoverflow.com/a/31107425/2177996 121 | * 122 | * @param int $length Length of the generated key 123 | * @param string $keyspace Character space to be used 124 | * @return string 125 | * @throws Exception 126 | */ 127 | protected function random_str( 128 | int $length = 64, 129 | string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ=$'): string { 130 | if ($length < 1) { 131 | throw new \RangeException("Length must be a positive integer"); 132 | } 133 | $pieces = []; 134 | $max = mb_strlen($keyspace, '8bit') - 1; 135 | for ($i = 0; $i < $length; ++$i) { 136 | $pieces []= $keyspace[random_int(0, $max)]; 137 | } 138 | return implode('', $pieces); 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /backend/modules/v1/controllers/RestController.php: -------------------------------------------------------------------------------- 1 | JwtHttpBearerAuth::class, 23 | 'except' => ['options'], 24 | ]; 25 | 26 | $behaviors['authenticator']['except'] = ['options']; 27 | 28 | 29 | $behaviors['corsFilter'] = [ 30 | 'class' => Cors::className(), 31 | 'cors' => [ 32 | 'Origin' => ['*'], 33 | 'Access-Control-Allow-Origin' => ['*'], 34 | 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], 35 | 'Access-Control-Request-Headers' => ['*'], 36 | 'Access-Control-Allow-Credentials' => null, 37 | 'Access-Control-Max-Age' => 86400, 38 | 'Access-Control-Expose-Headers' => [ 39 | 'X-Total-Count', 40 | 'X-Paging-PageSize', 41 | 'X-Pagination-Current-Page', 42 | 'X-Pagination-Page-Count', 43 | 'X-Pagination-Per-Page', 44 | 'X-Pagination-Total-Count', 45 | ] 46 | ] 47 | 48 | ]; 49 | 50 | $behaviors['access'] = [ 51 | 'class' => AccessControl::className(), 52 | 'rules' => [ 53 | [ 54 | 'allow' => true, 55 | 'roles' => ['@'], 56 | 'matchCallback' => function ($rule, $action) { 57 | return Yii::$app->user->can(Yii::$app->controller->id.':'.$action->id); 58 | }, 59 | ], 60 | ], 61 | 'denyCallback' => function ($rule, $action) { 62 | throw new \yii\web\ForbiddenHttpException('You are not allowed to access this page: '. json_encode($action)); 63 | } 64 | ]; 65 | 66 | 67 | return $behaviors; 68 | } 69 | 70 | public function beforeAction($action) 71 | { 72 | //your code 73 | Yii::$app->getResponse()->getHeaders()->set('Access-Control-Allow-Origin', '*'); 74 | 75 | if (Yii::$app->getRequest()->getMethod() === 'OPTIONS') { 76 | parent::beforeAction($action); 77 | Yii::$app->getResponse()->getHeaders()->set('Access-Control-Allow-Credentials', 'true'); 78 | Yii::$app->end(); 79 | } 80 | return parent::beforeAction($action); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /backend/modules/v1/controllers/UserController.php: -------------------------------------------------------------------------------- 1 | search(\Yii::$app->request->queryParams); 38 | } 39 | 40 | public function actionAvailableRoles(){ 41 | $roles = Yii::$app->authManager->getRoles(); 42 | return array_values($roles); 43 | } 44 | 45 | public function actionAssignRole(){ 46 | $roleName = Yii::$app->request->getBodyParam('role', null); 47 | $userid = Yii::$app->request->getBodyParam('user', null); 48 | if($roleName == null || $userid == null){ 49 | throw new ServerErrorHttpException("Some data not found in request"); 50 | } 51 | 52 | if($role = Yii::$app->authManager->getRole($roleName) == null){ 53 | throw new NotFoundHttpException("Specified role, \"{$roleName}\" not found."); 54 | } 55 | 56 | Yii::$app->authManager->assign($role, $userid); 57 | 58 | $response = Yii::$app->getResponse(); 59 | $response->setStatusCode(200); 60 | } 61 | 62 | /** @noinspection DuplicatedCode */ 63 | public function actionCreate(){ 64 | $model = new User(); 65 | 66 | //validate role 67 | $role = null; 68 | if(($roleName = Yii::$app->request->getBodyParam('role', false)) !== false && 69 | ($role = Yii::$app->authManager->getRole($roleName) === null)){ 70 | throw new NotFoundHttpException("Specified role, \"{$roleName}\" not found."); 71 | } 72 | 73 | $model->load(Yii::$app->request->getBodyParams(), ''); 74 | if ($model->save()) { 75 | //role is set 76 | if($role = Yii::$app->authManager->getRole($roleName)){ 77 | Yii::$app->authManager->assign($role, $model->id); 78 | } 79 | $response = Yii::$app->getResponse(); 80 | $response->setStatusCode(201); 81 | $response->getHeaders()->set('Location', Url::toRoute(['user/view', 'id' => $model->id], true)); 82 | } elseif (!$model->hasErrors()) { 83 | throw new ServerErrorHttpException('Failed to create the object for unknown reason.'); 84 | } 85 | 86 | return $model; 87 | } 88 | 89 | public function actionUpdate($id){ 90 | $model = User::findOne(['id' => $id]); 91 | //validate role 92 | $role = null; 93 | if(($roleName = Yii::$app->request->getBodyParam('role', false)) !== false && 94 | ($role = Yii::$app->authManager->getRole($roleName) === null)){ 95 | throw new NotFoundHttpException("Specified role, \"{$roleName}\" not found."); 96 | } 97 | 98 | $model->load(Yii::$app->getRequest()->getBodyParams(), ''); 99 | if ($model->save() === false && !$model->hasErrors()) { 100 | throw new ServerErrorHttpException('Failed to update the object for unknown reason.'); 101 | } 102 | 103 | if($role = Yii::$app->authManager->getRole($roleName)){ 104 | Yii::$app->authManager->revokeAll($model->id); 105 | Yii::$app->authManager->assign($role, $model->id); 106 | } 107 | 108 | return $model; 109 | } 110 | 111 | public function actionMe(){ 112 | return User::findOne(['id' => Yii::$app->user->id]); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /backend/requirements.php: -------------------------------------------------------------------------------- 1 | Error\n\n" 33 | . "

The path to yii framework seems to be incorrect.

\n" 34 | . '

You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . ".

\n" 35 | . '

Please refer to the README on how to install Yii.

\n"; 36 | 37 | if (!empty($_SERVER['argv'])) { 38 | // do not print HTML when used in console mode 39 | echo strip_tags($message); 40 | } else { 41 | echo $message; 42 | } 43 | exit(1); 44 | } 45 | 46 | require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); 47 | $requirementsChecker = new YiiRequirementChecker(); 48 | 49 | $gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.'; 50 | $gdOK = $imagickOK = false; 51 | 52 | if (extension_loaded('imagick')) { 53 | $imagick = new Imagick(); 54 | $imagickFormats = $imagick->queryFormats('PNG'); 55 | if (in_array('PNG', $imagickFormats)) { 56 | $imagickOK = true; 57 | } else { 58 | $imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.'; 59 | } 60 | } 61 | 62 | if (extension_loaded('gd')) { 63 | $gdInfo = gd_info(); 64 | if (!empty($gdInfo['FreeType Support'])) { 65 | $gdOK = true; 66 | } else { 67 | $gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.'; 68 | } 69 | } 70 | 71 | /** 72 | * Adjust requirements according to your application specifics. 73 | */ 74 | $requirements = array( 75 | // Database : 76 | array( 77 | 'name' => 'PDO extension', 78 | 'mandatory' => true, 79 | 'condition' => extension_loaded('pdo'), 80 | 'by' => 'All DB-related classes', 81 | ), 82 | array( 83 | 'name' => 'PDO SQLite extension', 84 | 'mandatory' => false, 85 | 'condition' => extension_loaded('pdo_sqlite'), 86 | 'by' => 'All DB-related classes', 87 | 'memo' => 'Required for SQLite database.', 88 | ), 89 | array( 90 | 'name' => 'PDO MySQL extension', 91 | 'mandatory' => false, 92 | 'condition' => extension_loaded('pdo_mysql'), 93 | 'by' => 'All DB-related classes', 94 | 'memo' => 'Required for MySQL database.', 95 | ), 96 | array( 97 | 'name' => 'PDO PostgreSQL extension', 98 | 'mandatory' => false, 99 | 'condition' => extension_loaded('pdo_pgsql'), 100 | 'by' => 'All DB-related classes', 101 | 'memo' => 'Required for PostgreSQL database.', 102 | ), 103 | // Cache : 104 | array( 105 | 'name' => 'Memcache extension', 106 | 'mandatory' => false, 107 | 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), 108 | 'by' => 'MemCache', 109 | 'memo' => extension_loaded('memcached') ? 'To use memcached set MemCache::useMemcached to true.' : '' 110 | ), 111 | // CAPTCHA: 112 | array( 113 | 'name' => 'GD PHP extension with FreeType support', 114 | 'mandatory' => false, 115 | 'condition' => $gdOK, 116 | 'by' => 'Captcha', 117 | 'memo' => $gdMemo, 118 | ), 119 | array( 120 | 'name' => 'ImageMagick PHP extension with PNG support', 121 | 'mandatory' => false, 122 | 'condition' => $imagickOK, 123 | 'by' => 'Captcha', 124 | 'memo' => $imagickMemo, 125 | ), 126 | // PHP ini : 127 | 'phpExposePhp' => array( 128 | 'name' => 'Expose PHP', 129 | 'mandatory' => false, 130 | 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), 131 | 'by' => 'Security reasons', 132 | 'memo' => '"expose_php" should be disabled at php.ini', 133 | ), 134 | 'phpAllowUrlInclude' => array( 135 | 'name' => 'PHP allow url include', 136 | 'mandatory' => false, 137 | 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), 138 | 'by' => 'Security reasons', 139 | 'memo' => '"allow_url_include" should be disabled at php.ini', 140 | ), 141 | 'phpSmtp' => array( 142 | 'name' => 'PHP mail SMTP', 143 | 'mandatory' => false, 144 | 'condition' => strlen(ini_get('SMTP')) > 0, 145 | 'by' => 'Email sending', 146 | 'memo' => 'PHP mail SMTP server required', 147 | ), 148 | ); 149 | 150 | // OPcache check 151 | if (!version_compare(phpversion(), '5.5', '>=')) { 152 | $requirements[] = array( 153 | 'name' => 'APC extension', 154 | 'mandatory' => false, 155 | 'condition' => extension_loaded('apc'), 156 | 'by' => 'ApcCache', 157 | ); 158 | } 159 | 160 | $result = $requirementsChecker->checkYii()->check($requirements)->getResult(); 161 | $requirementsChecker->render(); 162 | exit($result['summary']['errors'] === 0 ? 0 : 1); 163 | -------------------------------------------------------------------------------- /backend/runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /backend/tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | amOnPage(Url::toRoute('/site/about')); 10 | $I->see('About', 'h1'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/tests/acceptance/HomeCest.php: -------------------------------------------------------------------------------- 1 | amOnPage(Url::toRoute('/site/index')); 10 | $I->see('My Company'); 11 | 12 | $I->seeLink('About'); 13 | $I->click('About'); 14 | $I->wait(2); // wait for page to be opened 15 | 16 | $I->see('This is the About page.'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/tests/acceptance/LoginCest.php: -------------------------------------------------------------------------------- 1 | amOnPage(Url::toRoute('/site/login')); 10 | $I->see('Login', 'h1'); 11 | 12 | $I->amGoingTo('try to login with correct credentials'); 13 | $I->fillField('input[name="LoginForm[username]"]', 'admin'); 14 | $I->fillField('input[name="LoginForm[password]"]', 'admin'); 15 | $I->click('login-button'); 16 | $I->wait(2); // wait for button to be clicked 17 | 18 | $I->expectTo('see user info'); 19 | $I->see('Logout'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/tests/acceptance/_bootstrap.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'db' => require __DIR__ . '/../../config/test_db.php' 22 | ] 23 | ] 24 | ); 25 | 26 | 27 | $application = new yii\console\Application($config); 28 | $exitCode = $application->run(); 29 | exit($exitCode); 30 | -------------------------------------------------------------------------------- /backend/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 7 | rem @link http://www.yiiframework.com/ 8 | rem @copyright Copyright (c) 2008 Yii Software LLC 9 | rem @license http://www.yiiframework.com/license/ 10 | rem ------------------------------------------------------------- 11 | 12 | @setlocal 13 | 14 | set YII_PATH=%~dp0 15 | 16 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe 17 | 18 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 19 | 20 | @endlocal 21 | -------------------------------------------------------------------------------- /backend/tests/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for functional (integration) tests. 4 | # emulate web requests and make application process them. 5 | # (tip: better to use with frameworks). 6 | 7 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 8 | #basic/web/index.php 9 | class_name: FunctionalTester 10 | modules: 11 | enabled: 12 | - Filesystem 13 | - Yii2 14 | -------------------------------------------------------------------------------- /backend/tests/functional/ContactFormCest.php: -------------------------------------------------------------------------------- 1 | amOnPage(['site/contact']); 8 | } 9 | 10 | public function openContactPage(\FunctionalTester $I) 11 | { 12 | $I->see('Contact', 'h1'); 13 | } 14 | 15 | public function submitEmptyForm(\FunctionalTester $I) 16 | { 17 | $I->submitForm('#contact-form', []); 18 | $I->expectTo('see validations errors'); 19 | $I->see('Contact', 'h1'); 20 | $I->see('Name cannot be blank'); 21 | $I->see('Email cannot be blank'); 22 | $I->see('Subject cannot be blank'); 23 | $I->see('Body cannot be blank'); 24 | $I->see('The verification code is incorrect'); 25 | } 26 | 27 | public function submitFormWithIncorrectEmail(\FunctionalTester $I) 28 | { 29 | $I->submitForm('#contact-form', [ 30 | 'ContactForm[name]' => 'tester', 31 | 'ContactForm[email]' => 'tester.email', 32 | 'ContactForm[subject]' => 'test subject', 33 | 'ContactForm[body]' => 'test content', 34 | 'ContactForm[verifyCode]' => 'testme', 35 | ]); 36 | $I->expectTo('see that email address is wrong'); 37 | $I->dontSee('Name cannot be blank', '.help-inline'); 38 | $I->see('Email is not a valid email address.'); 39 | $I->dontSee('Subject cannot be blank', '.help-inline'); 40 | $I->dontSee('Body cannot be blank', '.help-inline'); 41 | $I->dontSee('The verification code is incorrect', '.help-inline'); 42 | } 43 | 44 | public function submitFormSuccessfully(\FunctionalTester $I) 45 | { 46 | $I->submitForm('#contact-form', [ 47 | 'ContactForm[name]' => 'tester', 48 | 'ContactForm[email]' => 'tester@example.com', 49 | 'ContactForm[subject]' => 'test subject', 50 | 'ContactForm[body]' => 'test content', 51 | 'ContactForm[verifyCode]' => 'testme', 52 | ]); 53 | $I->seeEmailIsSent(); 54 | $I->dontSeeElement('#contact-form'); 55 | $I->see('Thank you for contacting us. We will respond to you as soon as possible.'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /backend/tests/functional/LoginFormCest.php: -------------------------------------------------------------------------------- 1 | amOnRoute('site/login'); 8 | } 9 | 10 | public function openLoginPage(\FunctionalTester $I) 11 | { 12 | $I->see('Login', 'h1'); 13 | 14 | } 15 | 16 | // demonstrates `amLoggedInAs` method 17 | public function internalLoginById(\FunctionalTester $I) 18 | { 19 | $I->amLoggedInAs(100); 20 | $I->amOnPage('/'); 21 | $I->see('Logout (admin)'); 22 | } 23 | 24 | // demonstrates `amLoggedInAs` method 25 | public function internalLoginByInstance(\FunctionalTester $I) 26 | { 27 | $I->amLoggedInAs(\app\models\User::findByUsername('admin')); 28 | $I->amOnPage('/'); 29 | $I->see('Logout (admin)'); 30 | } 31 | 32 | public function loginWithEmptyCredentials(\FunctionalTester $I) 33 | { 34 | $I->submitForm('#login-form', []); 35 | $I->expectTo('see validations errors'); 36 | $I->see('Username cannot be blank.'); 37 | $I->see('Password cannot be blank.'); 38 | } 39 | 40 | public function loginWithWrongCredentials(\FunctionalTester $I) 41 | { 42 | $I->submitForm('#login-form', [ 43 | 'LoginForm[username]' => 'admin', 44 | 'LoginForm[password]' => 'wrong', 45 | ]); 46 | $I->expectTo('see validations errors'); 47 | $I->see('Incorrect username or password.'); 48 | } 49 | 50 | public function loginSuccessfully(\FunctionalTester $I) 51 | { 52 | $I->submitForm('#login-form', [ 53 | 'LoginForm[username]' => 'admin', 54 | 'LoginForm[password]' => 'admin', 55 | ]); 56 | $I->see('Logout (admin)'); 57 | $I->dontSeeElement('form#login-form'); 58 | } 59 | } -------------------------------------------------------------------------------- /backend/tests/functional/_bootstrap.php: -------------------------------------------------------------------------------- 1 | model = $this->getMockBuilder('app\models\ContactForm') 20 | ->setMethods(['validate']) 21 | ->getMock(); 22 | 23 | $this->model->expects($this->once()) 24 | ->method('validate') 25 | ->willReturn(true); 26 | 27 | $this->model->attributes = [ 28 | 'name' => 'Tester', 29 | 'email' => 'tester@example.com', 30 | 'subject' => 'very important letter subject', 31 | 'body' => 'body of current message', 32 | ]; 33 | 34 | expect_that($this->model->contact('admin@example.com')); 35 | 36 | // using Yii2 module actions to check email was sent 37 | $this->tester->seeEmailIsSent(); 38 | 39 | /** @var MessageInterface $emailMessage */ 40 | $emailMessage = $this->tester->grabLastSentEmail(); 41 | expect('valid email is sent', $emailMessage)->isInstanceOf('yii\mail\MessageInterface'); 42 | expect($emailMessage->getTo())->hasKey('admin@example.com'); 43 | expect($emailMessage->getFrom())->hasKey('noreply@example.com'); 44 | expect($emailMessage->getReplyTo())->hasKey('tester@example.com'); 45 | expect($emailMessage->getSubject())->equals('very important letter subject'); 46 | expect($emailMessage->toString())->contains('body of current message'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/tests/unit/models/LoginFormTest.php: -------------------------------------------------------------------------------- 1 | user->logout(); 14 | } 15 | 16 | public function testLoginNoUser() 17 | { 18 | $this->model = new LoginForm([ 19 | 'username' => 'not_existing_username', 20 | 'password' => 'not_existing_password', 21 | ]); 22 | 23 | expect_not($this->model->login()); 24 | expect_that(\Yii::$app->user->isGuest); 25 | } 26 | 27 | public function testLoginWrongPassword() 28 | { 29 | $this->model = new LoginForm([ 30 | 'username' => 'demo', 31 | 'password' => 'wrong_password', 32 | ]); 33 | 34 | expect_not($this->model->login()); 35 | expect_that(\Yii::$app->user->isGuest); 36 | expect($this->model->errors)->hasKey('password'); 37 | } 38 | 39 | public function testLoginCorrect() 40 | { 41 | $this->model = new LoginForm([ 42 | 'username' => 'demo', 43 | 'password' => 'demo', 44 | ]); 45 | 46 | expect_that($this->model->login()); 47 | expect_not(\Yii::$app->user->isGuest); 48 | expect($this->model->errors)->hasntKey('password'); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /backend/tests/unit/models/UserTest.php: -------------------------------------------------------------------------------- 1 | username)->equals('admin'); 13 | 14 | expect_not(User::findIdentity(999)); 15 | } 16 | 17 | public function testFindUserByAccessToken() 18 | { 19 | expect_that($user = User::findIdentityByAccessToken('100-token')); 20 | expect($user->username)->equals('admin'); 21 | 22 | expect_not(User::findIdentityByAccessToken('non-existing')); 23 | } 24 | 25 | public function testFindUserByUsername() 26 | { 27 | expect_that($user = User::findByUsername('admin')); 28 | expect_not(User::findByUsername('not-admin')); 29 | } 30 | 31 | /** 32 | * @depends testFindUserByUsername 33 | */ 34 | public function testValidateUser($user) 35 | { 36 | $user = User::findByUsername('admin'); 37 | expect_that($user->validateAuthKey('test100key')); 38 | expect_not($user->validateAuthKey('test102key')); 39 | 40 | expect_that($user->validatePassword('admin')); 41 | expect_not($user->validatePassword('123456')); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /backend/vagrant/config/.gitignore: -------------------------------------------------------------------------------- 1 | # local configuration 2 | vagrant-local.yml -------------------------------------------------------------------------------- /backend/vagrant/config/vagrant-local.example.yml: -------------------------------------------------------------------------------- 1 | # Your personal GitHub token 2 | github_token: 3 | # Read more: https://github.com/blog/1509-personal-api-tokens 4 | # You can generate it here: https://github.com/settings/tokens 5 | 6 | # Guest OS timezone 7 | timezone: Europe/London 8 | 9 | # Are we need check box updates for every 'vagrant up'? 10 | box_check_update: false 11 | 12 | # Virtual machine name 13 | machine_name: yii2basic 14 | 15 | # Virtual machine IP 16 | ip: 192.168.83.137 17 | 18 | # Virtual machine CPU cores number 19 | cpus: 1 20 | 21 | # Virtual machine RAM 22 | memory: 1024 23 | -------------------------------------------------------------------------------- /backend/vagrant/nginx/app.conf: -------------------------------------------------------------------------------- 1 | server { 2 | charset utf-8; 3 | client_max_body_size 128M; 4 | sendfile off; 5 | 6 | listen 80; ## listen for ipv4 7 | #listen [::]:80 default_server ipv6only=on; ## listen for ipv6 8 | 9 | server_name yii2basic.test; 10 | root /app/web/; 11 | index index.php; 12 | 13 | access_log /app/vagrant/nginx/log/yii2basic.access.log; 14 | error_log /app/vagrant/nginx/log/yii2basic.error.log; 15 | 16 | location / { 17 | # Redirect everything that isn't a real file to index.php 18 | try_files $uri $uri/ /index.php$is_args$args; 19 | } 20 | 21 | # uncomment to avoid processing of calls to non-existing static files by Yii 22 | #location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ { 23 | # try_files $uri =404; 24 | #} 25 | #error_page 404 /404.html; 26 | 27 | location ~ \.php$ { 28 | include fastcgi_params; 29 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 30 | #fastcgi_pass 127.0.0.1:9000; 31 | fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; 32 | try_files $uri =404; 33 | } 34 | 35 | location ~ /\.(ht|svn|git) { 36 | deny all; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/vagrant/nginx/log/.gitignore: -------------------------------------------------------------------------------- 1 | #nginx logs 2 | yii2basic.access.log 3 | yii2basic.error.log -------------------------------------------------------------------------------- /backend/vagrant/provision/always-as-root.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #== Bash helpers == 4 | 5 | function info { 6 | echo " " 7 | echo "--> $1" 8 | echo " " 9 | } 10 | 11 | #== Provision script == 12 | 13 | info "Provision-script user: `whoami`" 14 | 15 | info "Restart web-stack" 16 | service php7.0-fpm restart 17 | service nginx restart 18 | service mysql restart -------------------------------------------------------------------------------- /backend/vagrant/provision/once-as-root.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #== Import script args == 4 | 5 | timezone=$(echo "$1") 6 | 7 | #== Bash helpers == 8 | 9 | function info { 10 | echo " " 11 | echo "--> $1" 12 | echo " " 13 | } 14 | 15 | #== Provision script == 16 | 17 | info "Provision-script user: `whoami`" 18 | 19 | export DEBIAN_FRONTEND=noninteractive 20 | 21 | info "Configure timezone" 22 | timedatectl set-timezone ${timezone} --no-ask-password 23 | 24 | info "Prepare root password for MySQL" 25 | debconf-set-selections <<< "mariadb-server-10.0 mysql-server/root_password password \"''\"" 26 | debconf-set-selections <<< "mariadb-server-10.0 mysql-server/root_password_again password \"''\"" 27 | echo "Done!" 28 | 29 | info "Update OS software" 30 | apt-get update 31 | apt-get upgrade -y 32 | 33 | info "Install additional software" 34 | apt-get install -y php7.0-curl php7.0-cli php7.0-intl php7.0-mysqlnd php7.0-gd php7.0-fpm php7.0-mbstring php7.0-xml unzip nginx mariadb-server-10.0 php.xdebug 35 | 36 | info "Configure MySQL" 37 | sed -i "s/.*bind-address.*/bind-address = 0.0.0.0/" /etc/mysql/mariadb.conf.d/50-server.cnf 38 | mysql -uroot <<< "CREATE USER 'root'@'%' IDENTIFIED BY ''" 39 | mysql -uroot <<< "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'" 40 | mysql -uroot <<< "DROP USER 'root'@'localhost'" 41 | mysql -uroot <<< "FLUSH PRIVILEGES" 42 | echo "Done!" 43 | 44 | info "Configure PHP-FPM" 45 | sed -i 's/user = www-data/user = vagrant/g' /etc/php/7.0/fpm/pool.d/www.conf 46 | sed -i 's/group = www-data/group = vagrant/g' /etc/php/7.0/fpm/pool.d/www.conf 47 | sed -i 's/owner = www-data/owner = vagrant/g' /etc/php/7.0/fpm/pool.d/www.conf 48 | cat << EOF > /etc/php/7.0/mods-available/xdebug.ini 49 | zend_extension=xdebug.so 50 | xdebug.remote_enable=1 51 | xdebug.remote_connect_back=1 52 | xdebug.remote_port=9000 53 | xdebug.remote_autostart=1 54 | EOF 55 | echo "Done!" 56 | 57 | info "Configure NGINX" 58 | sed -i 's/user www-data/user vagrant/g' /etc/nginx/nginx.conf 59 | echo "Done!" 60 | 61 | info "Enabling site configuration" 62 | ln -s /app/vagrant/nginx/app.conf /etc/nginx/sites-enabled/app.conf 63 | echo "Done!" 64 | 65 | info "Removing default site configuration" 66 | rm /etc/nginx/sites-enabled/default 67 | echo "Done!" 68 | 69 | info "Initailize databases for MySQL" 70 | mysql -uroot <<< "CREATE DATABASE yii2basic" 71 | mysql -uroot <<< "CREATE DATABASE yii2basic_test" 72 | echo "Done!" 73 | 74 | info "Install composer" 75 | curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer -------------------------------------------------------------------------------- /backend/vagrant/provision/once-as-vagrant.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #== Import script args == 4 | 5 | github_token=$(echo "$1") 6 | 7 | #== Bash helpers == 8 | 9 | function info { 10 | echo " " 11 | echo "--> $1" 12 | echo " " 13 | } 14 | 15 | #== Provision script == 16 | 17 | info "Provision-script user: `whoami`" 18 | 19 | info "Configure composer" 20 | composer config --global github-oauth.github.com ${github_token} 21 | echo "Done!" 22 | 23 | info "Install project dependencies" 24 | cd /app 25 | composer --no-progress --prefer-dist install 26 | 27 | info "Create bash-alias 'app' for vagrant user" 28 | echo 'alias app="cd /app"' | tee /home/vagrant/.bash_aliases 29 | 30 | info "Enabling colorized prompt for guest console" 31 | sed -i "s/#force_color_prompt=yes/force_color_prompt=yes/" /home/vagrant/.bashrc 32 | -------------------------------------------------------------------------------- /backend/views/layouts/main.php: -------------------------------------------------------------------------------- 1 | 15 | beginPage() ?> 16 | 17 | 18 | 19 | 20 | 21 | 22 | registerCsrfMetaTags() ?> 23 | <?= Html::encode($this->title) ?> 24 | head() ?> 25 | 26 | 27 | beginBody() ?> 28 | 29 |
30 | Yii::$app->name, 33 | 'brandUrl' => Yii::$app->homeUrl, 34 | 'options' => [ 35 | 'class' => 'navbar-inverse navbar-fixed-top', 36 | ], 37 | ]); 38 | echo Nav::widget([ 39 | 'options' => ['class' => 'navbar-nav navbar-right'], 40 | 'items' => [ 41 | ['label' => 'Home', 'url' => ['/site/index']], 42 | ['label' => 'About', 'url' => ['/site/about']], 43 | ['label' => 'Contact', 'url' => ['/site/contact']], 44 | Yii::$app->user->isGuest ? ( 45 | ['label' => 'Login', 'url' => ['/site/login']] 46 | ) : ( 47 | '
  • ' 48 | . Html::beginForm(['/site/logout'], 'post') 49 | . Html::submitButton( 50 | 'Logout (' . Yii::$app->user->identity->username . ')', 51 | ['class' => 'btn btn-link logout'] 52 | ) 53 | . Html::endForm() 54 | . '
  • ' 55 | ) 56 | ], 57 | ]); 58 | NavBar::end(); 59 | ?> 60 | 61 |
    62 | isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], 64 | ]) ?> 65 | 66 | 67 |
    68 |
    69 | 70 |
    71 |
    72 |

    © My Company

    73 | 74 |

    75 |
    76 |
    77 | 78 | endBody() ?> 79 | 80 | 81 | endPage() ?> 82 | -------------------------------------------------------------------------------- /backend/web/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_FILENAME} !-d 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule . index.php [L] 5 | -------------------------------------------------------------------------------- /backend/web/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/web/css/site.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | .wrap { 7 | min-height: 100%; 8 | height: auto; 9 | margin: 0 auto -60px; 10 | padding: 0 0 60px; 11 | } 12 | 13 | .wrap > .container { 14 | padding: 70px 15px 20px; 15 | } 16 | 17 | .footer { 18 | height: 60px; 19 | background-color: #f5f5f5; 20 | border-top: 1px solid #ddd; 21 | padding-top: 20px; 22 | } 23 | 24 | .jumbotron { 25 | text-align: center; 26 | background-color: transparent; 27 | } 28 | 29 | .jumbotron .btn { 30 | font-size: 21px; 31 | padding: 14px 24px; 32 | } 33 | 34 | .not-set { 35 | color: #c55; 36 | font-style: italic; 37 | } 38 | 39 | /* add sorting icons to gridview sort links */ 40 | a.asc:after, a.desc:after { 41 | position: relative; 42 | top: 1px; 43 | display: inline-block; 44 | font-family: 'Glyphicons Halflings'; 45 | font-style: normal; 46 | font-weight: normal; 47 | line-height: 1; 48 | padding-left: 5px; 49 | } 50 | 51 | a.asc:after { 52 | content: /*"\e113"*/ "\e151"; 53 | } 54 | 55 | a.desc:after { 56 | content: /*"\e114"*/ "\e152"; 57 | } 58 | 59 | .sort-numerical a.asc:after { 60 | content: "\e153"; 61 | } 62 | 63 | .sort-numerical a.desc:after { 64 | content: "\e154"; 65 | } 66 | 67 | .sort-ordinal a.asc:after { 68 | content: "\e155"; 69 | } 70 | 71 | .sort-ordinal a.desc:after { 72 | content: "\e156"; 73 | } 74 | 75 | .grid-view th { 76 | white-space: nowrap; 77 | } 78 | 79 | .hint-block { 80 | display: block; 81 | margin-top: 5px; 82 | color: #999; 83 | } 84 | 85 | .error-summary { 86 | color: #a94442; 87 | background: #fdf7f7; 88 | border-left: 3px solid #eed3d7; 89 | padding: 10px 20px; 90 | margin: 0 0 15px 0; 91 | } 92 | 93 | /* align the logout "link" (button in form) of the navbar */ 94 | .nav li > form > button.logout { 95 | padding: 15px; 96 | border: none; 97 | } 98 | 99 | @media(max-width:767px) { 100 | .nav li > form > button.logout { 101 | display:block; 102 | text-align: left; 103 | width: 100%; 104 | padding: 10px 15px; 105 | } 106 | } 107 | 108 | .nav > li > form > button.logout:focus, 109 | .nav > li > form > button.logout:hover { 110 | text-decoration: none; 111 | } 112 | 113 | .nav > li > form > button.logout:focus { 114 | outline: none; 115 | } 116 | -------------------------------------------------------------------------------- /backend/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/backend/web/favicon.ico -------------------------------------------------------------------------------- /backend/web/index-test.php: -------------------------------------------------------------------------------- 1 | run(); 17 | -------------------------------------------------------------------------------- /backend/web/index.php: -------------------------------------------------------------------------------- 1 | run(); 13 | -------------------------------------------------------------------------------- /backend/web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /backend/widgets/Alert.php: -------------------------------------------------------------------------------- 1 | session->setFlash('error', 'This is the message'); 12 | * Yii::$app->session->setFlash('success', 'This is the message'); 13 | * Yii::$app->session->setFlash('info', 'This is the message'); 14 | * ``` 15 | * 16 | * Multiple messages could be set as follows: 17 | * 18 | * ```php 19 | * Yii::$app->session->setFlash('error', ['Error 1', 'Error 2']); 20 | * ``` 21 | * 22 | * @author Kartik Visweswaran 23 | * @author Alexander Makarov 24 | */ 25 | class Alert extends \yii\bootstrap\Widget 26 | { 27 | /** 28 | * @var array the alert types configuration for the flash messages. 29 | * This array is setup as $key => $value, where: 30 | * - key: the name of the session flash variable 31 | * - value: the bootstrap alert type (i.e. danger, success, info, warning) 32 | */ 33 | public $alertTypes = [ 34 | 'error' => 'alert-danger', 35 | 'danger' => 'alert-danger', 36 | 'success' => 'alert-success', 37 | 'info' => 'alert-info', 38 | 'warning' => 'alert-warning' 39 | ]; 40 | /** 41 | * @var array the options for rendering the close button tag. 42 | * Array will be passed to [[\yii\bootstrap\Alert::closeButton]]. 43 | */ 44 | public $closeButton = []; 45 | 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function run() 51 | { 52 | $session = Yii::$app->session; 53 | $flashes = $session->getAllFlashes(); 54 | $appendClass = isset($this->options['class']) ? ' ' . $this->options['class'] : ''; 55 | 56 | foreach ($flashes as $type => $flash) { 57 | if (!isset($this->alertTypes[$type])) { 58 | continue; 59 | } 60 | 61 | foreach ((array) $flash as $i => $message) { 62 | echo \yii\bootstrap\Alert::widget([ 63 | 'body' => $message, 64 | 'closeButton' => $this->closeButton, 65 | 'options' => array_merge($this->options, [ 66 | 'id' => $this->getId() . '-' . $type . '-' . $i, 67 | 'class' => $this->alertTypes[$type] . $appendClass, 68 | ]), 69 | ]); 70 | } 71 | 72 | $session->removeFlash($type); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /backend/yii: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 21 | exit($exitCode); 22 | -------------------------------------------------------------------------------- /backend/yii.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem ------------------------------------------------------------- 4 | rem Yii command line bootstrap script for Windows. 5 | rem 6 | rem @author Qiang Xue 7 | rem @link http://www.yiiframework.com/ 8 | rem @copyright Copyright (c) 2008 Yii Software LLC 9 | rem @license http://www.yiiframework.com/license/ 10 | rem ------------------------------------------------------------- 11 | 12 | @setlocal 13 | 14 | set YII_PATH=%~dp0 15 | 16 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe 17 | 18 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 19 | 20 | @endlocal 21 | -------------------------------------------------------------------------------- /screenshots/Create User.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/screenshots/Create User.png -------------------------------------------------------------------------------- /screenshots/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/screenshots/Dashboard.png -------------------------------------------------------------------------------- /screenshots/Login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/screenshots/Login.png -------------------------------------------------------------------------------- /screenshots/View Users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/databoxtech/yii2-angular-template/cfd740cc0ab31d5f932a9a0dbcdc21658f7d8250/screenshots/View Users.png --------------------------------------------------------------------------------