├── backend ├── src │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ └── auth.py │ ├── database │ │ ├── __init__.py │ │ ├── database.db │ │ └── models.py │ ├── .env │ └── api.py ├── requirements.txt ├── README.md └── udacity-fsnd-udaspicelatte.postman_collection.json ├── frontend ├── src │ ├── app │ │ ├── pages │ │ │ ├── tabs │ │ │ │ ├── tabs.page.scss │ │ │ │ ├── tabs.page.ts │ │ │ │ ├── tabs.page.html │ │ │ │ ├── tabs.module.ts │ │ │ │ ├── tabs.page.spec.ts │ │ │ │ └── tabs.router.module.ts │ │ │ ├── user-page │ │ │ │ ├── user-page.page.scss │ │ │ │ ├── user-page.page.ts │ │ │ │ ├── user-page.page.html │ │ │ │ ├── user-page.module.ts │ │ │ │ └── user-page.page.spec.ts │ │ │ └── drink-menu │ │ │ │ ├── drink-form │ │ │ │ ├── drink-form.component.scss │ │ │ │ ├── drink-form.component.spec.ts │ │ │ │ ├── drink-form.component.ts │ │ │ │ └── drink-form.component.html │ │ │ │ ├── drink-menu.page.scss │ │ │ │ ├── drink-graphic │ │ │ │ ├── drink-graphic.component.html │ │ │ │ ├── drink-graphic.component.scss │ │ │ │ ├── drink-graphic.component.ts │ │ │ │ └── drink-graphic.component.spec.ts │ │ │ │ ├── drink-menu.page.spec.ts │ │ │ │ ├── drink-menu.module.ts │ │ │ │ ├── drink-menu.page.ts │ │ │ │ └── drink-menu.page.html │ │ ├── app.component.html │ │ ├── app-routing.module.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.component.spec.ts │ │ └── services │ │ │ ├── auth.service.ts │ │ │ └── drinks.service.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── assets │ │ ├── icon │ │ │ └── favicon.png │ │ └── shapes.svg │ ├── zone-flags.ts │ ├── tsconfig.app.json │ ├── tslint.json │ ├── tsconfig.spec.json │ ├── main.ts │ ├── global.scss │ ├── test.ts │ ├── index.html │ ├── karma.conf.js │ ├── theme │ │ └── variables.scss │ └── polyfills.ts ├── ionic.config.json ├── e2e │ ├── src │ │ ├── app.po.ts │ │ └── app.e2e-spec.ts │ ├── tsconfig.e2e.json │ └── protractor.conf.js ├── tsconfig.json ├── .gitignore ├── tslint.json ├── package.json ├── README.md └── angular.json ├── .gitignore └── README.md /backend/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/tabs/tabs.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/pages/user-page/user-page.page.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-form/drink-form.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "udaspicelatte", 3 | "integrations": {}, 4 | "type": "angular" 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/database/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denzubechi/CoffeeShop-FullStack/HEAD/backend/src/database/database.db -------------------------------------------------------------------------------- /backend/src/.env: -------------------------------------------------------------------------------- 1 | 2 | AUTH0_CLIENT_ID=1r6x6J4TthdI770wddL0sT7I6t6J8VJ7 3 | AUTH0_DOMAIN=coffeeshopf.us.auth0.com 4 | ALGORITHMS=['RS256'] 5 | -------------------------------------------------------------------------------- /frontend/src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denzubechi/CoffeeShop-FullStack/HEAD/frontend/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /frontend/src/zone-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents Angular change detection from 3 | * running with certain Web Component callbacks 4 | */ 5 | (window as any).__Zone_disable_customElements = true; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .vscode/ 3 | __pycache__/ 4 | test.db 5 | 6 | # OS generated files # 7 | ###################### 8 | .DS_Store 9 | .DS_Store? 10 | ._* 11 | .Spotlight-V100 12 | .Trashes 13 | ehthumbs.db 14 | Thumbs.db -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-menu.page.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Playfair+Display:400,900&display=swap'); 2 | ion-title { 3 | font-family: 'Playfair Display', serif; 4 | font-size: 1.7em; 5 | } -------------------------------------------------------------------------------- /frontend/src/app/pages/tabs/tabs.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tabs', 5 | templateUrl: 'tabs.page.html', 6 | styleUrls: ['tabs.page.scss'] 7 | }) 8 | export class TabsPage {} 9 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getPageTitle() { 9 | return element(by.css('ion-title')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-graphic/drink-graphic.component.html: -------------------------------------------------------------------------------- 1 |
2 |
7 | {{t}} 8 |
9 |
-------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | astroid 2 | Click 3 | ecdsa 4 | Flask 5 | Flask-SQLAlchemy 6 | future 7 | isort 8 | itsdangerous 9 | Jinja2 10 | lazy-object-proxy 11 | MarkupSafe 12 | mccabe 13 | pycryptodome 14 | pylint 15 | python-jose-cryptodome 16 | six 17 | typed-ast 18 | Werkzeug 19 | wrapt 20 | Flask-Cors 21 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-graphic/drink-graphic.component.scss: -------------------------------------------------------------------------------- 1 | .cup { 2 | width: 50px; 3 | height: 50px; 4 | display: flex; 5 | flex-direction: column; 6 | border-radius: 7px; 7 | overflow: hidden; 8 | } 9 | 10 | .ingredient { 11 | width: 50px; 12 | flex-grow: 1; 13 | } -------------------------------------------------------------------------------- /frontend/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "page", 15 | "kebab-case" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getPageTitle()).toContain('Tab One'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "zone-flags.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /frontend/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.log(err)); 13 | -------------------------------------------------------------------------------- /frontend/src/app/pages/tabs/tabs.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Drink Menu 7 | 8 | 9 | 10 | 11 | User 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { path: '', loadChildren: './pages/tabs/tabs.module#TabsPageModule' }, 6 | ]; 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) 10 | ], 11 | exports: [RouterModule] 12 | }) 13 | export class AppRoutingModule {} 14 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-graphic/drink-graphic.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { Drink } from 'src/app/services/drinks.service'; 3 | 4 | @Component({ 5 | selector: 'app-drink-graphic', 6 | templateUrl: './drink-graphic.component.html', 7 | styleUrls: ['./drink-graphic.component.scss'], 8 | }) 9 | export class DrinkGraphicComponent implements OnInit { 10 | @Input() drink: Drink; 11 | 12 | constructor() { } 13 | 14 | ngOnInit() {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/app/pages/user-page/user-page.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../../services/auth.service'; 3 | 4 | @Component({ 5 | selector: 'app-user-page', 6 | templateUrl: './user-page.page.html', 7 | styleUrls: ['./user-page.page.scss'], 8 | }) 9 | export class UserPagePage implements OnInit { 10 | loginURL: string; 11 | 12 | constructor(public auth: AuthService) { 13 | this.loginURL = auth.build_login_link('/tabs/user-page'); 14 | } 15 | 16 | ngOnInit() { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/global.scss: -------------------------------------------------------------------------------- 1 | // http://ionicframework.com/docs/theming/ 2 | @import '~@ionic/angular/css/core.css'; 3 | @import '~@ionic/angular/css/normalize.css'; 4 | @import '~@ionic/angular/css/structure.css'; 5 | @import '~@ionic/angular/css/typography.css'; 6 | @import '~@ionic/angular/css/display.css'; 7 | @import '~@ionic/angular/css/padding.css'; 8 | @import '~@ionic/angular/css/float-elements.css'; 9 | @import '~@ionic/angular/css/text-alignment.css'; 10 | @import '~@ionic/angular/css/text-transformation.css'; 11 | @import '~@ionic/angular/css/flex-utils.css'; 12 | -------------------------------------------------------------------------------- /frontend/src/app/pages/tabs/tabs.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | 6 | import { TabsPageRoutingModule } from './tabs.router.module'; 7 | 8 | import { TabsPage } from './tabs.page'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | IonicModule, 13 | CommonModule, 14 | FormsModule, 15 | TabsPageRoutingModule 16 | ], 17 | declarations: [TabsPage] 18 | }) 19 | export class TabsPageModule {} 20 | -------------------------------------------------------------------------------- /frontend/src/app/pages/user-page/user-page.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | user-page 4 | 5 | 6 | 7 | 8 | Log In 9 | 10 | 11 | Log Out 12 | 13 | Active JWT 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .ionic/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | www/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | $RECYCLE.BIN/ 31 | 32 | .DS_Store 33 | Thumbs.db 34 | UserInterfaceState.xcuserstate 35 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | /* @TODO replace with your variables 2 | * ensure all variables on this page match your project 3 | */ 4 | 5 | export const environment = { 6 | production: false, 7 | apiServerUrl: 'http://127.0.0.1:5000', // the running FLASK api server url 8 | auth0: { 9 | url: 'coffeeshopf', // the auth0 domain prefix 10 | audience: 'coffee', // the audience set for the/your auth0 app 11 | clientId: 'zzUoxZT1mmHkLHC2eQ15Qa5wkWXkQwRD', // the client id generated for the/your auth0 app 12 | callbackURL: 'https://127.0.0.1:8100', // the base url of the running ionic application. 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coffee Shop Full Stack 2 | 3 | ## Full Stack Nano - IAM Final Project 4 | 5 | Udacity has decided to open a new digitally enabled cafe for students to order drinks, socialize, and study hard. But they need help setting up their menu experience. 6 | 7 | You have been called on to demonstrate your newly learned skills to create a full stack drink menu application. The application must: 8 | 9 | 1. Display graphics representing the ratios of ingredients in each drink. 10 | 2. Allow public users to view drink names and graphics. 11 | 3. Allow the shop baristas to see the recipe information. 12 | 4. Allow the shop managers to create new drinks and edit existing 13 | -------------------------------------------------------------------------------- /frontend/src/app/pages/user-page/user-page.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { UserPagePage } from './user-page.page'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: UserPagePage 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | FormsModule, 21 | IonicModule, 22 | RouterModule.forChild(routes) 23 | ], 24 | declarations: [UserPagePage] 25 | }) 26 | export class UserPagePageModule {} 27 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/app/pages/tabs/tabs.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { TabsPage } from './tabs.page'; 5 | 6 | describe('TabsPage', () => { 7 | let component: TabsPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [TabsPage], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(TabsPage); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /frontend/src/app/pages/user-page/user-page.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { UserPagePage } from './user-page.page'; 5 | 6 | describe('UserPagePage', () => { 7 | let component: UserPagePage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ UserPagePage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(UserPagePage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-menu.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { DrinkMenuPage } from './drink-menu.page'; 5 | 6 | describe('DrinkMenuPage', () => { 7 | let component: DrinkMenuPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ DrinkMenuPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(DrinkMenuPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-form/drink-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { DrinkFormComponent } from './drink-form.component'; 5 | 6 | describe('DrinkFormComponent', () => { 7 | let component: DrinkFormComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ DrinkFormComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(DrinkFormComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /frontend/src/app/pages/tabs/tabs.router.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { TabsPage } from './tabs.page'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'tabs', 8 | component: TabsPage, 9 | children: [ 10 | { path: 'drink-menu', loadChildren: '../drink-menu/drink-menu.module#DrinkMenuPageModule' }, 11 | { path: 'user-page', loadChildren: '../user-page/user-page.module#UserPagePageModule' }, 12 | { 13 | path: '', 14 | redirectTo: '/tabs/drink-menu', 15 | pathMatch: 'full' 16 | } 17 | ] 18 | }, 19 | { 20 | path: '', 21 | redirectTo: '/tabs/drink-menu', 22 | pathMatch: 'full' 23 | } 24 | ]; 25 | 26 | @NgModule({ 27 | imports: [ 28 | RouterModule.forChild(routes) 29 | ], 30 | exports: [RouterModule] 31 | }) 32 | export class TabsPageRoutingModule {} 33 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-graphic/drink-graphic.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { DrinkGraphicComponent } from './drink-graphic.component'; 5 | 6 | describe('DrinkGraphicComponent', () => { 7 | let component: DrinkGraphicComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ DrinkGraphicComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(DrinkGraphicComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-menu.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { DrinkMenuPage } from './drink-menu.page'; 9 | import { DrinkGraphicComponent } from './drink-graphic/drink-graphic.component'; 10 | import { DrinkFormComponent } from './drink-form/drink-form.component'; 11 | 12 | const routes: Routes = [ 13 | { 14 | path: '', 15 | component: DrinkMenuPage 16 | } 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [ 21 | CommonModule, 22 | FormsModule, 23 | IonicModule, 24 | RouterModule.forChild(routes) 25 | ], 26 | entryComponents: [DrinkFormComponent], 27 | declarations: [DrinkMenuPage, DrinkGraphicComponent, DrinkFormComponent], 28 | }) 29 | export class DrinkMenuPageModule {} 30 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { Platform } from '@ionic/angular'; 4 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 5 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 6 | import { AuthService } from './services/auth.service'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: 'app.component.html' 11 | }) 12 | export class AppComponent { 13 | constructor( 14 | private auth: AuthService, 15 | private platform: Platform, 16 | private splashScreen: SplashScreen, 17 | private statusBar: StatusBar 18 | ) { 19 | this.initializeApp(); 20 | } 21 | 22 | initializeApp() { 23 | this.platform.ready().then(() => { 24 | this.statusBar.styleDefault(); 25 | this.splashScreen.hide(); 26 | 27 | // Perform required auth actions 28 | this.auth.load_jwts(); 29 | this.auth.check_token_fragment(); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/src/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-menu.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DrinksService, Drink } from '../../services/drinks.service'; 3 | import { ModalController } from '@ionic/angular'; 4 | import { DrinkFormComponent } from './drink-form/drink-form.component'; 5 | import { AuthService } from 'src/app/services/auth.service'; 6 | 7 | @Component({ 8 | selector: 'app-drink-menu', 9 | templateUrl: './drink-menu.page.html', 10 | styleUrls: ['./drink-menu.page.scss'], 11 | }) 12 | export class DrinkMenuPage implements OnInit { 13 | Object = Object; 14 | 15 | constructor( 16 | private auth: AuthService, 17 | private modalCtrl: ModalController, 18 | public drinks: DrinksService 19 | ) { } 20 | 21 | ngOnInit() { 22 | this.drinks.getDrinks(); 23 | } 24 | 25 | async openForm(activedrink: Drink = null) { 26 | if (!this.auth.can('get:drinks-detail')) { 27 | return; 28 | } 29 | 30 | const modal = await this.modalCtrl.create({ 31 | component: DrinkFormComponent, 32 | componentProps: { drink: activedrink, isNew: !activedrink } 33 | }); 34 | 35 | modal.present(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouteReuseStrategy } from '@angular/router'; 4 | 5 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 6 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 7 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 8 | 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { AppComponent } from './app.component'; 11 | 12 | import { DrinksService } from './services/drinks.service'; 13 | import { AuthService } from './services/auth.service'; 14 | 15 | import { HttpClientModule } from '@angular/common/http'; 16 | 17 | import { from } from 'rxjs'; 18 | 19 | @NgModule({ 20 | declarations: [AppComponent], 21 | entryComponents: [], 22 | imports: [ 23 | BrowserModule, 24 | IonicModule.forRoot(), 25 | HttpClientModule, 26 | AppRoutingModule 27 | ], 28 | providers: [ 29 | StatusBar, 30 | SplashScreen, 31 | AuthService, 32 | DrinksService, 33 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } 34 | ], 35 | bootstrap: [AppComponent] 36 | }) 37 | export class AppModule {} 38 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-menu.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Uda-Spice Latte Cafe 4 | 5 | 6 | 7 | 8 |
9 | 13 | 14 | {{drinks.items[drink].title}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | Create Drink 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-form/drink-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { Drink, DrinksService } from 'src/app/services/drinks.service'; 3 | import { ModalController } from '@ionic/angular'; 4 | import { AuthService } from 'src/app/services/auth.service'; 5 | 6 | @Component({ 7 | selector: 'app-drink-form', 8 | templateUrl: './drink-form.component.html', 9 | styleUrls: ['./drink-form.component.scss'], 10 | }) 11 | export class DrinkFormComponent implements OnInit { 12 | @Input() drink: Drink; 13 | @Input() isNew: boolean; 14 | 15 | constructor( 16 | public auth: AuthService, 17 | private modalCtrl: ModalController, 18 | private drinkService: DrinksService 19 | ) { } 20 | 21 | ngOnInit() { 22 | if (this.isNew) { 23 | this.drink = { 24 | id: -1, 25 | title: '', 26 | recipe: [] 27 | }; 28 | this.addIngredient(); 29 | } 30 | } 31 | 32 | customTrackBy(index: number, obj: any): any { 33 | return index; 34 | } 35 | 36 | addIngredient(i: number = 0) { 37 | this.drink.recipe.splice(i + 1, 0, {name: '', color: 'white', parts: 1}); 38 | } 39 | 40 | removeIngredient(i: number) { 41 | this.drink.recipe.splice(i, 1); 42 | } 43 | 44 | closeModal() { 45 | this.modalCtrl.dismiss(); 46 | } 47 | 48 | saveClicked() { 49 | this.drinkService.saveDrink(this.drink); 50 | this.closeModal(); 51 | } 52 | 53 | deleteClicked() { 54 | this.drinkService.deleteDrink(this.drink); 55 | this.closeModal(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { TestBed, async } from '@angular/core/testing'; 3 | 4 | import { Platform } from '@ionic/angular'; 5 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 6 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 7 | 8 | import { AppComponent } from './app.component'; 9 | 10 | describe('AppComponent', () => { 11 | 12 | let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy; 13 | 14 | beforeEach(async(() => { 15 | statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']); 16 | splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']); 17 | platformReadySpy = Promise.resolve(); 18 | platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy }); 19 | 20 | TestBed.configureTestingModule({ 21 | declarations: [AppComponent], 22 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 23 | providers: [ 24 | { provide: StatusBar, useValue: statusBarSpy }, 25 | { provide: SplashScreen, useValue: splashScreenSpy }, 26 | { provide: Platform, useValue: platformSpy }, 27 | ], 28 | }).compileComponents(); 29 | })); 30 | 31 | it('should create the app', () => { 32 | const fixture = TestBed.createComponent(AppComponent); 33 | const app = fixture.debugElement.componentInstance; 34 | expect(app).toBeTruthy(); 35 | }); 36 | 37 | it('should initialize the app', async () => { 38 | TestBed.createComponent(AppComponent); 39 | expect(platformSpy.ready).toHaveBeenCalled(); 40 | await platformReadySpy; 41 | expect(statusBarSpy.styleDefault).toHaveBeenCalled(); 42 | expect(splashScreenSpy.hide).toHaveBeenCalled(); 43 | }); 44 | 45 | // TODO: add more tests! 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /frontend/src/app/pages/drink-menu/drink-form/drink-form.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | New Drink Creator 4 | Drink Editor 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | Drink Title 14 | 15 | 16 | 17 | 18 | Ingredient Name 19 | 20 | 21 | Number of Parts 22 | 23 | 24 | Color 25 | 26 | 27 | Remove
28 | ADD
29 | 30 |
31 | 32 | Delete
35 | Cancel 36 | Save 39 | 40 |
41 |
-------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-use-before-declare": true, 52 | "no-var-requires": false, 53 | "object-literal-key-quotes": [ 54 | true, 55 | "as-needed" 56 | ], 57 | "object-literal-sort-keys": false, 58 | "ordered-imports": false, 59 | "quotemark": [ 60 | true, 61 | "single" 62 | ], 63 | "trailing-comma": false, 64 | "no-output-on-prefix": true, 65 | "use-input-property-decorator": true, 66 | "use-output-property-decorator": true, 67 | "use-host-property-decorator": true, 68 | "no-input-rename": true, 69 | "no-output-rename": true, 70 | "use-life-cycle-interface": true, 71 | "use-pipe-transform-interface": true, 72 | "one-variable-per-declaration": false, 73 | "component-class-suffix": [true, "Page", "Component"], 74 | "directive-class-suffix": true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coffeespicelatte", 3 | "version": "0.0.1", 4 | "author": "Ionic Framework", 5 | "homepage": "https://ionicframework.com/", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "^14.0.5", 17 | "@angular/core": "^14.0.5", 18 | "@angular/forms": "^14.0.5", 19 | "@angular/http": "^7.2.16", 20 | "@angular/platform-browser": "^14.0.5", 21 | "@angular/platform-browser-dynamic": "^14.0.5", 22 | "@angular/router": "^14.0.5", 23 | "@auth0/angular-jwt": "^5.0.2", 24 | "@ionic-native/core": "^5.0.0", 25 | "@ionic-native/splash-screen": "^5.36.0", 26 | "@ionic-native/status-bar": "^5.36.0", 27 | "@ionic/angular": "^6.1.13", 28 | "core-js": "^3.23.4", 29 | "jwt-decode": "^3.1.2", 30 | "rxjs": "~6.5.1", 31 | "tslib": "^1.9.0", 32 | "zone.js": "~0.11.4", 33 | "lodash": "^4.17.13" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/architect": "~0.1400.5", 37 | "@angular-devkit/build-angular": "~14.0.5", 38 | "@angular-devkit/core": "~14.0.5", 39 | "@angular-devkit/schematics": "~14.0.5", 40 | "@angular/cli": "~14.0.5", 41 | "@angular/compiler": "~14.0.5", 42 | "@angular/compiler-cli": "~14.0.5", 43 | "@angular/language-service": "~14.0.5", 44 | "@ionic/angular-toolkit": "~1.5.1", 45 | "@types/node": "~12.0.0", 46 | "@types/jasmine": "~2.8.8", 47 | "@types/jasminewd2": "~2.0.3", 48 | "codelyzer": "~4.5.0", 49 | "jasmine-core": "~2.99.1", 50 | "jasmine-spec-reporter": "~4.2.1", 51 | "karma": "~4.1.0", 52 | "karma-chrome-launcher": "~2.2.0", 53 | "karma-coverage-istanbul-reporter": "~2.0.1", 54 | "karma-jasmine": "~1.1.2", 55 | "karma-jasmine-html-reporter": "^0.2.2", 56 | "protractor": "~5.4.0", 57 | "ts-node": "~8.1.0", 58 | "tslint": "~5.16.0", 59 | "typescript": "~3.1.6" 60 | }, 61 | "description": "An Ionic project" 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { JwtHelperService } from '@auth0/angular-jwt'; 3 | 4 | import { environment } from '../../environments/environment'; 5 | 6 | const JWTS_LOCAL_KEY = 'JWTS_LOCAL_KEY'; 7 | const JWTS_ACTIVE_INDEX_KEY = 'JWTS_ACTIVE_INDEX_KEY'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class AuthService { 13 | url = environment.auth0.url; 14 | audience = environment.auth0.audience; 15 | clientId = environment.auth0.clientId; 16 | callbackURL = environment.auth0.callbackURL; 17 | 18 | token: string; 19 | payload: any; 20 | 21 | constructor() { } 22 | 23 | build_login_link(callbackPath = '') { 24 | let link = 'https://'; 25 | link += this.url + '.auth0.com'; 26 | link += '/authorize?'; 27 | link += 'audience=' + this.audience + '&'; 28 | link += 'response_type=token&'; 29 | link += 'client_id=' + this.clientId + '&'; 30 | link += 'redirect_uri=' + this.callbackURL + callbackPath; 31 | return link; 32 | } 33 | 34 | // invoked in app.component on load 35 | check_token_fragment() { 36 | // parse the fragment 37 | const fragment = window.location.hash.substr(1).split('&')[0].split('='); 38 | // check if the fragment includes the access token 39 | if ( fragment[0] === 'access_token' ) { 40 | // add the access token to the jwt 41 | this.token = fragment[1]; 42 | // save jwts to localstore 43 | this.set_jwt(); 44 | } 45 | } 46 | 47 | set_jwt() { 48 | localStorage.setItem(JWTS_LOCAL_KEY, this.token); 49 | if (this.token) { 50 | this.decodeJWT(this.token); 51 | } 52 | } 53 | 54 | load_jwts() { 55 | this.token = localStorage.getItem(JWTS_LOCAL_KEY) || null; 56 | if (this.token) { 57 | this.decodeJWT(this.token); 58 | } 59 | } 60 | 61 | activeJWT() { 62 | return this.token; 63 | } 64 | 65 | decodeJWT(token: string) { 66 | const jwtservice = new JwtHelperService(); 67 | this.payload = jwtservice.decodeToken(token); 68 | return this.payload; 69 | } 70 | 71 | logout() { 72 | this.token = ''; 73 | this.payload = null; 74 | this.set_jwt(); 75 | } 76 | 77 | can(permission: string) { 78 | return this.payload && this.payload.permissions && this.payload.permissions.length && this.payload.permissions.indexOf(permission) >= 0; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | /** primary **/ 7 | --ion-color-primary: #3880ff; 8 | --ion-color-primary-rgb: 56, 128, 255; 9 | --ion-color-primary-contrast: #ffffff; 10 | --ion-color-primary-contrast-rgb: 255, 255, 255; 11 | --ion-color-primary-shade: #3171e0; 12 | --ion-color-primary-tint: #4c8dff; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #0cd1e8; 16 | --ion-color-secondary-rgb: 12, 209, 232; 17 | --ion-color-secondary-contrast: #ffffff; 18 | --ion-color-secondary-contrast-rgb: 255, 255, 255; 19 | --ion-color-secondary-shade: #0bb8cc; 20 | --ion-color-secondary-tint: #24d6ea; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #7044ff; 24 | --ion-color-tertiary-rgb: 112, 68, 255; 25 | --ion-color-tertiary-contrast: #ffffff; 26 | --ion-color-tertiary-contrast-rgb: 255, 255, 255; 27 | --ion-color-tertiary-shade: #633ce0; 28 | --ion-color-tertiary-tint: #7e57ff; 29 | 30 | /** success **/ 31 | --ion-color-success: #10dc60; 32 | --ion-color-success-rgb: 16, 220, 96; 33 | --ion-color-success-contrast: #ffffff; 34 | --ion-color-success-contrast-rgb: 255, 255, 255; 35 | --ion-color-success-shade: #0ec254; 36 | --ion-color-success-tint: #28e070; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffce00; 40 | --ion-color-warning-rgb: 255, 206, 0; 41 | --ion-color-warning-contrast: #ffffff; 42 | --ion-color-warning-contrast-rgb: 255, 255, 255; 43 | --ion-color-warning-shade: #e0b500; 44 | --ion-color-warning-tint: #ffd31a; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #f04141; 48 | --ion-color-danger-rgb: 245, 61, 61; 49 | --ion-color-danger-contrast: #ffffff; 50 | --ion-color-danger-contrast-rgb: 255, 255, 255; 51 | --ion-color-danger-shade: #d33939; 52 | --ion-color-danger-tint: #f25454; 53 | 54 | /** dark **/ 55 | --ion-color-dark: #222428; 56 | --ion-color-dark-rgb: 34, 34, 34; 57 | --ion-color-dark-contrast: #ffffff; 58 | --ion-color-dark-contrast-rgb: 255, 255, 255; 59 | --ion-color-dark-shade: #1e2023; 60 | --ion-color-dark-tint: #383a3e; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #989aa2; 64 | --ion-color-medium-rgb: 152, 154, 162; 65 | --ion-color-medium-contrast: #ffffff; 66 | --ion-color-medium-contrast-rgb: 255, 255, 255; 67 | --ion-color-medium-shade: #86888f; 68 | --ion-color-medium-tint: #a2a4ab; 69 | 70 | /** light **/ 71 | --ion-color-light: #f4f5f8; 72 | --ion-color-light-rgb: 244, 244, 244; 73 | --ion-color-light-contrast: #000000; 74 | --ion-color-light-contrast-rgb: 0, 0, 0; 75 | --ion-color-light-shade: #d7d8da; 76 | --ion-color-light-tint: #f5f6f9; 77 | } 78 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | import './zone-flags.ts'; 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | 61 | import 'zone.js/dist/zone'; // Included with Angular CLI. 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Coffee Shop Frontend 2 | 3 | ## Getting Setup 4 | 5 | > _tip_: this frontend is designed to work with [Flask-based Backend](../backend). It is recommended you stand up the backend first, test using Postman, and then the frontend should integrate smoothly. 6 | 7 | ### Installing Dependencies 8 | 9 | #### Installing Node and NPM 10 | 11 | This project depends on Nodejs and Node Package Manager (NPM). Before continuing, you must download and install Node (the download includes NPM) from [https://nodejs.com/en/download](https://nodejs.org/en/download/). 12 | 13 | #### Installing Ionic Cli 14 | 15 | The Ionic Command Line Interface is required to serve and build the frontend. Instructions for installing the CLI is in the [Ionic Framework Docs](https://ionicframework.com/docs/installation/cli). 16 | 17 | #### Installing project dependencies 18 | 19 | This project uses NPM to manage software dependencies. NPM Relies on the package.json file located in the `frontend` directory of this repository. After cloning, open your terminal and run: 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | > _tip_: **npm i** is shorthand for **npm install** 26 | 27 | ## Required Tasks 28 | 29 | ### Configure Environment Variables 30 | 31 | Ionic uses a configuration file to manage environment variables. These variables ship with the transpiled software and should not include secrets. 32 | 33 | - Open `./src/environments/environments.ts` and ensure each variable reflects the system you stood up for the backend. 34 | 35 | ## Running Your Frontend in Dev Mode 36 | 37 | Ionic ships with a useful development server which detects changes and transpiles as you work. The application is then accessible through the browser on a localhost port. To run the development server, cd into the `frontend` directory and run: 38 | 39 | ```bash 40 | ionic serve 41 | ``` 42 | 43 | > _tip_: Do not use **ionic serve** in production. Instead, build Ionic into a build artifact for your desired platforms. 44 | > [Checkout the Ionic docs to learn more](https://ionicframework.com/docs/cli/commands/build) 45 | 46 | ## Key Software Design Relevant to Our Coursework 47 | 48 | The frontend framework is a bit beefy; here are the two areas to focus your study. 49 | 50 | ### Authentication 51 | 52 | The authentication system used for this project is Auth0. `./src/app/services/auth.service.ts` contains the logic to direct a user to the Auth0 login page, managing the JWT token upon successful callback, and handle setting and retrieving the token from the local store. This token is then consumed by our DrinkService (`./src/app/services/drinks.service.ts`) and passed as an Authorization header when making requests to our backend. 53 | 54 | ### Authorization 55 | 56 | The Auth0 JWT includes claims for permissions based on the user's role within the Auth0 system. This project makes use of these claims using the `auth.can(permission)` method which checks if particular permissions exist within the JWT permissions claim of the currently logged in user. This method is defined in `./src/app/services/auth.service.ts` and is then used to enable and disable buttons in `./src/app/pages/drink-menu/drink-form/drink-form.html`. 57 | -------------------------------------------------------------------------------- /backend/src/database/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sqlalchemy import Column, String, Integer 3 | from flask_sqlalchemy import SQLAlchemy 4 | import json 5 | 6 | database_filename = "database.db" 7 | project_dir = os.path.dirname(os.path.abspath(__file__)) 8 | database_path = "sqlite:///{}".format( 9 | os.path.join(project_dir, database_filename)) 10 | 11 | db = SQLAlchemy() 12 | 13 | ''' 14 | setup_db(app) 15 | binds a flask application and a SQLAlchemy service 16 | ''' 17 | 18 | 19 | def setup_db(app): 20 | app.config["SQLALCHEMY_DATABASE_URI"] = database_path 21 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 22 | db.app = app 23 | db.init_app(app) 24 | 25 | 26 | ''' 27 | db_drop_and_create_all() 28 | drops the database tables and starts fresh 29 | can be used to initialize a clean database 30 | !!NOTE you can change the database_filename variable 31 | to have multiple verisons of a database 32 | ''' 33 | 34 | 35 | def db_drop_and_create_all(): 36 | db.drop_all() 37 | db.create_all() 38 | 39 | 40 | ''' 41 | Drink 42 | a persistent drink entity, extends the base SQLAlchemy Model 43 | ''' 44 | 45 | 46 | class Drink(db.Model): 47 | # Autoincrementing, unique primary key 48 | id = Column(Integer().with_variant(Integer, "sqlite"), primary_key=True) 49 | # String Title 50 | title = Column(String(80), unique=True) 51 | # the ingredients blob - this stores a lazy json blob 52 | # the required datatype - 53 | # [{'color': string, 'name':string, 'parts':number}] 54 | recipe = Column(String(180), nullable=False) 55 | 56 | ''' 57 | short() 58 | short form representation of the Drink model 59 | ''' 60 | def short(self): 61 | short_recipe = [ 62 | {'color': r['color'], 'parts': r['parts']} 63 | for r in json.loads(self.recipe) 64 | ] 65 | return { 66 | 'id': self.id, 67 | 'title': self.title, 68 | 'recipe': short_recipe 69 | } 70 | 71 | ''' 72 | long() 73 | long form representation of the Drink model 74 | ''' 75 | def long(self): 76 | return { 77 | 'id': self.id, 78 | 'title': self.title, 79 | 'recipe': json.loads(self.recipe) 80 | } 81 | 82 | ''' 83 | insert() 84 | inserts a new model into a database 85 | the model must have a unique name 86 | the model must have a unique id or null id 87 | EXAMPLE 88 | drink = Drink(title=req_title, recipe=req_recipe) 89 | drink.insert() 90 | ''' 91 | def insert(self): 92 | db.session.add(self) 93 | db.session.commit() 94 | 95 | ''' 96 | delete() 97 | deletes a new model into a database 98 | the model must exist in the database 99 | EXAMPLE 100 | drink = Drink(title=req_title, recipe=req_recipe) 101 | drink.delete() 102 | ''' 103 | def delete(self): 104 | db.session.delete(self) 105 | db.session.commit() 106 | 107 | ''' 108 | update() 109 | updates a new model into a database 110 | the model must exist in the database 111 | EXAMPLE 112 | drink = Drink.query.filter(Drink.id == id).one_or_none() 113 | drink.title = 'Black Coffee' 114 | drink.update() 115 | ''' 116 | def update(self): 117 | db.session.commit() 118 | 119 | def __repr__(self): 120 | return json.dumps(self.short()) -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Coffee Shop Backend 2 | 3 | ## Getting Started 4 | 5 | ### Installing Dependencies 6 | 7 | #### Python 3.7 8 | 9 | Follow instructions to install the latest version of python for your platform in the [python docs](https://docs.python.org/3/using/unix.html#getting-and-installing-the-latest-version-of-python) 10 | 11 | #### Virtual Environment 12 | 13 | We recommend working within a virtual environment whenever using Python for projects. This keeps your dependencies for each project separate and organized. Instructions for setting up a virtual environment for your platform can be found in the [python docs](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/) 14 | 15 | #### PIP Dependencies 16 | 17 | Once you have your virtual environment setup and running, install dependencies by naviging to the `/backend` directory and running: 18 | 19 | ```bash 20 | pip install -r requirements.txt 21 | ``` 22 | 23 | This will install all of the required packages we selected within the `requirements.txt` file. 24 | 25 | ##### Key Dependencies 26 | 27 | - [Flask](http://flask.pocoo.org/) is a lightweight backend microservices framework. Flask is required to handle requests and responses. 28 | 29 | - [SQLAlchemy](https://www.sqlalchemy.org/) and [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/2.x/) are libraries to handle the lightweight sqlite database. Since we want you to focus on auth, we handle the heavy lift for you in `./src/database/models.py`. We recommend skimming this code first so you know how to interface with the Drink model. 30 | 31 | - [jose](https://python-jose.readthedocs.io/en/latest/) JavaScript Object Signing and Encryption for JWTs. Useful for encoding, decoding, and verifying JWTS. 32 | 33 | ## Running the server 34 | 35 | From within the `./src` directory first ensure you are working using your created virtual environment. 36 | 37 | Each time you open a new terminal session, run: 38 | 39 | ```bash 40 | export FLASK_APP=api.py; 41 | ``` 42 | 43 | To run the server, execute: 44 | 45 | ```bash 46 | flask run --reload 47 | ``` 48 | 49 | The `--reload` flag will detect file changes and restart the server automatically. 50 | 51 | ## Tasks 52 | 53 | ### Setup Auth0 54 | 55 | 1. Create a new Auth0 Account 56 | 2. Select a unique tenant domain 57 | 3. Create a new, single page web application 58 | 4. Create a new API 59 | - in API Settings: 60 | - Enable RBAC 61 | - Enable Add Permissions in the Access Token 62 | 5. Create new API permissions: 63 | - `get:drinks` 64 | - `get:drinks-detail` 65 | - `post:drinks` 66 | - `patch:drinks` 67 | - `delete:drinks` 68 | 6. Create new roles for: 69 | - Barista 70 | - can `get:drinks-detail` 71 | - can `get:drinks` 72 | - Manager 73 | - can perform all actions 74 | 7. Test your endpoints with [Postman](https://getpostman.com). 75 | - Register 2 users - assign the Barista role to one and Manager role to the other. 76 | - Sign into each account and make note of the JWT. 77 | - Import the postman collection `./starter_code/backend/udacity-fsnd-udaspicelatte.postman_collection.json` 78 | - Right-clicking the collection folder for barista and manager, navigate to the authorization tab, and including the JWT in the token field (you should have noted these JWTs). 79 | - Run the collection and correct any errors. 80 | - Export the collection overwriting the one we've included so that we have your proper JWTs during review! 81 | 82 | ### Implement The Server 83 | 84 | There are `@TODO` comments throughout the `./backend/src`. We recommend tackling the files in order and from top to bottom: 85 | 86 | 1. `./src/auth/auth.py` 87 | 2. `./src/api.py` 88 | -------------------------------------------------------------------------------- /frontend/src/app/services/drinks.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | 4 | import { AuthService } from './auth.service'; 5 | import { environment } from 'src/environments/environment'; 6 | 7 | export interface Drink { 8 | id: number; 9 | title: string; 10 | recipe: Array<{ 11 | name: string, 12 | color: string, 13 | parts: number 14 | }>; 15 | } 16 | 17 | @Injectable({ 18 | providedIn: 'root' 19 | }) 20 | export class DrinksService { 21 | 22 | url = environment.apiServerUrl; 23 | 24 | public items: {[key: number]: Drink} = {}; 25 | // = { 26 | // 1: { 27 | // id: 1, 28 | // title: 'matcha shake', 29 | // recipe: [ 30 | // { 31 | // name: 'milk', 32 | // color: 'grey', 33 | // parts: 1 34 | // }, 35 | // { 36 | // name: 'matcha', 37 | // color: 'green', 38 | // parts: 3 39 | // }, 40 | // ] 41 | // }, 42 | // 2: { 43 | // id: 2, 44 | // title: 'flatwhite', 45 | // recipe: [ 46 | 47 | // { 48 | // name: 'milk', 49 | // color: 'grey', 50 | // parts: 3 51 | // }, 52 | // { 53 | // name: 'coffee', 54 | // color: 'brown', 55 | // parts: 1 56 | // }, 57 | // ] 58 | // }, 59 | // 3: { 60 | // id: 3, 61 | // title: 'cap', 62 | // recipe: [ 63 | // { 64 | // name: 'foam', 65 | // color: 'white', 66 | // parts: 1 67 | // }, 68 | // { 69 | // name: 'milk', 70 | // color: 'grey', 71 | // parts: 2 72 | // }, 73 | // { 74 | // name: 'coffee', 75 | // color: 'brown', 76 | // parts: 1 77 | // }, 78 | // ] 79 | // } 80 | // }; 81 | 82 | 83 | constructor(private auth: AuthService, private http: HttpClient) { } 84 | 85 | getHeaders() { 86 | const header = { 87 | headers: new HttpHeaders() 88 | .set('Authorization', `Bearer ${this.auth.activeJWT()}`) 89 | }; 90 | return header; 91 | } 92 | 93 | getDrinks() { 94 | if (this.auth.can('get:drinks-detail')) { 95 | this.http.get(this.url + '/drinks-detail', this.getHeaders()) 96 | .subscribe((res: any) => { 97 | this.drinksToItems(res.drinks); 98 | console.log(res); 99 | }); 100 | } else { 101 | this.http.get(this.url + '/drinks', this.getHeaders()) 102 | .subscribe((res: any) => { 103 | this.drinksToItems(res.drinks); 104 | console.log(res); 105 | }); 106 | } 107 | 108 | } 109 | 110 | saveDrink(drink: Drink) { 111 | if (drink.id >= 0) { // patch 112 | this.http.patch(this.url + '/drinks/' + drink.id, drink, this.getHeaders()) 113 | .subscribe( (res: any) => { 114 | if (res.success) { 115 | this.drinksToItems(res.drinks); 116 | } 117 | }); 118 | } else { // insert 119 | this.http.post(this.url + '/drinks', drink, this.getHeaders()) 120 | .subscribe( (res: any) => { 121 | if (res.success) { 122 | this.drinksToItems(res.drinks); 123 | } 124 | }); 125 | } 126 | 127 | } 128 | 129 | deleteDrink(drink: Drink) { 130 | delete this.items[drink.id]; 131 | this.http.delete(this.url + '/drinks/' + drink.id, this.getHeaders()) 132 | .subscribe( (res: any) => { 133 | 134 | }); 135 | } 136 | 137 | drinksToItems( drinks: Array) { 138 | for (const drink of drinks) { 139 | this.items[drink.id] = drink; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", 3 | "version": 1, 4 | "defaultProject": "app", 5 | "newProjectRoot": "projects", 6 | "projects": { 7 | "app": { 8 | "root": "", 9 | "sourceRoot": "src", 10 | "projectType": "application", 11 | "prefix": "app", 12 | "schematics": {}, 13 | "architect": { 14 | "build": { 15 | "builder": "@angular-devkit/build-angular:browser", 16 | "options": { 17 | "outputPath": "www", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "src/tsconfig.app.json", 22 | "assets": [ 23 | { 24 | "glob": "**/*", 25 | "input": "src/assets", 26 | "output": "assets" 27 | }, 28 | { 29 | "glob": "**/*.svg", 30 | "input": "node_modules/ionicons/dist/ionicons/svg", 31 | "output": "./svg" 32 | } 33 | ], 34 | "styles": [ 35 | { 36 | "input": "src/theme/variables.scss" 37 | }, 38 | { 39 | "input": "src/global.scss" 40 | } 41 | ], 42 | "scripts": [], 43 | "es5BrowserSupport": true 44 | }, 45 | "configurations": { 46 | "production": { 47 | "fileReplacements": [ 48 | { 49 | "replace": "src/environments/environment.ts", 50 | "with": "src/environments/environment.prod.ts" 51 | } 52 | ], 53 | "optimization": true, 54 | "outputHashing": "all", 55 | "sourceMap": false, 56 | "extractCss": true, 57 | "namedChunks": false, 58 | "aot": true, 59 | "extractLicenses": true, 60 | "vendorChunk": false, 61 | "buildOptimizer": true, 62 | "budgets": [ 63 | { 64 | "type": "initial", 65 | "maximumWarning": "2mb", 66 | "maximumError": "5mb" 67 | } 68 | ] 69 | }, 70 | "ci": { 71 | "progress": false 72 | } 73 | } 74 | }, 75 | "serve": { 76 | "builder": "@angular-devkit/build-angular:dev-server", 77 | "options": { 78 | "browserTarget": "app:build" 79 | }, 80 | "configurations": { 81 | "production": { 82 | "browserTarget": "app:build:production" 83 | }, 84 | "ci": { 85 | "progress": false 86 | } 87 | } 88 | }, 89 | "extract-i18n": { 90 | "builder": "@angular-devkit/build-angular:extract-i18n", 91 | "options": { 92 | "browserTarget": "app:build" 93 | } 94 | }, 95 | "test": { 96 | "builder": "@angular-devkit/build-angular:karma", 97 | "options": { 98 | "main": "src/test.ts", 99 | "polyfills": "src/polyfills.ts", 100 | "tsConfig": "src/tsconfig.spec.json", 101 | "karmaConfig": "src/karma.conf.js", 102 | "styles": [], 103 | "scripts": [], 104 | "assets": [ 105 | { 106 | "glob": "favicon.ico", 107 | "input": "src/", 108 | "output": "/" 109 | }, 110 | { 111 | "glob": "**/*", 112 | "input": "src/assets", 113 | "output": "/assets" 114 | } 115 | ] 116 | }, 117 | "configurations": { 118 | "ci": { 119 | "progress": false, 120 | "watch": false 121 | } 122 | } 123 | }, 124 | "lint": { 125 | "builder": "@angular-devkit/build-angular:tslint", 126 | "options": { 127 | "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], 128 | "exclude": ["**/node_modules/**"] 129 | } 130 | }, 131 | "ionic-cordova-build": { 132 | "builder": "@ionic/angular-toolkit:cordova-build", 133 | "options": { 134 | "browserTarget": "app:build" 135 | }, 136 | "configurations": { 137 | "production": { 138 | "browserTarget": "app:build:production" 139 | } 140 | } 141 | }, 142 | "ionic-cordova-serve": { 143 | "builder": "@ionic/angular-toolkit:cordova-serve", 144 | "options": { 145 | "cordovaBuildTarget": "app:ionic-cordova-build", 146 | "devServerTarget": "app:serve" 147 | }, 148 | "configurations": { 149 | "production": { 150 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 151 | "devServerTarget": "app:serve:production" 152 | } 153 | } 154 | } 155 | } 156 | }, 157 | "app-e2e": { 158 | "root": "e2e/", 159 | "projectType": "application", 160 | "architect": { 161 | "e2e": { 162 | "builder": "@angular-devkit/build-angular:protractor", 163 | "options": { 164 | "protractorConfig": "e2e/protractor.conf.js", 165 | "devServerTarget": "app:serve" 166 | }, 167 | "configurations": { 168 | "ci": { 169 | "devServerTarget": "app:serve:ci" 170 | } 171 | } 172 | }, 173 | "lint": { 174 | "builder": "@angular-devkit/build-angular:tslint", 175 | "options": { 176 | "tsConfig": "e2e/tsconfig.e2e.json", 177 | "exclude": ["**/node_modules/**"] 178 | } 179 | } 180 | } 181 | } 182 | }, 183 | "cli": { 184 | "defaultCollection": "@ionic/angular-toolkit" 185 | }, 186 | "schematics": { 187 | "@ionic/angular-toolkit:component": { 188 | "styleext": "scss" 189 | }, 190 | "@ionic/angular-toolkit:page": { 191 | "styleext": "scss" 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /backend/src/api.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, request, jsonify, abort 3 | from sqlalchemy import exc 4 | import json 5 | from flask_cors import CORS 6 | 7 | from .database.models import db_drop_and_create_all, setup_db, Drink 8 | from .auth.auth import AuthError, requires_auth 9 | 10 | app = Flask(__name__) 11 | setup_db(app) 12 | CORS(app) 13 | 14 | ''' 15 | !! NOTE THIS WILL DROP ALL RECORDS AND START YOUR DB FROM SCRATCH 16 | !! NOTE THIS MUST BE UNCOMMENTED ON FIRST RUN 17 | ''' 18 | 19 | 20 | db_drop_and_create_all() 21 | 22 | ## ROUTES 23 | @app.route('/') 24 | def index(): 25 | return jsonify({ 26 | 'success': True, 27 | 'message':'hello-coffee'}) 28 | ''' 29 | @TODO implement endpoint 30 | GET /drinks 31 | it should be a public endpoint 32 | it should contain only the drink.short() data representation 33 | returns status code 200 and json {"success": True, "drinks": drinks} where drinks is the list of drinks 34 | or appropriate status code indicating reason for failure 35 | ''' 36 | @app.route('/drinks', methods=['GET']) 37 | def get_drinks(): 38 | all_drinks = Drink.query.order_by(Drink.id).all() 39 | 40 | return jsonify({ 41 | 'success': True, 42 | 'drinks': [drink.short() for drink in all_drinks] 43 | }) 44 | 45 | 46 | ''' 47 | GET /drinks-detail 48 | it should require the 'get:drinks-detail' permission 49 | it should contain the drink.long() data representation 50 | returns status code 200 and json {"success": True, "drinks": drinks} where drinks is the list of drinks 51 | or appropriate status code indicating reason for failure 52 | ''' 53 | @app.route('/drinks-detail', methods=['GET']) 54 | @requires_auth('get:drinks-detail') 55 | def get_drink_detail(jwt): 56 | try: 57 | return json.dumps({ 58 | 'success': 59 | True, 60 | 'drinks': [drink.long() for drink in Drink.query.all()] 61 | }), 200 62 | except: 63 | return json.dumps({ 64 | 'success': False, 65 | 'error': "An error occurred" 66 | }), 500 67 | 68 | 69 | ''' 70 | 71 | POST /drinks 72 | it should create a new row in the drinks table 73 | it should require the 'post:drinks' permission 74 | it should contain the drink.long() data representation 75 | returns status code 200 and json {"success": True, "drinks": drink} where drink an array containing only the newly created drink 76 | or appropriate status code indicating reason for failure 77 | ''' 78 | @app.route('/drinks', methods=['POST']) 79 | @requires_auth('post:drinks') 80 | def post_drink(jwt): 81 | data = request.get_json() 82 | if 'title' and 'recipe' not in data: 83 | abort(422) 84 | 85 | title = data['title'] 86 | recipe_json = json.dumps(data['recipe']) 87 | 88 | drink = Drink(title=title, recipe=recipe_json) 89 | 90 | drink.insert() 91 | 92 | return jsonify({ 93 | 'success': True, 94 | 'drinks': [drink.long()] 95 | }) 96 | 97 | 98 | ''' 99 | PATCH /drinks/ 100 | where is the existing model id 101 | it should respond with a 404 error if is not found 102 | it should update the corresponding row for 103 | it should require the 'patch:drinks' permission 104 | it should contain the drink.long() data representation 105 | returns status code 200 and json {"success": True, "drinks": drink} where drink an array containing only the updated drink 106 | or appropriate status code indicating reason for failure 107 | ''' 108 | @app.route('/drinks/', methods=['PATCH']) 109 | @requires_auth('patch:drinks') 110 | def update_drink(jwt, id): 111 | drink = Drink.query.get(id) 112 | if drink is None: 113 | abort(404) 114 | 115 | data = request.get_json() 116 | if 'title' in data: 117 | drink.title = data['title'] 118 | 119 | if 'recipe' in data: 120 | drink.recipe = json.dumps(data['recipe']) 121 | 122 | drink.update() 123 | 124 | return jsonify({ 125 | 'success': True, 126 | 'drinks': [drink.long()] 127 | }) 128 | 129 | 130 | 131 | ''' 132 | DELETE /drinks/ 133 | where is the existing model id 134 | it should respond with a 404 error if is not found 135 | it should delete the corresponding row for 136 | it should require the 'delete:drinks' permission 137 | returns status code 200 and json {"success": True, "delete": id} where id is the id of the deleted record 138 | or appropriate status code indicating reason for failure 139 | ''' 140 | @app.route('/drinks/', methods=['DELETE']) 141 | @requires_auth('delete:drinks') 142 | def delete_drink(jwt, id): 143 | try: 144 | drink = Drink.query.filter(Drink.id == id).one_or_none() 145 | if not drink: 146 | abort(404) 147 | drink.delete() 148 | return jsonify({ 149 | 'success': True, 150 | 'delete': id 151 | }), 200 152 | except exc.SQLAlchemyError: 153 | abort(503) 154 | 155 | ''' 156 | Used for tesint Auth0 157 | ''' 158 | @app.route('/login-results', methods=['GET']) 159 | def login_results(): 160 | return (jsonify({'message': 'successful login'})) 161 | 162 | 163 | ## Error Handling 164 | ''' 165 | Example error handling for unprocessable entity 166 | ''' 167 | @app.errorhandler(422) 168 | def unprocessable(error): 169 | return jsonify({ 170 | "success": False, 171 | "error": 422, 172 | "message": "unprocessable" 173 | }), 422 174 | 175 | ''' 176 | each error handler should return (with approprate messages): 177 | jsonify({ 178 | "success": False, 179 | "error": 404, 180 | "message": "resource not found" 181 | }), 404 182 | ''' 183 | @app.errorhandler(404) 184 | def resource_not_found(error): 185 | return jsonify({ 186 | "success": False, 187 | "error": 404, 188 | "message": "resource not found" 189 | }), 404 190 | 191 | ''' 192 | error handler for 404 193 | error handler should conform to general task above 194 | ''' 195 | 196 | 197 | ''' 198 | error handler for AuthError 199 | error handler should conform to general task above 200 | ''' 201 | @app.errorhandler(401) 202 | def unauthorized(error): 203 | return jsonify({ 204 | "success": False, 205 | "error": 401, 206 | "message": "unauthorized" 207 | }), 401 208 | 209 | @app.errorhandler(AuthError) 210 | def process_AuthError(error): 211 | response = jsonify(error.error) 212 | response.status_code = error.status_code 213 | 214 | return response 215 | -------------------------------------------------------------------------------- /backend/src/auth/auth.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import Flask, request, _request_ctx_stack, abort 3 | from functools import wraps 4 | from jose import jwt 5 | from urllib.request import urlopen 6 | 7 | app = Flask(__name__) 8 | 9 | 10 | 11 | AUTH0_DOMAIN = 'your AUTH_DOMIAN' 12 | ALGORITHMS = ['RS256'] 13 | API_AUDIENCE = 'your audience' 14 | CLIENT_ID = 'your clientID' 15 | 16 | 17 | ## AuthError Exception 18 | ''' 19 | AuthError Exception 20 | A standardized way to communicate auth failure modes 21 | ''' 22 | class AuthError(Exception): 23 | def __init__(self, error, status_code): 24 | self.error = error 25 | self.status_code = status_code 26 | 27 | 28 | ## Auth Header 29 | 30 | ''' 31 | @TODO implement get_token_auth_header() method 32 | it should attempt to get the header from the request 33 | it should raise an AuthError if no header is present 34 | it should attempt to split bearer and the token 35 | it should raise an AuthError if the header is malformed 36 | return the token part of the header 37 | ''' 38 | def get_token_auth_header(): 39 | """Obtains the Access Token from the Authorization Header 40 | """ 41 | auth = request.headers.get('Authorization', None) 42 | if not auth: 43 | raise AuthError({ 44 | 'code': 'authorization_header_missing', 45 | 'description': 'Authorization header is expected.' 46 | }, 401) 47 | 48 | parts = auth.split() 49 | if parts[0].lower() != 'bearer': 50 | raise AuthError({ 51 | 'code': 'invalid_header', 52 | 'description': 'Authorization header must start with "Bearer".' 53 | }, 401) 54 | 55 | elif len(parts) == 1: 56 | raise AuthError({ 57 | 'code': 'invalid_header', 58 | 'description': 'Token not found.' 59 | }, 401) 60 | 61 | elif len(parts) > 2: 62 | raise AuthError({ 63 | 'code': 'invalid_header', 64 | 'description': 'Authorization header must be bearer token.' 65 | }, 401) 66 | 67 | token = parts[1] 68 | return token 69 | 70 | ''' 71 | @TODO implement check_permissions(permission, payload) method 72 | @INPUTS 73 | permission: string permission (i.e. 'post:drink') 74 | payload: decoded jwt payload 75 | it should raise an AuthError if permissions are not included in the payload 76 | !!NOTE check your RBAC settings in Auth0 77 | it should raise an AuthError if the requested permission string is not in the payload permissions array 78 | return true otherwise 79 | ''' 80 | def check_permissions(permission, payload): 81 | if 'permissions' not in payload: 82 | raise AuthError({ 83 | 'code': 'invalid_claims', 84 | 'description': 'Permissions not included in JWT.' 85 | }, 400) 86 | 87 | if permission not in payload['permissions']: 88 | raise AuthError({ 89 | 'code': 'unauthorized', 90 | 'description': 'Permission not found.' 91 | }, 403) 92 | return True 93 | 94 | ''' 95 | @TODO implement verify_decode_jwt(token) method 96 | @INPUTS 97 | token: a json web token (string) 98 | it should be an Auth0 token with key id (kid) 99 | it should verify the token using Auth0 /.well-known/jwks.json 100 | it should decode the payload from the token 101 | it should validate the claims 102 | return the decoded payload 103 | !!NOTE urlopen has a common certificate error described here: https://stackoverflow.com/questions/50236117/scraping-ssl-certificate-verify-failed-error-for-http-en-wikipedia-org 104 | ''' 105 | def verify_decode_jwt(token): 106 | jsonurl = urlopen(f'https://{AUTH0_DOMAIN}/.well-known/jwks.json') 107 | jwks = json.loads(jsonurl.read()) 108 | unverified_header = jwt.get_unverified_header(token) 109 | rsa_key = {} 110 | if 'kid' not in unverified_header: 111 | raise AuthError({ 112 | 'code': 'invalid_header', 113 | 'description': 'Authorization malformed.' 114 | }, 401) 115 | 116 | for key in jwks['keys']: 117 | if key['kid'] == unverified_header['kid']: 118 | rsa_key = { 119 | 'kty': key['kty'], 120 | 'kid': key['kid'], 121 | 'use': key['use'], 122 | 'n': key['n'], 123 | 'e': key['e'] 124 | } 125 | if rsa_key: 126 | try: 127 | payload = jwt.decode( 128 | token, 129 | rsa_key, 130 | algorithms=ALGORITHMS, 131 | audience=API_AUDIENCE, 132 | issuer='https://' + AUTH0_DOMAIN + '/' 133 | ) 134 | 135 | return payload 136 | 137 | except jwt.ExpiredSignatureError: 138 | raise AuthError({ 139 | 'code': 'token_expired', 140 | 'description': 'Token expired.' 141 | }, 401) 142 | 143 | except jwt.JWTClaimsError: 144 | raise AuthError({ 145 | 'code': 'invalid_claims', 146 | 'description': 'Incorrect claims. Please, check the audience and issuer.' 147 | }, 401) 148 | except Exception: 149 | raise AuthError({ 150 | 'code': 'invalid_header', 151 | 'description': 'Unable to parse authentication token.' 152 | }, 400) 153 | raise AuthError({ 154 | 'code': 'invalid_header', 155 | 'description': 'Unable to find the appropriate key.' 156 | }, 400) 157 | 158 | ''' 159 | @TODO implement @requires_auth(permission) decorator method 160 | @INPUTS 161 | permission: string permission (i.e. 'post:drink') 162 | it should use the get_token_auth_header method to get the token 163 | it should use the verify_decode_jwt method to decode the jwt 164 | it should use the check_permissions method validate claims and check the requested permission 165 | return the decorator which passes the decoded payload to the decorated method 166 | def requires_auth(permission=''): 167 | def requires_auth_decorator(f): 168 | @wraps(f) 169 | def wrapper(*args, **kwargs): 170 | token = get_token_auth_header() 171 | payload = verify_decode_jwt(token) 172 | check_permissions(permission, payload) 173 | return f(payload, *args, **kwargs) 174 | return wrapper 175 | return requires_auth_decorator 176 | ''' 177 | 178 | def requires_auth(permission=''): 179 | def requires_auth_decorator(f): 180 | @wraps(f) 181 | def wrapper(*args, **kwargs): 182 | token = get_token_auth_header() 183 | try: 184 | payload = verify_decode_jwt(token) 185 | except: 186 | abort(401) 187 | 188 | check_permissions(permission, payload) 189 | return f(payload, *args, **kwargs) 190 | 191 | return wrapper 192 | return requires_auth_decorator 193 | 194 | -------------------------------------------------------------------------------- /backend/udacity-fsnd-udaspicelatte.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "e8ad44ee-7536-4901-913c-46e718d56d9f", 4 | "name": "udacity-fsnd-udaspicelatte", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "public", 10 | "item": [ 11 | { 12 | "name": "/drinks", 13 | "event": [ 14 | { 15 | "listen": "test", 16 | "script": { 17 | "id": "05cea6f9-9af0-4e60-9aad-4c8faf37ef4c", 18 | "exec": [ 19 | "pm.test(\"Status code is 200\", function () {", 20 | " pm.response.to.have.status(200);", 21 | "});", 22 | "", 23 | "pm.test(\"value contains drinks array\", function () {", 24 | " var jsonData = pm.response.json();", 25 | " pm.expect(jsonData.drinks).to.be.an('array')", 26 | "});" 27 | ], 28 | "type": "text/javascript" 29 | } 30 | } 31 | ], 32 | "request": { 33 | "method": "GET", 34 | "header": [], 35 | "url": { 36 | "raw": "{{host}}/drinks", 37 | "host": [ 38 | "{{host}}" 39 | ], 40 | "path": [ 41 | "drinks" 42 | ] 43 | } 44 | }, 45 | "response": [] 46 | }, 47 | { 48 | "name": "/drinks-detail", 49 | "event": [ 50 | { 51 | "listen": "test", 52 | "script": { 53 | "id": "65a195fa-a734-44b7-a7e0-c629b32d1fbb", 54 | "exec": [ 55 | "pm.test(\"Status code is 401 since no credentials are present\", function () {", 56 | " pm.response.to.have.status(401);", 57 | "});" 58 | ], 59 | "type": "text/javascript" 60 | } 61 | } 62 | ], 63 | "request": { 64 | "method": "GET", 65 | "header": [], 66 | "url": { 67 | "raw": "{{host}}/drinks-detail", 68 | "host": [ 69 | "{{host}}" 70 | ], 71 | "path": [ 72 | "drinks-detail" 73 | ] 74 | } 75 | }, 76 | "response": [] 77 | }, 78 | { 79 | "name": "/drinks", 80 | "event": [ 81 | { 82 | "listen": "test", 83 | "script": { 84 | "id": "5050a5b9-2e15-474e-9981-0e61ec8d2ff1", 85 | "exec": [ 86 | "pm.test(\"Status code is 401 since no credentials are present\", function () {", 87 | " pm.response.to.have.status(401);", 88 | "});" 89 | ], 90 | "type": "text/javascript" 91 | } 92 | } 93 | ], 94 | "request": { 95 | "method": "POST", 96 | "header": [], 97 | "url": { 98 | "raw": "{{host}}/drinks", 99 | "host": [ 100 | "{{host}}" 101 | ], 102 | "path": [ 103 | "drinks" 104 | ] 105 | } 106 | }, 107 | "response": [] 108 | }, 109 | { 110 | "name": "/drinks/1", 111 | "event": [ 112 | { 113 | "listen": "test", 114 | "script": { 115 | "id": "ec1488aa-b4d7-468a-89f5-03484009e69c", 116 | "exec": [ 117 | "pm.test(\"Status code is 401 since no credentials are present\", function () {", 118 | " pm.response.to.have.status(401);", 119 | "});" 120 | ], 121 | "type": "text/javascript" 122 | } 123 | } 124 | ], 125 | "request": { 126 | "method": "PATCH", 127 | "header": [], 128 | "url": { 129 | "raw": "{{host}}/drinks/1", 130 | "host": [ 131 | "{{host}}" 132 | ], 133 | "path": [ 134 | "drinks", 135 | "1" 136 | ] 137 | } 138 | }, 139 | "response": [] 140 | }, 141 | { 142 | "name": "/drinks/1", 143 | "event": [ 144 | { 145 | "listen": "test", 146 | "script": { 147 | "id": "0aea66ad-0a6e-4533-b192-a8b0af746c78", 148 | "exec": [ 149 | "pm.test(\"Status code is 401 since no credentials are present\", function () {", 150 | " pm.response.to.have.status(401);", 151 | "});" 152 | ], 153 | "type": "text/javascript" 154 | } 155 | } 156 | ], 157 | "request": { 158 | "method": "DELETE", 159 | "header": [], 160 | "url": { 161 | "raw": "{{host}}/drinks/1", 162 | "host": [ 163 | "{{host}}" 164 | ], 165 | "path": [ 166 | "drinks", 167 | "1" 168 | ] 169 | } 170 | }, 171 | "response": [] 172 | } 173 | ] 174 | }, 175 | { 176 | "name": "barista", 177 | "item": [ 178 | { 179 | "name": "/drinks", 180 | "event": [ 181 | { 182 | "listen": "test", 183 | "script": { 184 | "id": "05cea6f9-9af0-4e60-9aad-4c8faf37ef4c", 185 | "exec": [ 186 | "pm.test(\"Status code is 200\", function () {", 187 | " pm.response.to.have.status(200);", 188 | "});", 189 | "", 190 | "pm.test(\"value contains drinks array\", function () {", 191 | " var jsonData = pm.response.json();", 192 | " pm.expect(jsonData.drinks).to.be.an('array')", 193 | "});" 194 | ], 195 | "type": "text/javascript" 196 | } 197 | } 198 | ], 199 | "request": { 200 | "method": "GET", 201 | "header": [], 202 | "url": { 203 | "raw": "{{host}}/drinks", 204 | "host": [ 205 | "{{host}}" 206 | ], 207 | "path": [ 208 | "drinks" 209 | ] 210 | } 211 | }, 212 | "response": [] 213 | }, 214 | { 215 | "name": "/drinks-detail", 216 | "event": [ 217 | { 218 | "listen": "test", 219 | "script": { 220 | "id": "65a195fa-a734-44b7-a7e0-c629b32d1fbb", 221 | "exec": [ 222 | "pm.test(\"Status code is 200\", function () {", 223 | " pm.response.to.have.status(200);", 224 | "});" 225 | ], 226 | "type": "text/javascript" 227 | } 228 | } 229 | ], 230 | "request": { 231 | "method": "GET", 232 | "header": [], 233 | "url": { 234 | "raw": "{{host}}/drinks-detail", 235 | "host": [ 236 | "{{host}}" 237 | ], 238 | "path": [ 239 | "drinks-detail" 240 | ] 241 | } 242 | }, 243 | "response": [] 244 | }, 245 | { 246 | "name": "/drinks", 247 | "event": [ 248 | { 249 | "listen": "test", 250 | "script": { 251 | "id": "5050a5b9-2e15-474e-9981-0e61ec8d2ff1", 252 | "exec": [ 253 | "pm.test(\"Status code is 403 since credentials are valid, but permission is not present\", function () {", 254 | " pm.response.to.have.status(403);", 255 | "});" 256 | ], 257 | "type": "text/javascript" 258 | } 259 | } 260 | ], 261 | "request": { 262 | "auth": { 263 | "type": "bearer", 264 | "bearer": [ 265 | { 266 | "key": "token", 267 | "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InpRNzhFd2t0ZDRhR052NENrcTNQaCJ9.eyJpc3MiOiJodHRwczovL2NvZmZlZXNob3BmLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw2MmQwYjgzOGJmMGM4YjQ2Y2U1N2FiNTEiLCJhdWQiOiJjb2ZmZWUiLCJpYXQiOjE2NTc5MzMyNzgsImV4cCI6MTY1Nzk0MDQ3OCwiYXpwIjoienpVb3haVDFtbUhrTEhDMmVRMTVRYTV3a1dYa1F3UkQiLCJzY29wZSI6IiIsInBlcm1pc3Npb25zIjpbImdldDpkcmlua3MiLCJnZXQ6ZHJpbmtzLWRldGFpbCJdfQ.lsZQBw_TuEe1LXPVrgFy3XMSYXm1kKBleEWigPsCck3YVoabO--SxR5Zstw53LYkRYpub4v_31cZ6B6X4QV8_KpCVzQbYHDPtsyfuA261JcEO8CAaRXpeJdjLAn3u2LoXgyJKG76MBG2rdUoRkAxEpgYqiwN37Tt9lpckWyxwZtMrzp978HOvYA8LbBKtJDARi-qDVQuCjwpRPiFuJz0pguq1vp8AG4LqChm13DAYdUMlDmbgq8PArhqoyzDDOOvMPNIpqyiInmCG4rV0E5bpic4DyNtqNJgTwmY6f1WHZngnjnAmoBOO_7asWWSFch-3IStlqB9Zx6vQtxtaq-3Yw", 268 | "type": "string" 269 | } 270 | ] 271 | }, 272 | "method": "POST", 273 | "header": [], 274 | "url": { 275 | "raw": "{{host}}/drinks", 276 | "host": [ 277 | "{{host}}" 278 | ], 279 | "path": [ 280 | "drinks" 281 | ] 282 | } 283 | }, 284 | "response": [] 285 | }, 286 | { 287 | "name": "/drinks/1", 288 | "event": [ 289 | { 290 | "listen": "test", 291 | "script": { 292 | "id": "ec1488aa-b4d7-468a-89f5-03484009e69c", 293 | "exec": [ 294 | "pm.test(\"Status code is 403 since credentials are valid, but permission is not present\", function () {", 295 | " pm.response.to.have.status(403);", 296 | "});" 297 | ], 298 | "type": "text/javascript" 299 | } 300 | } 301 | ], 302 | "request": { 303 | "method": "PATCH", 304 | "header": [], 305 | "url": { 306 | "raw": "{{host}}/drinks/1", 307 | "host": [ 308 | "{{host}}" 309 | ], 310 | "path": [ 311 | "drinks", 312 | "1" 313 | ] 314 | } 315 | }, 316 | "response": [] 317 | }, 318 | { 319 | "name": "/drinks/1", 320 | "event": [ 321 | { 322 | "listen": "test", 323 | "script": { 324 | "id": "0aea66ad-0a6e-4533-b192-a8b0af746c78", 325 | "exec": [ 326 | "pm.test(\"Status code is 403 since credentials are valid, but permission is not present\", function () {", 327 | " pm.response.to.have.status(403);", 328 | "});" 329 | ], 330 | "type": "text/javascript" 331 | } 332 | } 333 | ], 334 | "request": { 335 | "method": "DELETE", 336 | "header": [], 337 | "url": { 338 | "raw": "{{host}}/drinks/1", 339 | "host": [ 340 | "{{host}}" 341 | ], 342 | "path": [ 343 | "drinks", 344 | "1" 345 | ] 346 | } 347 | }, 348 | "response": [] 349 | } 350 | ], 351 | "auth": { 352 | "type": "bearer", 353 | "bearer": [ 354 | { 355 | "key": "token", 356 | "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InpRNzhFd2t0ZDRhR052NENrcTNQaCJ9.eyJpc3MiOiJodHRwczovL2NvZmZlZXNob3BmLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMDYxNjQxOTA4NTk1NTUzNzExOCIsImF1ZCI6ImNvZmZlZSIsImlhdCI6MTY1Nzg1NzIxNiwiZXhwIjoxNjU3ODY0NDE2LCJhenAiOiJ6elVveFpUMW1tSGtMSEMyZVExNVFhNXdrV1hrUXdSRCIsInNjb3BlIjoiIn0.UUuko1p4wdHKwKn9NMXAuawR0lxCyvVuHRk0EMTHMz7NXojFsomSWms1uLmeLOFBb4OGlZeUgRycl5W9vxGjzYpPE1lHKWZsP1kUc8nCkJcFUrIS1nwx6RdRXrK8ApZrhph8zNpXj0jDaWfVQDteaYTkJg8tVC2-yo1AttyN1tkvP2ksy7_d83VcEFaNAoqYAPbB0MD9ErwpZO0J82-55ISuYUsf5V8z9cSXR6QE_bxGWsJA58pK74oIXkAIJc-Gc5fYLV5GvL34CEnq-L2VqyqsBV9PeYAg9-BftgIzjKAynQ563m4JLO4hlyDJdVvyRhwnZqLXpyQSG1aK7RC6CQ", 357 | "type": "string" 358 | } 359 | ] 360 | }, 361 | "event": [ 362 | { 363 | "listen": "prerequest", 364 | "script": { 365 | "id": "f985b22c-34c4-4cfe-8308-f6fb98d799ac", 366 | "type": "text/javascript", 367 | "exec": [ 368 | "" 369 | ] 370 | } 371 | }, 372 | { 373 | "listen": "test", 374 | "script": { 375 | "id": "596425ac-59b4-4488-9f07-4c748e1ad082", 376 | "type": "text/javascript", 377 | "exec": [ 378 | "" 379 | ] 380 | } 381 | } 382 | ] 383 | }, 384 | { 385 | "name": "manager", 386 | "item": [ 387 | { 388 | "name": "/drinks", 389 | "event": [ 390 | { 391 | "listen": "test", 392 | "script": { 393 | "id": "05cea6f9-9af0-4e60-9aad-4c8faf37ef4c", 394 | "exec": [ 395 | "pm.test(\"Status code is 200\", function () {", 396 | " pm.response.to.have.status(200);", 397 | "});", 398 | "", 399 | "pm.test(\"value contains drinks array\", function () {", 400 | " var jsonData = pm.response.json();", 401 | " pm.expect(jsonData.drinks).to.be.an('array')", 402 | "});" 403 | ], 404 | "type": "text/javascript" 405 | } 406 | } 407 | ], 408 | "request": { 409 | "method": "GET", 410 | "header": [], 411 | "url": { 412 | "raw": "{{host}}/drinks", 413 | "host": [ 414 | "{{host}}" 415 | ], 416 | "path": [ 417 | "drinks" 418 | ] 419 | } 420 | }, 421 | "response": [] 422 | }, 423 | { 424 | "name": "/drinks-detail", 425 | "event": [ 426 | { 427 | "listen": "test", 428 | "script": { 429 | "id": "65a195fa-a734-44b7-a7e0-c629b32d1fbb", 430 | "exec": [ 431 | "pm.test(\"Status code is 200\", function () {", 432 | " pm.response.to.have.status(200);", 433 | "});", 434 | "", 435 | "pm.test(\"value contains drinks array\", function () {", 436 | " var jsonData = pm.response.json();", 437 | " pm.expect(jsonData.drinks).to.be.an('array')", 438 | "});" 439 | ], 440 | "type": "text/javascript" 441 | } 442 | } 443 | ], 444 | "request": { 445 | "method": "GET", 446 | "header": [], 447 | "url": { 448 | "raw": "{{host}}/drinks-detail", 449 | "host": [ 450 | "{{host}}" 451 | ], 452 | "path": [ 453 | "drinks-detail" 454 | ] 455 | } 456 | }, 457 | "response": [] 458 | }, 459 | { 460 | "name": "/drinks", 461 | "event": [ 462 | { 463 | "listen": "test", 464 | "script": { 465 | "id": "5050a5b9-2e15-474e-9981-0e61ec8d2ff1", 466 | "exec": [ 467 | "pm.test(\"Status code is 200\", function () {", 468 | " pm.response.to.have.status(200);", 469 | "});" 470 | ], 471 | "type": "text/javascript" 472 | } 473 | } 474 | ], 475 | "request": { 476 | "method": "POST", 477 | "header": [ 478 | { 479 | "key": "Content-Type", 480 | "name": "Content-Type", 481 | "value": "application/json", 482 | "type": "text" 483 | } 484 | ], 485 | "body": { 486 | "mode": "raw", 487 | "raw": "{\n \"title\": \"Water3\",\n \"recipe\": {\n \"name\": \"Water\",\n \"color\": \"blue\",\n \"parts\": 1\n }\n}" 488 | }, 489 | "url": { 490 | "raw": "{{host}}/drinks", 491 | "host": [ 492 | "{{host}}" 493 | ], 494 | "path": [ 495 | "drinks" 496 | ] 497 | } 498 | }, 499 | "response": [] 500 | }, 501 | { 502 | "name": "/drinks/1", 503 | "event": [ 504 | { 505 | "listen": "test", 506 | "script": { 507 | "id": "ec1488aa-b4d7-468a-89f5-03484009e69c", 508 | "exec": [ 509 | "pm.test(\"Status code is 200\", function () {", 510 | " pm.response.to.have.status(200);", 511 | "});", 512 | "", 513 | "pm.test(\"value contains drinks array\", function () {", 514 | " var jsonData = pm.response.json();", 515 | " pm.expect(jsonData.drinks).to.be.an('array')", 516 | "});" 517 | ], 518 | "type": "text/javascript" 519 | } 520 | } 521 | ], 522 | "request": { 523 | "method": "PATCH", 524 | "header": [ 525 | { 526 | "key": "Content-Type", 527 | "name": "Content-Type", 528 | "value": "application/json", 529 | "type": "text" 530 | } 531 | ], 532 | "body": { 533 | "mode": "raw", 534 | "raw": "{\n \"title\": \"Water5\"\n}" 535 | }, 536 | "url": { 537 | "raw": "{{host}}/drinks/1", 538 | "host": [ 539 | "{{host}}" 540 | ], 541 | "path": [ 542 | "drinks", 543 | "1" 544 | ] 545 | } 546 | }, 547 | "response": [] 548 | }, 549 | { 550 | "name": "/drinks/1", 551 | "event": [ 552 | { 553 | "listen": "test", 554 | "script": { 555 | "id": "0aea66ad-0a6e-4533-b192-a8b0af746c78", 556 | "exec": [ 557 | "pm.test(\"Status code is 200\", function () {", 558 | " pm.response.to.have.status(200);", 559 | "});" 560 | ], 561 | "type": "text/javascript" 562 | } 563 | } 564 | ], 565 | "request": { 566 | "method": "DELETE", 567 | "header": [], 568 | "url": { 569 | "raw": "{{host}}/drinks/1", 570 | "host": [ 571 | "{{host}}" 572 | ], 573 | "path": [ 574 | "drinks", 575 | "1" 576 | ] 577 | } 578 | }, 579 | "response": [] 580 | } 581 | ], 582 | "auth": { 583 | "type": "bearer", 584 | "bearer": [ 585 | { 586 | "key": "token", 587 | "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InpRNzhFd2t0ZDRhR052NENrcTNQaCJ9.eyJpc3MiOiJodHRwczovL2NvZmZlZXNob3BmLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMDYxNjQxOTA4NTk1NTUzNzExOCIsImF1ZCI6ImNvZmZlZSIsImlhdCI6MTY1NzkzMzAwMiwiZXhwIjoxNjU3OTQwMjAyLCJhenAiOiJ6elVveFpUMW1tSGtMSEMyZVExNVFhNXdrV1hrUXdSRCIsInNjb3BlIjoiIiwicGVybWlzc2lvbnMiOlsiZGVsZXRlOmRyaW5rcyIsImdldDpkcmlua3MiLCJnZXQ6ZHJpbmtzLWRldGFpbCIsInBhdGNoOmRyaW5rcyIsInBvc3Q6ZHJpbmtzIl19.Hov5xfFn4vU9pBDuVV-YIAqR8SCvs0T7NOjrQAqw4XPiO72l7q43WfJFKjPXrWuWEZnxigitvEa9XR-7-lkWrPx3nRhViwiFGd6TqmL91cDSE9kOTxghq5K77U6HGXNyrZ-ndG7oRk4qNpQUrRqBWVt6ata1WVbqnO8LpUh0O23bNhSiRBPXT6oOD2QILJ3bQWgvF1GV5UeHbNCHI-5P0swhxTZqhTFMZ1GF_yLxZlNqrlA7oLHg9lV9YomKdjsGtDGcmGwx4-KaG1NDK_EZRP0x8j-OrhUsAmZj3eNTdPgUqyiKk46NUtQqreUt3ni75AKwr25J4UIq7Ur5KVhRcw", 588 | "type": "string" 589 | } 590 | ] 591 | }, 592 | "event": [ 593 | { 594 | "listen": "prerequest", 595 | "script": { 596 | "id": "113ee6fe-418d-4bf4-a641-444b5816e46b", 597 | "type": "text/javascript", 598 | "exec": [ 599 | "" 600 | ] 601 | } 602 | }, 603 | { 604 | "listen": "test", 605 | "script": { 606 | "id": "5704da6d-dc7e-42b9-8214-aa7ecf075bca", 607 | "type": "text/javascript", 608 | "exec": [ 609 | "" 610 | ] 611 | } 612 | } 613 | ] 614 | } 615 | ], 616 | "event": [ 617 | { 618 | "listen": "prerequest", 619 | "script": { 620 | "id": "fc871e59-eaa4-46e8-975b-87b750ad1804", 621 | "type": "text/javascript", 622 | "exec": [ 623 | "" 624 | ] 625 | } 626 | }, 627 | { 628 | "listen": "test", 629 | "script": { 630 | "id": "e5766f23-c7f1-472b-ae34-825a91147815", 631 | "type": "text/javascript", 632 | "exec": [ 633 | "" 634 | ] 635 | } 636 | } 637 | ], 638 | "variable": [ 639 | { 640 | "id": "7da0fa13-ecd9-4d7d-8e38-fde2c81cfdaf", 641 | "key": "host", 642 | "value": "localhost:5000", 643 | "type": "string" 644 | } 645 | ] 646 | } 647 | --------------------------------------------------------------------------------