├── 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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------