├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions ├── .gitignore ├── package-lock.json ├── package.json ├── scripts │ └── init-admin.js ├── src │ ├── auth.middleware.ts │ ├── create-user.ts │ ├── index.ts │ ├── init.ts │ └── promotions-counter │ │ ├── on-add-course.ts │ │ ├── on-course-updated.ts │ │ └── on-delete-course.ts └── tsconfig.json ├── images ├── firebase-course-1.jpg └── firebase-course-2.jpg ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── about │ │ ├── about.component.css │ │ ├── about.component.html │ │ ├── about.component.ts │ │ └── db-data.ts │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── 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 │ ├── create-course │ │ ├── create-course.component.css │ │ ├── create-course.component.html │ │ └── create-course.component.ts │ ├── create-user │ │ ├── create-user.component.css │ │ ├── create-user.component.html │ │ └── create-user.component.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 │ ├── login │ │ ├── login.component.html │ │ ├── login.component.scss │ │ └── login.component.ts │ ├── model │ │ ├── course.ts │ │ ├── lesson.ts │ │ └── user-roles.ts │ └── services │ │ ├── auth-token.service.ts │ │ ├── auth.interceptor.ts │ │ ├── course.resolver.ts │ │ ├── courses.service.ts │ │ ├── db-utils.ts │ │ └── user.service.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── storage.rules ├── test-data ├── auth_export │ ├── accounts.json │ └── config.json ├── firebase-export-metadata.json └── firestore_export │ ├── all_namespaces │ └── all_kinds │ │ ├── all_namespaces_all_kinds.export_metadata │ │ └── output-0 │ └── firestore_export.overall_export_metadata ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "fir-course-recording-c7f3e" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.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 | /.firebase 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.angular/cache 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | firebase-debug.log 46 | firestore-debug.log 47 | ui-debug.log 48 | /try-emulator/node_modules 49 | /functions/service-accounts/*.json 50 | 51 | /functions/scripts/*.json 52 | -------------------------------------------------------------------------------- /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 | ## Firebase & AngularFire In Depth 3 | 4 | This repository currently contains the code for the [Firebase & AngularFire In Depth](https://angular-university.io/course/angularfire-course). 5 | 6 | This course is updated to Angular 13: 7 | 8 | ![Firebase & AngularFire In Depth](https://angular-university.s3-us-west-1.amazonaws.com/course-images/firebase-course-1.jpg) 9 | 10 | You can find the starting point of the course in the [1-start branch](https://github.com/angular-university/firebase-course/tree/1-start). 11 | 12 | This master branch contains the *final version of the course code*, that you can use as a reference if you choose to code along. 13 | 14 | 15 | 16 | # Installation pre-requisites 17 | 18 | IMPORTANT: Please use Node 16 LST (Long Term Support version). 19 | 20 | # Installing the Angular CLI 21 | 22 | With the following command the angular-cli will be installed globally in your machine: 23 | 24 | npm install -g @angular/cli 25 | 26 | # How To install this repository 27 | 28 | We can install the master branch using the following commands: 29 | 30 | git clone https://github.com/angular-university/firebase-course.git 31 | 32 | cd firebase-course 33 | npm ci 34 | 35 | Note: **We recommend using npm ci, instead of npm install**. This will ensure that you use the exact dependency versions set on package-lock.json, unlike npm install which might potentially change those versions. 36 | 37 | # To run the Development UI Server 38 | 39 | To run the frontend part of our code, we will use the Angular CLI: 40 | 41 | npm start 42 | 43 | The application is visible at port 4200: [http://localhost:4200](http://localhost:4200) 44 | 45 | # Other Courses 46 | # Modern Angular With Signals 47 | 48 | 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: 49 | 50 | ![Modern Angular With Signals Course](https://d3vigmphadbn9b.cloudfront.net/course-images/large-images/angular-signals-course.jpg) 51 | 52 | # Angular Forms In Depth 53 | 54 | If you are looking for the [Angular Forms In Depth](https://angular-university.io/course/angular-forms-course) course, the repo with the full code can be found here: 55 | 56 | ![Angular Forms In Depth](https://angular-university.s3-us-west-1.amazonaws.com/course-images/angular-forms-course-small.jpg) 57 | 58 | # Angular Router In Depth 59 | 60 | If you are looking for the [Angular Router In Depth](https://angular-university.io/course/angular-router-course) course, the repo with the full code can be found here: 61 | 62 | ![Angular Router In Depth](https://angular-university.s3-us-west-1.amazonaws.com/course-images/angular-router-course.jpg) 63 | 64 | # NgRx (with NgRx Data) - The Complete Guide 65 | 66 | 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: 67 | 68 | ![Ngrx (with NgRx Data) - The Complete Guide](https://angular-university.s3-us-west-1.amazonaws.com/course-images/ngrx-v2.png) 69 | 70 | 71 | # Angular Core Deep Dive Course 72 | 73 | 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: 74 | 75 | ![Angular Core Deep Dive](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-core-in-depth-small.png) 76 | 77 | # RxJs In Practice 78 | 79 | 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: 80 | 81 | ![RxJs In Practice Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/rxjs-in-practice-course.png) 82 | 83 | # NestJs In Practice (with MongoDB) 84 | 85 | If you are looking for the [NestJs In Practice Course](https://angular-university.io/course/nestjs-course), the repo with the full code can be found here: 86 | 87 | ![NestJs In Practice Course](https://angular-university.s3-us-west-1.amazonaws.com/course-images/nestjs-v2.png) 88 | 89 | # Angular Testing Course 90 | 91 | 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: 92 | 93 | ![Angular Testing Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-testing-small.png) 94 | 95 | # Serverless Angular with Firebase Course 96 | 97 | 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: 98 | 99 | ![Serverless Angular with Firebase Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/serverless-angular-small.png) 100 | 101 | # Angular Universal Course 102 | 103 | 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: 104 | 105 | ![Angular Universal Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-universal-small.png) 106 | 107 | # Angular PWA Course 108 | 109 | 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: 110 | 111 | ![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) 112 | 113 | # Angular Security Masterclass 114 | 115 | 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: 116 | 117 | [Angular Security Masterclass](https://github.com/angular-university/angular-security-course). 118 | 119 | ![Angular Security Masterclass](https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png) 120 | 121 | # Angular Advanced Library Laboratory Course 122 | 123 | If you are looking for the Angular Advanced Course, the repo with the full code can be found here: 124 | 125 | [Angular Advanced Library Laboratory Course: Build Your Own Library](https://angular-university.io/course/angular-advanced-course). 126 | 127 | ![Angular Advanced Library Laboratory Course: Build Your Own Library](https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png) 128 | 129 | 130 | ## RxJs and Reactive Patterns Angular Architecture Course 131 | 132 | If you are looking for the RxJs and Reactive Patterns Angular Architecture Course code, the repo with the full code can be found here: 133 | 134 | [RxJs and Reactive Patterns Angular Architecture Course](https://angular-university.io/course/reactive-angular-architecture-course) 135 | 136 | ![RxJs and Reactive Patterns Angular Architecture Course](https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png) 137 | 138 | 139 | ## Complete Typescript Course - Build A REST API 140 | 141 | If you are looking for the Complete Typescript 2 Course - Build a REST API, the repo with the full code can be found here: 142 | 143 | [https://angular-university.io/course/typescript-2-tutorial](https://github.com/angular-university/complete-typescript-course) 144 | 145 | [Github repo for this course](https://github.com/angular-university/complete-typescript-course) 146 | 147 | ![Complete Typescript Course](https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png) 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "firebase-course": { 10 | "projectType": "application", 11 | "schematics": {}, 12 | "root": "", 13 | "sourceRoot": "src", 14 | "prefix": "app", 15 | "architect": { 16 | "build": { 17 | "builder": "@angular-devkit/build-angular:browser", 18 | "options": { 19 | "outputPath": "dist", 20 | "index": "src/index.html", 21 | "main": "src/main.ts", 22 | "polyfills": "src/polyfills.ts", 23 | "tsConfig": "tsconfig.app.json", 24 | "assets": [ 25 | "src/favicon.ico", 26 | "src/assets" 27 | ], 28 | "styles": [ 29 | "./node_modules/@angular/material/prebuilt-themes/purple-green.css", 30 | "src/styles.scss" 31 | ], 32 | "scripts": [], 33 | "vendorChunk": true, 34 | "extractLicenses": false, 35 | "buildOptimizer": false, 36 | "sourceMap": true, 37 | "optimization": false, 38 | "namedChunks": true 39 | }, 40 | "configurations": { 41 | "production": { 42 | "fileReplacements": [ 43 | { 44 | "replace": "src/environments/environment.ts", 45 | "with": "src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": false, 51 | "namedChunks": false, 52 | "extractLicenses": true, 53 | "vendorChunk": false, 54 | "buildOptimizer": true, 55 | "budgets": [ 56 | { 57 | "type": "initial", 58 | "maximumWarning": "2mb", 59 | "maximumError": "5mb" 60 | }, 61 | { 62 | "type": "anyComponentStyle", 63 | "maximumWarning": "6kb", 64 | "maximumError": "10kb" 65 | } 66 | ] 67 | } 68 | } 69 | }, 70 | "serve": { 71 | "builder": "@angular-devkit/build-angular:dev-server", 72 | "options": { 73 | "browserTarget": "firebase-course:build" 74 | }, 75 | "configurations": { 76 | "production": { 77 | "browserTarget": "firebase-course:build:production" 78 | } 79 | } 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "browserTarget": "firebase-course:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "main": "src/test.ts", 91 | "polyfills": "src/polyfills.ts", 92 | "tsConfig": "tsconfig.spec.json", 93 | "karmaConfig": "karma.conf.js", 94 | "assets": [ 95 | "src/favicon.ico", 96 | "src/assets" 97 | ], 98 | "styles": [ 99 | "./node_modules/@angular/material/prebuilt-themes/purple-green.css", 100 | "src/styles.scss" 101 | ], 102 | "scripts": [] 103 | } 104 | }, 105 | "e2e": { 106 | "builder": "@angular-devkit/build-angular:protractor", 107 | "options": { 108 | "protractorConfig": "e2e/protractor.conf.js", 109 | "devServerTarget": "firebase-course:serve" 110 | }, 111 | "configurations": { 112 | "production": { 113 | "devServerTarget": "firebase-course:serve:production" 114 | } 115 | } 116 | } 117 | } 118 | } 119 | }, 120 | "defaultProject": "firebase-course" 121 | } 122 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | import { AppPage } from './app.po'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', async () => { 12 | await page.navigateTo(); 13 | expect(await page.getTitleText()).toEqual('try-emulator app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | async navigateTo(): Promise { 5 | return browser.get(browser.baseUrl); 6 | } 7 | 8 | async getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": { 7 | "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build" 8 | }, 9 | "hosting": { 10 | "public": "dist", 11 | "ignore": [ 12 | "firebase.json", 13 | "**/.*", 14 | "**/node_modules/**" 15 | ], 16 | "rewrites": [ 17 | { 18 | "source": "**", 19 | "destination": "/index.html" 20 | } 21 | ] 22 | }, 23 | "storage": { 24 | "rules": "storage.rules" 25 | }, 26 | "emulators": { 27 | "auth": { 28 | "port": 9099 29 | }, 30 | "functions": { 31 | "port": 5001 32 | }, 33 | "firestore": { 34 | "port": 8080 35 | }, 36 | "hosting": { 37 | "port": 5000 38 | }, 39 | "ui": { 40 | "enabled": true 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "courses", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "categories", 9 | "arrayConfig": "CONTAINS" 10 | }, 11 | { 12 | "fieldPath": "seqNo", 13 | "order": "ASCENDING" 14 | } 15 | ] 16 | }, 17 | { 18 | "collectionGroup": "courses", 19 | "queryScope": "COLLECTION", 20 | "fields": [ 21 | { 22 | "fieldPath": "url", 23 | "order": "ASCENDING" 24 | }, 25 | { 26 | "fieldPath": "seqNo", 27 | "order": "ASCENDING" 28 | } 29 | ] 30 | } 31 | ], 32 | "fieldOverrides": [ 33 | { 34 | "collectionGroup": "lessons", 35 | "fieldPath": "seqNo", 36 | "indexes": [ 37 | { 38 | "order": "ASCENDING", 39 | "queryScope": "COLLECTION" 40 | }, 41 | { 42 | "order": "DESCENDING", 43 | "queryScope": "COLLECTION" 44 | }, 45 | { 46 | "arrayConfig": "CONTAINS", 47 | "queryScope": "COLLECTION" 48 | }, 49 | { 50 | "order": "ASCENDING", 51 | "queryScope": "COLLECTION_GROUP" 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | service cloud.firestore { 4 | 5 | match /databases/{database}/documents { 6 | 7 | function isAdmin() { 8 | return isAuthenticated() && 'admin' in request.auth.token && 9 | request.auth.token.admin == true; 10 | } 11 | 12 | function isAuthenticated() { 13 | return request.auth.uid != null; 14 | } 15 | 16 | function isKnownUser() { 17 | return isAuthenticated() && 18 | exists(/databases/$(database)/documents/users/$(request.auth.uid)); 19 | } 20 | 21 | function isNonEmptyString(fieldName) { 22 | return request.resource.data[fieldName] is string && 23 | request.resource.data[fieldName].size() > 0; 24 | } 25 | 26 | function isValidCourse() { 27 | return request.resource.data.seqNo is number && 28 | request.resource.data.seqNo >= 0 && 29 | isNonEmptyString("url"); 30 | } 31 | 32 | match /courses/{courseId} { 33 | 34 | allow read: if isKnownUser(); 35 | 36 | allow create, update: if isValidCourse() && isAdmin(); 37 | allow delete: if isAdmin(); 38 | 39 | match /lessons/{lessonId} { 40 | allow read: if isKnownUser(); 41 | } 42 | 43 | } 44 | 45 | match /{path=**}/lessons/{lessonId} { 46 | allow read: if isKnownUser(); 47 | } 48 | 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled JavaScript files 2 | lib/**/*.js 3 | lib/**/*.js.map 4 | 5 | # TypeScript v1 declaration files 6 | typings/ 7 | 8 | # Node.js dependency directory 9 | node_modules/ 10 | -------------------------------------------------------------------------------- /functions/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "requires": true, 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "@firebase/app-types": { 7 | "version": "0.6.1", 8 | "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", 9 | "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" 10 | }, 11 | "@firebase/auth-interop-types": { 12 | "version": "0.1.5", 13 | "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", 14 | "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==" 15 | }, 16 | "@firebase/component": { 17 | "version": "0.1.21", 18 | "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.21.tgz", 19 | "integrity": "sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg==", 20 | "requires": { 21 | "@firebase/util": "0.3.4", 22 | "tslib": "^1.11.1" 23 | } 24 | }, 25 | "@firebase/database": { 26 | "version": "0.8.3", 27 | "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.8.3.tgz", 28 | "integrity": "sha512-i29rr3kcPltIkA8La9M1lgsSxx9bfu5lCQ0T+tbJptZ3UpqpcL1NzCcZa24cJjiLgq3HQNPyLvUvCtcPSFDlRg==", 29 | "requires": { 30 | "@firebase/auth-interop-types": "0.1.5", 31 | "@firebase/component": "0.1.21", 32 | "@firebase/database-types": "0.6.1", 33 | "@firebase/logger": "0.2.6", 34 | "@firebase/util": "0.3.4", 35 | "faye-websocket": "0.11.3", 36 | "tslib": "^1.11.1" 37 | } 38 | }, 39 | "@firebase/database-types": { 40 | "version": "0.6.1", 41 | "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.6.1.tgz", 42 | "integrity": "sha512-JtL3FUbWG+bM59iYuphfx9WOu2Mzf0OZNaqWiQ7lJR8wBe7bS9rIm9jlBFtksB7xcya1lZSQPA/GAy2jIlMIkA==", 43 | "requires": { 44 | "@firebase/app-types": "0.6.1" 45 | } 46 | }, 47 | "@firebase/logger": { 48 | "version": "0.2.6", 49 | "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", 50 | "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" 51 | }, 52 | "@firebase/util": { 53 | "version": "0.3.4", 54 | "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz", 55 | "integrity": "sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ==", 56 | "requires": { 57 | "tslib": "^1.11.1" 58 | } 59 | }, 60 | "@google-cloud/common": { 61 | "version": "3.6.0", 62 | "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.6.0.tgz", 63 | "integrity": "sha512-aHIFTqJZmeTNO9md8XxV+ywuvXF3xBm5WNmgWeeCK+XN5X+kGW0WEX94wGwj+/MdOnrVf4dL2RvSIt9J5yJG6Q==", 64 | "optional": true, 65 | "requires": { 66 | "@google-cloud/projectify": "^2.0.0", 67 | "@google-cloud/promisify": "^2.0.0", 68 | "arrify": "^2.0.1", 69 | "duplexify": "^4.1.1", 70 | "ent": "^2.2.0", 71 | "extend": "^3.0.2", 72 | "google-auth-library": "^7.0.2", 73 | "retry-request": "^4.1.1", 74 | "teeny-request": "^7.0.0" 75 | } 76 | }, 77 | "@google-cloud/firestore": { 78 | "version": "4.9.8", 79 | "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.9.8.tgz", 80 | "integrity": "sha512-M3Wc4m/5gvWFHtZpu+tDNUp90CyVhNi0P5Sfhbt1WGUORq+/NmqqtahojsPaQ7VqLMbEiFRgzbinLL7Zm23fGg==", 81 | "optional": true, 82 | "requires": { 83 | "fast-deep-equal": "^3.1.1", 84 | "functional-red-black-tree": "^1.0.1", 85 | "google-gax": "^2.9.2", 86 | "protobufjs": "^6.8.6" 87 | } 88 | }, 89 | "@google-cloud/paginator": { 90 | "version": "3.0.5", 91 | "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz", 92 | "integrity": "sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw==", 93 | "optional": true, 94 | "requires": { 95 | "arrify": "^2.0.0", 96 | "extend": "^3.0.2" 97 | } 98 | }, 99 | "@google-cloud/projectify": { 100 | "version": "2.0.1", 101 | "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", 102 | "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", 103 | "optional": true 104 | }, 105 | "@google-cloud/promisify": { 106 | "version": "2.0.3", 107 | "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", 108 | "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", 109 | "optional": true 110 | }, 111 | "@google-cloud/storage": { 112 | "version": "5.8.2", 113 | "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.8.2.tgz", 114 | "integrity": "sha512-R4MOLHhIbsQUqfQufV9QmYfxPE3TDJD+nwVOoN8mOKOx+XoVRm1ZoXaN5vwUMCBCHsDsgpWu7y9d6YvA+POXrg==", 115 | "optional": true, 116 | "requires": { 117 | "@google-cloud/common": "^3.6.0", 118 | "@google-cloud/paginator": "^3.0.0", 119 | "@google-cloud/promisify": "^2.0.0", 120 | "arrify": "^2.0.0", 121 | "async-retry": "^1.3.1", 122 | "compressible": "^2.0.12", 123 | "date-and-time": "^0.14.2", 124 | "duplexify": "^4.0.0", 125 | "extend": "^3.0.2", 126 | "gaxios": "^4.0.0", 127 | "gcs-resumable-upload": "^3.1.3", 128 | "get-stream": "^6.0.0", 129 | "hash-stream-validation": "^0.2.2", 130 | "mime": "^2.2.0", 131 | "mime-types": "^2.0.8", 132 | "onetime": "^5.1.0", 133 | "p-limit": "^3.0.1", 134 | "pumpify": "^2.0.0", 135 | "snakeize": "^0.1.0", 136 | "stream-events": "^1.0.1", 137 | "xdg-basedir": "^4.0.0" 138 | } 139 | }, 140 | "@grpc/grpc-js": { 141 | "version": "1.2.12", 142 | "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.2.12.tgz", 143 | "integrity": "sha512-+gPCklP1eqIgrNPyzddYQdt9+GvZqPlLpIjIo+TveE+gbtp74VV1A2ju8ExeO8ma8f7MbpaGZx/KJPYVWL9eDw==", 144 | "optional": true, 145 | "requires": { 146 | "@types/node": ">=12.12.47", 147 | "google-auth-library": "^6.1.1", 148 | "semver": "^6.2.0" 149 | }, 150 | "dependencies": { 151 | "@types/node": { 152 | "version": "14.14.36", 153 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.36.tgz", 154 | "integrity": "sha512-kjivUwDJfIjngzbhooRnOLhGYz6oRFi+L+EpMjxroDYXwDw9lHrJJ43E+dJ6KAd3V3WxWAJ/qZE9XKYHhjPOFQ==", 155 | "optional": true 156 | }, 157 | "google-auth-library": { 158 | "version": "6.1.6", 159 | "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.6.tgz", 160 | "integrity": "sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==", 161 | "optional": true, 162 | "requires": { 163 | "arrify": "^2.0.0", 164 | "base64-js": "^1.3.0", 165 | "ecdsa-sig-formatter": "^1.0.11", 166 | "fast-text-encoding": "^1.0.0", 167 | "gaxios": "^4.0.0", 168 | "gcp-metadata": "^4.2.0", 169 | "gtoken": "^5.0.4", 170 | "jws": "^4.0.0", 171 | "lru-cache": "^6.0.0" 172 | } 173 | } 174 | } 175 | }, 176 | "@grpc/proto-loader": { 177 | "version": "0.5.6", 178 | "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.6.tgz", 179 | "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", 180 | "optional": true, 181 | "requires": { 182 | "lodash.camelcase": "^4.3.0", 183 | "protobufjs": "^6.8.6" 184 | } 185 | }, 186 | "@protobufjs/aspromise": { 187 | "version": "1.1.2", 188 | "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 189 | "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", 190 | "optional": true 191 | }, 192 | "@protobufjs/base64": { 193 | "version": "1.1.2", 194 | "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 195 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", 196 | "optional": true 197 | }, 198 | "@protobufjs/codegen": { 199 | "version": "2.0.4", 200 | "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 201 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", 202 | "optional": true 203 | }, 204 | "@protobufjs/eventemitter": { 205 | "version": "1.1.0", 206 | "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 207 | "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", 208 | "optional": true 209 | }, 210 | "@protobufjs/fetch": { 211 | "version": "1.1.0", 212 | "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 213 | "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", 214 | "optional": true, 215 | "requires": { 216 | "@protobufjs/aspromise": "^1.1.1", 217 | "@protobufjs/inquire": "^1.1.0" 218 | } 219 | }, 220 | "@protobufjs/float": { 221 | "version": "1.0.2", 222 | "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 223 | "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", 224 | "optional": true 225 | }, 226 | "@protobufjs/inquire": { 227 | "version": "1.1.0", 228 | "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 229 | "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", 230 | "optional": true 231 | }, 232 | "@protobufjs/path": { 233 | "version": "1.1.2", 234 | "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 235 | "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", 236 | "optional": true 237 | }, 238 | "@protobufjs/pool": { 239 | "version": "1.1.0", 240 | "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 241 | "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", 242 | "optional": true 243 | }, 244 | "@protobufjs/utf8": { 245 | "version": "1.1.0", 246 | "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 247 | "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", 248 | "optional": true 249 | }, 250 | "@tootallnate/once": { 251 | "version": "1.1.2", 252 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", 253 | "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", 254 | "optional": true 255 | }, 256 | "@types/body-parser": { 257 | "version": "1.19.0", 258 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", 259 | "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", 260 | "requires": { 261 | "@types/connect": "*", 262 | "@types/node": "*" 263 | } 264 | }, 265 | "@types/connect": { 266 | "version": "3.4.34", 267 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", 268 | "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", 269 | "requires": { 270 | "@types/node": "*" 271 | } 272 | }, 273 | "@types/express": { 274 | "version": "4.17.3", 275 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", 276 | "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", 277 | "requires": { 278 | "@types/body-parser": "*", 279 | "@types/express-serve-static-core": "*", 280 | "@types/serve-static": "*" 281 | } 282 | }, 283 | "@types/express-serve-static-core": { 284 | "version": "4.17.19", 285 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz", 286 | "integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==", 287 | "requires": { 288 | "@types/node": "*", 289 | "@types/qs": "*", 290 | "@types/range-parser": "*" 291 | } 292 | }, 293 | "@types/lodash": { 294 | "version": "4.14.168", 295 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", 296 | "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", 297 | "dev": true 298 | }, 299 | "@types/long": { 300 | "version": "4.0.1", 301 | "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", 302 | "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", 303 | "optional": true 304 | }, 305 | "@types/mime": { 306 | "version": "1.3.2", 307 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", 308 | "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" 309 | }, 310 | "@types/node": { 311 | "version": "10.17.55", 312 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.55.tgz", 313 | "integrity": "sha512-koZJ89uLZufDvToeWO5BrC4CR4OUfHnUz2qoPs/daQH6qq3IN62QFxCTZ+bKaCE0xaoCAJYE4AXre8AbghCrhg==" 314 | }, 315 | "@types/qs": { 316 | "version": "6.9.6", 317 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", 318 | "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==" 319 | }, 320 | "@types/range-parser": { 321 | "version": "1.2.3", 322 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", 323 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" 324 | }, 325 | "@types/serve-static": { 326 | "version": "1.13.9", 327 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", 328 | "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", 329 | "requires": { 330 | "@types/mime": "^1", 331 | "@types/node": "*" 332 | } 333 | }, 334 | "abort-controller": { 335 | "version": "3.0.0", 336 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 337 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 338 | "optional": true, 339 | "requires": { 340 | "event-target-shim": "^5.0.0" 341 | } 342 | }, 343 | "accepts": { 344 | "version": "1.3.7", 345 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 346 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 347 | "requires": { 348 | "mime-types": "~2.1.24", 349 | "negotiator": "0.6.2" 350 | } 351 | }, 352 | "agent-base": { 353 | "version": "6.0.2", 354 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 355 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 356 | "optional": true, 357 | "requires": { 358 | "debug": "4" 359 | } 360 | }, 361 | "array-flatten": { 362 | "version": "1.1.1", 363 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 364 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 365 | }, 366 | "arrify": { 367 | "version": "2.0.1", 368 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", 369 | "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", 370 | "optional": true 371 | }, 372 | "async-retry": { 373 | "version": "1.3.1", 374 | "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", 375 | "integrity": "sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA==", 376 | "optional": true, 377 | "requires": { 378 | "retry": "0.12.0" 379 | } 380 | }, 381 | "base64-js": { 382 | "version": "1.5.1", 383 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 384 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 385 | "optional": true 386 | }, 387 | "bignumber.js": { 388 | "version": "9.0.1", 389 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", 390 | "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==", 391 | "optional": true 392 | }, 393 | "body-parser": { 394 | "version": "1.19.0", 395 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 396 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 397 | "requires": { 398 | "bytes": "3.1.0", 399 | "content-type": "~1.0.4", 400 | "debug": "2.6.9", 401 | "depd": "~1.1.2", 402 | "http-errors": "1.7.2", 403 | "iconv-lite": "0.4.24", 404 | "on-finished": "~2.3.0", 405 | "qs": "6.7.0", 406 | "raw-body": "2.4.0", 407 | "type-is": "~1.6.17" 408 | }, 409 | "dependencies": { 410 | "debug": { 411 | "version": "2.6.9", 412 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 413 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 414 | "requires": { 415 | "ms": "2.0.0" 416 | } 417 | }, 418 | "ms": { 419 | "version": "2.0.0", 420 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 421 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 422 | } 423 | } 424 | }, 425 | "buffer-equal-constant-time": { 426 | "version": "1.0.1", 427 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 428 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 429 | }, 430 | "bytes": { 431 | "version": "3.1.0", 432 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 433 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 434 | }, 435 | "compressible": { 436 | "version": "2.0.18", 437 | "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", 438 | "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", 439 | "optional": true, 440 | "requires": { 441 | "mime-db": ">= 1.43.0 < 2" 442 | } 443 | }, 444 | "configstore": { 445 | "version": "5.0.1", 446 | "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", 447 | "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", 448 | "optional": true, 449 | "requires": { 450 | "dot-prop": "^5.2.0", 451 | "graceful-fs": "^4.1.2", 452 | "make-dir": "^3.0.0", 453 | "unique-string": "^2.0.0", 454 | "write-file-atomic": "^3.0.0", 455 | "xdg-basedir": "^4.0.0" 456 | } 457 | }, 458 | "content-disposition": { 459 | "version": "0.5.3", 460 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 461 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 462 | "requires": { 463 | "safe-buffer": "5.1.2" 464 | }, 465 | "dependencies": { 466 | "safe-buffer": { 467 | "version": "5.1.2", 468 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 469 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 470 | } 471 | } 472 | }, 473 | "content-type": { 474 | "version": "1.0.4", 475 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 476 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 477 | }, 478 | "cookie": { 479 | "version": "0.4.0", 480 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 481 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 482 | }, 483 | "cookie-signature": { 484 | "version": "1.0.6", 485 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 486 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 487 | }, 488 | "cors": { 489 | "version": "2.8.5", 490 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 491 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 492 | "requires": { 493 | "object-assign": "^4", 494 | "vary": "^1" 495 | } 496 | }, 497 | "crypto-random-string": { 498 | "version": "2.0.0", 499 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", 500 | "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", 501 | "optional": true 502 | }, 503 | "date-and-time": { 504 | "version": "0.14.2", 505 | "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.2.tgz", 506 | "integrity": "sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==", 507 | "optional": true 508 | }, 509 | "debug": { 510 | "version": "4.3.1", 511 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 512 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 513 | "optional": true, 514 | "requires": { 515 | "ms": "2.1.2" 516 | } 517 | }, 518 | "depd": { 519 | "version": "1.1.2", 520 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 521 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 522 | }, 523 | "destroy": { 524 | "version": "1.0.4", 525 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 526 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 527 | }, 528 | "dicer": { 529 | "version": "0.3.0", 530 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", 531 | "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", 532 | "requires": { 533 | "streamsearch": "0.1.2" 534 | } 535 | }, 536 | "dot-prop": { 537 | "version": "5.3.0", 538 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", 539 | "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", 540 | "optional": true, 541 | "requires": { 542 | "is-obj": "^2.0.0" 543 | } 544 | }, 545 | "duplexify": { 546 | "version": "4.1.1", 547 | "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", 548 | "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", 549 | "optional": true, 550 | "requires": { 551 | "end-of-stream": "^1.4.1", 552 | "inherits": "^2.0.3", 553 | "readable-stream": "^3.1.1", 554 | "stream-shift": "^1.0.0" 555 | } 556 | }, 557 | "ecdsa-sig-formatter": { 558 | "version": "1.0.11", 559 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 560 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 561 | "requires": { 562 | "safe-buffer": "^5.0.1" 563 | } 564 | }, 565 | "ee-first": { 566 | "version": "1.1.1", 567 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 568 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 569 | }, 570 | "encodeurl": { 571 | "version": "1.0.2", 572 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 573 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 574 | }, 575 | "end-of-stream": { 576 | "version": "1.4.4", 577 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 578 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 579 | "optional": true, 580 | "requires": { 581 | "once": "^1.4.0" 582 | } 583 | }, 584 | "ent": { 585 | "version": "2.2.0", 586 | "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", 587 | "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", 588 | "optional": true 589 | }, 590 | "escape-html": { 591 | "version": "1.0.3", 592 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 593 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 594 | }, 595 | "etag": { 596 | "version": "1.8.1", 597 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 598 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 599 | }, 600 | "event-target-shim": { 601 | "version": "5.0.1", 602 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 603 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 604 | "optional": true 605 | }, 606 | "express": { 607 | "version": "4.17.1", 608 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 609 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 610 | "requires": { 611 | "accepts": "~1.3.7", 612 | "array-flatten": "1.1.1", 613 | "body-parser": "1.19.0", 614 | "content-disposition": "0.5.3", 615 | "content-type": "~1.0.4", 616 | "cookie": "0.4.0", 617 | "cookie-signature": "1.0.6", 618 | "debug": "2.6.9", 619 | "depd": "~1.1.2", 620 | "encodeurl": "~1.0.2", 621 | "escape-html": "~1.0.3", 622 | "etag": "~1.8.1", 623 | "finalhandler": "~1.1.2", 624 | "fresh": "0.5.2", 625 | "merge-descriptors": "1.0.1", 626 | "methods": "~1.1.2", 627 | "on-finished": "~2.3.0", 628 | "parseurl": "~1.3.3", 629 | "path-to-regexp": "0.1.7", 630 | "proxy-addr": "~2.0.5", 631 | "qs": "6.7.0", 632 | "range-parser": "~1.2.1", 633 | "safe-buffer": "5.1.2", 634 | "send": "0.17.1", 635 | "serve-static": "1.14.1", 636 | "setprototypeof": "1.1.1", 637 | "statuses": "~1.5.0", 638 | "type-is": "~1.6.18", 639 | "utils-merge": "1.0.1", 640 | "vary": "~1.1.2" 641 | }, 642 | "dependencies": { 643 | "debug": { 644 | "version": "2.6.9", 645 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 646 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 647 | "requires": { 648 | "ms": "2.0.0" 649 | } 650 | }, 651 | "ms": { 652 | "version": "2.0.0", 653 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 654 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 655 | }, 656 | "safe-buffer": { 657 | "version": "5.1.2", 658 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 659 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 660 | } 661 | } 662 | }, 663 | "extend": { 664 | "version": "3.0.2", 665 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 666 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 667 | "optional": true 668 | }, 669 | "fast-deep-equal": { 670 | "version": "3.1.3", 671 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 672 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 673 | "optional": true 674 | }, 675 | "fast-text-encoding": { 676 | "version": "1.0.3", 677 | "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", 678 | "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", 679 | "optional": true 680 | }, 681 | "faye-websocket": { 682 | "version": "0.11.3", 683 | "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", 684 | "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", 685 | "requires": { 686 | "websocket-driver": ">=0.5.1" 687 | } 688 | }, 689 | "finalhandler": { 690 | "version": "1.1.2", 691 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 692 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 693 | "requires": { 694 | "debug": "2.6.9", 695 | "encodeurl": "~1.0.2", 696 | "escape-html": "~1.0.3", 697 | "on-finished": "~2.3.0", 698 | "parseurl": "~1.3.3", 699 | "statuses": "~1.5.0", 700 | "unpipe": "~1.0.0" 701 | }, 702 | "dependencies": { 703 | "debug": { 704 | "version": "2.6.9", 705 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 706 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 707 | "requires": { 708 | "ms": "2.0.0" 709 | } 710 | }, 711 | "ms": { 712 | "version": "2.0.0", 713 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 714 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 715 | } 716 | } 717 | }, 718 | "firebase-admin": { 719 | "version": "9.5.0", 720 | "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.5.0.tgz", 721 | "integrity": "sha512-OPXFOTDcAE+NORpfhq7YMEDk+vFClBtjfpkrjm2JHRxb8DpMm+K3AcusonFPU/WOH4FhiVN9JHB0+NPE20S3gQ==", 722 | "requires": { 723 | "@firebase/database": "^0.8.1", 724 | "@firebase/database-types": "^0.6.1", 725 | "@google-cloud/firestore": "^4.5.0", 726 | "@google-cloud/storage": "^5.3.0", 727 | "@types/node": "^10.10.0", 728 | "dicer": "^0.3.0", 729 | "jsonwebtoken": "^8.5.1", 730 | "node-forge": "^0.10.0" 731 | } 732 | }, 733 | "firebase-functions": { 734 | "version": "3.13.2", 735 | "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.13.2.tgz", 736 | "integrity": "sha512-XHgAQZqA62awr4l9mNlJv6qnv5MkMkLuo+hafdW0T7IJj1PgrZtuIo5x+ib2npAcB0XhX5Sg0QR1hMYPAlfbaA==", 737 | "requires": { 738 | "@types/express": "4.17.3", 739 | "cors": "^2.8.5", 740 | "express": "^4.17.1", 741 | "lodash": "^4.17.14" 742 | } 743 | }, 744 | "firebase-functions-test": { 745 | "version": "0.2.3", 746 | "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-0.2.3.tgz", 747 | "integrity": "sha512-zYX0QTm53wCazuej7O0xqbHl90r/v1PTXt/hwa0jo1YF8nDM+iBKnLDlkIoW66MDd0R6aGg4BvKzTTdJpvigUA==", 748 | "dev": true, 749 | "requires": { 750 | "@types/lodash": "^4.14.104", 751 | "lodash": "^4.17.5" 752 | } 753 | }, 754 | "forwarded": { 755 | "version": "0.1.2", 756 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 757 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 758 | }, 759 | "fresh": { 760 | "version": "0.5.2", 761 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 762 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 763 | }, 764 | "functional-red-black-tree": { 765 | "version": "1.0.1", 766 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 767 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 768 | "optional": true 769 | }, 770 | "gaxios": { 771 | "version": "4.2.0", 772 | "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.2.0.tgz", 773 | "integrity": "sha512-Ms7fNifGv0XVU+6eIyL9LB7RVESeML9+cMvkwGS70xyD6w2Z80wl6RiqiJ9k1KFlJCUTQqFFc8tXmPQfSKUe8g==", 774 | "optional": true, 775 | "requires": { 776 | "abort-controller": "^3.0.0", 777 | "extend": "^3.0.2", 778 | "https-proxy-agent": "^5.0.0", 779 | "is-stream": "^2.0.0", 780 | "node-fetch": "^2.3.0" 781 | } 782 | }, 783 | "gcp-metadata": { 784 | "version": "4.2.1", 785 | "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", 786 | "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", 787 | "optional": true, 788 | "requires": { 789 | "gaxios": "^4.0.0", 790 | "json-bigint": "^1.0.0" 791 | } 792 | }, 793 | "gcs-resumable-upload": { 794 | "version": "3.1.3", 795 | "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.3.tgz", 796 | "integrity": "sha512-LjVrv6YVH0XqBr/iBW0JgRA1ndxhK6zfEFFJR4im51QVTj/4sInOXimY2evDZuSZ75D3bHxTaQAdXRukMc1y+w==", 797 | "optional": true, 798 | "requires": { 799 | "abort-controller": "^3.0.0", 800 | "configstore": "^5.0.0", 801 | "extend": "^3.0.2", 802 | "gaxios": "^4.0.0", 803 | "google-auth-library": "^7.0.0", 804 | "pumpify": "^2.0.0", 805 | "stream-events": "^1.0.4" 806 | } 807 | }, 808 | "get-stream": { 809 | "version": "6.0.0", 810 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", 811 | "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", 812 | "optional": true 813 | }, 814 | "google-auth-library": { 815 | "version": "7.0.3", 816 | "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.0.3.tgz", 817 | "integrity": "sha512-6wJNYqY1QUr5I2lWaUkkzOT2b9OCNhNQrdFOt/bsBbGb7T7NCdEvrBsXraUm+KTUGk2xGlQ7m9RgUd4Llcw8NQ==", 818 | "optional": true, 819 | "requires": { 820 | "arrify": "^2.0.0", 821 | "base64-js": "^1.3.0", 822 | "ecdsa-sig-formatter": "^1.0.11", 823 | "fast-text-encoding": "^1.0.0", 824 | "gaxios": "^4.0.0", 825 | "gcp-metadata": "^4.2.0", 826 | "gtoken": "^5.0.4", 827 | "jws": "^4.0.0", 828 | "lru-cache": "^6.0.0" 829 | } 830 | }, 831 | "google-gax": { 832 | "version": "2.11.2", 833 | "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.11.2.tgz", 834 | "integrity": "sha512-PNqXv7Oi5XBMgoMWVxLZHUidfMv7cPHrDSDXqLyEd6kY6pqFnVKC8jt2T1df4JPSc2+VLPdeo6L7X9mbdQG8Xw==", 835 | "optional": true, 836 | "requires": { 837 | "@grpc/grpc-js": "~1.2.0", 838 | "@grpc/proto-loader": "^0.5.1", 839 | "@types/long": "^4.0.0", 840 | "abort-controller": "^3.0.0", 841 | "duplexify": "^4.0.0", 842 | "fast-text-encoding": "^1.0.3", 843 | "google-auth-library": "^7.0.2", 844 | "is-stream-ended": "^0.1.4", 845 | "node-fetch": "^2.6.1", 846 | "protobufjs": "^6.10.2", 847 | "retry-request": "^4.0.0" 848 | } 849 | }, 850 | "google-p12-pem": { 851 | "version": "3.0.3", 852 | "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", 853 | "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", 854 | "optional": true, 855 | "requires": { 856 | "node-forge": "^0.10.0" 857 | } 858 | }, 859 | "graceful-fs": { 860 | "version": "4.2.6", 861 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", 862 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", 863 | "optional": true 864 | }, 865 | "gtoken": { 866 | "version": "5.2.1", 867 | "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", 868 | "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", 869 | "optional": true, 870 | "requires": { 871 | "gaxios": "^4.0.0", 872 | "google-p12-pem": "^3.0.3", 873 | "jws": "^4.0.0" 874 | } 875 | }, 876 | "hash-stream-validation": { 877 | "version": "0.2.4", 878 | "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", 879 | "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", 880 | "optional": true 881 | }, 882 | "http-errors": { 883 | "version": "1.7.2", 884 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 885 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 886 | "requires": { 887 | "depd": "~1.1.2", 888 | "inherits": "2.0.3", 889 | "setprototypeof": "1.1.1", 890 | "statuses": ">= 1.5.0 < 2", 891 | "toidentifier": "1.0.0" 892 | }, 893 | "dependencies": { 894 | "inherits": { 895 | "version": "2.0.3", 896 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 897 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 898 | } 899 | } 900 | }, 901 | "http-parser-js": { 902 | "version": "0.5.3", 903 | "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", 904 | "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==" 905 | }, 906 | "http-proxy-agent": { 907 | "version": "4.0.1", 908 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", 909 | "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", 910 | "optional": true, 911 | "requires": { 912 | "@tootallnate/once": "1", 913 | "agent-base": "6", 914 | "debug": "4" 915 | } 916 | }, 917 | "https-proxy-agent": { 918 | "version": "5.0.0", 919 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", 920 | "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", 921 | "optional": true, 922 | "requires": { 923 | "agent-base": "6", 924 | "debug": "4" 925 | } 926 | }, 927 | "iconv-lite": { 928 | "version": "0.4.24", 929 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 930 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 931 | "requires": { 932 | "safer-buffer": ">= 2.1.2 < 3" 933 | } 934 | }, 935 | "imurmurhash": { 936 | "version": "0.1.4", 937 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 938 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 939 | "optional": true 940 | }, 941 | "inherits": { 942 | "version": "2.0.4", 943 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 944 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 945 | "optional": true 946 | }, 947 | "ipaddr.js": { 948 | "version": "1.9.1", 949 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 950 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 951 | }, 952 | "is-obj": { 953 | "version": "2.0.0", 954 | "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", 955 | "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", 956 | "optional": true 957 | }, 958 | "is-stream": { 959 | "version": "2.0.0", 960 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 961 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", 962 | "optional": true 963 | }, 964 | "is-stream-ended": { 965 | "version": "0.1.4", 966 | "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", 967 | "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", 968 | "optional": true 969 | }, 970 | "is-typedarray": { 971 | "version": "1.0.0", 972 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 973 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 974 | "optional": true 975 | }, 976 | "json-bigint": { 977 | "version": "1.0.0", 978 | "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", 979 | "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", 980 | "optional": true, 981 | "requires": { 982 | "bignumber.js": "^9.0.0" 983 | } 984 | }, 985 | "jsonwebtoken": { 986 | "version": "8.5.1", 987 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 988 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 989 | "requires": { 990 | "jws": "^3.2.2", 991 | "lodash.includes": "^4.3.0", 992 | "lodash.isboolean": "^3.0.3", 993 | "lodash.isinteger": "^4.0.4", 994 | "lodash.isnumber": "^3.0.3", 995 | "lodash.isplainobject": "^4.0.6", 996 | "lodash.isstring": "^4.0.1", 997 | "lodash.once": "^4.0.0", 998 | "ms": "^2.1.1", 999 | "semver": "^5.6.0" 1000 | }, 1001 | "dependencies": { 1002 | "jwa": { 1003 | "version": "1.4.1", 1004 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 1005 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 1006 | "requires": { 1007 | "buffer-equal-constant-time": "1.0.1", 1008 | "ecdsa-sig-formatter": "1.0.11", 1009 | "safe-buffer": "^5.0.1" 1010 | } 1011 | }, 1012 | "jws": { 1013 | "version": "3.2.2", 1014 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 1015 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 1016 | "requires": { 1017 | "jwa": "^1.4.1", 1018 | "safe-buffer": "^5.0.1" 1019 | } 1020 | }, 1021 | "semver": { 1022 | "version": "5.7.1", 1023 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1024 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1025 | } 1026 | } 1027 | }, 1028 | "jwa": { 1029 | "version": "2.0.0", 1030 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", 1031 | "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", 1032 | "optional": true, 1033 | "requires": { 1034 | "buffer-equal-constant-time": "1.0.1", 1035 | "ecdsa-sig-formatter": "1.0.11", 1036 | "safe-buffer": "^5.0.1" 1037 | } 1038 | }, 1039 | "jws": { 1040 | "version": "4.0.0", 1041 | "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", 1042 | "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", 1043 | "optional": true, 1044 | "requires": { 1045 | "jwa": "^2.0.0", 1046 | "safe-buffer": "^5.0.1" 1047 | } 1048 | }, 1049 | "lodash": { 1050 | "version": "4.17.21", 1051 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 1052 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 1053 | }, 1054 | "lodash.camelcase": { 1055 | "version": "4.3.0", 1056 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 1057 | "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", 1058 | "optional": true 1059 | }, 1060 | "lodash.includes": { 1061 | "version": "4.3.0", 1062 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 1063 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 1064 | }, 1065 | "lodash.isboolean": { 1066 | "version": "3.0.3", 1067 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 1068 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 1069 | }, 1070 | "lodash.isinteger": { 1071 | "version": "4.0.4", 1072 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 1073 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 1074 | }, 1075 | "lodash.isnumber": { 1076 | "version": "3.0.3", 1077 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 1078 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 1079 | }, 1080 | "lodash.isplainobject": { 1081 | "version": "4.0.6", 1082 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 1083 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 1084 | }, 1085 | "lodash.isstring": { 1086 | "version": "4.0.1", 1087 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 1088 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 1089 | }, 1090 | "lodash.once": { 1091 | "version": "4.1.1", 1092 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 1093 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 1094 | }, 1095 | "long": { 1096 | "version": "4.0.0", 1097 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 1098 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", 1099 | "optional": true 1100 | }, 1101 | "lru-cache": { 1102 | "version": "6.0.0", 1103 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 1104 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 1105 | "optional": true, 1106 | "requires": { 1107 | "yallist": "^4.0.0" 1108 | } 1109 | }, 1110 | "make-dir": { 1111 | "version": "3.1.0", 1112 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 1113 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 1114 | "optional": true, 1115 | "requires": { 1116 | "semver": "^6.0.0" 1117 | } 1118 | }, 1119 | "media-typer": { 1120 | "version": "0.3.0", 1121 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1122 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 1123 | }, 1124 | "merge-descriptors": { 1125 | "version": "1.0.1", 1126 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 1127 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 1128 | }, 1129 | "methods": { 1130 | "version": "1.1.2", 1131 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1132 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 1133 | }, 1134 | "mime": { 1135 | "version": "2.5.2", 1136 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", 1137 | "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", 1138 | "optional": true 1139 | }, 1140 | "mime-db": { 1141 | "version": "1.46.0", 1142 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", 1143 | "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==" 1144 | }, 1145 | "mime-types": { 1146 | "version": "2.1.29", 1147 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", 1148 | "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", 1149 | "requires": { 1150 | "mime-db": "1.46.0" 1151 | } 1152 | }, 1153 | "mimic-fn": { 1154 | "version": "2.1.0", 1155 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 1156 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 1157 | "optional": true 1158 | }, 1159 | "ms": { 1160 | "version": "2.1.2", 1161 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1162 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1163 | }, 1164 | "negotiator": { 1165 | "version": "0.6.2", 1166 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 1167 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 1168 | }, 1169 | "node-fetch": { 1170 | "version": "2.6.1", 1171 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 1172 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", 1173 | "optional": true 1174 | }, 1175 | "node-forge": { 1176 | "version": "0.10.0", 1177 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", 1178 | "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" 1179 | }, 1180 | "object-assign": { 1181 | "version": "4.1.1", 1182 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1183 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 1184 | }, 1185 | "on-finished": { 1186 | "version": "2.3.0", 1187 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1188 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 1189 | "requires": { 1190 | "ee-first": "1.1.1" 1191 | } 1192 | }, 1193 | "once": { 1194 | "version": "1.4.0", 1195 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1196 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1197 | "optional": true, 1198 | "requires": { 1199 | "wrappy": "1" 1200 | } 1201 | }, 1202 | "onetime": { 1203 | "version": "5.1.2", 1204 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 1205 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 1206 | "optional": true, 1207 | "requires": { 1208 | "mimic-fn": "^2.1.0" 1209 | } 1210 | }, 1211 | "p-limit": { 1212 | "version": "3.1.0", 1213 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1214 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1215 | "optional": true, 1216 | "requires": { 1217 | "yocto-queue": "^0.1.0" 1218 | } 1219 | }, 1220 | "parseurl": { 1221 | "version": "1.3.3", 1222 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1223 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1224 | }, 1225 | "path-to-regexp": { 1226 | "version": "0.1.7", 1227 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1228 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 1229 | }, 1230 | "protobufjs": { 1231 | "version": "6.10.2", 1232 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", 1233 | "integrity": "sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==", 1234 | "optional": true, 1235 | "requires": { 1236 | "@protobufjs/aspromise": "^1.1.2", 1237 | "@protobufjs/base64": "^1.1.2", 1238 | "@protobufjs/codegen": "^2.0.4", 1239 | "@protobufjs/eventemitter": "^1.1.0", 1240 | "@protobufjs/fetch": "^1.1.0", 1241 | "@protobufjs/float": "^1.0.2", 1242 | "@protobufjs/inquire": "^1.1.0", 1243 | "@protobufjs/path": "^1.1.2", 1244 | "@protobufjs/pool": "^1.1.0", 1245 | "@protobufjs/utf8": "^1.1.0", 1246 | "@types/long": "^4.0.1", 1247 | "@types/node": "^13.7.0", 1248 | "long": "^4.0.0" 1249 | }, 1250 | "dependencies": { 1251 | "@types/node": { 1252 | "version": "13.13.47", 1253 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.47.tgz", 1254 | "integrity": "sha512-R6851wTjN1YJza8ZIeX6puNBSi/ZULHVh4WVleA7q256l+cP2EtXnKbO455fTs2ytQk3dL9qkU+Wh8l/uROdKg==", 1255 | "optional": true 1256 | } 1257 | } 1258 | }, 1259 | "proxy-addr": { 1260 | "version": "2.0.6", 1261 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", 1262 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", 1263 | "requires": { 1264 | "forwarded": "~0.1.2", 1265 | "ipaddr.js": "1.9.1" 1266 | } 1267 | }, 1268 | "pump": { 1269 | "version": "3.0.0", 1270 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 1271 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 1272 | "optional": true, 1273 | "requires": { 1274 | "end-of-stream": "^1.1.0", 1275 | "once": "^1.3.1" 1276 | } 1277 | }, 1278 | "pumpify": { 1279 | "version": "2.0.1", 1280 | "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", 1281 | "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", 1282 | "optional": true, 1283 | "requires": { 1284 | "duplexify": "^4.1.1", 1285 | "inherits": "^2.0.3", 1286 | "pump": "^3.0.0" 1287 | } 1288 | }, 1289 | "qs": { 1290 | "version": "6.7.0", 1291 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 1292 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 1293 | }, 1294 | "range-parser": { 1295 | "version": "1.2.1", 1296 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1297 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1298 | }, 1299 | "raw-body": { 1300 | "version": "2.4.0", 1301 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 1302 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 1303 | "requires": { 1304 | "bytes": "3.1.0", 1305 | "http-errors": "1.7.2", 1306 | "iconv-lite": "0.4.24", 1307 | "unpipe": "1.0.0" 1308 | } 1309 | }, 1310 | "readable-stream": { 1311 | "version": "3.6.0", 1312 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 1313 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 1314 | "optional": true, 1315 | "requires": { 1316 | "inherits": "^2.0.3", 1317 | "string_decoder": "^1.1.1", 1318 | "util-deprecate": "^1.0.1" 1319 | } 1320 | }, 1321 | "retry": { 1322 | "version": "0.12.0", 1323 | "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", 1324 | "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", 1325 | "optional": true 1326 | }, 1327 | "retry-request": { 1328 | "version": "4.1.3", 1329 | "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz", 1330 | "integrity": "sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ==", 1331 | "optional": true, 1332 | "requires": { 1333 | "debug": "^4.1.1" 1334 | } 1335 | }, 1336 | "safe-buffer": { 1337 | "version": "5.2.1", 1338 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1339 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1340 | }, 1341 | "safer-buffer": { 1342 | "version": "2.1.2", 1343 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1344 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1345 | }, 1346 | "semver": { 1347 | "version": "6.3.0", 1348 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1349 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", 1350 | "optional": true 1351 | }, 1352 | "send": { 1353 | "version": "0.17.1", 1354 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 1355 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 1356 | "requires": { 1357 | "debug": "2.6.9", 1358 | "depd": "~1.1.2", 1359 | "destroy": "~1.0.4", 1360 | "encodeurl": "~1.0.2", 1361 | "escape-html": "~1.0.3", 1362 | "etag": "~1.8.1", 1363 | "fresh": "0.5.2", 1364 | "http-errors": "~1.7.2", 1365 | "mime": "1.6.0", 1366 | "ms": "2.1.1", 1367 | "on-finished": "~2.3.0", 1368 | "range-parser": "~1.2.1", 1369 | "statuses": "~1.5.0" 1370 | }, 1371 | "dependencies": { 1372 | "debug": { 1373 | "version": "2.6.9", 1374 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1375 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1376 | "requires": { 1377 | "ms": "2.0.0" 1378 | }, 1379 | "dependencies": { 1380 | "ms": { 1381 | "version": "2.0.0", 1382 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1383 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1384 | } 1385 | } 1386 | }, 1387 | "mime": { 1388 | "version": "1.6.0", 1389 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1390 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 1391 | }, 1392 | "ms": { 1393 | "version": "2.1.1", 1394 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1395 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 1396 | } 1397 | } 1398 | }, 1399 | "serve-static": { 1400 | "version": "1.14.1", 1401 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 1402 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 1403 | "requires": { 1404 | "encodeurl": "~1.0.2", 1405 | "escape-html": "~1.0.3", 1406 | "parseurl": "~1.3.3", 1407 | "send": "0.17.1" 1408 | } 1409 | }, 1410 | "setprototypeof": { 1411 | "version": "1.1.1", 1412 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1413 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1414 | }, 1415 | "signal-exit": { 1416 | "version": "3.0.3", 1417 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 1418 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", 1419 | "optional": true 1420 | }, 1421 | "snakeize": { 1422 | "version": "0.1.0", 1423 | "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", 1424 | "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", 1425 | "optional": true 1426 | }, 1427 | "statuses": { 1428 | "version": "1.5.0", 1429 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1430 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1431 | }, 1432 | "stream-events": { 1433 | "version": "1.0.5", 1434 | "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", 1435 | "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", 1436 | "optional": true, 1437 | "requires": { 1438 | "stubs": "^3.0.0" 1439 | } 1440 | }, 1441 | "stream-shift": { 1442 | "version": "1.0.1", 1443 | "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", 1444 | "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", 1445 | "optional": true 1446 | }, 1447 | "streamsearch": { 1448 | "version": "0.1.2", 1449 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", 1450 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" 1451 | }, 1452 | "string_decoder": { 1453 | "version": "1.3.0", 1454 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1455 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1456 | "optional": true, 1457 | "requires": { 1458 | "safe-buffer": "~5.2.0" 1459 | } 1460 | }, 1461 | "stubs": { 1462 | "version": "3.0.0", 1463 | "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", 1464 | "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", 1465 | "optional": true 1466 | }, 1467 | "teeny-request": { 1468 | "version": "7.0.1", 1469 | "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz", 1470 | "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==", 1471 | "optional": true, 1472 | "requires": { 1473 | "http-proxy-agent": "^4.0.0", 1474 | "https-proxy-agent": "^5.0.0", 1475 | "node-fetch": "^2.6.1", 1476 | "stream-events": "^1.0.5", 1477 | "uuid": "^8.0.0" 1478 | } 1479 | }, 1480 | "toidentifier": { 1481 | "version": "1.0.0", 1482 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1483 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1484 | }, 1485 | "tslib": { 1486 | "version": "1.14.1", 1487 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 1488 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" 1489 | }, 1490 | "type-is": { 1491 | "version": "1.6.18", 1492 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1493 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1494 | "requires": { 1495 | "media-typer": "0.3.0", 1496 | "mime-types": "~2.1.24" 1497 | } 1498 | }, 1499 | "typedarray-to-buffer": { 1500 | "version": "3.1.5", 1501 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 1502 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 1503 | "optional": true, 1504 | "requires": { 1505 | "is-typedarray": "^1.0.0" 1506 | } 1507 | }, 1508 | "typescript": { 1509 | "version": "3.9.9", 1510 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", 1511 | "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", 1512 | "dev": true 1513 | }, 1514 | "unique-string": { 1515 | "version": "2.0.0", 1516 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", 1517 | "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", 1518 | "optional": true, 1519 | "requires": { 1520 | "crypto-random-string": "^2.0.0" 1521 | } 1522 | }, 1523 | "unpipe": { 1524 | "version": "1.0.0", 1525 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1526 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1527 | }, 1528 | "util-deprecate": { 1529 | "version": "1.0.2", 1530 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1531 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1532 | "optional": true 1533 | }, 1534 | "utils-merge": { 1535 | "version": "1.0.1", 1536 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1537 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1538 | }, 1539 | "uuid": { 1540 | "version": "8.3.2", 1541 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 1542 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 1543 | "optional": true 1544 | }, 1545 | "vary": { 1546 | "version": "1.1.2", 1547 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1548 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1549 | }, 1550 | "websocket-driver": { 1551 | "version": "0.7.4", 1552 | "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", 1553 | "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", 1554 | "requires": { 1555 | "http-parser-js": ">=0.5.1", 1556 | "safe-buffer": ">=5.1.0", 1557 | "websocket-extensions": ">=0.1.1" 1558 | } 1559 | }, 1560 | "websocket-extensions": { 1561 | "version": "0.1.4", 1562 | "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", 1563 | "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" 1564 | }, 1565 | "wrappy": { 1566 | "version": "1.0.2", 1567 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1568 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1569 | "optional": true 1570 | }, 1571 | "write-file-atomic": { 1572 | "version": "3.0.3", 1573 | "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", 1574 | "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", 1575 | "optional": true, 1576 | "requires": { 1577 | "imurmurhash": "^0.1.4", 1578 | "is-typedarray": "^1.0.0", 1579 | "signal-exit": "^3.0.2", 1580 | "typedarray-to-buffer": "^3.1.5" 1581 | } 1582 | }, 1583 | "xdg-basedir": { 1584 | "version": "4.0.0", 1585 | "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", 1586 | "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", 1587 | "optional": true 1588 | }, 1589 | "yallist": { 1590 | "version": "4.0.0", 1591 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1592 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 1593 | "optional": true 1594 | }, 1595 | "yocto-queue": { 1596 | "version": "0.1.0", 1597 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1598 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1599 | "optional": true 1600 | } 1601 | } 1602 | } 1603 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "build": "tsc", 5 | "serve": "npm run build && firebase emulators:start --only functions", 6 | "shell": "npm run build && firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "12" 13 | }, 14 | "main": "lib/index.js", 15 | "dependencies": { 16 | "firebase-admin": "^9.2.0", 17 | "firebase-functions": "^3.11.0" 18 | }, 19 | "devDependencies": { 20 | "typescript": "^3.8.0", 21 | "firebase-functions-test": "^0.2.0" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /functions/scripts/init-admin.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const admin = require('firebase-admin'); 4 | 5 | 6 | const serviceAccountPath = process.argv[2], 7 | userUid = process.argv[3]; 8 | 9 | console.log(`Using service account ${serviceAccountPath}.`); 10 | console.log(`Setting user ${userUid} as admin.`); 11 | 12 | 13 | admin.initializeApp({ 14 | credential: admin.credential.cert(serviceAccountPath) 15 | }); 16 | 17 | 18 | async function initAdmin(adminUid) { 19 | 20 | await admin.auth().setCustomUserClaims(adminUid, {admin:true}); 21 | 22 | console.log("User is now an admin."); 23 | 24 | } 25 | 26 | 27 | initAdmin(userUid) 28 | .then(() => { 29 | console.log("Exiting."); 30 | process.exit(); 31 | }); 32 | -------------------------------------------------------------------------------- /functions/src/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as functions from 'firebase-functions'; 3 | import {auth} from "./init"; 4 | 5 | 6 | 7 | export function getUserCredentialsMiddleware(req, res, next) { 8 | 9 | functions.logger.debug(`Attempting to extract user credentials from request.`); 10 | 11 | const jwt = req.headers.authorization; 12 | 13 | 14 | if (jwt) { 15 | auth.verifyIdToken(jwt) 16 | .then(jwtPayload => { 17 | 18 | req["uid"] = jwtPayload.uid; 19 | req["admin"] = jwtPayload.admin; 20 | 21 | functions.logger.debug( 22 | `Credentials: uid=${jwtPayload.uid}, admin=${jwtPayload.admin}`); 23 | 24 | next(); 25 | }) 26 | .catch(err => { 27 | console.log("Error ocurred while validating JWT", err); 28 | next(); 29 | }); 30 | 31 | } 32 | else { 33 | next(); 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /functions/src/create-user.ts: -------------------------------------------------------------------------------- 1 | import {auth, db} from "./init"; 2 | 3 | const express = require('express'); 4 | import * as functions from 'firebase-functions'; 5 | import {getUserCredentialsMiddleware} from "./auth.middleware"; 6 | 7 | const bodyParser = require('body-parser'); 8 | const cors = require('cors'); 9 | 10 | export const createUserApp = express(); 11 | 12 | createUserApp.use(bodyParser.json()); 13 | createUserApp.use(cors({origin:true})); 14 | createUserApp.use(getUserCredentialsMiddleware); 15 | 16 | 17 | createUserApp.post("/", async (req, res) => { 18 | 19 | functions.logger.debug(`Calling create user function.`); 20 | 21 | try { 22 | 23 | if (!(req["uid"] && req["admin"])) { 24 | const message = `Denied access to user creation service.`; 25 | functions.logger.debug(message); 26 | res.status(403).json({message}); 27 | return; 28 | } 29 | 30 | const email = req.body.email, 31 | password = req.body.password, 32 | admin = req.body.admin; 33 | 34 | const user = await auth.createUser({ 35 | email, 36 | password 37 | }); 38 | 39 | await auth.setCustomUserClaims(user.uid, {admin}); 40 | 41 | db.doc(`users/${user.uid}`).set({}); 42 | 43 | 44 | res.status(200).json({message:"User created successfully."}); 45 | 46 | } 47 | catch(err) { 48 | functions.logger.error(`Could not create user.`, err); 49 | res.status(500).json({message: "Could not create user."}); 50 | } 51 | 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | import {createUserApp} from "./create-user"; 3 | 4 | // 5 | // Start writing Firebase Functions 6 | // https://firebase.google.com/docs/functions/typescript 7 | // 8 | 9 | 10 | export const createUser = functions.https.onRequest(createUserApp); 11 | 12 | 13 | export const onAddCourseUpdatePromoCounter = 14 | functions 15 | .runWith({ 16 | timeoutSeconds: 300, 17 | memory: "128MB" 18 | }) 19 | .firestore.document("courses/{courseId}") 20 | .onCreate(async(snap, context) => { 21 | await ( 22 | await import("./promotions-counter/on-add-course")) 23 | .default(snap, context); 24 | }); 25 | 26 | 27 | export const onCourseUpdatedUpdatePromoCounter = 28 | functions.firestore 29 | .document('courses/{courseId}') 30 | .onUpdate(async (change, context) => { 31 | await (await import('./promotions-counter/on-course-updated')) 32 | .default(change, context); 33 | 34 | }) 35 | 36 | export const onCourseDeletedUpdatePromoCounter = 37 | functions.firestore 38 | .document('courses/{courseId}') 39 | .onDelete(async(snap, context) => { 40 | await ( 41 | await import("./promotions-counter/on-delete-course")) 42 | .default(snap, context); 43 | }) 44 | -------------------------------------------------------------------------------- /functions/src/init.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const admin = require('firebase-admin'); 4 | 5 | 6 | admin.initializeApp(); 7 | 8 | export const db = admin.firestore(); 9 | 10 | export const auth = admin.auth(); 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /functions/src/promotions-counter/on-add-course.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | import {db} from "../init"; 3 | 4 | import {firestore} from 'firebase-admin/lib/firestore'; 5 | import FieldValue = firestore.FieldValue; 6 | 7 | 8 | export default async (snap, context) => { 9 | 10 | functions.logger.debug( 11 | `Running add course trigger for courseId ${context.params.courseId}`); 12 | 13 | const course = snap.data(); 14 | 15 | if (course.promo) { 16 | 17 | return db.doc("courses/stats").update({ 18 | totalPromo: FieldValue.increment(1) 19 | }) 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /functions/src/promotions-counter/on-course-updated.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | 3 | import {db} from "../init"; 4 | 5 | import {firestore} from 'firebase-admin/lib/firestore'; 6 | import FieldValue = firestore.FieldValue; 7 | 8 | export default async(change, context) => { 9 | 10 | if(context.params.courseId == 'stats') { 11 | return; 12 | } 13 | 14 | functions.logger.debug( 15 | `Running update course trigger for courseId ${context.params.courseId}`); 16 | 17 | const newData = change.after.data(), 18 | oldData = change.before.data(); 19 | 20 | let increment = 0; 21 | 22 | if (!oldData.promo && newData.promo) { 23 | increment = 1; 24 | } 25 | else if (oldData.promo && !newData.promo) { 26 | increment = -1; 27 | } 28 | 29 | if (increment == 0) { 30 | return; 31 | } 32 | 33 | return db.doc(`courses/stats`).update({ 34 | totalPromo: FieldValue.increment(increment) 35 | }) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /functions/src/promotions-counter/on-delete-course.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as functions from 'firebase-functions'; 3 | import {firestore} from 'firebase-admin/lib/firestore'; 4 | import FieldValue = firestore.FieldValue; 5 | import {db} from "../init"; 6 | 7 | export default async (snap, context) => { 8 | 9 | functions.logger.debug( 10 | `Running delete course trigger for courseId ${context.params.courseId}`); 11 | 12 | const course = snap.data(); 13 | 14 | if (!course.promo) { 15 | return; 16 | } 17 | 18 | return db.doc("courses/stats").update({ 19 | totalPromo: FieldValue.increment(-1) 20 | }); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017", 10 | "noImplicitAny": false 11 | }, 12 | "compileOnSave": true, 13 | "include": [ 14 | "src" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /images/firebase-course-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/firebase-course/9ed1525d4ced3a6476baad0be2d16cd6c9da7143/images/firebase-course-1.jpg -------------------------------------------------------------------------------- /images/firebase-course-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/firebase-course/9ed1525d4ced3a6476baad0be2d16cd6c9da7143/images/firebase-course-2.jpg -------------------------------------------------------------------------------- /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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/try-emulator'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularfire-course", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "firebase": "firebase", 12 | "local-dev": "firebase emulators:start --only firestore,auth,functions --import test-data", 13 | "empty-emulator": "firebase emulators:start", 14 | "emulator": "firebase emulators:start --import test-data", 15 | "export": "firebase emulators:export test-data" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/animations": "~13.0.3", 20 | "@angular/cdk": "~13.0.3", 21 | "@angular/common": "~13.0.3", 22 | "@angular/compiler": "~13.0.3", 23 | "@angular/core": "~13.0.3", 24 | "@angular/fire": "^6.1.4", 25 | "@angular/forms": "~13.0.3", 26 | "@angular/material": "~13.0.3", 27 | "@angular/platform-browser": "~13.0.3", 28 | "@angular/platform-browser-dynamic": "~13.0.3", 29 | "@angular/router": "~13.0.3", 30 | "firebase": "^8.2.6", 31 | "firebase-tools": "^11.1.0", 32 | "firebaseui": "^4.7.3", 33 | "rxjs": "~6.6.0", 34 | "tslib": "^2.0.0", 35 | "zone.js": "~0.11.4" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "~13.0.4", 39 | "@angular/cli": "~13.0.4", 40 | "@angular/compiler-cli": "~13.0.3", 41 | "@types/jasmine": "~3.6.0", 42 | "@types/node": "^12.11.1", 43 | "codelyzer": "^6.0.0", 44 | "jasmine-core": "~3.6.0", 45 | "jasmine-spec-reporter": "~5.0.0", 46 | "karma": "~6.3.2", 47 | "karma-chrome-launcher": "~3.1.0", 48 | "karma-coverage": "~2.0.3", 49 | "karma-jasmine": "~4.0.0", 50 | "karma-jasmine-html-reporter": "^1.5.0", 51 | "protractor": "~7.0.0", 52 | "ts-node": "~8.3.0", 53 | "tslint": "~6.1.0", 54 | "typescript": "~4.4.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/about/about.component.css: -------------------------------------------------------------------------------- 1 | .course-image { 2 | max-width: 350px; 3 | border-radius: 4px; 4 | } 5 | 6 | 7 | .about { 8 | padding: 40px; 9 | } 10 | 11 | .upload-btn { 12 | margin-top: 30px; 13 | } 14 | 15 | button { 16 | margin-bottom: 10px; 17 | display: block; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

Firebase & AngularFire In Depth

5 | 6 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 17 | 18 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | 4 | import 'firebase/firestore'; 5 | 6 | import {AngularFirestore} from '@angular/fire/firestore'; 7 | import {COURSES, findLessonsForCourse} from './db-data'; 8 | import {first, take} from "rxjs/operators"; 9 | 10 | 11 | 12 | @Component({ 13 | selector: 'about', 14 | templateUrl: './about.component.html', 15 | styleUrls: ['./about.component.css'] 16 | }) 17 | export class AboutComponent { 18 | 19 | constructor(private db: AngularFirestore) {} 20 | 21 | async uploadData() { 22 | const coursesCollection = this.db.collection('courses'); 23 | const courses = await this.db.collection('courses').get(); 24 | for (let course of Object.values(COURSES)) { 25 | const newCourse = this.removeId(course); 26 | const courseRef = await coursesCollection.add(newCourse); 27 | const lessons = await courseRef.collection('lessons'); 28 | const courseLessons = findLessonsForCourse(course['id']); 29 | console.log(`Uploading course ${course['description']}`); 30 | for (const lesson of courseLessons) { 31 | const newLesson = this.removeId(lesson); 32 | delete newLesson.courseId; 33 | await lessons.add(newLesson); 34 | } 35 | } 36 | } 37 | 38 | removeId(data: any) { 39 | const newData: any = {...data}; 40 | delete newData.id; 41 | return newData; 42 | } 43 | 44 | 45 | onReadDoc() { 46 | 47 | this.db.doc("courses/1CErZJychQ4KET9Yi96K") 48 | .valueChanges() 49 | .subscribe(course => { 50 | 51 | console.log(course); 52 | 53 | }); 54 | 55 | } 56 | 57 | onReadCollection() { 58 | this.db.collection( 59 | "courses", 60 | ref => ref.where("seqNo", "<=", 20) 61 | .where("url", "==", "angular-forms-course") 62 | .orderBy("seqNo") 63 | ).get() 64 | .subscribe(snaps => { 65 | 66 | snaps.forEach(snap => { 67 | 68 | console.log(snap.id); 69 | console.log(snap.data()); 70 | 71 | }) 72 | 73 | }); 74 | 75 | } 76 | 77 | onReadCollectionGroup() { 78 | 79 | this.db.collectionGroup("lessons", 80 | ref => ref.where("seqNo", "==", 1) ) 81 | .get() 82 | .subscribe(snaps => { 83 | 84 | snaps.forEach(snap => { 85 | 86 | console.log(snap.id); 87 | console.log(snap.data()); 88 | 89 | }) 90 | 91 | }); 92 | 93 | } 94 | } 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/app/about/db-data.ts: -------------------------------------------------------------------------------- 1 | export const COURSES: any = { 2 | 3 | 20: { 4 | id: 20, 5 | description: 'Firebase & AngularFire In Depth', 6 | longDescription: 'Full stack Development with Angular, AngularFire, Firestore, Firebase Storage, Hosting & Cloud Functions.', 7 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/firebase-course-1.jpg', 8 | lessonsCount: 10, 9 | categories: ['BEGINNER'], 10 | seqNo: 0, 11 | url: 'serverless-angular' 12 | }, 13 | 14 | 19: { 15 | id: 19, 16 | description: 'Angular Forms In Depth', 17 | longDescription: 'Build complex enterprise data forms with the powerful Angular Forms module', 18 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/angular-forms-course-small.jpg', 19 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 20 | categories: ['BEGINNER'], 21 | lessonsCount: 10, 22 | seqNo: 1, 23 | url: 'angular-forms-course', 24 | price: 50 25 | }, 26 | 27 | 28 | 18: { 29 | id: 18, 30 | description: 'Angular Router In Depth', 31 | longDescription: 'Build large-scale Single Page Applications with the powerful Angular Router', 32 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/angular-router-course.jpg', 33 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 34 | categories: ['BEGINNER'], 35 | lessonsCount: 10, 36 | seqNo: 2, 37 | url: 'angular-router-course', 38 | price: 50 39 | }, 40 | 41 | 17: { 42 | id: 17, 43 | description: 'Reactive Angular Course', 44 | longDescription: 'How to build Angular applications in Reactive style using plain RxJs - Patterns and Anti-Patterns', 45 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/reactive-angular-course.jpg', 46 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 47 | categories: ['BEGINNER'], 48 | lessonsCount: 10, 49 | seqNo: 3, 50 | url: 'reactive-angular-course', 51 | price: 50 52 | 53 | }, 54 | 3: { 55 | id: 3, 56 | description: 'RxJs In Practice Course', 57 | longDescription: 'Understand the RxJs Observable pattern, learn the RxJs Operators via practical examples', 58 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/rxjs-in-practice-course.png', 59 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 60 | categories: ['BEGINNER'], 61 | lessonsCount: 10, 62 | seqNo: 4, 63 | url: 'rxjs-course', 64 | price: 50 65 | }, 66 | 67 | 4: { 68 | id: 4, 69 | description: 'NgRx (with NgRx Data) - The Complete Guide', 70 | longDescription: 'Learn the modern Ngrx Ecosystem, including NgRx Data, Store, Effects, Router Store, Ngrx Entity, and Dev Tools.', 71 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/ngrx-v2.png', 72 | categories: ['BEGINNER'], 73 | lessonsCount: 10, 74 | seqNo: 5, 75 | url: 'ngrx-course', 76 | promo: false, 77 | price: 50 78 | }, 79 | 80 | 81 | 2: { 82 | id: 2, 83 | description: 'Angular Core Deep Dive', 84 | longDescription: 'A detailed walk-through of the most important part of Angular - the Core and Common modules', 85 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-core-in-depth-small.png', 86 | lessonsCount: 10, 87 | categories: ['BEGINNER'], 88 | seqNo: 6, 89 | url: 'angular-core-course', 90 | price: 50 91 | }, 92 | 93 | 94 | 5: { 95 | id: 5, 96 | 97 | description: 'Angular for Beginners', 98 | longDescription: 'Establish a solid layer of fundamentals, learn what\'s under the hood of Angular', 99 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/angular2-for-beginners-small-v2.png', 100 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 101 | categories: ['BEGINNER'], 102 | lessonsCount: 10, 103 | seqNo: 7, 104 | url: 'angular-for-beginners', 105 | price: 50 106 | }, 107 | 108 | 12: { 109 | id: 12, 110 | description: 'Angular Testing Course', 111 | longDescription: 'In-depth guide to Unit Testing and E2E Testing of Angular Applications', 112 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-testing-small.png', 113 | categories: ['BEGINNER'], 114 | seqNo: 8, 115 | url: 'angular-testing-course', 116 | lessonsCount: 10, 117 | promo: false, 118 | price: 50 119 | }, 120 | 121 | 16: { 122 | id: 16, 123 | description: 'Stripe Payments In Practice', 124 | longDescription: 'Build your own ecommerce store & membership website with Firebase, Stripe and Express', 125 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/stripe-course.jpg', 126 | lessonsCount: 10, 127 | categories: ['BEGINNER'], 128 | seqNo: 10, 129 | url: 'stripe-course', 130 | price: 50 131 | }, 132 | 133 | 134 | 14: { 135 | id: 14, 136 | description: 'NestJs In Practice (with MongoDB)', 137 | longDescription: 'Build a modern REST backend using Typescript, MongoDB and the familiar Angular API.', 138 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/nestjs-v2.png', 139 | categories: ['BEGINNER'], 140 | lessonsCount: 10, 141 | seqNo: 11, 142 | url: 'nestjs-course', 143 | promo: false, 144 | price: 50 145 | }, 146 | 147 | 148 | 6: { 149 | id: 6, 150 | description: 'Angular Security Course - Web Security Fundamentals', 151 | longDescription: 'Learn Web Security Fundamentals and apply them to defend an Angular / Node Application from multiple types of attacks.', 152 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png', 153 | courseListIcon: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/lock-v2.png', 154 | categories: ['ADVANCED'], 155 | lessonsCount: 11, 156 | seqNo: 12, 157 | url: 'angular-security-course', 158 | price: 50 159 | }, 160 | 161 | 7: { 162 | id: 7, 163 | description: 'Angular PWA - Progressive Web Apps Course', 164 | longDescription: 'Learn Angular Progressive Web Applications, build the future of the Web Today.', 165 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-pwa-course.png', 166 | courseListIcon: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/alien.png', 167 | categories: ['ADVANCED'], 168 | lessonsCount: 8, 169 | seqNo: 14, 170 | url: 'angular-pwa-course', 171 | price: 50 172 | }, 173 | 174 | 8: { 175 | id: 8, 176 | description: 'Angular Advanced Library Laboratory: Build Your Own Library', 177 | longDescription: 'Learn Advanced Angular functionality typically used in Library Development. Advanced Components, Directives, Testing, Npm', 178 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png', 179 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/thumbnails/angular-advanced-lesson-icon.png', 180 | categories: ['ADVANCED'], 181 | seqNo: 15, 182 | url: 'angular-advanced-course', 183 | price: 50 184 | }, 185 | 186 | 9: { 187 | id: 9, 188 | description: 'The Complete Typescript Course', 189 | longDescription: 'Complete Guide to Typescript From Scratch: Learn the language in-depth and use it to build a Node REST API.', 190 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png', 191 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-lesson.png', 192 | categories: ['BEGINNER'], 193 | seqNo: 16, 194 | url: 'typescript-course', 195 | price: 50 196 | }, 197 | 198 | 11: { 199 | id: 11, 200 | description: 'Angular Material Course', 201 | longDescription: 'Build Applications with the official Angular Widget Library', 202 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/material_design.png', 203 | categories: ['BEGINNER'], 204 | seqNo: 17, 205 | url: 'angular-material-course', 206 | price: 50 207 | } 208 | 209 | }; 210 | 211 | export const LESSONS = { 212 | 213 | 1: { 214 | id: 1, 215 | 'description': 'Angular Tutorial For Beginners - Build Your First App - Hello World Step By Step', 216 | 'duration': '4:17', 217 | 'seqNo': 1, 218 | courseId: 5 219 | }, 220 | 2: { 221 | id: 2, 222 | 'description': 'Building Your First Component - Component Composition', 223 | 'duration': '2:07', 224 | 'seqNo': 2, 225 | courseId: 5 226 | }, 227 | 3: { 228 | id: 3, 229 | 'description': 'Component @Input - How To Pass Input Data To an Component', 230 | 'duration': '2:33', 231 | 'seqNo': 3, 232 | courseId: 5 233 | }, 234 | 4: { 235 | id: 4, 236 | 'description': ' Component Events - Using @Output to create custom events', 237 | 'duration': '4:44', 238 | 'seqNo': 4, 239 | courseId: 5 240 | }, 241 | 5: { 242 | id: 5, 243 | 'description': ' Component Templates - Inline Vs External', 244 | 'duration': '2:55', 245 | 'seqNo': 5, 246 | courseId: 5 247 | }, 248 | 6: { 249 | id: 6, 250 | 'description': 'Styling Components - Learn About Component Style Isolation', 251 | 'duration': '3:27', 252 | 'seqNo': 6, 253 | courseId: 5 254 | }, 255 | 7: { 256 | id: 7, 257 | 'description': ' Component Interaction - Extended Components Example', 258 | 'duration': '9:22', 259 | 'seqNo': 7, 260 | courseId: 5 261 | }, 262 | 8: { 263 | id: 8, 264 | 'description': ' Components Tutorial For Beginners - Components Exercise !', 265 | 'duration': '1:26', 266 | 'seqNo': 8, 267 | courseId: 5 268 | }, 269 | 9: { 270 | id: 9, 271 | 'description': ' Components Tutorial For Beginners - Components Exercise Solution Inside', 272 | 'duration': '2:08', 273 | 'seqNo': 9, 274 | courseId: 5 275 | }, 276 | 10: { 277 | id: 10, 278 | 'description': ' Directives - Inputs, Output Event Emitters and How To Export Template References', 279 | 'duration': '4:01', 280 | 'seqNo': 10, 281 | courseId: 5 282 | }, 283 | 284 | 285 | // Security Course 286 | 11: { 287 | id: 11, 288 | 'description': 'Course Helicopter View', 289 | 'duration': '08:19', 290 | 'seqNo': 1, 291 | courseId: 6 292 | }, 293 | 294 | 12: { 295 | id: 12, 296 | 'description': 'Installing Git, Node, NPM and Choosing an IDE', 297 | 'duration': '04:17', 298 | 'seqNo': 2, 299 | courseId: 6 300 | }, 301 | 302 | 13: { 303 | id: 13, 304 | 'description': 'Installing The Lessons Code - Learn Why Its Essential To Use NPM 5', 305 | 'duration': '06:05', 306 | 'seqNo': 3, 307 | courseId: 6 308 | }, 309 | 310 | 14: { 311 | id: 14, 312 | 'description': 'How To Run Node In TypeScript With Hot Reloading', 313 | 'duration': '03:57', 314 | 'seqNo': 4, 315 | courseId: 6 316 | }, 317 | 318 | 15: { 319 | id: 15, 320 | 'description': 'Guided Tour Of The Sample Application', 321 | 'duration': '06:00', 322 | 'seqNo': 5, 323 | courseId: 6 324 | }, 325 | 16: { 326 | id: 16, 327 | 'description': 'Client Side Authentication Service - API Design', 328 | 'duration': '04:53', 329 | 'seqNo': 6, 330 | courseId: 6 331 | }, 332 | 17: { 333 | id: 17, 334 | 'description': 'Client Authentication Service - Design and Implementation', 335 | 'duration': '09:14', 336 | 'seqNo': 7, 337 | courseId: 6 338 | }, 339 | 18: { 340 | id: 18, 341 | 'description': 'The New Angular HTTP Client - Doing a POST Call To The Server', 342 | 'duration': '06:08', 343 | 'seqNo': 8, 344 | courseId: 6 345 | }, 346 | 19: { 347 | id: 19, 348 | 'description': 'User Sign Up Server-Side Implementation in Express', 349 | 'duration': '08:50', 350 | 'seqNo': 9, 351 | courseId: 6 352 | }, 353 | 20: { 354 | id: 20, 355 | 'description': 'Introduction To Cryptographic Hashes - A Running Demo', 356 | 'duration': '05:46', 357 | 'seqNo': 10, 358 | courseId: 6 359 | }, 360 | 21: { 361 | id: 21, 362 | 'description': 'Some Interesting Properties Of Hashing Functions - Validating Passwords', 363 | 'duration': '06:31', 364 | 'seqNo': 11, 365 | courseId: 6 366 | }, 367 | 368 | 369 | // PWA course 370 | 371 | 22: { 372 | id: 22, 373 | 'description': 'Course Kick-Off - Install Node, NPM, IDE And Service Workers Section Code', 374 | 'duration': '07:19', 375 | 'seqNo': 1, 376 | courseId: 7 377 | }, 378 | 23: { 379 | id: 23, 380 | 'description': 'Service Workers In a Nutshell - Service Worker Registration', 381 | 'duration': '6:59', 382 | 'seqNo': 2, 383 | courseId: 7 384 | }, 385 | 24: { 386 | id: 24, 387 | 'description': 'Service Workers Hello World - Lifecycle Part 1 and PWA Chrome Dev Tools', 388 | 'duration': '7:28', 389 | 'seqNo': 3, 390 | courseId: 7 391 | }, 392 | 25: { 393 | id: 25, 394 | 'description': 'Service Workers and Application Versioning - Install & Activate Lifecycle Phases', 395 | 'duration': '10:17', 396 | 'seqNo': 4, 397 | courseId: 7 398 | }, 399 | 400 | 26: { 401 | id: 26, 402 | 'description': 'Downloading The Offline Page - The Service Worker Installation Phase', 403 | 'duration': '09:50', 404 | 'seqNo': 5, 405 | courseId: 7 406 | }, 407 | 27: { 408 | id: 27, 409 | 'description': 'Introduction to the Cache Storage PWA API', 410 | 'duration': '04:44', 411 | 'seqNo': 6, 412 | courseId: 7 413 | }, 414 | 28: { 415 | id: 28, 416 | 'description': 'View Service Workers HTTP Interception Features In Action', 417 | 'duration': '06:07', 418 | 'seqNo': 7, 419 | courseId: 7 420 | }, 421 | 29: { 422 | id: 29, 423 | 'description': 'Service Workers Error Handling - Serving The Offline Page', 424 | 'duration': '5:38', 425 | 'seqNo': 8, 426 | courseId: 7 427 | }, 428 | 429 | // Firebase & AngularFire Course 430 | 431 | 30: { 432 | id: 30, 433 | description: 'Development Environment Setup', 434 | 'duration': '5:38', 435 | 'seqNo': 1, 436 | courseId: 20 437 | }, 438 | 439 | 31: { 440 | id: 31, 441 | description: 'Introduction to the Firebase Ecosystem', 442 | 'duration': '5:12', 443 | 'seqNo': 2, 444 | courseId: 20 445 | }, 446 | 447 | 32: { 448 | id: 32, 449 | description: 'Importing Data into Firestore', 450 | 'duration': '4:07', 451 | 'seqNo': 3, 452 | courseId: 20 453 | }, 454 | 455 | 33: { 456 | id: 33, 457 | description: 'Firestore Documents in Detail', 458 | 'duration': '7:32', 459 | 'seqNo': 4, 460 | courseId: 20 461 | }, 462 | 463 | 34: { 464 | id: 34, 465 | description: 'Firestore Collections in Detail', 466 | 'duration': '6:28', 467 | 'seqNo': 5, 468 | courseId: 20 469 | }, 470 | 471 | 35: { 472 | id: 35, 473 | description: 'Firestore Unique Identifiers', 474 | 'duration': '4:38', 475 | 'seqNo': 6, 476 | courseId: 20 477 | }, 478 | 479 | 36: { 480 | id: 36, 481 | description: 'Querying Firestore Collections', 482 | 'duration': '7:54', 483 | 'seqNo': 7, 484 | courseId: 20 485 | }, 486 | 487 | 37: { 488 | id: 37, 489 | description: 'Firebase Security Rules In Detail', 490 | 'duration': '5:31', 491 | 'seqNo': 8, 492 | courseId: 20 493 | }, 494 | 495 | 38: { 496 | id: 38, 497 | description: 'Firebase Cloud Functions In Detail', 498 | 'duration': '8:19', 499 | 'seqNo': 9, 500 | courseId: 20 501 | }, 502 | 503 | 39: { 504 | id: 39, 505 | description: 'Firebase Storage In Detail', 506 | 'duration': '7:05', 507 | 'seqNo': 10, 508 | courseId: 20 509 | }, 510 | 511 | 512 | // Angular Testing Course 513 | 514 | 40: { 515 | id: 40, 516 | description: 'Angular Testing Course - Helicopter View', 517 | 'duration': '5:38', 518 | 'seqNo': 1, 519 | courseId: 12 520 | }, 521 | 522 | 41: { 523 | id: 41, 524 | description: 'Setting Up the Development Environment', 525 | 'duration': '5:12', 526 | 'seqNo': 2, 527 | courseId: 12 528 | }, 529 | 530 | 42: { 531 | id: 42, 532 | description: 'Introduction to Jasmine, Spies and specs', 533 | 'duration': '4:07', 534 | 'seqNo': 3, 535 | courseId: 12 536 | }, 537 | 538 | 43: { 539 | id: 43, 540 | description: 'Introduction to Service Testing', 541 | 'duration': '7:32', 542 | 'seqNo': 4, 543 | courseId: 12 544 | }, 545 | 546 | 44: { 547 | id: 44, 548 | description: 'Settting up the Angular TestBed', 549 | 'duration': '6:28', 550 | 'seqNo': 5, 551 | courseId: 12 552 | }, 553 | 554 | 45: { 555 | id: 45, 556 | description: 'Mocking Angular HTTP requests', 557 | 'duration': '4:38', 558 | 'seqNo': 6, 559 | courseId: 12 560 | }, 561 | 562 | 46: { 563 | id: 46, 564 | description: 'Simulating Failing HTTP Requests', 565 | 'duration': '7:54', 566 | 'seqNo': 7, 567 | courseId: 12 568 | }, 569 | 570 | 47: { 571 | id: 47, 572 | description: 'Introduction to Angular Component Testing', 573 | 'duration': '5:31', 574 | 'seqNo': 8, 575 | courseId: 12 576 | }, 577 | 578 | 48: { 579 | id: 48, 580 | description: 'Testing Angular Components without the DOM', 581 | 'duration': '8:19', 582 | 'seqNo': 9, 583 | courseId: 12 584 | }, 585 | 586 | 49: { 587 | id: 49, 588 | description: 'Testing Angular Components with the DOM', 589 | 'duration': '7:05', 590 | 'seqNo': 10, 591 | courseId: 12 592 | }, 593 | 594 | 595 | // Ngrx Course 596 | 50: { 597 | id: 50, 598 | 'description': 'Welcome to the Angular Ngrx Course', 599 | 'duration': '6:53', 600 | 'seqNo': 1, 601 | courseId: 4 602 | 603 | }, 604 | 51: { 605 | id: 51, 606 | 'description': 'The Angular Ngrx Architecture Course - Helicopter View', 607 | 'duration': '5:52', 608 | 'seqNo': 2, 609 | courseId: 4 610 | }, 611 | 52: { 612 | id: 52, 613 | 'description': 'The Origins of Flux - Understanding the Famous Facebook Bug Problem', 614 | 'duration': '8:17', 615 | 'seqNo': 3, 616 | courseId: 4 617 | }, 618 | 53: { 619 | id: 53, 620 | 'description': 'Custom Global Events - Why Don\'t They Scale In Complexity?', 621 | 'duration': '7:47', 622 | 'seqNo': 4, 623 | courseId: 4 624 | }, 625 | 54: { 626 | id: 54, 627 | 'description': 'The Flux Architecture - How Does it Solve Facebook Counter Problem?', 628 | 'duration': '9:22', 629 | 'seqNo': 5, 630 | courseId: 4 631 | }, 632 | 55: { 633 | id: 55, 634 | 'description': 'Unidirectional Data Flow And The Angular Development Mode', 635 | 'duration': '7:07', 636 | 'seqNo': 6, 637 | courseId: 4 638 | }, 639 | 640 | 56: { 641 | id: 56, 642 | 'description': 'Dispatching an Action - Implementing the Login Component', 643 | 'duration': '4:39', 644 | 'seqNo': 7, 645 | courseId: 4 646 | }, 647 | 57: { 648 | id: 57, 649 | 'description': 'Setting Up the Ngrx DevTools - Demo', 650 | 'duration': '4:44', 651 | 'seqNo': 8, 652 | courseId: 4 653 | }, 654 | 58: { 655 | id: 58, 656 | 'description': 'Understanding Reducers - Writing Our First Reducer', 657 | 'duration': '9:10', 658 | 'seqNo': 9, 659 | courseId: 4 660 | }, 661 | 59: { 662 | id: 59, 663 | 'description': 'How To Define the Store Initial State', 664 | 'duration': '9:10', 665 | 'seqNo': 10, 666 | courseId: 4 667 | }, 668 | 669 | // NestJs Course 670 | 671 | 60: { 672 | id: 60, 673 | 'description': 'Introduction to NestJs', 674 | 'duration': '4:29', 675 | 'seqNo': 1, 676 | courseId: 14 677 | }, 678 | 61: { 679 | id: 61, 680 | 'description': 'Development Environment Setup', 681 | 'duration': '6:37', 682 | 'seqNo': 2, 683 | courseId: 14 684 | }, 685 | 62: { 686 | id: 62, 687 | 'description': 'Setting up a MongoDB Database', 688 | 'duration': '6:38', 689 | 'seqNo': 3, 690 | courseId: 14 691 | }, 692 | 63: { 693 | id: 63, 694 | 'description': 'CRUD with NestJs - Controllers and Repositories', 695 | 'duration': '12:12', 696 | 'seqNo': 4, 697 | courseId: 14 698 | }, 699 | 64: { 700 | id: 64, 701 | 'description': 'First REST endpoint - Get All Courses', 702 | 'duration': '3:42', 703 | 'seqNo': 5, 704 | courseId: 14 705 | }, 706 | 65: { 707 | id: 65, 708 | 'description': 'Error Handling', 709 | 'duration': '5:15', 710 | 'seqNo': 6, 711 | courseId: 14 712 | }, 713 | 66: { 714 | id: 66, 715 | 'description': 'NestJs Middleware', 716 | 'duration': '7:08', 717 | 'seqNo': 7, 718 | courseId: 14 719 | }, 720 | 67: { 721 | id: 67, 722 | 'description': 'Authentication in NestJs', 723 | 'duration': '13:22', 724 | 'seqNo': 8, 725 | courseId: 14 726 | }, 727 | 68: { 728 | id: 68, 729 | 'description': 'Authorization in NestJs', 730 | 'duration': '6:43', 731 | 'seqNo': 9, 732 | courseId: 14 733 | }, 734 | 69: { 735 | id: 69, 736 | 'description': 'Guards & Interceptors', 737 | 'duration': '8:16', 738 | 'seqNo': 10, 739 | courseId: 14 740 | }, 741 | 742 | // Stripe Course 743 | 744 | 70: { 745 | id: 70, 746 | 'description': 'Introduction to Stripe Payments', 747 | 'duration': '03:45', 748 | 'seqNo': 0, 749 | courseId: 16 750 | }, 751 | 71: { 752 | id: 71, 753 | 'description': 'The advantages of Stripe Checkout', 754 | 'duration': '08:36', 755 | 'seqNo': 1, 756 | courseId: 16 757 | }, 758 | 72: { 759 | id: 72, 760 | 'description': 'Setting up the development environment', 761 | 'duration': '09:10', 762 | 'seqNo': 2, 763 | courseId: 16 764 | }, 765 | 73: { 766 | id: 73, 767 | 'description': 'Creating a server Checkout Session', 768 | 'duration': '07:20', 769 | 'seqNo': 3, 770 | courseId: 16 771 | }, 772 | 74: { 773 | id: 74, 774 | 'description': 'Redirecting to the Stripe Checkout page', 775 | 'duration': '11:47', 776 | 'seqNo': 4, 777 | courseId: 16 778 | }, 779 | 75: { 780 | id: 75, 781 | 'description': 'Order fulfillment webhook', 782 | 'duration': '06:30', 783 | 'seqNo': 5, 784 | courseId: 16 785 | }, 786 | 76: { 787 | id: 76, 788 | 'description': 'Installing the Stripe CLI', 789 | 'duration': '4:13', 790 | 'seqNo': 6, 791 | courseId: 16 792 | }, 793 | 77: { 794 | id: 77, 795 | 'description': 'Firestore Security Rules for protecting Premium content', 796 | 'duration': '05:47', 797 | 'seqNo': 7, 798 | courseId: 16 799 | }, 800 | 78: { 801 | id: 78, 802 | 'description': 'Stripe Subscriptions with Stripe Checkout', 803 | 'duration': '05:17', 804 | 'seqNo': 8, 805 | courseId: 16 806 | }, 807 | 79: { 808 | id: 79, 809 | 'description': 'Stripe Subscription Fulfillment', 810 | 'duration': '07:50', 811 | 'seqNo': 9, 812 | courseId: 16 813 | }, 814 | 815 | 816 | // Reactive Angular Course 817 | 818 | 80: { 819 | id: 80, 820 | 'description': 'Introduction to Reactive Programming', 821 | 'duration': '03:45', 822 | 'seqNo': 0, 823 | courseId: 17, 824 | videoId: 'Df1QnesgB_s', 825 | }, 826 | 81: { 827 | id: 81, 828 | 'description': 'Introduction to RxJs', 829 | 'duration': '08:36', 830 | 'seqNo': 1, 831 | courseId: 17, 832 | videoId: '8m5RrAtqlyw', 833 | }, 834 | 82: { 835 | id: 82, 836 | 'description': 'Setting up the development environment', 837 | 'duration': '09:10', 838 | 'seqNo': 2, 839 | courseId: 17, 840 | videoId: '3fDbUB-nKqc', 841 | }, 842 | 83: { 843 | id: 83, 844 | 'description': 'Designing and building a Service Layer', 845 | 'duration': '07:20', 846 | 'seqNo': 3, 847 | courseId: 17, 848 | videoId: '', 849 | }, 850 | 84: { 851 | id: 84, 852 | 'description': 'Stateless Observable Services', 853 | 'duration': '11:47', 854 | 'seqNo': 4, 855 | courseId: 17, 856 | videoId: 'qvDPnRs_ZPA', 857 | }, 858 | 85: { 859 | id: 85, 860 | 'description': 'Smart vs Presentational Components', 861 | 'duration': '06:30', 862 | 'seqNo': 5, 863 | courseId: 17, 864 | videoId: '5bsZJGAelFM', 865 | }, 866 | 86: { 867 | id: 86, 868 | 'description': 'Lightweight state management', 869 | 'duration': '4:13', 870 | 'seqNo': 6, 871 | courseId: 17, 872 | videoId: '9m3_HHeP9Ko', 873 | }, 874 | 87: { 875 | id: 87, 876 | 'description': 'Event bubbling anti-pattern', 877 | 'duration': '05:47', 878 | 'seqNo': 7, 879 | courseId: 17, 880 | videoId: 'PRQCAL_RMVo', 881 | }, 882 | 88: { 883 | id: 88, 884 | 'description': 'Master detail with cached master table', 885 | 'duration': '05:17', 886 | 'seqNo': 8, 887 | courseId: 17, 888 | videoId: 'du4ib4jBUG0' 889 | }, 890 | 89: { 891 | id: 89, 892 | 'description': 'Error handling', 893 | 'duration': '07:50', 894 | 'seqNo': 9, 895 | courseId: 17, 896 | videoId: '8m5RrAtqlyw' 897 | }, 898 | 899 | 900 | // Angular Router Course 901 | 90: { 902 | id: 90, 903 | 'description': 'What is a Single Page Application?', 904 | 'duration': '04:00', 905 | 'seqNo': 1, 906 | courseId: 18, 907 | videoId: 'VES1eTNxi1s' 908 | }, 909 | 91: { 910 | id: 91, 911 | 'description': 'Setting Up The Development Environment', 912 | 'duration': '06:05', 913 | 'seqNo': 2, 914 | courseId: 18, 915 | videoId: 'ANfplcxnl78' 916 | }, 917 | 92: { 918 | id: 92, 919 | 'description': 'Angular Router Setup', 920 | 'duration': '02:36', 921 | 'seqNo': 3, 922 | courseId: 18, 923 | videoId: '9ez72LAd6mM' 924 | }, 925 | 93: { 926 | id: 93, 927 | 'description': 'Configuring a Home Route and Fallback Route', 928 | 'duration': '02:55', 929 | 'seqNo': 4, 930 | courseId: 18, 931 | videoId: 'Clj-jZpl64w' 932 | }, 933 | 94: { 934 | id: 94, 935 | 'description': 'Styling Active Routes With The routerLinkActive And routerLinkActiveOptions', 936 | 'duration': '07:50', 937 | 'seqNo': 5, 938 | courseId: 18, 939 | videoId: 'zcgnsmPVc30' 940 | }, 941 | 95: { 942 | id: 95, 943 | 'description': 'Child Routes - How To Setup a Master Detail Route', 944 | 'duration': '04:10', 945 | 'seqNo': 6, 946 | courseId: 18, 947 | videoId: 'zcgnsmPVc30' 948 | }, 949 | 96: { 950 | id: 96, 951 | 'description': 'Programmatic Router Navigation via the Router API ', 952 | 'duration': '03:59', 953 | 'seqNo': 7, 954 | courseId: 18, 955 | videoId: 'VES1eTNxi1s' 956 | }, 957 | 97: { 958 | id: 97, 959 | 'description': 'Relative And Absolute Router Navigation', 960 | 'duration': '04:58', 961 | 'seqNo': 8, 962 | courseId: 18, 963 | videoId: 'MQl9Zs3QqGM' 964 | }, 965 | 98: { 966 | id: 98, 967 | 'description': 'Master Detail Navigation And Route Parameters', 968 | 'duration': '06:03', 969 | 'seqNo': 9, 970 | courseId: 18, 971 | videoId: 'ANfplcxnl78' 972 | }, 973 | 974 | 99: { 975 | id: 99, 976 | 'description': 'The Route Parameters Observable', 977 | 'duration': '06:50', 978 | 'seqNo': 10, 979 | courseId: 18, 980 | videoId: 'zcgnsmPVc30' 981 | }, 982 | 100: { 983 | id: 100, 984 | 'description': 'Optional Route Query Parameters', 985 | 'duration': '03:03', 986 | 'seqNo': 11, 987 | courseId: 18, 988 | videoId: '0Qsg8fyKwO4' 989 | }, 990 | 101: { 991 | id: 101, 992 | 'description': 'The queryParams Directive and the Query Parameters Observable', 993 | 'duration': '07:50', 994 | 'seqNo': 12, 995 | courseId: 18, 996 | videoId: 'VES1eTNxi1s' 997 | }, 998 | 102: { 999 | id: 102, 1000 | 'description': 'Exiting an Angular Route - How To Prevent Memory Leaks', 1001 | 'duration': '07:50', 1002 | 'seqNo': 13, 1003 | courseId: 18, 1004 | videoId: 'ANfplcxnl78' 1005 | }, 1006 | 103: { 1007 | id: 103, 1008 | 'description': 'CanDeactivate Route Guard', 1009 | 'duration': '04:50', 1010 | 'seqNo': 14, 1011 | courseId: 18, 1012 | videoId: '9ez72LAd6mM' 1013 | }, 1014 | 104: { 1015 | id: 104, 1016 | 'description': 'CanActivate Route Guard - An Example of An Asynchronous Route Guard', 1017 | 'duration': '03:32', 1018 | 'seqNo': 15, 1019 | courseId: 18, 1020 | videoId: 'Clj-jZpl64w' 1021 | }, 1022 | 1023 | 1024 | 105: { 1025 | id: 105, 1026 | 'description': 'Configure Auxiliary Routes in the Angular Router', 1027 | 'duration': '05:16', 1028 | 'seqNo': 16, 1029 | courseId: 18, 1030 | videoId: 'zcgnsmPVc30' 1031 | }, 1032 | 1033 | 106: { 1034 | id: 106, 1035 | 'description': 'Angular Auxiliary Routes - How To Pass Router Parameters', 1036 | 'duration': '07:50', 1037 | 'seqNo': 17, 1038 | courseId: 18, 1039 | videoId: 'yjQUkNHb1Is' 1040 | }, 1041 | 107: { 1042 | id: 107, 1043 | 'description': 'Angular Router Redirects and Path Matching', 1044 | 'duration': '02:59', 1045 | 'seqNo': 18, 1046 | courseId: 18, 1047 | videoId: 'VES1eTNxi1s' 1048 | }, 1049 | 108: { 1050 | id: 108, 1051 | 'description': 'Angular Router Hash Location Strategy', 1052 | 'duration': '07:50', 1053 | 'seqNo': 19, 1054 | courseId: 18, 1055 | videoId: 'MQl9Zs3QqGM' 1056 | }, 1057 | 109: { 1058 | id: 109, 1059 | 'description': 'Angular Router Lazy Loading and Shared Modules', 1060 | 'duration': '08:45', 1061 | 'seqNo': 20, 1062 | courseId: 18, 1063 | videoId: '0Qsg8fyKwO4' 1064 | }, 1065 | 110: { 1066 | id: 110, 1067 | 'description': 'Exercise - Implement a Widget Dashboard', 1068 | 'duration': '07:50', 1069 | 'seqNo': 21, 1070 | courseId: 18, 1071 | videoId: 'VES1eTNxi1s' 1072 | }, 1073 | 111: { 1074 | id: 111, 1075 | 'description': 'Exercise Solution ', 1076 | 'duration': '07:50', 1077 | 'seqNo': 22, 1078 | courseId: 18, 1079 | videoId: '0Qsg8fyKwO4' 1080 | } 1081 | 1082 | 1083 | }; 1084 | 1085 | 1086 | export const USERS = { 1087 | 1: { 1088 | id: 1, 1089 | email: 'test@angular-university.io', 1090 | password: 'test', 1091 | pictureUrl: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png' 1092 | } 1093 | 1094 | }; 1095 | 1096 | 1097 | export function findCourseById(courseId: number) { 1098 | return COURSES[courseId]; 1099 | } 1100 | 1101 | export function findLessonsForCourse(courseId: number) { 1102 | return Object.values(LESSONS).filter(lesson => lesson.courseId == courseId); 1103 | } 1104 | 1105 | export function authenticate(email: string, password: string) { 1106 | 1107 | const user: any = Object.values(USERS).find(user => user.email === email); 1108 | 1109 | if (user && user.password == password) { 1110 | return user; 1111 | } else { 1112 | return undefined; 1113 | } 1114 | 1115 | } 1116 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {Routes, RouterModule} from '@angular/router'; 3 | import {HomeComponent} from './home/home.component'; 4 | import {AboutComponent} from './about/about.component'; 5 | import {CourseComponent} from './course/course.component'; 6 | import {LoginComponent} from './login/login.component'; 7 | import {CreateCourseComponent} from './create-course/create-course.component'; 8 | import {AngularFireAuthGuard, customClaims, hasCustomClaim, redirectUnauthorizedTo} 9 | from '@angular/fire/auth-guard'; 10 | import {CreateUserComponent} from './create-user/create-user.component'; 11 | import {CourseResolver} from "./services/course.resolver"; 12 | import {pipe} from "rxjs"; 13 | import {map} from "rxjs/operators"; 14 | 15 | const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['login']); 16 | 17 | const adminOnly = () => hasCustomClaim("admin"); 18 | 19 | const routes: Routes = [ 20 | { 21 | path: '', 22 | component: HomeComponent, 23 | canActivate: [AngularFireAuthGuard], 24 | data: { 25 | authGuardPipe: redirectUnauthorizedToLogin 26 | } 27 | }, 28 | { 29 | path: 'create-course', 30 | component: CreateCourseComponent, 31 | canActivate: [AngularFireAuthGuard], 32 | data: { 33 | authGuardPipe: adminOnly 34 | } 35 | }, 36 | { 37 | path: 'create-user', 38 | component: CreateUserComponent, 39 | canActivate: [AngularFireAuthGuard], 40 | data: { 41 | authGuardPipe: adminOnly 42 | } 43 | }, 44 | { 45 | path: 'about', 46 | component: AboutComponent 47 | }, 48 | { 49 | path: 'login', 50 | component: LoginComponent 51 | }, 52 | { 53 | path: 'courses/:courseUrl', 54 | component: CourseComponent, 55 | resolve: { 56 | course: CourseResolver 57 | }, 58 | canActivate: [AngularFireAuthGuard], 59 | data: { 60 | authGuardPipe: redirectUnauthorizedToLogin 61 | } 62 | }, 63 | { 64 | path: '**', 65 | redirectTo: '/' 66 | } 67 | ]; 68 | 69 | @NgModule({ 70 | imports: [RouterModule.forRoot(routes)], 71 | exports: [RouterModule] 72 | }) 73 | export class AppRoutingModule { 74 | } 75 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | 2 | >>> body { 3 | margin: 0; 4 | } 5 | 6 | main { 7 | margin: 30px; 8 | } 9 | 10 | .user-avatar { 11 | max-height: 35px; 12 | border-radius: 4px; 13 | 14 | } 15 | 16 | .toolbar-tools { 17 | display: flex; 18 | width: 100%; 19 | } 20 | 21 | 22 | .filler { 23 | flex: 1 1 auto; 24 | width: 100%; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | library_books 8 | Courses 9 | 10 | 11 | 12 | add_circle_outline 13 | Create user 14 | 15 | 16 | 17 | question_answer 18 | About 19 | 20 | 21 | person_add 22 | Register 23 | 24 | 25 | 26 | account_circle 27 | Login 28 | 29 | 30 | 31 | exit_to_app 32 | Logout 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 | 58 |
59 | 60 | 61 | 62 |
63 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {AngularFireAuth} from '@angular/fire/auth'; 3 | import {from, Observable} from 'rxjs'; 4 | import {concatMap, filter, map} from 'rxjs/operators'; 5 | import {AngularFirestore} from '@angular/fire/firestore'; 6 | import {Router} from '@angular/router'; 7 | import {UserService} from "./services/user.service"; 8 | import {AuthTokenService} from "./services/auth-token.service"; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | templateUrl: './app.component.html', 13 | styleUrls: ['./app.component.css'] 14 | }) 15 | export class AppComponent implements OnInit { 16 | 17 | constructor( 18 | public user: UserService, 19 | private token: AuthTokenService) { 20 | 21 | } 22 | 23 | ngOnInit() { 24 | 25 | } 26 | 27 | logout() { 28 | this.user.logout(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppComponent } from './app.component'; 4 | import {AngularFireAuthModule, USE_EMULATOR as USE_AUTH_EMULATOR} from '@angular/fire/auth'; 5 | import {AngularFirestoreModule, USE_EMULATOR as USE_FIRESTORE_EMULATOR} from '@angular/fire/firestore'; 6 | import {AngularFireFunctionsModule, USE_EMULATOR as USE_FUNCTIONS_EMULATOR} from '@angular/fire/functions'; 7 | import {environment} from '../environments/environment'; 8 | import {AngularFireModule} from '@angular/fire'; 9 | import {AngularFireStorageModule} from '@angular/fire/storage'; 10 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 11 | import {MatMenuModule} from '@angular/material/menu'; 12 | import {MatButtonModule} from '@angular/material/button'; 13 | import {MatIconModule} from '@angular/material/icon'; 14 | import {MatCardModule} from '@angular/material/card'; 15 | import {MatTabsModule} from '@angular/material/tabs'; 16 | import {MatSidenavModule} from '@angular/material/sidenav'; 17 | import {MatListModule} from '@angular/material/list'; 18 | import {MatToolbarModule} from '@angular/material/toolbar'; 19 | import {MatInputModule} from '@angular/material/input'; 20 | import {MatTableModule} from '@angular/material/table'; 21 | import {MatPaginatorModule} from '@angular/material/paginator'; 22 | import {MatSortModule} from '@angular/material/sort'; 23 | import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 24 | import {MatProgressBarModule} from '@angular/material/progress-bar'; 25 | import {MatDialogModule} from '@angular/material/dialog'; 26 | import {MatSelectModule} from '@angular/material/select'; 27 | import {MatDatepickerModule} from '@angular/material/datepicker'; 28 | import {ReactiveFormsModule} from '@angular/forms'; 29 | import {HomeComponent} from './home/home.component'; 30 | import {AboutComponent} from './about/about.component'; 31 | import {EditCourseDialogComponent} from './edit-course-dialog/edit-course-dialog.component'; 32 | import {LoginComponent} from './login/login.component'; 33 | import {CoursesCardListComponent} from './courses-card-list/courses-card-list.component'; 34 | import {AppRoutingModule} from './app-routing.module'; 35 | import {CourseComponent} from './course/course.component'; 36 | import {CreateCourseComponent} from './create-course/create-course.component'; 37 | import {MatSlideToggleModule} from '@angular/material/slide-toggle'; 38 | import {CreateUserComponent} from './create-user/create-user.component'; 39 | import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; 40 | import {MatNativeDateModule} from '@angular/material/core'; 41 | import {AuthInterceptor} from "./services/auth.interceptor"; 42 | 43 | @NgModule({ 44 | declarations: [ 45 | AppComponent, 46 | HomeComponent, 47 | AboutComponent, 48 | CourseComponent, 49 | CoursesCardListComponent, 50 | EditCourseDialogComponent, 51 | LoginComponent, 52 | CreateCourseComponent, 53 | CreateUserComponent 54 | ], 55 | imports: [ 56 | BrowserModule, 57 | BrowserAnimationsModule, 58 | MatMenuModule, 59 | MatButtonModule, 60 | MatIconModule, 61 | MatCardModule, 62 | MatTabsModule, 63 | MatSidenavModule, 64 | MatSlideToggleModule, 65 | MatListModule, 66 | MatToolbarModule, 67 | MatInputModule, 68 | MatTableModule, 69 | MatPaginatorModule, 70 | MatSortModule, 71 | MatProgressSpinnerModule, 72 | MatProgressBarModule, 73 | MatDialogModule, 74 | MatSelectModule, 75 | MatDatepickerModule, 76 | MatNativeDateModule, 77 | ReactiveFormsModule, 78 | AppRoutingModule, 79 | HttpClientModule, 80 | AngularFireModule.initializeApp(environment.firebase), 81 | AngularFirestoreModule, 82 | AngularFireStorageModule, 83 | AngularFireAuthModule, 84 | AngularFireFunctionsModule 85 | ], 86 | providers: [ 87 | { provide: USE_AUTH_EMULATOR, useValue: environment.useEmulators ? ['localhost', 9099] : undefined }, 88 | { provide: USE_FIRESTORE_EMULATOR, useValue: environment.useEmulators ? ['localhost', 8080] : undefined }, 89 | { provide: USE_FUNCTIONS_EMULATOR, useValue: environment.useEmulators ? ['localhost', 5001] : undefined }, 90 | { 91 | provide: HTTP_INTERCEPTORS, 92 | useClass: AuthInterceptor, 93 | multi: true 94 | } 95 | ], 96 | bootstrap: [AppComponent] 97 | }) 98 | export class AppModule { } 99 | -------------------------------------------------------------------------------- /src/app/course/course.component.css: -------------------------------------------------------------------------------- 1 | 2 | .course { 3 | text-align: center; 4 | max-width: 390px; 5 | margin: 20px auto 0 auto; 6 | } 7 | 8 | .course-thumbnail { 9 | width: 200px; 10 | border-radius: 4px; 11 | margin: 20px auto; 12 | display: block; 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 | .spinner-container { 31 | height: 360px; 32 | width: 390px; 33 | position: fixed; 34 | } 35 | 36 | .lessons-table { 37 | min-height: 360px; 38 | margin-top: 10px; 39 | } 40 | 41 | .spinner-container mat-spinner { 42 | margin: 130px auto 0 auto; 43 | } 44 | 45 | .bottom-toolbar { 46 | margin: 30px 0; 47 | font-size: 18px; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/course/course.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{course?.description}}

4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 15 | 16 | 17 | 18 | # 19 | 20 | {{lesson.seqNo}} 21 | 22 | 23 | 24 | 25 | 26 | Description 27 | 28 | {{lesson.description}} 30 | 31 | 32 | 33 | 34 | 35 | Duration 36 | 37 | {{lesson.duration}} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/app/course/course.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core'; 2 | import {ActivatedRoute} from '@angular/router'; 3 | import {Course} from '../model/course'; 4 | import {finalize, tap} from 'rxjs/operators'; 5 | import {Observable} from 'rxjs'; 6 | import {Lesson} from '../model/lesson'; 7 | import {CoursesService} from "../services/courses.service"; 8 | 9 | 10 | @Component({ 11 | selector: 'course', 12 | templateUrl: './course.component.html', 13 | styleUrls: ['./course.component.css'] 14 | }) 15 | export class CourseComponent implements OnInit { 16 | 17 | course:Course; 18 | 19 | lessons: Lesson[]; 20 | 21 | loading = false; 22 | 23 | lastPageLoaded = 0; 24 | 25 | displayedColumns = ['seqNo', 'description', 'duration']; 26 | 27 | constructor( 28 | private route: ActivatedRoute, 29 | private coursesService: CoursesService) { 30 | 31 | } 32 | 33 | ngOnInit() { 34 | 35 | this.course = this.route.snapshot.data["course"]; 36 | 37 | this.loading = true; 38 | 39 | this.coursesService.findLessons(this.course.id) 40 | .pipe( 41 | finalize(() => this.loading = false) 42 | ) 43 | .subscribe( 44 | lessons => this.lessons = lessons 45 | ); 46 | 47 | } 48 | 49 | loadMore() { 50 | 51 | this.lastPageLoaded++; 52 | 53 | this.loading = true; 54 | 55 | this.coursesService.findLessons(this.course.id, "asc", 56 | this.lastPageLoaded) 57 | .pipe( 58 | finalize(() => this.loading = false) 59 | ) 60 | .subscribe(lessons => this.lessons = this.lessons.concat(lessons)) 61 | 62 | } 63 | } 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/app/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 | } -------------------------------------------------------------------------------- /src/app/courses-card-list/courses-card-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{course.description}} 6 | 7 | 8 | 9 | 10 | 11 | 12 |

{{course.longDescription}}

13 |
14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /src/app/courses-card-list/courses-card-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation} from '@angular/core'; 2 | import {Course} from "../model/course"; 3 | import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; 4 | import {EditCourseDialogComponent} from "../edit-course-dialog/edit-course-dialog.component"; 5 | import {catchError, tap} from 'rxjs/operators'; 6 | import {throwError} from 'rxjs'; 7 | import {Router} from '@angular/router'; 8 | import {CoursesService} from "../services/courses.service"; 9 | import {UserService} from "../services/user.service"; 10 | 11 | @Component({ 12 | selector: 'courses-card-list', 13 | templateUrl: './courses-card-list.component.html', 14 | styleUrls: ['./courses-card-list.component.css'] 15 | }) 16 | export class CoursesCardListComponent implements OnInit { 17 | 18 | @Input() 19 | courses: Course[]; 20 | 21 | @Output() 22 | courseEdited = new EventEmitter(); 23 | 24 | @Output() 25 | courseDeleted = new EventEmitter(); 26 | 27 | constructor( 28 | private dialog: MatDialog, 29 | private router: Router, 30 | private coursesService:CoursesService, 31 | public user: UserService) { 32 | } 33 | 34 | ngOnInit() { 35 | 36 | } 37 | 38 | editCourse(course:Course) { 39 | 40 | const dialogConfig = new MatDialogConfig(); 41 | 42 | dialogConfig.disableClose = true; 43 | dialogConfig.autoFocus = true; 44 | dialogConfig.minWidth = "400px"; 45 | 46 | dialogConfig.data = course; 47 | 48 | this.dialog.open(EditCourseDialogComponent, dialogConfig) 49 | .afterClosed() 50 | .subscribe(val => { 51 | if (val) { 52 | this.courseEdited.emit(); 53 | } 54 | }); 55 | 56 | } 57 | 58 | onDeleteCourse(course: Course) { 59 | 60 | this.coursesService.deleteCourseAndLessons(course.id) 61 | .pipe( 62 | tap(() => { 63 | console.log("Deleted course", course); 64 | this.courseDeleted.emit(course); 65 | }), 66 | catchError(err => { 67 | console.log(err); 68 | alert("Could not delete course."); 69 | return throwError(err); 70 | }) 71 | ) 72 | .subscribe(); 73 | 74 | } 75 | 76 | 77 | } 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/app/create-course/create-course.component.css: -------------------------------------------------------------------------------- 1 | 2 | :host { 3 | display: flex; 4 | justify-content: center; 5 | } 6 | 7 | .create-course { 8 | margin-top: 20px; 9 | } 10 | 11 | .course-thumbnail { 12 | max-width: 200px; 13 | margin-bottom: 10px; 14 | border-radius: 4px; 15 | } 16 | 17 | .create-course form { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | .course-image-upload { 23 | margin: 0 0 20px 0; 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | 28 | .promo { 29 | margin-bottom: 20px; 30 | } 31 | 32 | .course-image-upload span { 33 | margin-bottom:10px; 34 | } 35 | 36 | .upload-progress { 37 | display: flex; 38 | margin: 10px; 39 | max-width: 250px; 40 | } 41 | 42 | .progress-bar { 43 | margin: 10px; 44 | width: 300px; 45 | } 46 | 47 | .uploaded-image { 48 | max-width: 250px; 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/app/create-course/create-course.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Create New Course

4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | Course thumbnail: 16 | 17 | 20 | 21 | 22 | 23 |
24 | 25 | 28 | 29 | 30 | 31 | {{percentage / 100 | percent}} 32 | 33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 | Beginner 41 | Intermediate 42 | Advanced 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | Course in promotion? 62 | 63 | 64 | 65 | Promotion start date 66 | 67 | 68 | 69 | 70 | 71 | 75 | 76 |
77 | 78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /src/app/create-course/create-course.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {AngularFirestore} from '@angular/fire/firestore'; 4 | import {Course} from '../model/course'; 5 | import {catchError, concatMap, last, map, take, tap} from 'rxjs/operators'; 6 | import {from, Observable, throwError} from 'rxjs'; 7 | import {Router} from '@angular/router'; 8 | import {AngularFireStorage} from '@angular/fire/storage'; 9 | import firebase from 'firebase/app'; 10 | import Timestamp = firebase.firestore.Timestamp; 11 | import {CoursesService} from "../services/courses.service"; 12 | 13 | @Component({ 14 | selector: 'create-course', 15 | templateUrl: 'create-course.component.html', 16 | styleUrls: ['create-course.component.css'] 17 | }) 18 | export class CreateCourseComponent implements OnInit { 19 | 20 | courseId: string; 21 | 22 | percentageChanges$: Observable; 23 | 24 | iconUrl:string; 25 | 26 | 27 | form = this.fb.group({ 28 | description: ['', Validators.required], 29 | category: ["BEGINNER", Validators.required], 30 | url: [''], 31 | longDescription: ['', Validators.required], 32 | promo: [false], 33 | promoStartAt: [null] 34 | }); 35 | 36 | constructor(private fb: FormBuilder, 37 | private coursesService: CoursesService, 38 | private afs: AngularFirestore, 39 | private router: Router, 40 | private storage: AngularFireStorage) { 41 | 42 | } 43 | 44 | uploadThumbnail(event) { 45 | 46 | const file: File = event.target.files[0]; 47 | 48 | console.log(file.name); 49 | 50 | const filePath = `courses/${this.courseId}/${file.name}`; 51 | 52 | const task = this.storage.upload(filePath, file, { 53 | cacheControl: "max-age=2592000,public" 54 | }); 55 | 56 | this.percentageChanges$ = task.percentageChanges(); 57 | 58 | task.snapshotChanges() 59 | .pipe( 60 | last(), 61 | concatMap(() => this.storage.ref(filePath).getDownloadURL()), 62 | tap(url => this.iconUrl = url), 63 | catchError(err => { 64 | console.log(err); 65 | alert("Could not create thumbnail url."); 66 | return throwError(err); 67 | }) 68 | 69 | ) 70 | .subscribe(); 71 | 72 | 73 | } 74 | 75 | ngOnInit() { 76 | this.courseId = this.afs.createId(); 77 | } 78 | 79 | onCreateCourse() { 80 | 81 | const val = this.form.value; 82 | 83 | const newCourse: Partial = { 84 | description: val.description, 85 | url: val.url, 86 | longDescription: val.longDescription, 87 | promo: val.promo, 88 | categories: [val.category] 89 | }; 90 | 91 | newCourse.promoStartAt = Timestamp.fromDate(this.form.value.promoStartAt); 92 | 93 | this.coursesService.createCourse(newCourse, this.courseId) 94 | .pipe( 95 | tap(course => { 96 | console.log("Created new course: ", course); 97 | this.router.navigateByUrl("/courses"); 98 | }), 99 | catchError(err => { 100 | console.log(err); 101 | alert("Could not create the course."); 102 | return throwError(err); 103 | }) 104 | ) 105 | .subscribe(); 106 | 107 | 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/app/create-user/create-user.component.css: -------------------------------------------------------------------------------- 1 | 2 | :host { 3 | display: flex; 4 | justify-content: center; 5 | } 6 | 7 | .create-user { 8 | margin-top: 20px; 9 | } 10 | 11 | .create-user form { 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | .promo { 17 | margin-bottom: 20px; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/create-user/create-user.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Create New User

4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | Is the user an administrator? 22 | 23 | 24 | 28 | 29 |
30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/create-user/create-user.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {HttpClient} from '@angular/common/http'; 4 | import {catchError} from 'rxjs/operators'; 5 | import {throwError} from 'rxjs'; 6 | import {environment} from "../../environments/environment"; 7 | 8 | @Component({ 9 | selector: 'create-user', 10 | templateUrl: 'create-user.component.html', 11 | styleUrls: ['create-user.component.css'] 12 | }) 13 | export class CreateUserComponent { 14 | 15 | form = this.fb.group({ 16 | email: ['', [Validators.email, Validators.required]], 17 | password: ['', [Validators.required, Validators.minLength(5)]], 18 | admin: [false] 19 | }); 20 | 21 | constructor( 22 | private fb: FormBuilder, 23 | private http: HttpClient) { 24 | 25 | } 26 | 27 | onCreateUser() { 28 | 29 | const user = this.form.value; 30 | 31 | console.log(user); 32 | 33 | this.http.post(environment.api.createUser, { 34 | email: user.email, 35 | password: user.password, 36 | admin: user.admin 37 | }) 38 | .pipe( 39 | catchError(err => { 40 | console.log(err); 41 | alert('Could not create user'); 42 | return throwError(err); 43 | }) 44 | ) 45 | .subscribe(() => { 46 | alert("User created successfully!"); 47 | this.form.reset(); 48 | }); 49 | 50 | 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/app/edit-course-dialog/edit-course-dialog.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .mat-form-field { 4 | display: block; 5 | } 6 | 7 | .promo { 8 | margin-bottom: 20px; 9 | } 10 | 11 | textarea { 12 | height: 100px; 13 | resize: vertical; 14 | } 15 | 16 | mat-dialog-actions { 17 | display: flex; 18 | justify-content: flex-end; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/edit-course-dialog/edit-course-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 |

Edit Course

3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | Course in promotion? 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/app/edit-course-dialog/edit-course-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; 3 | import {Course} from "../model/course"; 4 | import {FormBuilder, Validators, FormGroup} from "@angular/forms"; 5 | import {AngularFireStorage} from '@angular/fire/storage'; 6 | import {Observable} from 'rxjs'; 7 | import {CoursesService} from "../services/courses.service"; 8 | 9 | 10 | @Component({ 11 | selector: 'edit-course-dialog', 12 | templateUrl: './edit-course-dialog.component.html', 13 | styleUrls: ['./edit-course-dialog.component.css'] 14 | }) 15 | export class EditCourseDialogComponent { 16 | 17 | form:FormGroup; 18 | 19 | course: Course; 20 | 21 | constructor( 22 | private dialogRef: MatDialogRef, 23 | private fb: FormBuilder, 24 | @Inject(MAT_DIALOG_DATA) course: Course, 25 | private coursesService: CoursesService 26 | ) { 27 | 28 | this.course = course; 29 | 30 | this.form = this.fb.group({ 31 | description: [course.description, Validators.required], 32 | longDescription: [course.longDescription, Validators.required], 33 | promo: [course.promo] 34 | }); 35 | 36 | } 37 | 38 | close() { 39 | this.dialogRef.close(); 40 | 41 | } 42 | 43 | save() { 44 | 45 | const changes = this.form.value; 46 | 47 | this.coursesService.updateCourse(this.course.id, changes) 48 | .subscribe(() => { 49 | 50 | this.dialogRef.close(changes); 51 | 52 | }); 53 | 54 | 55 | } 56 | } 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/app/home/home.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .courses-panel { 4 | max-width: 400px; 5 | margin: 20px auto 0 auto; 6 | } 7 | 8 | 9 | .header { 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .title { 16 | text-align: center; 17 | margin-right: 15px; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |

All Courses

7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Course} from '../model/course'; 3 | import {Observable, of} from 'rxjs'; 4 | import {catchError, map} from 'rxjs/operators'; 5 | import {AngularFirestore} from '@angular/fire/firestore'; 6 | import {Router} from '@angular/router'; 7 | import {CoursesService} from "../services/courses.service"; 8 | import {UserService} from "../services/user.service"; 9 | 10 | 11 | @Component({ 12 | selector: 'home', 13 | templateUrl: './home.component.html', 14 | styleUrls: ['./home.component.css'] 15 | }) 16 | export class HomeComponent implements OnInit { 17 | 18 | beginnersCourses$: Observable; 19 | 20 | advancedCourses$: Observable; 21 | 22 | constructor( 23 | private router: Router, 24 | private coursesService: CoursesService, 25 | public user: UserService) { 26 | 27 | } 28 | 29 | ngOnInit() { 30 | 31 | this.reloadCourses(); 32 | 33 | } 34 | 35 | reloadCourses() { 36 | 37 | this.beginnersCourses$ = this.coursesService.loadCoursesByCategory("BEGINNER"); 38 | 39 | this.advancedCourses$ = this.coursesService.loadCoursesByCategory("ADVANCED"); 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .auth-container { 3 | 4 | } 5 | 6 | 7 | .login { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | margin-top: 40px; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, NgZone, OnDestroy, OnInit} from '@angular/core'; 2 | import * as firebaseui from 'firebaseui'; 3 | import {AngularFireAuth} from '@angular/fire/auth'; 4 | import {Router} from '@angular/router'; 5 | import firebase from 'firebase/app'; 6 | import EmailAuthProvider = firebase.auth.EmailAuthProvider; 7 | import GoogleAuthProvider = firebase.auth.GoogleAuthProvider; 8 | 9 | 10 | @Component({ 11 | selector: 'login', 12 | templateUrl: './login.component.html', 13 | styleUrls: ['./login.component.scss'] 14 | }) 15 | export class LoginComponent implements OnInit, OnDestroy { 16 | 17 | ui: firebaseui.auth.AuthUI; 18 | 19 | constructor( 20 | private afAuth: AngularFireAuth, 21 | private router: Router) { 22 | 23 | } 24 | 25 | ngOnInit() { 26 | 27 | this.afAuth.app.then(app => { 28 | const uiConfig = { 29 | signInOptions: [ 30 | EmailAuthProvider.PROVIDER_ID, 31 | GoogleAuthProvider.PROVIDER_ID 32 | ], 33 | callbacks: { 34 | signInSuccessWithAuthResult: this.onLoginSuccessful.bind(this) 35 | } 36 | }; 37 | 38 | this.ui = new firebaseui.auth.AuthUI(app.auth()); 39 | 40 | this.ui.start("#firebaseui-auth-container", uiConfig); 41 | 42 | this.ui.disableAutoSignIn(); 43 | }); 44 | 45 | 46 | } 47 | 48 | ngOnDestroy() { 49 | this.ui.delete(); 50 | } 51 | 52 | onLoginSuccessful(result) { 53 | 54 | console.log('Firebase UI result:', result); 55 | 56 | this.router.navigateByUrl("/courses"); 57 | 58 | 59 | 60 | } 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/app/model/course.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import Timestamp = firebase.firestore.Timestamp; 3 | 4 | export interface Course { 5 | id: string; 6 | description: string; 7 | url:string; 8 | longDescription: string; 9 | iconUrl: string; 10 | seqNo:number; 11 | categories: string[]; 12 | lessonsCount: number; 13 | promo:boolean; 14 | promoStartAt: Timestamp; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/app/model/lesson.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface Lesson { 4 | id: number; 5 | description: string; 6 | duration: string; 7 | seqNo: number; 8 | courseId: number; 9 | } -------------------------------------------------------------------------------- /src/app/model/user-roles.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface UserRoles { 4 | admin:boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/services/auth-token.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {AngularFireAuth} from "@angular/fire/auth"; 3 | 4 | 5 | @Injectable({ 6 | providedIn: "root" 7 | }) 8 | export class AuthTokenService { 9 | 10 | authJwtToken:string; 11 | 12 | constructor(private afAuth: AngularFireAuth) { 13 | afAuth.idToken 14 | .subscribe(jwt => this.authJwtToken = jwt); 15 | } 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/services/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 | import {AuthTokenService} from "./auth-token.service"; 5 | 6 | 7 | @Injectable() 8 | export class AuthInterceptor implements HttpInterceptor { 9 | 10 | constructor(private token: AuthTokenService) { 11 | 12 | } 13 | 14 | 15 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 16 | 17 | if (this.token.authJwtToken) { 18 | 19 | const cloned = req.clone({ 20 | headers: req.headers 21 | .set("Authorization", this.token.authJwtToken) 22 | }); 23 | 24 | return next.handle(cloned); 25 | } 26 | else { 27 | return next.handle(req); 28 | } 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/services/course.resolver.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from "@angular/router"; 3 | import {Course} from "../model/course"; 4 | import {CoursesService} from "./courses.service"; 5 | import {Observable} from "rxjs"; 6 | 7 | @Injectable({ 8 | providedIn: "root" 9 | }) 10 | export class CourseResolver implements Resolve{ 11 | 12 | constructor(private coursesService: CoursesService) {} 13 | 14 | resolve(route: ActivatedRouteSnapshot, 15 | state: RouterStateSnapshot): Observable { 16 | 17 | const courseUrl = route.paramMap.get("courseUrl"); 18 | 19 | return this.coursesService.findCourseByUrl(courseUrl); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/services/courses.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {AngularFirestore} from "@angular/fire/firestore"; 3 | import {from, Observable, of} from "rxjs"; 4 | import {Course} from "../model/course"; 5 | import {concatMap, map, tap} from "rxjs/operators"; 6 | import {convertSnaps} from "./db-utils"; 7 | import {Lesson} from "../model/lesson"; 8 | 9 | import firebase from "firebase"; 10 | import OrderByDirection = firebase.firestore.OrderByDirection; 11 | 12 | 13 | @Injectable({ 14 | providedIn: "root" 15 | }) 16 | export class CoursesService { 17 | 18 | constructor(private db: AngularFirestore) { 19 | 20 | } 21 | 22 | findLessons(courseId:string, sortOrder: OrderByDirection = 'asc', 23 | pageNumber = 0, pageSize = 3): Observable { 24 | return this.db.collection(`courses/${courseId}/lessons`, 25 | ref => ref.orderBy("seqNo",sortOrder) 26 | .limit(pageSize) 27 | .startAfter(pageNumber * pageSize) 28 | ) 29 | .get() 30 | .pipe( 31 | map(results => convertSnaps(results)) 32 | ) 33 | } 34 | 35 | findCourseByUrl(courseUrl: string): Observable { 36 | return this.db.collection("courses", 37 | ref => ref.where("url", "==", courseUrl)) 38 | .get() 39 | .pipe( 40 | map(results => { 41 | 42 | const courses = convertSnaps(results); 43 | 44 | return courses.length == 1 ? courses[0] : null; 45 | 46 | }) 47 | ); 48 | } 49 | 50 | deleteCourseAndLessons(courseId:string) { 51 | return this.db.collection(`courses/${courseId}/lessons`) 52 | .get() 53 | .pipe( 54 | concatMap(results => { 55 | 56 | const lessons = convertSnaps(results); 57 | 58 | const batch = this.db.firestore.batch(); 59 | 60 | const courseRef = this.db.doc(`courses/${courseId}`).ref; 61 | 62 | batch.delete(courseRef); 63 | 64 | for (let lesson of lessons) { 65 | const lessonRef = 66 | this.db.doc(`courses/${courseId}/lessons/${lesson.id}`).ref; 67 | 68 | batch.delete(lessonRef); 69 | } 70 | 71 | return from(batch.commit()); 72 | 73 | }) 74 | ); 75 | } 76 | 77 | deleteCourse(courseId:string) { 78 | return from(this.db.doc(`courses/${courseId}`).delete()); 79 | } 80 | 81 | updateCourse(courseId:string, changes: Partial):Observable { 82 | return from(this.db.doc(`courses/${courseId}`).update(changes)); 83 | } 84 | 85 | createCourse(newCourse: Partial, courseId?:string) { 86 | return this.db.collection("courses", 87 | ref => ref.orderBy("seqNo", "desc").limit(1)) 88 | .get() 89 | .pipe( 90 | concatMap(result => { 91 | 92 | const courses = convertSnaps(result); 93 | 94 | const lastCourseSeqNo = courses[0]?.seqNo ?? 0; 95 | 96 | const course = { 97 | ...newCourse, 98 | seqNo: lastCourseSeqNo + 1 99 | } 100 | 101 | let save$: Observable; 102 | 103 | if (courseId) { 104 | save$ = from(this.db.doc(`courses/${courseId}`).set(course)); 105 | } 106 | else { 107 | save$ = from(this.db.collection("courses").add(course)); 108 | } 109 | 110 | return save$ 111 | .pipe( 112 | map(res => { 113 | return { 114 | id: courseId ?? res.id, 115 | ...course 116 | } 117 | }) 118 | ); 119 | 120 | 121 | }) 122 | ) 123 | } 124 | 125 | loadCoursesByCategory(category:string): Observable { 126 | return this.db.collection( 127 | "courses", 128 | ref => ref.where("categories", "array-contains", category) 129 | .orderBy("seqNo") 130 | ) 131 | .get() 132 | .pipe( 133 | map(result => convertSnaps(result)) 134 | ); 135 | 136 | } 137 | 138 | } 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/app/services/db-utils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function convertSnaps(results) { 4 | return results.docs.map(snap => { 5 | return { 6 | id: snap.id, 7 | ...snap.data() 8 | } 9 | }) 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {Observable} from "rxjs"; 3 | import {AngularFireAuth} from "@angular/fire/auth"; 4 | import {map} from "rxjs/operators"; 5 | import {Router} from "@angular/router"; 6 | import {UserRoles} from "../model/user-roles"; 7 | 8 | 9 | @Injectable({ 10 | providedIn: "root" 11 | }) 12 | export class UserService { 13 | 14 | isLoggedIn$ : Observable; 15 | 16 | isLoggedOut$: Observable; 17 | 18 | pictureUrl$: Observable; 19 | 20 | roles$ : Observable; 21 | 22 | constructor( 23 | private afAuth: AngularFireAuth, 24 | private router: Router) { 25 | 26 | this.isLoggedIn$ = afAuth.authState.pipe(map(user => !!user)); 27 | 28 | this.isLoggedOut$ = this.isLoggedIn$.pipe(map(loggedIn => !loggedIn)); 29 | 30 | this.pictureUrl$ = 31 | afAuth.authState.pipe(map(user => user? user.photoURL : null)); 32 | 33 | this.roles$ = this.afAuth.idTokenResult 34 | .pipe( 35 | map(token => token?.claims ?? {admin:false}) 36 | ) 37 | 38 | } 39 | 40 | 41 | logout() { 42 | this.afAuth.signOut(); 43 | this.router.navigateByUrl('/login'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/firebase-course/9ed1525d4ced3a6476baad0be2d16cd6c9da7143/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | useEmulators: false, 4 | firebase: { 5 | apiKey: "AIzaSyB9LOREMGhj1jpVXOHTKIwQu2oM7pVfjQg", 6 | authDomain: "fir-course-recording-c7f3e.firebaseapp.com", 7 | projectId: "fir-course-recording-c7f3e", 8 | storageBucket: "fir-course-recording-c7f3e.appspot.com", 9 | messagingSenderId: "927953565493", 10 | appId: "1:927953565493:web:0d4a8e79cc45fd38733e7c" 11 | }, 12 | api: { 13 | createUser: " https://us-central1-fir-course-recording-c7f3e.cloudfunctions.net/createUser" 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | useEmulators: true, 8 | firebase: { 9 | apiKey: "AIzaSyB9LOREMGhj1jpVXOHTKIwQu2oM7pVfjQg", 10 | authDomain: "fir-course-recording-c7f3e.firebaseapp.com", 11 | projectId: "fir-course-recording-c7f3e", 12 | storageBucket: "fir-course-recording-c7f3e.appspot.com", 13 | messagingSenderId: "927953565493", 14 | appId: "1:927953565493:web:0d4a8e79cc45fd38733e7c" 15 | }, 16 | api: { 17 | createUser: "http://localhost:5001/fir-course-recording-c7f3e/us-central1/createUser" 18 | } 19 | }; 20 | 21 | /* 22 | * For easier debugging in development mode, you can import the following file 23 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 24 | * 25 | * This import should be commented out in production mode because it will have a negative impact 26 | * on performance if an error is thrown. 27 | */ 28 | import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 29 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/firebase-course/9ed1525d4ced3a6476baad0be2d16cd6c9da7143/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Firebase & AngularFire In Depth Course 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | 4 | @use '@angular/material' as mat; 5 | @import "~firebaseui/dist/firebaseui.css"; 6 | 7 | 8 | 9 | // Include non-theme styles for core. 10 | @include mat.core(); 11 | 12 | $mat-primary: ( 13 | 50: #e3f2fd, 14 | 100: #bbdefb, 15 | 200: #90caf9, 16 | 300: #64b5f6, 17 | 400: #42a5f5, 18 | 500: #1a66d2, 19 | 600: #1e88e5, 20 | 700: #1976d2, 21 | 800: #1565c0, 22 | 900: #0d47a1, 23 | A100: #82b1ff, 24 | A200: #448aff, 25 | A400: #2979ff, 26 | A700: #2962ff, 27 | contrast: ( 28 | 50: rgba(black, 0.87), 29 | 100: rgba(black, 0.87), 30 | 200: rgba(black, 0.87), 31 | 300: rgba(black, 0.87), 32 | 400: rgba(black, 0.87), 33 | 500: white, 34 | 600: white, 35 | 700: white, 36 | 800: white, 37 | 900: white, 38 | A100: rgba(black, 0.87), 39 | A200: white, 40 | A400: white, 41 | A700: white, 42 | ) 43 | ); 44 | 45 | $mat-accent: ( 46 | 50: #fff3e0, 47 | 100: #ffe0b2, 48 | 200: #ffcc80, 49 | 300: #ffb74d, 50 | 400: #ffa726, 51 | 500: #f6830f, 52 | 600: #fb8c00, 53 | 700: #f57c00, 54 | 800: #ef6c00, 55 | 900: #e65100, 56 | A100: #ffd180, 57 | A200: #ffab40, 58 | A400: #ff9100, 59 | A700: #ff6d00, 60 | contrast: ( 61 | 50: rgba(black, 0.87), 62 | 100: rgba(black, 0.87), 63 | 200: rgba(black, 0.87), 64 | 300: rgba(black, 0.87), 65 | 400: rgba(black, 0.87), 66 | 500: white, 67 | 600: white, 68 | 700: white, 69 | 800: white, 70 | 900: white, 71 | A100: rgba(black, 0.87), 72 | A200: white, 73 | A400: white, 74 | A700: white, 75 | ) 76 | ); 77 | 78 | // Define a theme. 79 | $primary: mat.define-palette($mat-primary); 80 | $accent: mat.define-palette($mat-accent, A200, A100, A400); 81 | 82 | $theme: mat.define-light-theme($primary, $accent); 83 | 84 | // Include all theme styles for the components. 85 | @include mat.all-component-themes($theme); 86 | -------------------------------------------------------------------------------- /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/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), { 21 | teardown: { destroyAfterEach: false } 22 | } 23 | ); 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | service firebase.storage { 4 | 5 | match /b/{bucket}/o { 6 | 7 | match /courses/{courseId}/{fileName} { 8 | 9 | allow read; 10 | 11 | allow write: if request.auth != null && 12 | request.resource.size < 5 * 1024 * 1024; 13 | 14 | } 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /test-data/auth_export/accounts.json: -------------------------------------------------------------------------------- 1 | {"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"4V0e27zS0dQ6AoFNtIapohrChOzv","createdAt":"1612970345479","lastLoginAt":"1613137733169","displayName":"Angular University","photoUrl":"https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png","customAttributes":"{\n \"admin\":true\n}","providerUserInfo":[{"providerId":"google.com","rawId":"8703414989311999201251387704843473343030","federatedId":"8703414989311999201251387704843473343030","displayName":"Angular University","photoUrl":"https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png","email":"admin@angular-university.io","screenName":"Angular University"}],"validSince":"1614268882","email":"admin@angular-university.io","emailVerified":true,"disabled":false},{"localId":"JOwLIhIJBxQzTU6CMRIypJu5IJ6S","createdAt":"1612969718879","lastLoginAt":"1612969718878","displayName":"Angular University","photoUrl":"https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png","passwordHash":"fakeHash:salt=fakeSaltHYqFvM2JP5F1aii3NItJ:password=angularuniv","salt":"fakeSaltHYqFvM2JP5F1aii3NItJ","passwordUpdatedAt":1614268882224,"customAttributes":"{\n \"admin\":true\n}","providerUserInfo":[{"providerId":"password","email":"helpdesk@angular-university.io","federatedId":"helpdesk@angular-university.io","rawId":"helpdesk@angular-university.io","displayName":"Angular University","photoUrl":"https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png"}],"validSince":"1614268882","email":"helpdesk@angular-university.io","emailVerified":false,"disabled":false},{"localId":"TS7PEYLl3l3jgGDtjhkdoE8k2d29","createdAt":"1613128562900","lastLoginAt":"1613128562900","displayName":"Unknown","photoUrl":"","passwordHash":"fakeHash:salt=fakeSalt93nbUORHCt96MlAlEfDK:password=unknown","salt":"fakeSalt93nbUORHCt96MlAlEfDK","passwordUpdatedAt":1614268882224,"providerUserInfo":[{"providerId":"password","email":"unknown@angular-university.io","federatedId":"unknown@angular-university.io","rawId":"unknown@angular-university.io","displayName":"Unknown","photoUrl":""}],"validSince":"1614268882","email":"unknown@angular-university.io","emailVerified":false,"disabled":false},{"localId":"fAzjbeq9zIa8LHyFH6z2HpGCUBpW","createdAt":"1613128293265","lastLoginAt":"1613128293264","displayName":"Student","photoUrl":"https://lh3.googleusercontent.com/-1pUNnTB3vaA/AAAAAAAAAAI/AAAAAAAAAAA/AMZuuclQ_JHkVP2OIxrltQ7ddz6TwJ0Z-A/s48-c/photo.jpg","passwordHash":"fakeHash:salt=fakeSalt8vqD5cG4qOHoL1uTWl68:password=student","salt":"fakeSalt8vqD5cG4qOHoL1uTWl68","passwordUpdatedAt":1614268882224,"providerUserInfo":[{"providerId":"password","email":"student@angular-university.io","federatedId":"student@angular-university.io","rawId":"student@angular-university.io","displayName":"Student","photoUrl":"https://lh3.googleusercontent.com/-1pUNnTB3vaA/AAAAAAAAAAI/AAAAAAAAAAA/AMZuuclQ_JHkVP2OIxrltQ7ddz6TwJ0Z-A/s48-c/photo.jpg"}],"validSince":"1614268882","email":"student@angular-university.io","emailVerified":false,"disabled":false}]} -------------------------------------------------------------------------------- /test-data/auth_export/config.json: -------------------------------------------------------------------------------- 1 | {"signIn":{"allowDuplicateEmails":false}} -------------------------------------------------------------------------------- /test-data/firebase-export-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "9.3.0", 3 | "firestore": { 4 | "version": "1.11.12", 5 | "path": "firestore_export", 6 | "metadata_file": "firestore_export/firestore_export.overall_export_metadata" 7 | }, 8 | "auth": { 9 | "version": "9.3.0", 10 | "path": "auth_export" 11 | } 12 | } -------------------------------------------------------------------------------- /test-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/firebase-course/9ed1525d4ced3a6476baad0be2d16cd6c9da7143/test-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata -------------------------------------------------------------------------------- /test-data/firestore_export/all_namespaces/all_kinds/output-0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/firebase-course/9ed1525d4ced3a6476baad0be2d16cd6c9da7143/test-data/firestore_export/all_namespaces/all_kinds/output-0 -------------------------------------------------------------------------------- /test-data/firestore_export/firestore_export.overall_export_metadata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/firebase-course/9ed1525d4ced3a6476baad0be2d16cd6c9da7143/test-data/firestore_export/firestore_export.overall_export_metadata -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "enableI18nLegacyMessageIdFormat": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 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-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | --------------------------------------------------------------------------------