├── .browserslistrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── proxy.json ├── rest-api ├── .gitignore ├── .prettierrc ├── db-data.ts ├── package-lock.json ├── package.json ├── populate-db.ts ├── src │ ├── app.module.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ └── users.schema.ts │ ├── constants.ts │ ├── courses │ │ ├── controllers │ │ │ ├── courses.controller.ts │ │ │ └── lessons.controller.ts │ │ ├── courses.module.ts │ │ ├── repositories │ │ │ ├── courses-repository.ts │ │ │ ├── courses.repository.ts │ │ │ └── lessons.repository.ts │ │ └── schemas │ │ │ ├── courses.schema.ts │ │ │ └── lessons.schema.ts │ ├── filters │ │ ├── fallback.filter.ts │ │ ├── http.filter.ts │ │ ├── validation.exception.ts │ │ └── validation.filter.ts │ ├── guards │ │ ├── admin.guard.ts │ │ ├── authentication.guard.ts │ │ └── authorization.guard.ts │ ├── main.ts │ ├── middleware │ │ └── get-user.middleware.ts │ └── pipes │ │ └── to-integer.pipe.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── tslint.json ├── shared ├── course.ts └── lesson.ts ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth.interceptor.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── login │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ └── login.component.ts │ │ └── model │ │ │ └── user.model.ts │ └── courses │ │ ├── course │ │ ├── course.component.css │ │ ├── course.component.html │ │ └── course.component.ts │ │ ├── courses-card-list │ │ ├── courses-card-list.component.css │ │ ├── courses-card-list.component.html │ │ └── courses-card-list.component.ts │ │ ├── courses.module.ts │ │ ├── edit-course-dialog │ │ ├── edit-course-dialog.component.css │ │ ├── edit-course-dialog.component.html │ │ └── edit-course-dialog.component.ts │ │ ├── home │ │ ├── home.component.css │ │ ├── home.component.html │ │ └── home.component.ts │ │ ├── services │ │ └── courses-http.service.ts │ │ └── shared │ │ └── default-dialog-config.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 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'. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | -------------------------------------------------------------------------------- /.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 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | 45 | server/dist 46 | tmp.json 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Angular University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## NestJs In Practice Course 3 | 4 | This repository contains the code of the [NestJs In Practice Course](https://angular-university.io/course/nestjs-course). 5 | 6 | ![NestJs Course](https://angular-university.s3-us-west-1.amazonaws.com/course-images/nestjs-v2.png) 7 | 8 | 9 | # Installation pre-requisites 10 | 11 | For taking the course we recommend installing Node 16. 12 | 13 | To easily switch between node versions on your machine, we recommend using a node virtual environment tool such as [nave](https://www.npmjs.com/package/nave) or [nvm-windows](https://github.com/coreybutler/nvm-windows), depending on your operating system. 14 | 15 | For example, here is how you switch to a new node version using nave: 16 | 17 | # note that you don't even need to update your node version before installing nave 18 | npm install -g nave 19 | 20 | nave use 16.13.0 21 | node -v 22 | v16.13.0 23 | 24 | # Installing the Angular CLI 25 | 26 | With the following command the angular-cli will be installed globally in your machine: 27 | 28 | npm install -g @angular/cli 29 | 30 | 31 | # How To install this repository 32 | 33 | We can install the master branch using the following commands: 34 | 35 | git clone https://github.com/angular-university/nestjs-course.git 36 | 37 | This repository is made of several separate npm modules, that are installable separately. For example, to run the au-input module, we can do the following: 38 | 39 | cd nestjs-course 40 | npm install 41 | 42 | This should take a couple of minutes. If there are issues, please post the complete error message in the Questions section of the course. 43 | 44 | # To Run the Development Backend Server 45 | 46 | We can start the sample application backend with the following command: 47 | 48 | cd rest-api 49 | npm install 50 | npm run server 51 | 52 | This launches a small Node REST API server, built using NestJs. Notice that this has a separate package.json, so you really need to run a second npm install from inside the rest-api directory. 53 | 54 | # To run the Development UI Server 55 | 56 | To run the frontend part of our code, we will use the Angular CLI: 57 | 58 | npm start 59 | 60 | The application is visible at port 4200: [http://localhost:4200](http://localhost:4200) 61 | 62 | 63 | 64 | # Important 65 | 66 | This repository has multiple branches, have a look at the beginning of each section to see the name of the branch. 67 | 68 | At certain points along the course, you will be asked to checkout other remote branches other than master. You can view all branches that you have available remotely using the following command: 69 | 70 | git branch -a 71 | 72 | The remote branches have their starting in origin, such as for example 1-start. 73 | 74 | We can checkout the remote branch and start tracking it with a local branch that has the same name, by using the following command: 75 | 76 | git checkout -b 1-start 77 | 78 | It's also possible to download a ZIP file for a given branch, using the branch dropdown on this page on the top left, and then selecting the Clone or Download / Download as ZIP button. 79 | 80 | # Other Courses 81 | # Modern Angular With Signals 82 | 83 | If you are looking for the [Modern Angular With Signals Course](https://angular-university.io/course/angular-signals-course), the repo with the full code can be found here: 84 | 85 | ![Modern Angular With Signals Course](https://d3vigmphadbn9b.cloudfront.net/course-images/large-images/angular-signals-course.jpg) 86 | 87 | 88 | # NgRx (with NgRx Data) - The Complete Guide 89 | 90 | If you are looking for the [Ngrx (with NgRx Data) - The Complete Guide](https://angular-university.io/course/ngrx-course), the repo with the full code can be found here: 91 | 92 | ![Ngrx (with NgRx Data) - The Complete Guide](https://angular-university.s3-us-west-1.amazonaws.com/course-images/ngrx-v2.png) 93 | 94 | 95 | # Angular Core Deep Dive Course 96 | 97 | If you are looking for the [Angular Core Deep Dive Course](https://angular-university.io/course/angular-course), the repo with the full code can be found here: 98 | 99 | ![Angular Core Deep Dive](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-core-in-depth-small.png) 100 | 101 | # RxJs In Practice 102 | 103 | If you are looking for the [RxJs In Practice](https://angular-university.io/course/rxjs-course), the repo with the full code can be found here: 104 | 105 | ![RxJs In Practice Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/rxjs-in-practice-course.png) 106 | 107 | 108 | # Angular Testing Course 109 | 110 | If you are looking for the [Angular Testing Course](https://angular-university.io/course/angular-testing-course), the repo with the full code can be found here: 111 | 112 | ![Angular Testing Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-testing-small.png) 113 | 114 | # Serverless Angular with Firebase Course 115 | 116 | If you are looking for the [Serverless Angular with Firebase Course](https://angular-university.io/course/firebase-course), the repo with the full code can be found here: 117 | 118 | ![Serverless Angular with Firebase Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/serverless-angular-small.png) 119 | 120 | # Angular Universal Course 121 | 122 | If you are looking for the [Angular Universal Course](https://angular-university.io/course/angular-universal-course), the repo with the full code can be found here: 123 | 124 | ![Angular Universal Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-universal-small.png) 125 | 126 | # Angular PWA Course 127 | 128 | If you are looking for the [Angular PWA Course](https://angular-university.io/course/angular-pwa-course), the repo with the full code can be found here: 129 | 130 | ![Angular PWA Course - Build the future of the Web Today](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-pwa-course.png) 131 | 132 | # Angular Security Masterclass 133 | 134 | If you are looking for the [Angular Security Masterclass](https://angular-university.io/course/angular-security-course), the repo with the full code can be found here: 135 | 136 | [Angular Security Masterclass](https://github.com/angular-university/angular-security-course). 137 | 138 | ![Angular Security Masterclass](https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png) 139 | 140 | # Angular Advanced Library Laboratory Course 141 | 142 | If you are looking for the Angular Advanced Course, the repo with the full code can be found here: 143 | 144 | [Angular Advanced Library Laboratory Course: Build Your Own Library](https://angular-university.io/course/angular-advanced-course). 145 | 146 | ![Angular Advanced Library Laboratory Course: Build Your Own Library](https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png) 147 | 148 | 149 | ## RxJs and Reactive Patterns Angular Architecture Course 150 | 151 | If you are looking for the RxJs and Reactive Patterns Angular Architecture Course code, the repo with the full code can be found here: 152 | 153 | [RxJs and Reactive Patterns Angular Architecture Course](https://angular-university.io/course/reactive-angular-architecture-course) 154 | 155 | ![RxJs and Reactive Patterns Angular Architecture Course](https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png) 156 | 157 | 158 | ## Complete Typescript Course - Build A REST API 159 | 160 | If you are looking for the Complete Typescript 2 Course - Build a REST API, the repo with the full code can be found here: 161 | 162 | [https://angular-university.io/course/typescript-2-tutorial](https://github.com/angular-university/complete-typescript-course) 163 | 164 | [Github repo for this course](https://github.com/angular-university/complete-typescript-course) 165 | 166 | ![Complete Typescript Course](https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png) 167 | 168 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-ngrx-course": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico" 22 | ], 23 | "styles": [ 24 | "src/styles.scss" 25 | ], 26 | "scripts": [], 27 | "vendorChunk": true, 28 | "extractLicenses": false, 29 | "buildOptimizer": false, 30 | "sourceMap": true, 31 | "optimization": false, 32 | "namedChunks": true 33 | }, 34 | "configurations": { 35 | "production": { 36 | "budgets": [ 37 | { 38 | "type": "anyComponentStyle", 39 | "maximumWarning": "6kb" 40 | } 41 | ], 42 | "optimization": true, 43 | "outputHashing": "all", 44 | "sourceMap": false, 45 | "namedChunks": false, 46 | "extractLicenses": true, 47 | "vendorChunk": false, 48 | "buildOptimizer": true, 49 | "fileReplacements": [ 50 | { 51 | "replace": "src/environments/environment.ts", 52 | "with": "src/environments/environment.prod.ts" 53 | } 54 | ] 55 | } 56 | } 57 | }, 58 | "serve": { 59 | "builder": "@angular-devkit/build-angular:dev-server", 60 | "options": { 61 | "browserTarget": "angular-ngrx-course:build" 62 | }, 63 | "configurations": { 64 | "production": { 65 | "browserTarget": "angular-ngrx-course:build:production" 66 | } 67 | } 68 | }, 69 | "extract-i18n": { 70 | "builder": "@angular-devkit/build-angular:extract-i18n", 71 | "options": { 72 | "browserTarget": "angular-ngrx-course:build" 73 | } 74 | }, 75 | "test": { 76 | "builder": "@angular-devkit/build-angular:karma", 77 | "options": { 78 | "main": "src/test.ts", 79 | "karmaConfig": "./karma.conf.js", 80 | "polyfills": "src/polyfills.ts", 81 | "tsConfig": "src/tsconfig.spec.json", 82 | "scripts": [], 83 | "styles": [ 84 | "src/styles.scss" 85 | ], 86 | "assets": [ 87 | "src/assets", 88 | "src/favicon.ico" 89 | ] 90 | } 91 | } 92 | } 93 | }, 94 | "angular-ngrx-course-e2e": { 95 | "root": "", 96 | "sourceRoot": "", 97 | "projectType": "application", 98 | "architect": { 99 | "e2e": { 100 | "builder": "@angular-devkit/build-angular:protractor", 101 | "options": { 102 | "protractorConfig": "./protractor.conf.js", 103 | "devServerTarget": "angular-ngrx-course:serve" 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "schematics": { 110 | "@ngrx/schematics:component": { 111 | "prefix": "", 112 | "styleext": "scss" 113 | }, 114 | "@ngrx/schematics:directive": { 115 | "prefix": "" 116 | } 117 | }, 118 | "cli": { 119 | "analytics": "6b135dc6-59b6-4502-8525-ae7dbabc5565", 120 | "schematicCollections": [ 121 | "@ngrx/schematics" 122 | ] 123 | } 124 | } -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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'), reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-course", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --proxy-config ./proxy.json", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular-devkit/schematics": "^14.0.1", 16 | "@angular/animations": "^14.0.1", 17 | "@angular/cdk": "^14.0.1", 18 | "@angular/common": "^14.0.1", 19 | "@angular/compiler": "^14.0.1", 20 | "@angular/core": "^14.0.1", 21 | "@angular/forms": "^14.0.1", 22 | "@angular/material": "^14.0.1", 23 | "@angular/material-moment-adapter": "^14.0.1", 24 | "@angular/platform-browser": "^14.0.1", 25 | "@angular/platform-browser-dynamic": "^14.0.1", 26 | "@angular/router": "^14.0.1", 27 | "@nestjs/common": "^8.2.3", 28 | "@nestjs/core": "^7.5.5", 29 | "@nestjs/platform-express": "^7.5.5", 30 | "body-parser": "^1.18.2", 31 | "class-validator": "^0.13.2", 32 | "core-js": "^2.4.1", 33 | "express": "^4.16.2", 34 | "moment": "^2.22.2", 35 | "reflect-metadata": "0.1.13", 36 | "rimraf": "3.0.0", 37 | "rxjs": "^6.3.3", 38 | "tslib": "^2.0.0", 39 | "zone.js": "~0.11.4" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "^14.0.1", 43 | "@angular/cli": "^14.0.1", 44 | "@angular/compiler-cli": "^14.0.1", 45 | "@angular/language-service": "^14.0.1", 46 | "@ngrx/schematics": "^8.0.1", 47 | "@types/express": "4.17.1", 48 | "@types/jasmine": "~3.6.0", 49 | "@types/jasminewd2": "~2.0.2", 50 | "@types/jest": "24.0.17", 51 | "@types/node": "^12.11.1", 52 | "@types/supertest": "2.0.8", 53 | "codelyzer": "^0.0.28", 54 | "jasmine-core": "~3.6.0", 55 | "jasmine-spec-reporter": "~5.0.0", 56 | "karma": "~6.3.2", 57 | "karma-chrome-launcher": "~3.1.0", 58 | "karma-cli": "~1.0.1", 59 | "karma-coverage-istanbul-reporter": "~3.0.2", 60 | "karma-jasmine": "~4.0.0", 61 | "karma-jasmine-html-reporter": "^1.5.0", 62 | "protractor": "~7.0.0", 63 | "supertest": "4.0.2", 64 | "ts-node": "8.3.0", 65 | "tsc-watch": "^4.5.0", 66 | "tsconfig-paths": "3.8.0", 67 | "tslint": "~6.1.0", 68 | "typescript": "~4.7.3" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:9000", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /rest-api/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | 27 | # Mongo Explorer plugin: 28 | .idea/**/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | ### VisualStudio template 53 | ## Ignore Visual Studio temporary files, build results, and 54 | ## files generated by popular Visual Studio add-ons. 55 | ## 56 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 57 | 58 | # User-specific files 59 | *.suo 60 | *.user 61 | *.userosscache 62 | *.sln.docstates 63 | 64 | # User-specific files (MonoDevelop/Xamarin Studio) 65 | *.userprefs 66 | 67 | # Build results 68 | [Dd]ebug/ 69 | [Dd]ebugPublic/ 70 | [Rr]elease/ 71 | [Rr]eleases/ 72 | x64/ 73 | x86/ 74 | bld/ 75 | [Bb]in/ 76 | [Oo]bj/ 77 | [Ll]og/ 78 | 79 | # Visual Studio 2015 cache/options directory 80 | .vs/ 81 | # Uncomment if you have tasks that create the project's static files in wwwroot 82 | #wwwroot/ 83 | 84 | # MSTest test Results 85 | [Tt]est[Rr]esult*/ 86 | [Bb]uild[Ll]og.* 87 | 88 | # NUNIT 89 | *.VisualState.xml 90 | TestResult.xml 91 | 92 | # Build Results of an ATL Project 93 | [Dd]ebugPS/ 94 | [Rr]eleasePS/ 95 | dlldata.c 96 | 97 | # Benchmark Results 98 | BenchmarkDotNet.Artifacts/ 99 | 100 | # .NET Core 101 | project.lock.json 102 | project.fragment.lock.json 103 | artifacts/ 104 | **/Properties/launchSettings.json 105 | 106 | *_i.c 107 | *_p.c 108 | *_i.h 109 | *.ilk 110 | *.meta 111 | *.obj 112 | *.pch 113 | *.pdb 114 | *.pgc 115 | *.pgd 116 | *.rsp 117 | *.sbr 118 | *.tlb 119 | *.tli 120 | *.tlh 121 | *.tmp 122 | *.tmp_proj 123 | *.log 124 | *.vspscc 125 | *.vssscc 126 | .builds 127 | *.pidb 128 | *.svclog 129 | *.scc 130 | 131 | # Chutzpah Test files 132 | _Chutzpah* 133 | 134 | # Visual C++ cache files 135 | ipch/ 136 | *.aps 137 | *.ncb 138 | *.opendb 139 | *.opensdf 140 | *.sdf 141 | *.cachefile 142 | *.VC.db 143 | *.VC.VC.opendb 144 | 145 | # Visual Studio profiler 146 | *.psess 147 | *.vsp 148 | *.vspx 149 | *.sap 150 | 151 | # Visual Studio Trace Files 152 | *.e2e 153 | 154 | # TFS 2012 Local Workspace 155 | $tf/ 156 | 157 | # Guidance Automation Toolkit 158 | *.gpState 159 | 160 | # ReSharper is a .NET coding add-in 161 | _ReSharper*/ 162 | *.[Rr]e[Ss]harper 163 | *.DotSettings.user 164 | 165 | # JustCode is a .NET coding add-in 166 | .JustCode 167 | 168 | # TeamCity is a build add-in 169 | _TeamCity* 170 | 171 | # DotCover is a Code Coverage Tool 172 | *.dotCover 173 | 174 | # AxoCover is a Code Coverage Tool 175 | .axoCover/* 176 | !.axoCover/settings.json 177 | 178 | # Visual Studio code coverage results 179 | *.coverage 180 | *.coveragexml 181 | 182 | # NCrunch 183 | _NCrunch_* 184 | .*crunch*.local.xml 185 | nCrunchTemp_* 186 | 187 | # MightyMoose 188 | *.mm.* 189 | AutoTest.Net/ 190 | 191 | # Web workbench (sass) 192 | .sass-cache/ 193 | 194 | # Installshield output folder 195 | [Ee]xpress/ 196 | 197 | # DocProject is a documentation generator add-in 198 | DocProject/buildhelp/ 199 | DocProject/Help/*.HxT 200 | DocProject/Help/*.HxC 201 | DocProject/Help/*.hhc 202 | DocProject/Help/*.hhk 203 | DocProject/Help/*.hhp 204 | DocProject/Help/Html2 205 | DocProject/Help/html 206 | 207 | # Click-Once directory 208 | publish/ 209 | 210 | # Publish Web Output 211 | *.[Pp]ublish.xml 212 | *.azurePubxml 213 | # Note: Comment the next line if you want to checkin your web deploy settings, 214 | # but database connection strings (with potential passwords) will be unencrypted 215 | *.pubxml 216 | *.publishproj 217 | 218 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 219 | # checkin your Azure Web App publish settings, but sensitive information contained 220 | # in these scripts will be unencrypted 221 | PublishScripts/ 222 | 223 | # NuGet Packages 224 | *.nupkg 225 | # The packages folder can be ignored because of Package Restore 226 | **/[Pp]ackages/* 227 | # except build/, which is used as an MSBuild target. 228 | !**/[Pp]ackages/build/ 229 | # Uncomment if necessary however generally it will be regenerated when needed 230 | #!**/[Pp]ackages/repositories.config 231 | # NuGet v3's project.json files produces more ignorable files 232 | *.nuget.props 233 | *.nuget.targets 234 | 235 | # Microsoft Azure Build Output 236 | csx/ 237 | *.build.csdef 238 | 239 | # Microsoft Azure Emulator 240 | ecf/ 241 | rcf/ 242 | 243 | # Windows Store app package directories and files 244 | AppPackages/ 245 | BundleArtifacts/ 246 | Package.StoreAssociation.xml 247 | _pkginfo.txt 248 | *.appx 249 | 250 | # Visual Studio cache files 251 | # files ending in .cache can be ignored 252 | *.[Cc]ache 253 | # but keep track of directories ending in .cache 254 | !*.[Cc]ache/ 255 | 256 | # Others 257 | ClientBin/ 258 | ~$* 259 | *~ 260 | *.dbmdl 261 | *.dbproj.schemaview 262 | *.jfm 263 | *.pfx 264 | *.publishsettings 265 | orleans.codegen.cs 266 | 267 | # Since there are multiple workflows, uncomment next line to ignore bower_components 268 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 269 | #bower_components/ 270 | 271 | # RIA/Silverlight projects 272 | Generated_Code/ 273 | 274 | # Backup & report files from converting an old project file 275 | # to a newer Visual Studio version. Backup files are not needed, 276 | # because we have git ;-) 277 | _UpgradeReport_Files/ 278 | Backup*/ 279 | UpgradeLog*.XML 280 | UpgradeLog*.htm 281 | 282 | # SQL Server files 283 | *.mdf 284 | *.ldf 285 | *.ndf 286 | 287 | # Business Intelligence projects 288 | *.rdl.data 289 | *.bim.layout 290 | *.bim_*.settings 291 | 292 | # Microsoft Fakes 293 | FakesAssemblies/ 294 | 295 | # GhostDoc plugin setting file 296 | *.GhostDoc.xml 297 | 298 | # Node.js Tools for Visual Studio 299 | .ntvs_analysis.dat 300 | node_modules/ 301 | 302 | # Typescript v1 declaration files 303 | typings/ 304 | 305 | # Visual Studio 6 build log 306 | *.plg 307 | 308 | # Visual Studio 6 workspace options file 309 | *.opt 310 | 311 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 312 | *.vbw 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # JetBrains Rider 330 | .idea/ 331 | *.sln.iml 332 | 333 | # CodeRush 334 | .cr/ 335 | 336 | # Python Tools for Visual Studio (PTVS) 337 | __pycache__/ 338 | *.pyc 339 | 340 | # Cake - Uncomment if you are using it 341 | # tools/** 342 | # !tools/packages.config 343 | 344 | # Tabs Studio 345 | *.tss 346 | 347 | # Telerik's JustMock configuration file 348 | *.jmconfig 349 | 350 | # BizTalk build output 351 | *.btp.cs 352 | *.btm.cs 353 | *.odx.cs 354 | *.xsd.cs 355 | 356 | # OpenCover UI analysis results 357 | OpenCover/ 358 | coverage/ 359 | 360 | ### macOS template 361 | # General 362 | .DS_Store 363 | .AppleDouble 364 | .LSOverride 365 | 366 | # Icon must end with two \r 367 | Icon 368 | 369 | # Thumbnails 370 | ._* 371 | 372 | # Files that might appear in the root of a volume 373 | .DocumentRevisions-V100 374 | .fseventsd 375 | .Spotlight-V100 376 | .TemporaryItems 377 | .Trashes 378 | .VolumeIcon.icns 379 | .com.apple.timemachine.donotpresent 380 | 381 | # Directories potentially created on remote AFP share 382 | .AppleDB 383 | .AppleDesktop 384 | Network Trash Folder 385 | Temporary Items 386 | .apdisk 387 | 388 | ======= 389 | # Local 390 | docker-compose.yml 391 | .env 392 | dist 393 | -------------------------------------------------------------------------------- /rest-api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /rest-api/db-data.ts: -------------------------------------------------------------------------------- 1 | 2 | export const USERS = { 3 | 1: { 4 | id: 1, 5 | email: 'test@angular-university.io', 6 | password: 'test' 7 | } 8 | 9 | }; 10 | 11 | 12 | export const COURSES: any = { 13 | 14 | 14: { 15 | id: 14, 16 | description: "NestJs In Practice Course", 17 | longDescription: 'Build a modern REST backend using Typescript, MongoDB and the familiar Angular API.', 18 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/nestjs-v2.png', 19 | category: 'BEGINNER', 20 | lessonsCount: 10, 21 | seqNo: 0, 22 | url: 'nestjs-course', 23 | promo:false 24 | }, 25 | 4: { 26 | id: 4, 27 | description: 'NgRx (with NgRx Data) - The Complete Guide', 28 | longDescription: 'Learn the modern Ngrx Ecosystem, including NgRx Data, Store, Effects, Router Store, Ngrx Entity, and Dev Tools.', 29 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/ngrx-v2.png', 30 | category: 'BEGINNER', 31 | lessonsCount: 10, 32 | seqNo: 1, 33 | url: 'ngrx-course', 34 | promo:false 35 | }, 36 | 37 | 2: { 38 | id: 2, 39 | description: 'Angular Core Deep Dive', 40 | longDescription: 'A detailed walk-through of the most important part of Angular - the Core and Common modules', 41 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-core-in-depth-small.png', 42 | lessonsCount: 10, 43 | category: 'BEGINNER', 44 | seqNo: 2, 45 | url: 'angular-core-course', 46 | promo:false 47 | }, 48 | 49 | 3: { 50 | id: 3, 51 | description: 'RxJs In Practice Course', 52 | longDescription: 'Understand the RxJs Observable pattern, learn the RxJs Operators via practical examples', 53 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/rxjs-in-practice-course.png', 54 | category: 'BEGINNER', 55 | lessonsCount: 10, 56 | seqNo: 3, 57 | url: 'rxjs-course', 58 | promo:false 59 | }, 60 | 61 | 1: { 62 | id: 1, 63 | description: 'Serverless Angular with Firebase Course', 64 | longDescription: 'Serveless Angular with Firestore, Firebase Storage & Hosting, Firebase Cloud Functions & AngularFire', 65 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/serverless-angular-small.png', 66 | lessonsCount: 10, 67 | category: 'BEGINNER', 68 | seqNo: 4, 69 | url: 'serverless-angular', 70 | promo:false 71 | }, 72 | 73 | /* 74 | 75 | 76 | 5: { 77 | id: 5, 78 | description: 'Angular for Beginners', 79 | longDescription: "Establish a solid layer of fundamentals, learn what's under the hood of Angular", 80 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/angular2-for-beginners-small-v2.png', 81 | category: 'BEGINNER', 82 | lessonsCount: 10, 83 | seqNo: 5, 84 | url: 'angular-for-beginners' 85 | }, 86 | 87 | */ 88 | 89 | 12: { 90 | id: 12, 91 | description: 'Angular Testing Course', 92 | longDescription: 'In-depth guide to Unit Testing and E2E Testing of Angular Applications', 93 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-testing-small.png', 94 | category: 'BEGINNER', 95 | seqNo: 6, 96 | url: 'angular-testing-course', 97 | lessonsCount: 10, 98 | promo:false 99 | }, 100 | 101 | 6: { 102 | id: 6, 103 | description: 'Angular Security Course - Web Security Fundamentals', 104 | longDescription: 'Learn Web Security Fundamentals and apply them to defend an Angular / Node Application from multiple types of attacks.', 105 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png', 106 | category: 'ADVANCED', 107 | lessonsCount: 11, 108 | seqNo: 7, 109 | url: 'angular-security-course', 110 | promo:false 111 | }, 112 | 113 | 7: { 114 | id: 7, 115 | description: 'Angular PWA - Progressive Web Apps Course', 116 | longDescription: 'Learn Angular Progressive Web Applications, build the future of the Web Today.', 117 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-pwa-course.png', 118 | category: 'ADVANCED', 119 | lessonsCount: 8, 120 | seqNo: 8, 121 | url: 'angular-pwa-course', 122 | promo:false 123 | }, 124 | 125 | 8: { 126 | id: 8, 127 | description: 'Angular Advanced Library Laboratory: Build Your Own Library', 128 | longDescription: 'Learn Advanced Angular functionality typically used in Library Development. Advanced Components, Directives, Testing, Npm', 129 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png', 130 | category: 'ADVANCED', 131 | seqNo: 9, 132 | url: 'angular-advanced-course', 133 | promo:false 134 | }, 135 | 136 | 9: { 137 | id: 9, 138 | description: 'The Complete Typescript Course', 139 | longDescription: 'Complete Guide to Typescript From Scratch: Learn the language in-depth and use it to build a Node REST API.', 140 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png', 141 | category: 'BEGINNER', 142 | seqNo: 10, 143 | url: 'typescript-course', 144 | promo:false 145 | }, 146 | 147 | 10: { 148 | id: 10, 149 | description: 'Rxjs and Reactive Patterns Angular Architecture Course', 150 | longDescription: 'Learn the core RxJs Observable Pattern as well and many other Design Patterns for building Reactive Angular Applications.', 151 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png', 152 | category: 'BEGINNER', 153 | seqNo: 11, 154 | url: 'rxjs-patterns-course', 155 | promo:false 156 | }, 157 | 158 | 11: { 159 | id: 11, 160 | description: 'Angular Material Course', 161 | longDescription: 'Build Applications with the official Angular Widget Library', 162 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/material_design.png', 163 | category: 'BEGINNER', 164 | seqNo: 12, 165 | url: 'angular-material-course', 166 | promo:false 167 | } 168 | 169 | }; 170 | 171 | 172 | export const LESSONS = { 173 | 174 | 1: { 175 | id: 1, 176 | 'description': 'Angular Tutorial For Beginners - Build Your First App - Hello World Step By Step', 177 | 'duration': '4:17', 178 | 'seqNo': 1, 179 | courseId: 5 180 | }, 181 | 2: { 182 | id: 2, 183 | 'description': 'Building Your First Component - Component Composition', 184 | 'duration': '2:07', 185 | 'seqNo': 2, 186 | courseId: 5 187 | }, 188 | 3: { 189 | id: 3, 190 | 'description': 'Component @Input - How To Pass Input Data To an Component', 191 | 'duration': '2:33', 192 | 'seqNo': 3, 193 | courseId: 5 194 | }, 195 | 4: { 196 | id: 4, 197 | 'description': ' Component Events - Using @Output to create custom events', 198 | 'duration': '4:44', 199 | 'seqNo': 4, 200 | courseId: 5 201 | }, 202 | 5: { 203 | id: 5, 204 | 'description': ' Component Templates - Inline Vs External', 205 | 'duration': '2:55', 206 | 'seqNo': 5, 207 | courseId: 5 208 | }, 209 | 6: { 210 | id: 6, 211 | 'description': 'Styling Components - Learn About Component Style Isolation', 212 | 'duration': '3:27', 213 | 'seqNo': 6, 214 | courseId: 5 215 | }, 216 | 7: { 217 | id: 7, 218 | 'description': ' Component Interaction - Extended Components Example', 219 | 'duration': '9:22', 220 | 'seqNo': 7, 221 | courseId: 5 222 | }, 223 | 8: { 224 | id: 8, 225 | 'description': ' Components Tutorial For Beginners - Components Exercise !', 226 | 'duration': '1:26', 227 | 'seqNo': 8, 228 | courseId: 5 229 | }, 230 | 9: { 231 | id: 9, 232 | 'description': ' Components Tutorial For Beginners - Components Exercise Solution Inside', 233 | 'duration': '2:08', 234 | 'seqNo': 9, 235 | courseId: 5 236 | }, 237 | 10: { 238 | id: 10, 239 | 'description': ' Directives - Inputs, Output Event Emitters and How To Export Template References', 240 | 'duration': '4:01', 241 | 'seqNo': 10, 242 | courseId: 5 243 | }, 244 | 245 | 246 | // Security Course 247 | 11: { 248 | id: 11, 249 | 'description': 'Course Helicopter View', 250 | 'duration': '08:19', 251 | 'seqNo': 1, 252 | courseId: 6 253 | }, 254 | 255 | 12: { 256 | id: 12, 257 | 'description': 'Installing Git, Node, NPM and Choosing an IDE', 258 | 'duration': '04:17', 259 | 'seqNo': 2, 260 | courseId: 6 261 | }, 262 | 263 | 13: { 264 | id: 13, 265 | 'description': 'Installing The Lessons Code - Learn Why Its Essential To Use NPM 5', 266 | 'duration': '06:05', 267 | 'seqNo': 3, 268 | courseId: 6 269 | }, 270 | 271 | 14: { 272 | id: 14, 273 | 'description': 'How To Run Node In TypeScript With Hot Reloading', 274 | 'duration': '03:57', 275 | 'seqNo': 4, 276 | courseId: 6 277 | }, 278 | 279 | 15: { 280 | id: 15, 281 | 'description': 'Guided Tour Of The Sample Application', 282 | 'duration': '06:00', 283 | 'seqNo': 5, 284 | courseId: 6 285 | }, 286 | 16: { 287 | id: 16, 288 | 'description': 'Client Side Authentication Service - API Design', 289 | 'duration': '04:53', 290 | 'seqNo': 6, 291 | courseId: 6 292 | }, 293 | 17: { 294 | id: 17, 295 | 'description': 'Client Authentication Service - Design and Implementation', 296 | 'duration': '09:14', 297 | 'seqNo': 7, 298 | courseId: 6 299 | }, 300 | 18: { 301 | id: 18, 302 | 'description': 'The New Angular HTTP Client - Doing a POST Call To The Server', 303 | 'duration': '06:08', 304 | 'seqNo': 8, 305 | courseId: 6 306 | }, 307 | 19: { 308 | id: 19, 309 | 'description': 'User Sign Up Server-Side Implementation in Express', 310 | 'duration': '08:50', 311 | 'seqNo': 9, 312 | courseId: 6 313 | }, 314 | 20: { 315 | id: 20, 316 | 'description': 'Introduction To Cryptographic Hashes - A Running Demo', 317 | 'duration': '05:46', 318 | 'seqNo': 10, 319 | courseId: 6 320 | }, 321 | 21: { 322 | id: 21, 323 | 'description': 'Some Interesting Properties Of Hashing Functions - Validating Passwords', 324 | 'duration': '06:31', 325 | 'seqNo': 11, 326 | courseId: 6 327 | }, 328 | 329 | 330 | // PWA course 331 | 332 | 22: { 333 | id: 22, 334 | 'description': 'Course Kick-Off - Install Node, NPM, IDE And Service Workers Section Code', 335 | 'duration': '07:19', 336 | 'seqNo': 1, 337 | courseId: 7 338 | }, 339 | 23: { 340 | id: 23, 341 | 'description': 'Service Workers In a Nutshell - Service Worker Registration', 342 | 'duration': '6:59', 343 | 'seqNo': 2, 344 | courseId: 7 345 | }, 346 | 24: { 347 | id: 24, 348 | 'description': 'Service Workers Hello World - Lifecycle Part 1 and PWA Chrome Dev Tools', 349 | 'duration': '7:28', 350 | 'seqNo': 3, 351 | courseId: 7 352 | }, 353 | 25: { 354 | id: 25, 355 | 'description': 'Service Workers and Application Versioning - Install & Activate Lifecycle Phases', 356 | 'duration': '10:17', 357 | 'seqNo': 4, 358 | courseId: 7 359 | }, 360 | 361 | 26: { 362 | id: 26, 363 | 'description': 'Downloading The Offline Page - The Service Worker Installation Phase', 364 | 'duration': '09:50', 365 | 'seqNo': 5, 366 | courseId: 7 367 | }, 368 | 27: { 369 | id: 27, 370 | 'description': 'Introduction to the Cache Storage PWA API', 371 | 'duration': '04:44', 372 | 'seqNo': 6, 373 | courseId: 7 374 | }, 375 | 28: { 376 | id: 28, 377 | 'description': 'View Service Workers HTTP Interception Features In Action', 378 | 'duration': '06:07', 379 | 'seqNo': 7, 380 | courseId: 7 381 | }, 382 | 29: { 383 | id: 29, 384 | 'description': 'Service Workers Error Handling - Serving The Offline Page', 385 | 'duration': '5:38', 386 | 'seqNo': 8, 387 | courseId: 7 388 | }, 389 | 390 | // Serverless Angular with Firebase Course 391 | 392 | 30: { 393 | id: 30, 394 | description: 'Development Environment Setup', 395 | 'duration': '5:38', 396 | 'seqNo': 1, 397 | courseId: 1 398 | }, 399 | 400 | 31: { 401 | id: 31, 402 | description: 'Introduction to the Firebase Ecosystem', 403 | 'duration': '5:12', 404 | 'seqNo': 2, 405 | courseId: 1 406 | }, 407 | 408 | 32: { 409 | id: 32, 410 | description: 'Importing Data into Firestore', 411 | 'duration': '4:07', 412 | 'seqNo': 3, 413 | courseId: 1 414 | }, 415 | 416 | 33: { 417 | id: 33, 418 | description: 'Firestore Documents in Detail', 419 | 'duration': '7:32', 420 | 'seqNo': 4, 421 | courseId: 1 422 | }, 423 | 424 | 34: { 425 | id: 34, 426 | description: 'Firestore Collections in Detail', 427 | 'duration': '6:28', 428 | 'seqNo': 5, 429 | courseId: 1 430 | }, 431 | 432 | 35: { 433 | id: 35, 434 | description: 'Firestore Unique Identifiers', 435 | 'duration': '4:38', 436 | 'seqNo': 6, 437 | courseId: 1 438 | }, 439 | 440 | 36: { 441 | id: 36, 442 | description: 'Querying Firestore Collections', 443 | 'duration': '7:54', 444 | 'seqNo': 7, 445 | courseId: 1 446 | }, 447 | 448 | 37: { 449 | id: 37, 450 | description: 'Firebase Security Rules In Detail', 451 | 'duration': '5:31', 452 | 'seqNo': 8, 453 | courseId: 1 454 | }, 455 | 456 | 38: { 457 | id: 38, 458 | description: 'Firebase Cloud Functions In Detail', 459 | 'duration': '8:19', 460 | 'seqNo': 9, 461 | courseId: 1 462 | }, 463 | 464 | 39: { 465 | id: 39, 466 | description: 'Firebase Storage In Detail', 467 | 'duration': '7:05', 468 | 'seqNo': 10, 469 | courseId: 1 470 | }, 471 | 472 | 473 | // Angular Testing Course 474 | 475 | 40: { 476 | id: 40, 477 | description: 'Angular Testing Course - Helicopter View', 478 | 'duration': '5:38', 479 | 'seqNo': 1, 480 | courseId: 12 481 | }, 482 | 483 | 41: { 484 | id: 41, 485 | description: 'Setting Up the Development Environment', 486 | 'duration': '5:12', 487 | 'seqNo': 2, 488 | courseId: 12 489 | }, 490 | 491 | 42: { 492 | id: 42, 493 | description: 'Introduction to Jasmine, Spies and specs', 494 | 'duration': '4:07', 495 | 'seqNo': 3, 496 | courseId: 12 497 | }, 498 | 499 | 43: { 500 | id: 43, 501 | description: 'Introduction to Service Testing', 502 | 'duration': '7:32', 503 | 'seqNo': 4, 504 | courseId: 12 505 | }, 506 | 507 | 44: { 508 | id: 44, 509 | description: 'Settting up the Angular TestBed', 510 | 'duration': '6:28', 511 | 'seqNo': 5, 512 | courseId: 12 513 | }, 514 | 515 | 45: { 516 | id: 45, 517 | description: 'Mocking Angular HTTP requests', 518 | 'duration': '4:38', 519 | 'seqNo': 6, 520 | courseId: 12 521 | }, 522 | 523 | 46: { 524 | id: 46, 525 | description: 'Simulating Failing HTTP Requests', 526 | 'duration': '7:54', 527 | 'seqNo': 7, 528 | courseId: 12 529 | }, 530 | 531 | 47: { 532 | id: 47, 533 | description: 'Introduction to Angular Component Testing', 534 | 'duration': '5:31', 535 | 'seqNo': 8, 536 | courseId: 12 537 | }, 538 | 539 | 48: { 540 | id: 48, 541 | description: 'Testing Angular Components without the DOM', 542 | 'duration': '8:19', 543 | 'seqNo': 9, 544 | courseId: 12 545 | }, 546 | 547 | 49: { 548 | id: 49, 549 | description: 'Testing Angular Components with the DOM', 550 | 'duration': '7:05', 551 | 'seqNo': 10, 552 | courseId: 12 553 | }, 554 | 555 | 556 | // Ngrx Course 557 | 50: { 558 | id: 50, 559 | "description": "Welcome to the Angular Ngrx Course", 560 | "duration": "6:53", 561 | "seqNo": 1, 562 | courseId: 4 563 | 564 | }, 565 | 51: { 566 | id: 51, 567 | "description": "The Angular Ngrx Architecture Course - Helicopter View", 568 | "duration": "5:52", 569 | "seqNo": 2, 570 | courseId: 4 571 | }, 572 | 52: { 573 | id: 52, 574 | "description": "The Origins of Flux - Understanding the Famous Facebook Bug Problem", 575 | "duration": "8:17", 576 | "seqNo": 3, 577 | courseId: 4 578 | }, 579 | 53: { 580 | id: 53, 581 | "description": "Custom Global Events - Why Don't They Scale In Complexity?", 582 | "duration": "7:47", 583 | "seqNo": 4, 584 | courseId: 4 585 | }, 586 | 54: { 587 | id: 54, 588 | "description": "The Flux Architecture - How Does it Solve Facebook Counter Problem?", 589 | "duration": "9:22", 590 | "seqNo": 5, 591 | courseId: 4 592 | }, 593 | 55: { 594 | id: 55, 595 | "description": "Unidirectional Data Flow And The Angular Development Mode", 596 | "duration": "7:07", 597 | "seqNo": 6, 598 | courseId: 4 599 | }, 600 | 601 | 56: { 602 | id: 56, 603 | "description": "Dispatching an Action - Implementing the Login Component", 604 | "duration": "4:39", 605 | "seqNo": 7, 606 | courseId: 4 607 | }, 608 | 57: { 609 | id: 57, 610 | "description": "Setting Up the Ngrx DevTools - Demo", 611 | "duration": "4:44", 612 | "seqNo": 8, 613 | courseId: 4 614 | }, 615 | 58: { 616 | id: 58, 617 | "description": "Understanding Reducers - Writing Our First Reducer", 618 | "duration": "9:10", 619 | "seqNo": 9, 620 | courseId: 4 621 | }, 622 | 59: { 623 | id: 59, 624 | "description": "How To Define the Store Initial State", 625 | "duration": "9:10", 626 | "seqNo": 10, 627 | courseId: 4 628 | }, 629 | 630 | 60: { 631 | id:60, 632 | "description": "Introduction to NestJs", 633 | "duration": "4:29", 634 | "seqNo": 1, 635 | courseId: 14 636 | }, 637 | 61: { 638 | id:61, 639 | "description": "Development Environment Setup", 640 | "duration": "6:37", 641 | "seqNo": 2, 642 | courseId: 14 643 | }, 644 | 62: { 645 | id:62, 646 | "description": "Setting up a MongoDB Database", 647 | "duration": "6:38", 648 | "seqNo": 3, 649 | courseId: 14 650 | }, 651 | 63: { 652 | id:63, 653 | "description": "CRUD with NestJs - Controllers and Repositories", 654 | "duration": "12:12", 655 | "seqNo": 4, 656 | courseId: 14 657 | }, 658 | 64: { 659 | id:64, 660 | "description": "First REST endpoint - Get All Courses", 661 | "duration": "3:42", 662 | "seqNo": 5, 663 | courseId: 14 664 | }, 665 | 65: { 666 | id:65, 667 | "description": "Error Handling", 668 | "duration": "5:15", 669 | "seqNo": 6, 670 | courseId: 14 671 | }, 672 | 66: { 673 | id:66, 674 | "description": "NestJs Middleware", 675 | "duration": "7:08", 676 | "seqNo": 7, 677 | courseId: 14 678 | }, 679 | 67: { 680 | id:67, 681 | "description": "Authentication in NestJs", 682 | "duration": "13:22", 683 | "seqNo": 8, 684 | courseId: 14 685 | }, 686 | 68: { 687 | id:68, 688 | "description": "Authorization in NestJs", 689 | "duration": "6:43", 690 | "seqNo": 9, 691 | courseId: 14 692 | }, 693 | 69: { 694 | id:69, 695 | "description": "Guards & Interceptors", 696 | "duration": "8:16", 697 | "seqNo": 10, 698 | courseId: 14 699 | } 700 | 701 | }; 702 | 703 | 704 | export function findAllCourses() { 705 | return Object.values(COURSES); 706 | } 707 | 708 | 709 | export function findCourseById(courseId: number) { 710 | return COURSES[courseId]; 711 | } 712 | 713 | export function findLessonsForCourse(courseId: number) { 714 | return Object.values(LESSONS).filter(lesson => lesson.courseId == courseId); 715 | } 716 | 717 | 718 | export function authenticate(email: string, password: string) { 719 | 720 | const user: any = Object.values(USERS).find(user => user.email === email); 721 | 722 | if (user && user.password == password) { 723 | return user; 724 | } else { 725 | return undefined; 726 | } 727 | 728 | } 729 | -------------------------------------------------------------------------------- /rest-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-course-backend", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "NestJs Course", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.build.json", 9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 10 | "clean": "rimraf dist", 11 | "run-populate-db": "node ./dist/rest-api/populate-db.js", 12 | "populate-db": "run-s clean build run-populate-db", 13 | "start:local": "tsc-watch -p tsconfig.build.json --onSuccess \"node dist/rest-api/src/main.js\"", 14 | "server": "run-s clean start:local", 15 | "start:debug": "tsc-watch -p tsconfig.build.json --onSuccess \"node --inspect-brk dist/rest-api/src/main.js\"", 16 | "start:prod": "node dist/rest-api/src/main.js", 17 | "lint": "tslint -p tsconfig.json -c tslint.json", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^6.5.3", 26 | "@nestjs/core": "^6.5.3", 27 | "@nestjs/microservices": "^6.5.3", 28 | "@nestjs/mongoose": "^6.1.2", 29 | "@nestjs/platform-express": "^6.5.3", 30 | "@nestjs/websockets": "^6.5.3", 31 | "class-transformer": "^0.2.3", 32 | "class-validator": "^0.10.1", 33 | "jsonwebtoken": "^8.5.1", 34 | "mongoose": "^5.7.1", 35 | "npm-run-all": "^4.1.5", 36 | "password-hash-and-salt": "^0.1.4", 37 | "reflect-metadata": "0.1.13", 38 | "rimraf": "3.0.0", 39 | "rxjs": "6.5.2" 40 | }, 41 | "devDependencies": { 42 | "@nestjs/testing": "6.5.3", 43 | "@types/express": "4.17.1", 44 | "@types/node": "12.7.2", 45 | "@types/supertest": "2.0.8", 46 | "jest": "^26.6.3", 47 | "prettier": "1.18.2", 48 | "supertest": "4.0.2", 49 | "ts-jest": "^26.4.4", 50 | "ts-node": "8.3.0", 51 | "tsc-watch": "2.4.0", 52 | "tsconfig-paths": "3.8.0", 53 | "tslint": "5.18.0", 54 | "typescript": "3.5.3" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".spec.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "collectCoverageFrom": [ 68 | "**/*.(t|j)s" 69 | ], 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rest-api/populate-db.ts: -------------------------------------------------------------------------------- 1 | import {findAllCourses, findLessonsForCourse} from './db-data'; 2 | 3 | console.log("Populating the MongoDB database with some sample data ..."); 4 | 5 | const MongoClient = require('mongodb').MongoClient; 6 | var ObjectId = require('mongodb').ObjectID; 7 | 8 | 9 | /***************************************************************************************************** 10 | * 11 | * 12 | * IMPORTANT!!! 13 | * 14 | * MongoDB Connection URL - create your own url with the right cluster name, username, password and database name 15 | * 16 | * Format: mongodb+srv://username:password@clustername 17 | * 18 | * Example (don't use this as you don't have write access): 19 | * 20 | * mongodb+srv://nestjs:ZeEjdswOWHwoenQO@cluster0-dbucq.gcp.mongodb.net 21 | * 22 | *****************************************************************************************************/ 23 | 24 | const MONGODB_CONNECTION_URL = 'mongodb+srv://nestjs-admin:BSFGkUY7T0XcJfpI@cluster0-4r2ye.mongodb.net/test?retryWrites=true&w=majority'; 25 | 26 | // Database Name 27 | const dbName = 'nestjs-course'; 28 | 29 | 30 | 31 | 32 | 33 | // Create a new MongoClient 34 | const client = new MongoClient(MONGODB_CONNECTION_URL); 35 | 36 | // Use connect method to connect to the Server 37 | client.connect(async (err, client) => { 38 | 39 | try { 40 | 41 | if (err) { 42 | console.log("Error connecting to database, please check the username and password, exiting."); 43 | process.exit(); 44 | } 45 | 46 | console.log("Connected correctly to server"); 47 | 48 | const db = client.db(dbName); 49 | 50 | const courses = findAllCourses(); 51 | 52 | for (let i = 0; i < courses.length; i++) { 53 | 54 | const course:any = courses[i]; 55 | 56 | const newCourse: any = {...course}; 57 | delete newCourse.id; 58 | 59 | console.log("Inserting course ", newCourse); 60 | 61 | const result = await db.collection('courses').insertOne(newCourse); 62 | 63 | const courseId = result.insertedId; 64 | 65 | console.log("new course id", courseId); 66 | 67 | const lessons = findLessonsForCourse(course.id); 68 | 69 | for (let j = 0; j< lessons.length; j++) { 70 | 71 | const lesson = lessons[j]; 72 | 73 | const newLesson:any = {...lesson}; 74 | delete newLesson.id; 75 | delete newLesson.courseId; 76 | newLesson.course = new ObjectId(courseId); 77 | 78 | console.log("Inserting lesson", newLesson); 79 | 80 | await db.collection("lessons").insertOne(newLesson); 81 | 82 | } 83 | 84 | } 85 | 86 | console.log('Finished uploading data, creating indexes.'); 87 | 88 | await db.collection('courses').createIndex( { "url": 1 }, { unique: true } ); 89 | 90 | console.log("Finished creating indexes, exiting."); 91 | 92 | client.close(); 93 | process.exit(); 94 | 95 | } 96 | catch (error) { 97 | console.log("Error caught, exiting: ", error); 98 | client.close(); 99 | process.exit(); 100 | } 101 | 102 | }); 103 | 104 | console.log('updloading data to MongoDB...'); 105 | 106 | process.stdin.resume(); 107 | -------------------------------------------------------------------------------- /rest-api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import {MiddlewareConsumer, Module, NestModule} from '@nestjs/common'; 2 | import {CoursesModule} from './courses/courses.module'; 3 | import {MongooseModule} from '@nestjs/mongoose'; 4 | import {MONGO_CONNECTION} from './constants'; 5 | import {AuthModule} from './auth/auth.module'; 6 | import {GetUserMiddleware} from './middleware/get-user.middleware'; 7 | import {CoursesController} from './courses/controllers/courses.controller'; 8 | import {LessonsController} from './courses/controllers/lessons.controller'; 9 | 10 | 11 | @Module({ 12 | imports: [ 13 | CoursesModule, 14 | AuthModule, 15 | MongooseModule.forRoot(MONGO_CONNECTION) 16 | ] 17 | 18 | }) 19 | export class AppModule implements NestModule { 20 | 21 | configure(consumer: MiddlewareConsumer): void { 22 | 23 | consumer 24 | .apply(GetUserMiddleware) 25 | .forRoutes( 26 | CoursesController, 27 | LessonsController 28 | ); 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /rest-api/src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import {Body, Controller, Post, UnauthorizedException} from '@nestjs/common'; 2 | import { Model } from 'mongoose'; 3 | import {InjectModel} from '@nestjs/mongoose'; 4 | import * as password from 'password-hash-and-salt'; 5 | import * as jwt from 'jsonwebtoken'; 6 | import {JWT_SECRET} from '../constants'; 7 | 8 | 9 | @Controller("login") 10 | export class AuthController { 11 | 12 | constructor( 13 | @InjectModel("User") private userModel: Model) { 14 | 15 | } 16 | 17 | @Post() 18 | async login(@Body("email") email:string, 19 | @Body("password") plaintextPassword:string) { 20 | 21 | const user = await this.userModel.findOne({email}); 22 | 23 | if(!user) { 24 | console.log("User does exist on the database."); 25 | throw new UnauthorizedException(); 26 | } 27 | 28 | return new Promise((resolve, reject) => { 29 | password(plaintextPassword).verifyAgainst( 30 | user.passwordHash, 31 | (err, verified) => { 32 | if (!verified) { 33 | reject(new UnauthorizedException()); 34 | } 35 | 36 | const authJwtToken = 37 | jwt.sign({email, roles: user.roles}, 38 | JWT_SECRET); 39 | 40 | resolve({authJwtToken}); 41 | } 42 | ); 43 | }); 44 | } 45 | 46 | } 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /rest-api/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import {AuthController} from './auth.controller'; 3 | import {MongooseModule} from '@nestjs/mongoose'; 4 | import {UsersSchema} from './users.schema'; 5 | 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([ 10 | { 11 | name: "User", schema: UsersSchema 12 | } 13 | ]) 14 | ], 15 | controllers: [ 16 | AuthController 17 | ] 18 | }) 19 | export class AuthModule { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /rest-api/src/auth/users.schema.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as mongoose from 'mongoose'; 4 | 5 | 6 | export const UsersSchema = new mongoose.Schema({ 7 | email: String, 8 | roles: Array, 9 | passwordHash: String 10 | }); 11 | -------------------------------------------------------------------------------- /rest-api/src/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | export const MONGO_CONNECTION = 'mongodb+srv://nestjs-admin:BSFGkUY7T0XcJfpI@cluster0-4r2ye.mongodb.net/nestjs-course?retryWrites=true&w=majority'; 3 | 4 | export const JWT_SECRET = "vp9eb22K5Sz4"; 5 | -------------------------------------------------------------------------------- /rest-api/src/courses/controllers/courses.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | Controller, 5 | Delete, 6 | Get, 7 | HttpException, 8 | NotFoundException, 9 | Param, 10 | Post, 11 | Put, 12 | Req, 13 | Res, 14 | UseFilters, UseGuards 15 | } from '@nestjs/common'; 16 | import {Course} from '../../../../shared/course'; 17 | import {findAllCourses} from '../../../db-data'; 18 | import {CoursesRepository} from '../repositories/courses.repository'; 19 | import {Request, Response} from 'express'; 20 | import {HttpExceptionFilter} from '../../filters/http.filter'; 21 | import {ToIntegerPipe} from '../../pipes/to-integer.pipe'; 22 | import {ParseIntPipe} from "@nestjs/common"; 23 | import {AuthenticationGuard} from '../../guards/authentication.guard'; 24 | import {AdminGuard} from '../../guards/admin.guard'; 25 | 26 | 27 | @Controller("courses") 28 | @UseGuards(AuthenticationGuard) 29 | export class CoursesController { 30 | 31 | constructor(private coursesDB: CoursesRepository) { 32 | 33 | } 34 | 35 | @Post() 36 | @UseGuards(AdminGuard) 37 | async createCourse(@Body() course:Course) 38 | : Promise { 39 | 40 | console.log("creating new course"); 41 | 42 | return this.coursesDB.addCourse(course); 43 | } 44 | 45 | @Get() 46 | async findAllCourses(): Promise { 47 | return this.coursesDB.findAll(); 48 | } 49 | 50 | @Get(":courseUrl") 51 | async findCourseByUrl(@Param("courseUrl") courseUrl:string) { 52 | 53 | console.log("Finding by courseUrl", courseUrl); 54 | 55 | const course = await this.coursesDB.findCourseByUrl(courseUrl); 56 | 57 | if (!course) { 58 | throw new NotFoundException( 59 | "Could not find course for url " + courseUrl); 60 | } 61 | 62 | return course; 63 | } 64 | 65 | 66 | 67 | @Put(':courseId') 68 | @UseGuards(AdminGuard) 69 | async updateCourse( 70 | @Param("courseId") courseId:string, 71 | @Body() changes: Course):Promise { 72 | 73 | console.log("updating course"); 74 | 75 | if (changes._id) { 76 | throw new BadRequestException("Can't update course id"); 77 | } 78 | 79 | return this.coursesDB.updateCourse(courseId, changes); 80 | } 81 | 82 | @Delete(':courseId') 83 | @UseGuards(AdminGuard) 84 | async deleteCourse(@Param("courseId") courseId:string) { 85 | 86 | console.log("deleting course " + courseId); 87 | 88 | return this.coursesDB.deleteCourse(courseId); 89 | } 90 | 91 | 92 | 93 | } 94 | -------------------------------------------------------------------------------- /rest-api/src/courses/controllers/lessons.controller.ts: -------------------------------------------------------------------------------- 1 | import {BadRequestException, Controller, Get, ParseIntPipe, Query} from '@nestjs/common'; 2 | import {LessonsRepository} from '../repositories/lessons.repository'; 3 | 4 | 5 | @Controller("lessons") 6 | export class LessonsController { 7 | 8 | constructor(private lessonsDB: LessonsRepository) { 9 | 10 | } 11 | 12 | @Get() 13 | searchLesson( 14 | @Query("courseId") courseId:string, 15 | @Query("sortOrder") sortOrder = "asc", 16 | @Query("pageNumber", ParseIntPipe) pageNumber = 0, 17 | @Query("pageSize", ParseIntPipe) pageSize = 3) { 18 | 19 | if (!courseId) { 20 | throw new BadRequestException("courseId must be defined"); 21 | } 22 | 23 | if(sortOrder != "asc" && sortOrder != 'desc') { 24 | throw new BadRequestException('sortOrder must be asc or desc'); 25 | } 26 | 27 | return this.lessonsDB.search(courseId, 28 | sortOrder, pageNumber, pageSize); 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /rest-api/src/courses/courses.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import {CoursesController} from './controllers/courses.controller'; 3 | import {MongooseModule} from '@nestjs/mongoose'; 4 | import {CoursesSchema} from './schemas/courses.schema'; 5 | import {CoursesRepository} from './repositories/courses.repository'; 6 | import {LessonsSchema} from './schemas/lessons.schema'; 7 | import {LessonsRepository} from './repositories/lessons.repository'; 8 | import {LessonsController} from './controllers/lessons.controller'; 9 | 10 | 11 | @Module({ 12 | imports: [ 13 | MongooseModule.forFeature([ 14 | {name: "Course", schema: CoursesSchema}, 15 | {name: "Lesson", schema: LessonsSchema} 16 | ]) 17 | ], 18 | controllers: [ 19 | CoursesController, 20 | LessonsController 21 | ], 22 | providers: [ 23 | CoursesRepository, 24 | LessonsRepository 25 | ] 26 | }) 27 | export class CoursesModule { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /rest-api/src/courses/repositories/courses-repository.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import { Model } from 'mongoose'; 3 | import {Course} from '../../../../shared/course'; 4 | import {InjectModel} from '@nestjs/mongoose'; 5 | 6 | 7 | 8 | @Injectable() 9 | export class CoursesRepository { 10 | 11 | constructor(@InjectModel('Course') private courseModel: Model) { 12 | 13 | } 14 | 15 | async findAll(): Promise { 16 | return this.courseModel.find(); 17 | } 18 | 19 | async findCourseByUrl(courseUrl: string): Promise { 20 | return this.courseModel.findOne({url:courseUrl}); 21 | } 22 | 23 | async updateCourse(courseId: string, changes: Partial): Promise { 24 | return this.courseModel.findOneAndUpdate({ _id: courseId }, changes, {new:true}); 25 | } 26 | 27 | deleteCourse(courseId: string) { 28 | return this.courseModel.deleteOne({_id: courseId}); 29 | } 30 | 31 | async addCourse(course: Partial) { 32 | 33 | // another way of creating a model, when we want to save it immediately 34 | //const result = await this.courseModel.create(course); 35 | 36 | // this allows to manipulate the model in memory, before saving it 37 | const newCourse = this.courseModel(course); 38 | 39 | await newCourse.save(); 40 | 41 | return newCourse.toObject({versionKey:false}); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /rest-api/src/courses/repositories/courses.repository.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {Course} from '../../../../shared/course'; 3 | 4 | import { Model } from 'mongoose'; 5 | import {InjectModel} from '@nestjs/mongoose'; 6 | 7 | @Injectable() 8 | export class CoursesRepository { 9 | 10 | 11 | constructor(@InjectModel('Course') 12 | private courseModel: Model) { 13 | 14 | } 15 | 16 | async findAll(): Promise { 17 | return this.courseModel.find(); 18 | } 19 | 20 | async findCourseByUrl(courseUrl:string): Promise { 21 | return this.courseModel.findOne({url:courseUrl}); 22 | } 23 | 24 | updateCourse(courseId: string, changes: Partial) 25 | :Promise { 26 | return this.courseModel.findOneAndUpdate( 27 | {_id: courseId}, 28 | changes, 29 | {new:true}); 30 | } 31 | 32 | deleteCourse(courseId: string) { 33 | return this.courseModel.deleteOne({_id:courseId}); 34 | } 35 | 36 | async addCourse(course: Partial): Promise { 37 | 38 | const newCourse = this.courseModel(course); 39 | 40 | await newCourse.save(); 41 | 42 | return newCourse.toObject({versionKey:false}); 43 | 44 | } 45 | } 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /rest-api/src/courses/repositories/lessons.repository.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {Lesson} from '../../../../shared/lesson'; 3 | import { Model } from 'mongoose'; 4 | import {InjectModel} from '@nestjs/mongoose'; 5 | 6 | @Injectable() 7 | export class LessonsRepository { 8 | 9 | constructor(@InjectModel("Lesson") 10 | private lessonsModel: Model) { 11 | 12 | } 13 | 14 | search(courseId:string, 15 | sortOrder:string, 16 | pageNumber: number, 17 | pageSize:number) { 18 | 19 | console.log("searching for lessons ", courseId, sortOrder, pageNumber, pageSize); 20 | 21 | return this.lessonsModel.find({ 22 | course: courseId 23 | }, null, 24 | { 25 | skip: pageNumber * pageSize, 26 | limit: pageSize, 27 | sort: { 28 | seqNo: sortOrder 29 | } 30 | }); 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /rest-api/src/courses/schemas/courses.schema.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as mongoose from 'mongoose'; 3 | 4 | export const CoursesSchema = new mongoose.Schema({ 5 | seqNo: { 6 | type:Number, 7 | required:true 8 | }, 9 | url: String, 10 | iconUrl: String, 11 | courseListIcon: String, 12 | description: String, 13 | longDescription: String, 14 | category: String, 15 | lessonsCount: Number, 16 | promo: Boolean 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /rest-api/src/courses/schemas/lessons.schema.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as mongoose from 'mongoose'; 4 | 5 | 6 | 7 | export const LessonsSchema = new mongoose.Schema({ 8 | description: String, 9 | duration: String, 10 | seqNo: Number, 11 | course: { 12 | type: mongoose.Schema.Types.ObjectId, 13 | ref: 'Course' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /rest-api/src/filters/fallback.filter.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentsHost, Catch, ExceptionFilter} from '@nestjs/common'; 2 | 3 | @Catch() 4 | export class FallbackExceptionFilter implements ExceptionFilter{ 5 | 6 | catch(exception: any, host: ArgumentsHost) { 7 | 8 | console.log("fallback exception handler triggered", 9 | JSON.stringify(exception)); 10 | 11 | const ctx = host.switchToHttp(), 12 | response = ctx.getResponse(); 13 | 14 | 15 | return response.status(500).json({ 16 | statusCode: 500, 17 | createdBy: "FallbackExceptionFilter", 18 | errorMessage: exception.message ? exception.message : 19 | 'Unexpected error ocurred' 20 | }) 21 | 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /rest-api/src/filters/http.filter.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentsHost, Catch, ExceptionFilter, HttpException} from '@nestjs/common'; 2 | 3 | 4 | @Catch(HttpException) 5 | export class HttpExceptionFilter implements ExceptionFilter { 6 | 7 | catch(exception: HttpException, host: ArgumentsHost) { 8 | 9 | console.log("HTTP exception handler triggered", 10 | JSON.stringify(exception)); 11 | 12 | const ctx = host.switchToHttp(); 13 | 14 | const response = ctx.getResponse(), 15 | request = ctx.getRequest(), 16 | statusCode = exception.getStatus(); 17 | 18 | 19 | return response.status(statusCode).json({ 20 | status: statusCode, 21 | createdBy: "HttpExceptionFilter", 22 | errorMessage: exception.message.message 23 | }); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rest-api/src/filters/validation.exception.ts: -------------------------------------------------------------------------------- 1 | import {BadRequestException} from '@nestjs/common'; 2 | 3 | 4 | export class ValidationException extends BadRequestException { 5 | 6 | constructor(public validationErrors:string[]) { 7 | super(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /rest-api/src/filters/validation.filter.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentsHost, Catch, ExceptionFilter} from '@nestjs/common'; 2 | import {ValidationException} from './validation.exception'; 3 | 4 | @Catch(ValidationException) 5 | export class ValidationFilter implements ExceptionFilter { 6 | 7 | catch(exception: ValidationException, host: ArgumentsHost): any { 8 | 9 | 10 | const ctx = host.switchToHttp(), 11 | response = ctx.getResponse(); 12 | 13 | return response.status(400).json({ 14 | statusCode:400, 15 | createdBy: "ValidationFilter", 16 | validationErrors: exception.validationErrors 17 | }); 18 | 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /rest-api/src/guards/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {AuthorizationGuard} from './authorization.guard'; 3 | 4 | 5 | @Injectable() 6 | export class AdminGuard extends AuthorizationGuard { 7 | 8 | constructor() { 9 | super(["ADMIN"]); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /rest-api/src/guards/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import {CanActivate, ExecutionContext, Injectable, UnauthorizedException} from '@nestjs/common'; 2 | 3 | 4 | @Injectable() 5 | export class AuthenticationGuard implements CanActivate { 6 | 7 | canActivate(context: ExecutionContext): boolean { 8 | 9 | const host = context.switchToHttp(), 10 | request = host.getRequest(); 11 | 12 | const user = request["user"]; 13 | 14 | if (!user) { 15 | console.log("User not authenticated, denying access..."); 16 | throw new UnauthorizedException(); 17 | } 18 | 19 | console.log("User is authenticated, allowing access"); 20 | 21 | return true; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /rest-api/src/guards/authorization.guard.ts: -------------------------------------------------------------------------------- 1 | import {CanActivate, ExecutionContext, ForbiddenException, Injectable} from '@nestjs/common'; 2 | 3 | 4 | @Injectable() 5 | export class AuthorizationGuard implements CanActivate { 6 | 7 | constructor(private allowedRoles:string[]) { 8 | 9 | } 10 | 11 | canActivate(context: ExecutionContext): boolean { 12 | 13 | const host = context.switchToHttp(), 14 | request = host.getRequest(); 15 | 16 | const user = request["user"]; 17 | 18 | const allowed = this.isAllowed(user.roles); 19 | 20 | console.log("user is allowed: ", allowed); 21 | 22 | if (!allowed) { 23 | console.log("User is authenticated but not authorized, denying access..."); 24 | throw new ForbiddenException(); 25 | } 26 | 27 | console.log("User is authorized, allowing access"); 28 | 29 | return true; 30 | } 31 | 32 | isAllowed(userRoles:string[]) { 33 | 34 | console.log("Comparing roles: ", this.allowedRoles, userRoles); 35 | 36 | let allowed = false; 37 | 38 | userRoles.forEach(userRole => { 39 | console.log("Checking if role is allowed ", userRole); 40 | if (!allowed && this.allowedRoles.includes(userRole)) { 41 | allowed = true; 42 | } 43 | }); 44 | 45 | return allowed; 46 | 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /rest-api/src/main.ts: -------------------------------------------------------------------------------- 1 | import {NestFactory} from '@nestjs/core'; 2 | import {AppModule} from './app.module'; 3 | import {HttpExceptionFilter} from './filters/http.filter'; 4 | import {FallbackExceptionFilter} from './filters/fallback.filter'; 5 | import * as mongoose from 'mongoose'; 6 | import {ValidationError, ValidationPipe} from '@nestjs/common'; 7 | import {ValidationFilter} from './filters/validation.filter'; 8 | import {ValidationException} from './filters/validation.exception'; 9 | 10 | mongoose.set('useFindAndModify', false); 11 | 12 | async function bootstrap() { 13 | 14 | const app = await NestFactory.create(AppModule); 15 | 16 | app.setGlobalPrefix("api"); 17 | 18 | app.useGlobalFilters( 19 | new FallbackExceptionFilter(), 20 | new HttpExceptionFilter(), 21 | new ValidationFilter() 22 | ); 23 | 24 | app.useGlobalPipes(new ValidationPipe({ 25 | skipMissingProperties:true, 26 | exceptionFactory: (errors: ValidationError[]) => { 27 | 28 | const messages = errors.map( 29 | error=> `${error.property} has wrong value ${error.value}, 30 | ${Object.values(error.constraints).join(', ')} ` 31 | ); 32 | 33 | return new ValidationException(messages); 34 | } 35 | })); 36 | 37 | await app.listen(9000); 38 | 39 | } 40 | 41 | bootstrap(); 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /rest-api/src/middleware/get-user.middleware.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, NestMiddleware} from '@nestjs/common'; 2 | import {Request, Response} from "express"; 3 | import * as jwt from 'jsonwebtoken'; 4 | import {JWT_SECRET} from '../constants'; 5 | 6 | @Injectable() 7 | export class GetUserMiddleware implements NestMiddleware { 8 | 9 | use(req: Request, res: Response, next: () => void){ 10 | 11 | const authJwtToken = req.headers.authorization; 12 | 13 | if(!authJwtToken) { 14 | next(); 15 | return; 16 | } 17 | 18 | try { 19 | 20 | const user = jwt.verify(authJwtToken, JWT_SECRET); 21 | 22 | if (user) { 23 | console.log("Found user details in JWT: ", user); 24 | req["user"] = user; 25 | } 26 | } 27 | catch(err) { 28 | console.log("Error handling authentication JWT: ", err); 29 | } 30 | next(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /rest-api/src/pipes/to-integer.pipe.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentMetadata, BadRequestException, PipeTransform} from '@nestjs/common'; 2 | 3 | 4 | export class ToIntegerPipe implements PipeTransform { 5 | 6 | transform(value: string, metadata: ArgumentMetadata): number { 7 | 8 | const val = parseInt(value); 9 | 10 | if (isNaN(val)) { 11 | throw new BadRequestException( 12 | 'conversion to number failed' + value); 13 | } 14 | 15 | return val; 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /rest-api/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { AppModule } from './../src/app.module'; 4 | import { INestApplication } from '@nestjs/common'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /rest-api/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /rest-api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true 5 | }, 6 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /rest-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "experimentalDecorators": true, 7 | "skipLibCheck": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /rest-api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /shared/course.ts: -------------------------------------------------------------------------------- 1 | import {IsBoolean, IsInt, IsMongoId, IsString} from 'class-validator'; 2 | 3 | export class Course { 4 | @IsString() 5 | @IsMongoId() 6 | _id: string; 7 | @IsInt({message: 'seqNo must be numeric'}) seqNo: number; 8 | @IsString({always: false}) url: string; 9 | @IsString() iconUrl: string; 10 | @IsString() courseListIcon: string; 11 | @IsString() description: string; 12 | @IsString() longDescription?: string; 13 | @IsString() category: string; 14 | @IsInt() lessonsCount: number; 15 | @IsBoolean() promo: boolean; 16 | } 17 | 18 | 19 | export function compareCourses(c1: Course, c2: Course) { 20 | 21 | const compare = c1.seqNo - c2.seqNo; 22 | 23 | if (compare > 0) { 24 | return 1; 25 | } else if (compare < 0) { 26 | return -1; 27 | } else { 28 | return 0; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /shared/lesson.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface Lesson { 4 | _id: number; 5 | description: string; 6 | duration: string; 7 | seqNo: number; 8 | courseId: number; 9 | } 10 | 11 | 12 | export function compareLessons(l1:Lesson, l2: Lesson) { 13 | 14 | const compareCourses = l1.courseId - l2.courseId; 15 | 16 | if (compareCourses > 0) { 17 | return 1; 18 | } 19 | else if (compareCourses < 0){ 20 | return -1; 21 | } 22 | else { 23 | return l1.seqNo - l2.seqNo; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | 2 | >>> body { 3 | margin: 0; 4 | } 5 | 6 | main { 7 | margin: 30px; 8 | } 9 | 10 | .menu-button { 11 | background: #e1234e; 12 | color: white; 13 | border: none; 14 | cursor:pointer; 15 | outline:none; 16 | } 17 | 18 | 19 | .spinner-container { 20 | width: 100%; 21 | margin-top: 100px; 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | library_books 8 | Courses 9 | 10 | 11 | 12 | account_circle 13 | Login 14 | 15 | 16 | 17 | exit_to_app 18 | Logout 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class AppComponent implements OnInit { 11 | 12 | 13 | 14 | constructor(private router:Router) { 15 | 16 | } 17 | 18 | ngOnInit() { 19 | 20 | 21 | } 22 | 23 | logout() { 24 | 25 | localStorage.removeItem("authJwtToken"); 26 | 27 | this.router.navigateByUrl('/login'); 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | 4 | import {AppComponent} from './app.component'; 5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 6 | import {MatMenuModule} from '@angular/material/menu'; 7 | import {MatIconModule} from '@angular/material/icon'; 8 | 9 | import {MatListModule} from '@angular/material/list'; 10 | import {MatSidenavModule} from '@angular/material/sidenav'; 11 | import {MatToolbarModule} from '@angular/material/toolbar'; 12 | import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; 13 | 14 | import {RouterModule, Routes} from '@angular/router'; 15 | import {AuthModule} from './auth/auth.module'; 16 | import {environment} from '../environments/environment'; 17 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 18 | import {AuthInterceptor} from './auth/auth.interceptor'; 19 | 20 | 21 | const routes: Routes = [ 22 | { 23 | path: 'courses', 24 | loadChildren: () => import('./courses/courses.module').then(m => m.CoursesModule) 25 | }, 26 | { 27 | path: '**', 28 | redirectTo: '/' 29 | } 30 | ]; 31 | 32 | 33 | @NgModule({ 34 | declarations: [ 35 | AppComponent 36 | ], 37 | imports: [ 38 | BrowserModule, 39 | BrowserAnimationsModule, 40 | RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' }), 41 | HttpClientModule, 42 | MatMenuModule, 43 | MatIconModule, 44 | MatSidenavModule, 45 | MatProgressSpinnerModule, 46 | MatListModule, 47 | MatToolbarModule, 48 | AuthModule.forRoot() 49 | ], 50 | providers: [ 51 | { 52 | provide: HTTP_INTERCEPTORS, 53 | useClass: AuthInterceptor, 54 | multi: true, 55 | }, 56 | ], 57 | bootstrap: [AppComponent] 58 | }) 59 | export class AppModule { 60 | } 61 | -------------------------------------------------------------------------------- /src/app/auth/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; 3 | import {Observable} from 'rxjs'; 4 | 5 | @Injectable() 6 | export class AuthInterceptor implements HttpInterceptor { 7 | 8 | intercept(req: HttpRequest, 9 | next: HttpHandler): Observable> { 10 | 11 | const authJwtToken = localStorage.getItem("authJwtToken"); 12 | 13 | if (authJwtToken) { 14 | const cloned = req.clone({ 15 | headers: req.headers 16 | .set('Authorization',`${authJwtToken}`) 17 | }); 18 | 19 | return next.handle(cloned); 20 | } 21 | else { 22 | return next.handle(req); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import {ModuleWithProviders, NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {LoginComponent} from './login/login.component'; 4 | import {MatCardModule} from "@angular/material/card"; 5 | import { MatInputModule } from "@angular/material/input"; 6 | import {RouterModule} from "@angular/router"; 7 | import {ReactiveFormsModule} from "@angular/forms"; 8 | import {MatButtonModule} from "@angular/material/button"; 9 | import {AuthService} from "./auth.service"; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | ReactiveFormsModule, 15 | MatCardModule, 16 | MatInputModule, 17 | MatButtonModule, 18 | RouterModule.forChild([{path: '', component: LoginComponent}]), 19 | 20 | ], 21 | declarations: [LoginComponent], 22 | exports: [LoginComponent] 23 | }) 24 | export class AuthModule { 25 | static forRoot(): ModuleWithProviders { 26 | return { 27 | ngModule: AuthModule, 28 | providers: [ 29 | AuthService 30 | ] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {HttpClient} from "@angular/common/http"; 3 | import {Observable} from "rxjs"; 4 | import {User} from "./model/user.model"; 5 | 6 | 7 | 8 | 9 | @Injectable() 10 | export class AuthService { 11 | 12 | constructor(private http:HttpClient) { 13 | 14 | } 15 | 16 | login(email:string, password:string): Observable { 17 | return this.http.post('/api/login', {email,password}); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .login-page { 4 | max-width: 350px; 5 | margin: 50px auto 0 auto; 6 | } 7 | 8 | .login-form { 9 | display: flex; 10 | flex-direction: column; 11 | } -------------------------------------------------------------------------------- /src/app/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from "@angular/forms"; 3 | 4 | import {AuthService} from "../auth.service"; 5 | import {tap} from "rxjs/operators"; 6 | import {noop} from "rxjs"; 7 | import {Router} from "@angular/router"; 8 | 9 | @Component({ 10 | selector: 'login', 11 | templateUrl: './login.component.html', 12 | styleUrls: ['./login.component.scss'] 13 | }) 14 | export class LoginComponent implements OnInit { 15 | 16 | form: FormGroup; 17 | 18 | constructor( 19 | private fb:FormBuilder, 20 | private auth: AuthService, 21 | private router:Router) { 22 | 23 | this.form = fb.group({ 24 | email: ['student@angular-university.io', [Validators.required]], 25 | password: ['password', [Validators.required]] 26 | }); 27 | 28 | } 29 | 30 | ngOnInit() { 31 | 32 | } 33 | 34 | login() { 35 | 36 | const val = this.form.value; 37 | 38 | this.auth.login(val.email, val.password) 39 | .subscribe( 40 | (reply:any) => { 41 | 42 | localStorage.setItem("authJwtToken", 43 | reply.authJwtToken); 44 | 45 | this.router.navigateByUrl('/courses'); 46 | 47 | }, 48 | err => { 49 | console.log("Login failed:", err); 50 | alert('Login failed.'); 51 | } 52 | ); 53 | 54 | 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/app/auth/model/user.model.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface User { 4 | id: string; 5 | email: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/courses/course/course.component.css: -------------------------------------------------------------------------------- 1 | 2 | .course { 3 | text-align: center; 4 | max-width: 390px; 5 | margin: 0 auto; 6 | } 7 | 8 | .course-thumbnail { 9 | width: 175px; 10 | margin: 20px auto 0 auto; 11 | display: block; 12 | border-radius: 4px; 13 | } 14 | 15 | .description-cell { 16 | text-align: left; 17 | margin: 10px auto; 18 | } 19 | 20 | .duration-cell { 21 | text-align: center; 22 | } 23 | 24 | .duration-cell mat-icon { 25 | display: inline-block; 26 | vertical-align: middle; 27 | font-size: 20px; 28 | } 29 | 30 | .lessons-table { 31 | min-height: 360px; 32 | margin-top: 10px; 33 | } 34 | 35 | .spinner-container mat-spinner { 36 | margin: 95px auto 0 auto; 37 | } 38 | 39 | .action-toolbar { 40 | margin-top: 20px; 41 | } 42 | 43 | h2 { 44 | font-family: "Roboto"; 45 | } 46 | 47 | 48 | .bottom-toolbar { 49 | margin-top: 20px; 50 | margin-bottom: 200px; 51 | } 52 | 53 | 54 | .spinner-container { 55 | width:390px; 56 | } 57 | -------------------------------------------------------------------------------- /src/app/courses/course/course.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{course?.description}}

4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | # 14 | 15 | {{lesson.seqNo}} 16 | 17 | 18 | 19 | 20 | 21 | Description 22 | 23 | {{lesson.description}} 25 | 26 | 27 | 28 | 29 | 30 | Duration 31 | 32 | {{lesson.duration}} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 |
48 | 49 |
50 | -------------------------------------------------------------------------------- /src/app/courses/course/course.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core'; 2 | import {ActivatedRoute} from '@angular/router'; 3 | import {Course} from '../../../../shared/course'; 4 | import {Observable} from 'rxjs'; 5 | import {Lesson} from '../../../../shared/lesson'; 6 | import {concatMap, delay, filter, first, map, shareReplay, switchMap, tap, withLatestFrom} from 'rxjs/operators'; 7 | import {CoursesHttpService} from '../services/courses-http.service'; 8 | import { MatPaginator, PageEvent } from '@angular/material/paginator'; 9 | 10 | 11 | @Component({ 12 | selector: 'course', 13 | templateUrl: './course.component.html', 14 | styleUrls: ['./course.component.css'] 15 | }) 16 | export class CourseComponent implements OnInit { 17 | 18 | course$: Observable; 19 | 20 | lessons$: Observable; 21 | 22 | displayedColumns = ['seqNo', 'description', 'duration']; 23 | 24 | currentPage = 0; 25 | 26 | @ViewChildren(MatPaginator) 27 | paginators: QueryList; 28 | 29 | @ViewChild(MatPaginator) 30 | paginator: MatPaginator; 31 | 32 | constructor( 33 | private coursesService: CoursesHttpService, 34 | private route: ActivatedRoute) { 35 | 36 | } 37 | 38 | ngOnInit() { 39 | 40 | const courseUrl = this.route.snapshot.paramMap.get("courseUrl"); 41 | 42 | this.course$ = this.coursesService.findCourseByUrl(courseUrl); 43 | 44 | this.loadLessonsPage(); 45 | 46 | } 47 | 48 | ngAfterViewInit() { 49 | 50 | this.paginators.changes 51 | .pipe( 52 | filter(paginators => paginators.length > 0), 53 | switchMap(paginators => paginators.first.page) 54 | ) 55 | .subscribe( 56 | (page:PageEvent) => { 57 | this.currentPage = page.pageIndex; 58 | this.loadLessonsPage(); 59 | } 60 | ); 61 | } 62 | 63 | loadLessonsPage() { 64 | console.log("current page", this.currentPage); 65 | this.lessons$ = this.course$.pipe( 66 | concatMap(course => this.coursesService.findLessons(course._id, this.currentPage, 3)), 67 | ); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/app/courses/courses-card-list/courses-card-list.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .course-card { 4 | margin: 20px 10px; 5 | } 6 | 7 | .course-actions { 8 | text-align: center; 9 | 10 | } 11 | 12 | .course-actions button { 13 | margin-right: 10px; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/app/courses/courses-card-list/courses-card-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{course.description}} 8 | 9 | 10 | 11 | 12 | 13 | 14 |

{{course.longDescription}}

15 |
16 | 17 | 18 | 19 | 22 | 23 | 26 | 27 | 30 | 31 | 32 | 33 |
34 | -------------------------------------------------------------------------------- /src/app/courses/courses-card-list/courses-card-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation} from '@angular/core'; 2 | import {Course} from "../../../../shared/course"; 3 | import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; 4 | import {EditCourseDialogComponent} from "../edit-course-dialog/edit-course-dialog.component"; 5 | import {defaultDialogConfig} from '../shared/default-dialog-config'; 6 | import {CoursesHttpService} from '../services/courses-http.service'; 7 | 8 | @Component({ 9 | selector: 'courses-card-list', 10 | templateUrl: './courses-card-list.component.html', 11 | styleUrls: ['./courses-card-list.component.css'] 12 | }) 13 | export class CoursesCardListComponent implements OnInit { 14 | 15 | @Input() 16 | courses: Course[]; 17 | 18 | @Output() 19 | courseChanged = new EventEmitter(); 20 | 21 | constructor( 22 | private dialog: MatDialog, 23 | private coursesService: CoursesHttpService) { 24 | } 25 | 26 | ngOnInit() { 27 | 28 | } 29 | 30 | editCourse(course:Course) { 31 | 32 | const dialogConfig = defaultDialogConfig(); 33 | 34 | dialogConfig.data = { 35 | dialogTitle:"Edit Course", 36 | course, 37 | mode: 'update' 38 | }; 39 | 40 | this.dialog.open(EditCourseDialogComponent, dialogConfig) 41 | .afterClosed() 42 | .subscribe(() => this.courseChanged.emit()); 43 | 44 | } 45 | 46 | onDeleteCourse(course:Course) { 47 | this.coursesService.deleteCourse(course._id) 48 | .subscribe( 49 | () => this.courseChanged.emit()); 50 | } 51 | 52 | } 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/app/courses/courses.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {HomeComponent} from './home/home.component'; 4 | import {CoursesCardListComponent} from './courses-card-list/courses-card-list.component'; 5 | import {EditCourseDialogComponent} from './edit-course-dialog/edit-course-dialog.component'; 6 | import {CoursesHttpService} from './services/courses-http.service'; 7 | import {CourseComponent} from './course/course.component'; 8 | import {MatDatepickerModule} from '@angular/material/datepicker'; 9 | import {MatDialogModule} from '@angular/material/dialog'; 10 | import {MatInputModule} from '@angular/material/input'; 11 | import {MatPaginatorModule} from '@angular/material/paginator'; 12 | import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 13 | import {MatSelectModule} from '@angular/material/select'; 14 | import {MatSlideToggleModule} from '@angular/material/slide-toggle'; 15 | import {MatSortModule} from '@angular/material/sort'; 16 | import {MatTableModule} from '@angular/material/table'; 17 | import {MatTabsModule} from '@angular/material/tabs'; 18 | import {ReactiveFormsModule} from '@angular/forms'; 19 | import {MatMomentDateModule} from '@angular/material-moment-adapter'; 20 | import {MatCardModule} from '@angular/material/card'; 21 | import {MatButtonModule} from '@angular/material/button'; 22 | import {MatIconModule} from '@angular/material/icon'; 23 | import {RouterModule, Routes} from '@angular/router'; 24 | import {compareCourses, Course} from '../../../shared/course'; 25 | import {compareLessons, Lesson} from '../../../shared/lesson'; 26 | 27 | 28 | export const coursesRoutes: Routes = [ 29 | { 30 | path: '', 31 | component: HomeComponent 32 | 33 | }, 34 | { 35 | path: ':courseUrl', 36 | component: CourseComponent 37 | } 38 | ]; 39 | 40 | 41 | @NgModule({ 42 | imports: [ 43 | CommonModule, 44 | MatButtonModule, 45 | MatIconModule, 46 | MatCardModule, 47 | MatTabsModule, 48 | MatInputModule, 49 | MatTableModule, 50 | MatPaginatorModule, 51 | MatSortModule, 52 | MatProgressSpinnerModule, 53 | MatSlideToggleModule, 54 | MatDialogModule, 55 | MatSelectModule, 56 | MatDatepickerModule, 57 | MatMomentDateModule, 58 | ReactiveFormsModule, 59 | RouterModule.forChild(coursesRoutes) 60 | ], 61 | declarations: [ 62 | HomeComponent, 63 | CoursesCardListComponent, 64 | EditCourseDialogComponent, 65 | CourseComponent 66 | ], 67 | exports: [ 68 | HomeComponent, 69 | CoursesCardListComponent, 70 | EditCourseDialogComponent, 71 | CourseComponent 72 | ], 73 | providers: [ 74 | CoursesHttpService 75 | ] 76 | }) 77 | export class CoursesModule { 78 | 79 | constructor() { 80 | 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/app/courses/edit-course-dialog/edit-course-dialog.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .mat-form-field { 4 | display: block; 5 | } 6 | 7 | textarea { 8 | height: 100px; 9 | resize: vertical; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/courses/edit-course-dialog/edit-course-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 |

{{dialogTitle}}

3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | 50 | Beginner 51 | 52 | Advanced 53 | 54 | 55 | 56 | 57 | 58 | Promotion On 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 |
76 | 77 | 78 | 79 | 82 | 83 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/app/courses/edit-course-dialog/edit-course-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject} from '@angular/core'; 2 | import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; 3 | import {Course} from '../../../../shared/course'; 4 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 5 | import {Observable} from 'rxjs'; 6 | import {CoursesHttpService} from '../services/courses-http.service'; 7 | 8 | @Component({ 9 | selector: 'course-dialog', 10 | templateUrl: './edit-course-dialog.component.html', 11 | styleUrls: ['./edit-course-dialog.component.css'] 12 | }) 13 | export class EditCourseDialogComponent { 14 | 15 | form: FormGroup; 16 | 17 | dialogTitle: string; 18 | 19 | course: Course; 20 | 21 | mode: 'create' | 'update'; 22 | 23 | loading$:Observable; 24 | 25 | constructor( 26 | private fb: FormBuilder, 27 | private dialogRef: MatDialogRef, 28 | @Inject(MAT_DIALOG_DATA) data, 29 | private coursesService: CoursesHttpService) { 30 | 31 | this.dialogTitle = data.dialogTitle; 32 | this.course = data.course; 33 | this.mode = data.mode; 34 | 35 | const formControls = { 36 | description: ['', Validators.required], 37 | category: ['', Validators.required], 38 | longDescription: ['', Validators.required], 39 | promo: [false, []] 40 | }; 41 | 42 | if (this.mode == 'update') { 43 | this.form = this.fb.group(formControls); 44 | this.form.patchValue({...data.course}); 45 | } 46 | else if (this.mode == 'create') { 47 | this.form = this.fb.group({ 48 | ...formControls, 49 | url: ['', Validators.required], 50 | iconUrl: ['', Validators.required] 51 | }); 52 | } 53 | } 54 | 55 | onClose() { 56 | this.dialogRef.close(); 57 | } 58 | 59 | onSave() { 60 | 61 | const changes: Partial = { 62 | ...this.form.value 63 | }; 64 | 65 | if (this.mode == 'update') { 66 | this.coursesService.updateCourse(this.course._id, changes) 67 | .subscribe( 68 | course => this.dialogRef.close(course) 69 | ) 70 | } 71 | else if (this.mode == "create") { 72 | this.coursesService.createCourse(changes) 73 | .subscribe( 74 | course => this.dialogRef.close(course) 75 | ) 76 | 77 | } 78 | 79 | 80 | 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/app/courses/home/home.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .title { 4 | text-align: center; 5 | margin-right: 15px; 6 | 7 | } 8 | 9 | .courses-panel { 10 | max-width: 350px; 11 | margin: 0 auto; 12 | } 13 | 14 | 15 | .counters { 16 | display: flex; 17 | } 18 | 19 | .filler { 20 | flex: 1 1 auto; 21 | } 22 | 23 | h2 { 24 | font-family: "Roboto"; 25 | } 26 | 27 | .header { 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | 32 | } 33 | 34 | 35 | 36 | .spinner-container { 37 | margin-top: 100px; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/courses/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

All Courses

6 | 7 | 10 | 11 |
12 | 13 |
14 |

In Promo: {{promoTotal$ | async}}

15 |
16 | 17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | -------------------------------------------------------------------------------- /src/app/courses/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {compareCourses, Course} from '../../../../shared/course'; 3 | import {Observable} from "rxjs"; 4 | import {defaultDialogConfig} from '../shared/default-dialog-config'; 5 | import {EditCourseDialogComponent} from '../edit-course-dialog/edit-course-dialog.component'; 6 | import { MatDialog } from '@angular/material/dialog'; 7 | import {map, shareReplay} from 'rxjs/operators'; 8 | import {CoursesHttpService} from '../services/courses-http.service'; 9 | 10 | 11 | 12 | @Component({ 13 | selector: 'home', 14 | templateUrl: './home.component.html', 15 | styleUrls: ['./home.component.css'] 16 | }) 17 | export class HomeComponent implements OnInit { 18 | 19 | promoTotal$: Observable; 20 | 21 | loading$: Observable; 22 | 23 | beginnerCourses$: Observable; 24 | 25 | advancedCourses$: Observable; 26 | 27 | 28 | constructor( 29 | private dialog: MatDialog, 30 | private coursesHttpService: CoursesHttpService) { 31 | 32 | } 33 | 34 | ngOnInit() { 35 | this.reload(); 36 | } 37 | 38 | reload() { 39 | 40 | const courses$ = this.coursesHttpService.findAllCourses() 41 | .pipe( 42 | map(courses => courses.sort(compareCourses)), 43 | shareReplay() 44 | ); 45 | 46 | this.loading$ = courses$.pipe(map(courses => !!courses)); 47 | 48 | this.beginnerCourses$ = courses$ 49 | .pipe( 50 | map(courses => courses.filter(course => course.category == 'BEGINNER')) 51 | ); 52 | 53 | 54 | this.advancedCourses$ = courses$ 55 | .pipe( 56 | map(courses => courses.filter(course => course.category == 'ADVANCED')) 57 | ); 58 | 59 | this.promoTotal$ = courses$ 60 | .pipe( 61 | map(courses => courses.filter(course => course.promo).length) 62 | ); 63 | 64 | } 65 | 66 | onAddCourse() { 67 | 68 | const dialogConfig = defaultDialogConfig(); 69 | 70 | dialogConfig.data = { 71 | dialogTitle:"Create Course", 72 | mode: 'create' 73 | }; 74 | 75 | this.dialog.open(EditCourseDialogComponent, dialogConfig) 76 | .afterClosed() 77 | .subscribe(data => { 78 | if (data) { 79 | this.reload(); 80 | } 81 | }); 82 | 83 | } 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/app/courses/services/courses-http.service.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {Injectable} from "@angular/core"; 4 | import {HttpClient, HttpParams} from "@angular/common/http"; 5 | import {Observable} from "rxjs"; 6 | import {compareCourses, Course} from '../../../../shared/course'; 7 | import {map} from "rxjs/operators"; 8 | import {Lesson} from "../../../../shared/lesson"; 9 | 10 | 11 | @Injectable() 12 | export class CoursesHttpService { 13 | 14 | constructor(private http:HttpClient) { 15 | 16 | } 17 | 18 | findAllCourses(): Observable { 19 | return this.http.get('/api/courses') 20 | .pipe( 21 | map(courses => courses.sort(compareCourses)) 22 | ); 23 | } 24 | 25 | findCourseByUrl(courseUrl: string): Observable { 26 | return this.http.get(`/api/courses/${courseUrl}`); 27 | } 28 | 29 | findLessons( 30 | courseId:string, 31 | pageNumber = 0, pageSize = 3): Observable { 32 | 33 | return this.http.get('/api/lessons', { 34 | params: new HttpParams() 35 | .set('courseId', courseId) 36 | .set('sortOrder', 'asc') 37 | .set('pageNumber', pageNumber.toString()) 38 | .set('pageSize', pageSize.toString()) 39 | }); 40 | } 41 | 42 | 43 | updateCourse(courseId: string, changes: Partial) { 44 | return this.http.put('/api/courses/' + courseId, changes); 45 | } 46 | 47 | 48 | deleteCourse(courseId: string) { 49 | return this.http.delete('/api/courses/' + courseId); 50 | } 51 | 52 | createCourse(changes: Partial) { 53 | return this.http.post('/api/courses', changes); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/courses/shared/default-dialog-config.ts: -------------------------------------------------------------------------------- 1 | import { MatDialogConfig } from '@angular/material/dialog'; 2 | 3 | 4 | export function defaultDialogConfig() { 5 | const dialogConfig = new MatDialogConfig(); 6 | 7 | dialogConfig.disableClose = true; 8 | dialogConfig.autoFocus = true; 9 | dialogConfig.width = '400px'; 10 | 11 | return dialogConfig; 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/nestjs-course/17e770630b649ece37c52abca69f6d1bc3ca741b/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/nestjs-course/17e770630b649ece37c52abca69f6d1bc3ca741b/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NestJs In Practice Course 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 | 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .catch(err => console.log(err)); 15 | -------------------------------------------------------------------------------- /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/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for the Reflect API. */ 38 | // import 'core-js/es6/reflect'; 39 | 40 | 41 | /** Evergreen browsers require these. **/ 42 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 43 | 44 | 45 | 46 | /*************************************************************************************************** 47 | * Zone JS is required by Angular itself. 48 | */ 49 | import 'zone.js'; // Included with Angular CLI. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * APPLICATION IMPORTS 55 | */ 56 | 57 | /** 58 | * Date, currency, decimal and percent pipes. 59 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 60 | */ 61 | // import 'intl'; // Run `npm install --save intl`. 62 | /** 63 | * Need to import at least one locale-data with intl. 64 | */ 65 | // import 'intl/locale-data/jsonp/en'; 66 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | /* You can add global styles to this file, and also import other style files */ 3 | 4 | 5 | 6 | 7 | // Include non-theme styles for core. 8 | @include mat.core(); 9 | 10 | $mat-custom-theme: ( 11 | 50: #ffebee, 12 | 100: #ffcdd2, 13 | 200: #ef9a9a, 14 | 300: #e57373, 15 | 400: #ef5350, 16 | 500: #e1234e, 17 | 600: #e53935, 18 | 700: #d32f2f, 19 | 800: #c62828, 20 | 900: #b71c1c, 21 | A100: #ff8a80, 22 | A200: #ff5252, 23 | A400: #ff1744, 24 | A700: #d50000, 25 | contrast: ( 26 | 50: rgba(black, 0.87), 27 | 100: rgba(black, 0.87), 28 | 200: rgba(black, 0.87), 29 | 300: rgba(black, 0.87), 30 | 400: rgba(black, 0.87), 31 | 500: white, 32 | 600: white, 33 | 700: white, 34 | 800: white, 35 | 900: white, 36 | A100: rgba(black, 0.87), 37 | A200: white, 38 | A400: white, 39 | A700: white, 40 | ) 41 | ); 42 | 43 | 44 | // Define a theme. 45 | $primary: mat.define-palette($mat-custom-theme); 46 | $accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); 47 | 48 | $theme: mat.define-light-theme($primary, $accent); 49 | 50 | // Include all theme styles for the components. 51 | @include mat.all-component-themes($theme); 52 | 53 | 54 | .spinner-container { 55 | position: fixed; 56 | height: 300px; 57 | width: 345px; 58 | display: flex; 59 | background: white; 60 | z-index: 1; 61 | opacity: 0.5; 62 | justify-content: center; 63 | max-width: 350px; 64 | margin: 70px auto 0 auto; 65 | } 66 | -------------------------------------------------------------------------------- /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/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting(), { 26 | teardown: { destroyAfterEach: false } 27 | } 28 | ); 29 | // Then we find all the tests. 30 | const context = require.context('./', true, /\.spec\.ts$/); 31 | // And load the modules. 32 | context.keys().map(context); 33 | // Finally, start Karma to run the tests. 34 | __karma__.start(); 35 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "main.ts", 10 | "polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "target": "es2020", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom", 17 | "ES2017.object" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warning" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 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-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-var-keyword": true, 76 | "object-literal-sort-keys": false, 77 | "one-line": [ 78 | true, 79 | "check-open-brace", 80 | "check-catch", 81 | "check-else", 82 | "check-whitespace" 83 | ], 84 | "prefer-const": true, 85 | "quotemark": [ 86 | true, 87 | "single" 88 | ], 89 | "radix": true, 90 | "semicolon": [ 91 | true, 92 | "always" 93 | ], 94 | "triple-equals": [ 95 | true, 96 | "allow-null-check" 97 | ], 98 | "typedef-whitespace": [ 99 | true, 100 | { 101 | "call-signature": "nospace", 102 | "index-signature": "nospace", 103 | "parameter": "nospace", 104 | "property-declaration": "nospace", 105 | "variable-declaration": "nospace" 106 | } 107 | ], 108 | "typeof-compare": true, 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "directive-selector": [ 120 | true, 121 | "attribute", 122 | "app", 123 | "camelCase" 124 | ], 125 | "component-selector": [ 126 | true, 127 | "element", 128 | "app", 129 | "kebab-case" 130 | ], 131 | "no-inputs-metadata-property": true, 132 | "no-outputs-metadata-property": true, 133 | "no-host-metadata-property": true, 134 | "no-input-rename": true, 135 | "no-output-rename": true, 136 | "use-lifecycle-interface": true, 137 | "use-pipe-transform-interface": true, 138 | "component-class-suffix": true, 139 | "directive-class-suffix": true, 140 | "invoke-injectable": true 141 | } 142 | } 143 | --------------------------------------------------------------------------------