├── .gitignore ├── api ├── .env ├── .gitignore ├── .prettierrc ├── README.md ├── graphql │ └── schema.gql ├── nest-cli.json ├── package.json ├── prisma │ ├── dev.db │ ├── migrations │ │ └── dev │ │ │ └── watch-20191119112151 │ │ │ ├── README.md │ │ │ ├── schema.prisma │ │ │ └── steps.json │ └── schema.prisma ├── src │ ├── app.module.ts │ ├── auth │ │ ├── auth.controller.spec.ts │ │ ├── auth.controller.ts │ │ ├── auth.d.ts │ │ ├── auth.module.ts │ │ ├── auth.service.spec.ts │ │ ├── auth.service.ts │ │ ├── constants.ts │ │ ├── dto │ │ │ └── mimicUser.dto.ts │ │ ├── gql.authGuard.ts │ │ ├── gql.currentUser.ts │ │ ├── jwt.strategy.ts │ │ ├── local.strategy.ts │ │ └── role.authGuard.ts │ ├── book │ │ ├── book.controller.spec.ts │ │ ├── book.controller.ts │ │ ├── book.module.ts │ │ ├── book.resolver.ts │ │ ├── book.service.spec.ts │ │ ├── book.service.ts │ │ ├── dto │ │ │ ├── book.args.ts │ │ │ └── book.crud.dto.ts │ │ └── model │ │ │ └── book.ts │ ├── main.ts │ ├── photon │ │ └── photon.service.ts │ └── user │ │ ├── model │ │ └── user.ts │ │ ├── user.module.ts │ │ ├── user.resolver.ts │ │ ├── user.service.spec.ts │ │ └── user.service.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── tslint.json ├── app ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── .prettierrc ├── .stylintrc ├── README.md ├── babel.config.js ├── cypress.json ├── graphql │ └── schema.graphql ├── jest.config.js ├── package.json ├── quasar.conf.js ├── quasar.extensions.json ├── quasar.testing.json ├── shim.d.ts ├── src-ssr │ ├── extension.js │ └── index.js ├── src │ ├── App.vue │ ├── areas │ │ ├── default │ │ │ ├── layouts │ │ │ │ └── Default.vue │ │ │ └── pages │ │ │ │ └── Index.vue │ │ ├── loggedIn │ │ │ ├── layouts │ │ │ │ └── Default.vue │ │ │ └── pages │ │ │ │ ├── Group2Up.vue │ │ │ │ ├── Group3Up.vue │ │ │ │ ├── Group6Up.vue │ │ │ │ ├── Group7Up.vue │ │ │ │ └── Index.vue │ │ └── notLoggedIn │ │ │ ├── layouts │ │ │ └── Default.vue │ │ │ └── pages │ │ │ ├── Index.vue │ │ │ ├── Login.vue │ │ │ └── Page2.vue │ ├── assets │ │ ├── quasar-logo-full.svg │ │ └── sad.svg │ ├── boot │ │ ├── .gitkeep │ │ ├── axios.ts │ │ ├── components.ts │ │ ├── directives.ts │ │ ├── i18n.ts │ │ └── routeGuard.ts │ ├── components │ │ ├── .gitkeep │ │ ├── CanBeAccessBy.vue │ │ ├── SelectUserToMimic.vue │ │ ├── directives │ │ │ └── can.ts │ │ └── mixins │ │ │ └── base.ts │ ├── css │ │ ├── app.sass │ │ └── quasar.variables.sass │ ├── env.d.ts │ ├── i18n │ │ ├── en-us │ │ │ └── index.ts │ │ └── index.ts │ ├── index.template.html │ ├── index.ts │ ├── modules │ │ ├── _base │ │ │ ├── apollo │ │ │ │ ├── __tests__ │ │ │ │ │ └── fakes │ │ │ │ │ │ └── apolloClient.service.mock.ts │ │ │ │ ├── apolloClient.service.interface.ts │ │ │ │ ├── apolloClient.service.ts │ │ │ │ └── baseApolloCrud.service.ts │ │ │ ├── auth │ │ │ │ ├── auth.service.interface.ts │ │ │ │ └── auth.service.ts │ │ │ ├── axios │ │ │ │ ├── axios.service.interface.ts │ │ │ │ └── axios.service.ts │ │ │ ├── base.model.ts │ │ │ ├── baseCrud.service.interface.ts │ │ │ ├── baseCrud.service.ts │ │ │ ├── store.service.interface.ts │ │ │ ├── store.service.ts │ │ │ └── user │ │ │ │ ├── dto │ │ │ │ └── userCrud.dto.ts │ │ │ │ ├── user.model.ts │ │ │ │ ├── user.service.interface.ts │ │ │ │ └── user.service.ts │ │ ├── book │ │ │ ├── __tests__ │ │ │ │ ├── bookComponent.spec.ts │ │ │ │ ├── bookService.spec.ts │ │ │ │ └── fakes │ │ │ │ │ └── book.service.mock.ts │ │ │ ├── book.model.ts │ │ │ ├── book.service.interface.ts │ │ │ ├── book.service.ts │ │ │ ├── components │ │ │ │ ├── book.component.ts │ │ │ │ └── book.component.vue │ │ │ └── dto │ │ │ │ └── bookCrud.dto.ts │ │ └── diContainer.ts │ ├── pages │ │ └── Error404.vue │ ├── router │ │ ├── index.ts │ │ └── routes.ts │ ├── shims-vue.d.ts │ ├── statics │ │ ├── app-logo-128x128.png │ │ └── icons │ │ │ ├── apple-icon-120x120.png │ │ │ ├── apple-icon-152x152.png │ │ │ ├── apple-icon-167x167.png │ │ │ ├── apple-icon-180x180.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-96x96.png │ │ │ ├── favicon.ico │ │ │ ├── icon-128x128.png │ │ │ ├── icon-192x192.png │ │ │ ├── icon-256x256.png │ │ │ ├── icon-384x384.png │ │ │ ├── icon-512x512.png │ │ │ ├── ms-icon-144x144.png │ │ │ └── safari-pinned-tab.svg │ └── store │ │ ├── actions │ │ ├── auth.ts │ │ ├── error.ts │ │ ├── ui.ts │ │ └── user.ts │ │ ├── index.ts │ │ ├── modules │ │ ├── auth │ │ │ ├── actions.ts │ │ │ ├── getters.ts │ │ │ ├── index.ts │ │ │ ├── mutations.ts │ │ │ ├── state.ts │ │ │ └── types.ts │ │ ├── book │ │ │ ├── actions.ts │ │ │ ├── getters.ts │ │ │ ├── index.ts │ │ │ ├── mutations.ts │ │ │ ├── state.ts │ │ │ └── types.ts │ │ ├── ui │ │ │ ├── actions.ts │ │ │ ├── getters.ts │ │ │ ├── index.ts │ │ │ ├── mutations.ts │ │ │ ├── state.ts │ │ │ └── types.ts │ │ └── user │ │ │ ├── actions.ts │ │ │ ├── getters.ts │ │ │ ├── index.ts │ │ │ ├── mutations.ts │ │ │ ├── state.ts │ │ │ └── types.ts │ │ └── types.ts ├── test │ ├── .gitkeep │ ├── cypress │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ ├── .cypress-sample │ │ │ │ └── example_spec.js │ │ │ └── home │ │ │ │ └── init.spec.js │ │ ├── plugins │ │ │ └── index.js │ │ ├── screenshots │ │ │ └── .gitkeep │ │ ├── support │ │ │ ├── commands.js │ │ │ └── index.js │ │ └── videos │ │ │ └── .gitkeep │ └── jest │ │ ├── __tests__ │ │ ├── App.spec.js │ │ └── demo │ │ │ └── QBtn-demo.vue │ │ ├── jest.setup.js │ │ └── utils │ │ ├── index.js │ │ └── stub.css └── tsconfig.json ├── readme.md └── shared └── common ├── .gitignore ├── index.js ├── package.json ├── src └── auth │ ├── app.roles.d.ts │ ├── app.roles.js │ └── app.roles.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .quasar 2 | .DS_Store 3 | .thumbs.db 4 | node_modules 5 | /dist 6 | /src-cordova/node_modules 7 | /src-cordova/platforms 8 | /src-cordova/plugins 9 | /src-cordova/www 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | -------------------------------------------------------------------------------- /api/.env: -------------------------------------------------------------------------------- 1 | ENGINE_API_KEY=service:webnoob-7547:OvZaHWiCXEvvPFqtJ6uVhw 2 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 6 | [travis-url]: https://travis-ci.org/nestjs/nest 7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 8 | [linux-url]: https://travis-ci.org/nestjs/nest 9 | 10 |

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

11 |

12 | NPM Version 13 | Package License 14 | NPM Downloads 15 | Travis 16 | Linux 17 | Coverage 18 | Gitter 19 | Backers on Open Collective 20 | Sponsors on Open Collective 21 | 22 | 23 |

24 | 26 | 27 | ## Description 28 | 29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 30 | 31 | ## Installation 32 | 33 | ```bash 34 | $ npm install 35 | ``` 36 | 37 | ## Running the app 38 | 39 | ```bash 40 | # development 41 | $ npm run start 42 | 43 | # watch mode 44 | $ npm run start:dev 45 | 46 | # production mode 47 | $ npm run start:prod 48 | ``` 49 | 50 | ## Test 51 | 52 | ```bash 53 | # unit tests 54 | $ npm run test 55 | 56 | # e2e tests 57 | $ npm run test:e2e 58 | 59 | # test coverage 60 | $ npm run test:cov 61 | ``` 62 | 63 | ## Support 64 | 65 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 66 | 67 | ## Stay in touch 68 | 69 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 70 | - Website - [https://nestjs.com](https://nestjs.com/) 71 | - Twitter - [@nestframework](https://twitter.com/nestframework) 72 | 73 | ## License 74 | 75 | Nest is [MIT licensed](LICENSE). 76 | -------------------------------------------------------------------------------- /api/graphql/schema.gql: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- 2 | # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! 3 | # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! 4 | # ----------------------------------------------- 5 | 6 | type Book { 7 | id: String! 8 | title: String! 9 | description: String 10 | } 11 | 12 | type Mutation { 13 | create(id: String, title: String = "", description: String): Book! 14 | delete(id: String!): Book! 15 | } 16 | 17 | type Query { 18 | books(skip: Int = 0, take: Int = 25): [Book!]! 19 | getUsersToMimic(needle: String!): [User!]! 20 | } 21 | 22 | type User { 23 | id: String! 24 | username: String! 25 | emailAddress: String! 26 | password: String! 27 | role: Int! 28 | canActAs: [String!] 29 | hasActingAs: [String!] 30 | } 31 | -------------------------------------------------------------------------------- /api/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rimraf dist && tsc -p tsconfig.build.json", 9 | "format": "prettier --write \"src/**/*.ts\"", 10 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 11 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node dist/main.js\"", 12 | "start:debug": "tsc-watch -p tsconfig.build.json --onSuccess \"node --inspect-brk dist/main.js\"", 13 | "start:prod": "node dist/main.js", 14 | "lint": "tslint -p tsconfig.json -c tslint.json", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "jest --config ./test/jest-e2e.json", 20 | "seed": "ts-node prisma/seed.ts", 21 | "postinstall": "prisma2 generate" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^6.7.2", 25 | "@nestjs/core": "^6.7.2", 26 | "@nestjs/graphql": "^6.5.3", 27 | "@nestjs/jwt": "^6.1.1", 28 | "@nestjs/passport": "^6.1.0", 29 | "@nestjs/platform-express": "^6.7.2", 30 | "apollo-server-express": "^2.9.5", 31 | "sass-common": "link:../shared/common", 32 | "graphql": "^14.5.8", 33 | "graphql-tools": "^4.0.5", 34 | "nest-access-control": "^2.0.1", 35 | "passport": "^0.4.0", 36 | "passport-jwt": "^4.0.0", 37 | "passport-local": "^1.0.0", 38 | "reflect-metadata": "^0.1.13", 39 | "rimraf": "^3.0.0", 40 | "rxjs": "^6.5.3", 41 | "type-graphql": "^0.17.5" 42 | }, 43 | "devDependencies": { 44 | "@nestjs/cli": "^6.9.0", 45 | "@nestjs/schematics": "^6.7.0", 46 | "@nestjs/testing": "^6.7.1", 47 | "@types/express": "^4.17.1", 48 | "@types/jest": "^24.0.18", 49 | "@types/node": "^12.7.5", 50 | "@types/passport-jwt": "^3.0.2", 51 | "@types/passport-local": "^1.0.33", 52 | "@types/supertest": "^2.0.8", 53 | "jest": "^24.9.0", 54 | "prettier": "^1.18.2", 55 | "prisma2": "^2.0.0-preview013.3", 56 | "supertest": "^4.0.2", 57 | "ts-jest": "^24.1.0", 58 | "ts-loader": "^6.1.1", 59 | "ts-node": "^8.4.1", 60 | "tsc-watch": "^4.0.0", 61 | "tsconfig-paths": "^3.9.0", 62 | "tslint": "^5.20.0", 63 | "typescript": "^3.6.3" 64 | }, 65 | "jest": { 66 | "moduleFileExtensions": [ 67 | "js", 68 | "json", 69 | "ts" 70 | ], 71 | "rootDir": "src", 72 | "testRegex": ".spec.ts$", 73 | "transform": { 74 | "^.+\\.(t|j)s$": "ts-jest" 75 | }, 76 | "coverageDirectory": "./coverage", 77 | "testEnvironment": "node" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /api/prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/api/prisma/dev.db -------------------------------------------------------------------------------- /api/prisma/migrations/dev/watch-20191119112151/README.md: -------------------------------------------------------------------------------- 1 | # Migration `watch-20191119112151` 2 | 3 | This migration has been generated by Allan Gaunt at 11/19/2019, 11:21:51 AM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "lift"."Book" ( 10 | "description" TEXT , 11 | "id" TEXT NOT NULL , 12 | "title" TEXT NOT NULL DEFAULT '' , 13 | PRIMARY KEY ("id") 14 | ); 15 | 16 | CREATE TABLE "lift"."User" ( 17 | "emailAddress" TEXT NOT NULL DEFAULT '' , 18 | "id" TEXT NOT NULL , 19 | "password" TEXT NOT NULL DEFAULT '' , 20 | "role" INTEGER NOT NULL DEFAULT 0 , 21 | "username" TEXT NOT NULL DEFAULT '' , 22 | PRIMARY KEY ("id") 23 | ); 24 | 25 | CREATE TABLE "lift"."_canActAs" ( 26 | "A" TEXT REFERENCES "User"(id) ON DELETE CASCADE, 27 | "B" TEXT REFERENCES "User"(id) ON DELETE CASCADE 28 | ); 29 | 30 | CREATE UNIQUE INDEX "lift"."Book.id" ON "Book"("id") 31 | 32 | CREATE UNIQUE INDEX "lift"."User.id" ON "User"("id") 33 | 34 | CREATE UNIQUE INDEX "lift"."User.username" ON "User"("username") 35 | 36 | CREATE UNIQUE INDEX "lift"."User.emailAddress" ON "User"("emailAddress") 37 | 38 | CREATE UNIQUE INDEX "lift"."_canActAs_AB_unique" ON "_canActAs"("A","B") 39 | ``` 40 | 41 | ## Changes 42 | 43 | ```diff 44 | diff --git datamodel.mdl datamodel.mdl 45 | migration ..watch-20191119112151 46 | --- datamodel.dml 47 | +++ datamodel.dml 48 | @@ -1,0 +1,24 @@ 49 | +datasource db { 50 | + provider = "sqlite" 51 | + url = "file:dev.db" 52 | +} 53 | + 54 | +generator photon { 55 | + provider = "photonjs" 56 | +} 57 | + 58 | +model Book { 59 | + id String @default(cuid()) @id @unique 60 | + title String 61 | + description String? 62 | +} 63 | + 64 | +model User { 65 | + id String @default(cuid()) @id @unique 66 | + username String @unique 67 | + emailAddress String @unique 68 | + password String 69 | + role Int 70 | + canActAs User[] @relation(name: "canActAs") 71 | + hasActingAs User[] @relation(name: "canActAs") 72 | +} 73 | ``` 74 | 75 | ## Photon Usage 76 | 77 | You can use a specific Photon built for this migration (watch-20191119112151) 78 | in your `before` or `after` migration script like this: 79 | 80 | ```ts 81 | import Photon from '@generated/photon/watch-20191119112151' 82 | 83 | const photon = new Photon() 84 | 85 | async function main() { 86 | const result = await photon.users() 87 | console.dir(result, { depth: null }) 88 | } 89 | 90 | main() 91 | 92 | ``` 93 | -------------------------------------------------------------------------------- /api/prisma/migrations/dev/watch-20191119112151/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = "***" 4 | } 5 | 6 | generator photon { 7 | provider = "photonjs" 8 | } 9 | 10 | model Book { 11 | id String @default(cuid()) @id @unique 12 | title String 13 | description String? 14 | } 15 | 16 | model User { 17 | id String @default(cuid()) @id @unique 18 | username String @unique 19 | emailAddress String @unique 20 | password String 21 | role Int 22 | canActAs User[] @relation(name: "canActAs") 23 | hasActingAs User[] @relation(name: "canActAs") 24 | } 25 | -------------------------------------------------------------------------------- /api/prisma/migrations/dev/watch-20191119112151/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "stepType": "CreateModel", 6 | "name": "Book", 7 | "embedded": false 8 | }, 9 | { 10 | "stepType": "CreateModel", 11 | "name": "User", 12 | "embedded": false 13 | }, 14 | { 15 | "stepType": "CreateField", 16 | "model": "Book", 17 | "name": "id", 18 | "type": { 19 | "Base": "String" 20 | }, 21 | "arity": "required", 22 | "isUnique": true, 23 | "id": { 24 | "strategy": "Auto", 25 | "sequence": null 26 | }, 27 | "default": { 28 | "Expression": [ 29 | "cuid", 30 | "String", 31 | [] 32 | ] 33 | } 34 | }, 35 | { 36 | "stepType": "CreateField", 37 | "model": "Book", 38 | "name": "title", 39 | "type": { 40 | "Base": "String" 41 | }, 42 | "arity": "required", 43 | "isUnique": false 44 | }, 45 | { 46 | "stepType": "CreateField", 47 | "model": "Book", 48 | "name": "description", 49 | "type": { 50 | "Base": "String" 51 | }, 52 | "arity": "optional", 53 | "isUnique": false 54 | }, 55 | { 56 | "stepType": "CreateField", 57 | "model": "User", 58 | "name": "id", 59 | "type": { 60 | "Base": "String" 61 | }, 62 | "arity": "required", 63 | "isUnique": true, 64 | "id": { 65 | "strategy": "Auto", 66 | "sequence": null 67 | }, 68 | "default": { 69 | "Expression": [ 70 | "cuid", 71 | "String", 72 | [] 73 | ] 74 | } 75 | }, 76 | { 77 | "stepType": "CreateField", 78 | "model": "User", 79 | "name": "username", 80 | "type": { 81 | "Base": "String" 82 | }, 83 | "arity": "required", 84 | "isUnique": true 85 | }, 86 | { 87 | "stepType": "CreateField", 88 | "model": "User", 89 | "name": "emailAddress", 90 | "type": { 91 | "Base": "String" 92 | }, 93 | "arity": "required", 94 | "isUnique": true 95 | }, 96 | { 97 | "stepType": "CreateField", 98 | "model": "User", 99 | "name": "password", 100 | "type": { 101 | "Base": "String" 102 | }, 103 | "arity": "required", 104 | "isUnique": false 105 | }, 106 | { 107 | "stepType": "CreateField", 108 | "model": "User", 109 | "name": "role", 110 | "type": { 111 | "Base": "Int" 112 | }, 113 | "arity": "required", 114 | "isUnique": false 115 | }, 116 | { 117 | "stepType": "CreateField", 118 | "model": "User", 119 | "name": "canActAs", 120 | "type": { 121 | "Relation": { 122 | "to": "User", 123 | "to_fields": [ 124 | "id" 125 | ], 126 | "name": "canActAs", 127 | "on_delete": "None" 128 | } 129 | }, 130 | "arity": "list", 131 | "isUnique": false 132 | }, 133 | { 134 | "stepType": "CreateField", 135 | "model": "User", 136 | "name": "hasActingAs", 137 | "type": { 138 | "Relation": { 139 | "to": "User", 140 | "to_fields": [ 141 | "id" 142 | ], 143 | "name": "canActAs", 144 | "on_delete": "None" 145 | } 146 | }, 147 | "arity": "list", 148 | "isUnique": false 149 | } 150 | ] 151 | } -------------------------------------------------------------------------------- /api/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = "file:dev.db" 4 | } 5 | 6 | generator photon { 7 | provider = "photonjs" 8 | } 9 | 10 | model Book { 11 | id String @default(cuid()) @id @unique 12 | title String 13 | description String? 14 | } 15 | 16 | model User { 17 | id String @default(cuid()) @id @unique 18 | username String @unique 19 | emailAddress String @unique 20 | password String 21 | role Int 22 | canActAs User[] @relation(name: "canActAs") 23 | hasActingAs User[] @relation(name: "canActAs") 24 | } 25 | -------------------------------------------------------------------------------- /api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { GraphQLModule } from '@nestjs/graphql' 3 | import { BookModule } from './book/book.module' 4 | import { AuthModule } from './auth/auth.module' 5 | import { UserModule } from './user/user.module' 6 | 7 | @Module({ 8 | imports: [ 9 | BookModule, 10 | UserModule, 11 | GraphQLModule.forRoot({ 12 | autoSchemaFile: 'graphql/schema.gql', 13 | debug: true, 14 | playground: true, 15 | context: ({ req }) => ({ req }) 16 | }), 17 | AuthModule 18 | ] 19 | }) 20 | export class AppModule {} 21 | -------------------------------------------------------------------------------- /api/src/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthController } from './auth.controller'; 3 | 4 | describe('Auth Controller', () => { 5 | let controller: AuthController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [AuthController], 10 | }).compile(); 11 | 12 | controller = module.get(AuthController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Request, Post, UseGuards, Get, Body } from '@nestjs/common' 2 | import { AuthGuard } from '@nestjs/passport' 3 | 4 | import { AuthService } from './auth.service' 5 | import MimicUserDto from './dto/mimicUser.dto' 6 | 7 | @Controller('auth') 8 | export class AuthController { 9 | constructor (private readonly authService: AuthService) {} 10 | 11 | @UseGuards(AuthGuard('local')) 12 | @Post('login') 13 | async login (@Request() req) { 14 | return this.authService.login(req.user) 15 | } 16 | 17 | @UseGuards(AuthGuard('jwt')) 18 | @Post('logout') 19 | logout (@Request() req) { 20 | return req.logout() 21 | } 22 | 23 | @UseGuards(AuthGuard('jwt')) 24 | @Post('setMimicUser') 25 | setMimicUser (@Request() req, @Body() mimicUserDto: MimicUserDto) { 26 | return this.authService.setMimicUser(req.user, mimicUserDto.id) 27 | } 28 | 29 | @UseGuards(AuthGuard('jwt')) 30 | @Get('profile') 31 | getProfile (@Request() req) { 32 | return req.user 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/src/auth/auth.d.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../user/model/user' 2 | 3 | interface LoginToken { 4 | data: string, 5 | expires: number 6 | } 7 | 8 | export interface LoginResult { 9 | user: User, 10 | token: LoginToken, 11 | mimicUser?: User 12 | } 13 | -------------------------------------------------------------------------------- /api/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | import { JwtModule } from '@nestjs/jwt' 4 | 5 | import { AuthService } from './auth.service' 6 | import { UserModule } from '../user/user.module' 7 | import { LocalStrategy } from './local.strategy' 8 | import { AuthController } from './auth.controller'; 9 | import { jwtConstants } from './constants' 10 | import { JwtStrategy } from './jwt.strategy' 11 | 12 | @Module({ 13 | imports: [ 14 | UserModule, 15 | PassportModule, 16 | JwtModule.register({ 17 | secret: jwtConstants.secret, 18 | signOptions: { expiresIn: '43200s' }, 19 | }) 20 | ], 21 | providers: [AuthService, LocalStrategy, JwtStrategy], 22 | controllers: [AuthController] 23 | }) 24 | export class AuthModule {} 25 | -------------------------------------------------------------------------------- /api/src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { JwtService } from '@nestjs/jwt' 3 | 4 | import { UserService } from '../user/user.service' 5 | import { User } from '../user/model/user' 6 | import { jwtConstants } from './constants' 7 | import { LoginResult } from './auth' 8 | import { UnauthorizedError } from 'type-graphql' 9 | 10 | @Injectable() 11 | export class AuthService { 12 | constructor ( 13 | private readonly userService: UserService, 14 | private readonly jwtService: JwtService 15 | ) {} 16 | 17 | private makeUserResult (user: User): any { 18 | const { password, ...result } = user 19 | return result 20 | } 21 | 22 | private getLoginResult (user: User, mimicUser: User): LoginResult { 23 | let payload: any = { 24 | username: user.username, 25 | sub: user.id, 26 | role: user.role, 27 | canActAs: user.canActAs, 28 | hasActingAs: user.hasActingAs 29 | } 30 | 31 | if (mimicUser) { 32 | payload = { 33 | ...payload, 34 | mimicUsername: mimicUser.username, 35 | mimicSub: mimicUser.id, 36 | mimicRole: mimicUser.role 37 | } 38 | } 39 | 40 | const token: string = this.jwtService.sign(payload) 41 | const decodedToken: any = this.jwtService.decode(token, { 42 | // @ts-ignore 43 | secret: jwtConstants.secret 44 | }) 45 | 46 | return { 47 | user: this.makeUserResult(user), 48 | token: { 49 | data: token, 50 | expires: decodedToken.exp 51 | }, 52 | mimicUser: mimicUser ? this.makeUserResult(mimicUser) : null 53 | } 54 | } 55 | 56 | async validateUser (username: string, password: string): Promise { 57 | // Try and find a user 58 | const user = await this.userService.getByUsername(username) 59 | 60 | // Do we have a user with the password that matches? 61 | if (user && user.password === password) { 62 | return this.makeUserResult(user) 63 | } 64 | 65 | // No user found, return null so passport knows auth failed. 66 | return null 67 | } 68 | 69 | async login (user: any): Promise { 70 | return this.getLoginResult(user, null) 71 | } 72 | 73 | async setMimicUser (user: any, mimicUserId: string): Promise { 74 | // Send a login token down with the mimic user cleared. 75 | if (mimicUserId === null) { 76 | return this.getLoginResult(user, null) 77 | } 78 | 79 | return this.userService.getById(mimicUserId).then((mimicUser: User) => { 80 | if ( 81 | mimicUser === null || // No user for that id (they've interfered 82 | !mimicUser.hasActingAs.includes(user.id) // Don't have permission to act as this user 83 | ) { 84 | // Throw UnauthorizedError here so we're not giving any hints away as 85 | // to whether a user exists or not (in case they fake an id) 86 | throw new UnauthorizedError() 87 | } 88 | 89 | // If we're here, there is a mimic user and the user is allowed to act as them. 90 | return this.getLoginResult(user, mimicUser) 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /api/src/auth/constants.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = { 2 | secret: 'OilOfOlay!' // No, just no. Do not ever. NO! Link to env var for real world usage. 3 | } 4 | -------------------------------------------------------------------------------- /api/src/auth/dto/mimicUser.dto.ts: -------------------------------------------------------------------------------- 1 | export default class MimicUserDto { 2 | readonly id: string | null 3 | } 4 | -------------------------------------------------------------------------------- /api/src/auth/gql.authGuard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common' 2 | import { AuthGuard } from '@nestjs/passport' 3 | import { GqlExecutionContext } from '@nestjs/graphql' 4 | 5 | @Injectable() 6 | export class GqlAuthGuard extends AuthGuard('jwt') { 7 | getRequest (context: ExecutionContext) { 8 | const ctx = GqlExecutionContext.create(context) 9 | return ctx.getContext().req 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api/src/auth/gql.currentUser.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common' 2 | 3 | export const CurrentUser = createParamDecorator( 4 | (data, [root, args, ctx, info]) => ctx.req.user 5 | ) 6 | -------------------------------------------------------------------------------- /api/src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport' 2 | import { Injectable } from '@nestjs/common' 3 | 4 | import { ExtractJwt, Strategy } from 'passport-jwt' 5 | import { jwtConstants } from './constants' 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor () { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | secretOrKey: jwtConstants.secret, 14 | }); 15 | } 16 | 17 | async validate (payload: any) { 18 | return { 19 | // re-assigning to id here so this object "looks" like a User 20 | // and it makes sense we'd try and access user.id 21 | id: payload.sub, 22 | username: payload.username, 23 | tokenExpires: payload.exp, 24 | role: payload.role, 25 | canActAs: payload.canActAs, 26 | hasActingAs: payload.hasActingAs, 27 | mimicUsername: payload.mimicUsername, 28 | mimicId: payload.mimicSub, 29 | mimicRole: payload.mimicRole 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /api/src/auth/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local' 2 | import { PassportStrategy } from '@nestjs/passport' 3 | import { Injectable, UnauthorizedException } from '@nestjs/common' 4 | import { AuthService } from './auth.service' 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor (private readonly authService: AuthService) { 9 | super() 10 | } 11 | 12 | async validate (username: string, password: string): Promise { 13 | const user = await this.authService.validateUser(username, password) 14 | if (!user) { 15 | throw new UnauthorizedException() 16 | } 17 | 18 | return user 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api/src/auth/role.authGuard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { GqlExecutionContext } from '@nestjs/graphql' 4 | import { AppRolePermissions, getAppRolePermissions } from 'saas-common/dist/app.roles' 5 | 6 | @Injectable() 7 | export class RoleAuthGuard implements CanActivate { 8 | constructor ( 9 | private readonly permissionRole: AppRolePermissions 10 | ) {} 11 | 12 | validate (req): boolean { 13 | return !!(getAppRolePermissions(req.user.role) & this.permissionRole) || 14 | !!(getAppRolePermissions(req.user.mimicRole) & this.permissionRole) 15 | } 16 | 17 | canActivate (context: ExecutionContext): boolean | Promise | Observable { 18 | const ctx = GqlExecutionContext.create(context) 19 | const req = ctx.getContext().req 20 | return this.validate(req) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api/src/book/book.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BookController } from './book.controller'; 3 | 4 | describe('Book Controller', () => { 5 | let controller: BookController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [BookController], 10 | }).compile(); 11 | 12 | controller = module.get(BookController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/book/book.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | @Controller('book') 4 | export class BookController { 5 | } 6 | -------------------------------------------------------------------------------- /api/src/book/book.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BookController } from './book.controller'; 3 | import { BookService } from './book.service'; 4 | import { BookResolver } from './book.resolver' 5 | import { PhotonService } from '../photon/photon.service' 6 | 7 | @Module({ 8 | controllers: [BookController], 9 | providers: [BookService, BookResolver, PhotonService] 10 | }) 11 | export class BookModule {} 12 | -------------------------------------------------------------------------------- /api/src/book/book.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' 2 | import { BookService } from './book.service' 3 | import { Book } from './model/book' 4 | import { BookArgs } from './dto/book.args' 5 | import BookCrudDto from './dto/book.crud.dto' 6 | import { UseGuards } from '@nestjs/common' 7 | import { GqlAuthGuard } from '../auth/gql.authGuard' 8 | import { CurrentUser } from '../auth/gql.currentUser' 9 | import { User } from '../user/model/user' 10 | import { RoleAuthGuard } from '../auth/role.authGuard' 11 | import { AppRolePermissions } from 'saas-common/dist/app.roles' 12 | 13 | @Resolver(of => Book) 14 | @UseGuards(GqlAuthGuard) 15 | export class BookResolver { 16 | constructor ( 17 | private readonly bookService: BookService 18 | ) {} 19 | 20 | @Query(returns => [Book]) 21 | @UseGuards(new RoleAuthGuard(AppRolePermissions.ReadBooks)) 22 | books ( 23 | @CurrentUser() user: User, 24 | @Args() args: BookArgs 25 | ): Promise { 26 | return this.bookService.findAll(args) 27 | } 28 | 29 | @Mutation(returns => Book) 30 | @UseGuards(new RoleAuthGuard(AppRolePermissions.CreateBooks)) 31 | create (@Args() newBookData: BookCrudDto): Promise { 32 | return this.bookService.create(newBookData) 33 | } 34 | 35 | @Mutation(returns => Book) 36 | @UseGuards(new RoleAuthGuard(AppRolePermissions.DeleteBooks)) 37 | delete (@Args('id') id: string): Promise { 38 | return this.bookService.delete(id) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api/src/book/book.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BookService } from './book.service'; 3 | 4 | describe('BookService', () => { 5 | let service: BookService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BookService], 10 | }).compile(); 11 | 12 | service = module.get(BookService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/book/book.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { BookArgs } from './dto/book.args' 3 | import { Book } from './model/book' 4 | import BookCrudDto from './dto/book.crud.dto' 5 | import { PhotonService } from '../photon/photon.service' 6 | 7 | @Injectable() 8 | export class BookService { 9 | constructor (private readonly photonService: PhotonService) { } 10 | 11 | findAll (args: BookArgs): Promise { 12 | return this.photonService.books.findMany() 13 | } 14 | 15 | create (newBookData: BookCrudDto) { 16 | return this.photonService.books.create({ 17 | data: { 18 | title: newBookData.title 19 | } 20 | }) 21 | } 22 | 23 | delete (id: string) { 24 | return this.photonService.books.delete({ 25 | where: { 26 | id 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/book/dto/book.args.ts: -------------------------------------------------------------------------------- 1 | import { Max, Min } from 'class-validator' 2 | import { ArgsType, Field, Int } from 'type-graphql' 3 | 4 | @ArgsType() 5 | export class BookArgs { 6 | @Field(type => Int) 7 | @Min(0) 8 | skip: number = 0 9 | 10 | @Field(type => Int) 11 | @Min(1) 12 | @Max(50) 13 | take: number = 25 14 | } 15 | -------------------------------------------------------------------------------- /api/src/book/dto/book.crud.dto.ts: -------------------------------------------------------------------------------- 1 | import { MaxLength } from 'class-validator' 2 | import { ArgsType, Field, Int } from 'type-graphql' 3 | 4 | @ArgsType() 5 | export default class BookCrudDto { 6 | @Field({ nullable: true }) 7 | id?: string 8 | 9 | @Field(type => String) 10 | @MaxLength(200) 11 | title: string = '' 12 | 13 | @Field({ nullable: true }) 14 | description?: string 15 | } 16 | -------------------------------------------------------------------------------- /api/src/book/model/book.ts: -------------------------------------------------------------------------------- 1 | import { Field, Int, ObjectType } from 'type-graphql' 2 | 3 | @ObjectType() 4 | export class Book { 5 | @Field(type => String) 6 | id: string 7 | 8 | @Field(type => String) 9 | title: string = '' 10 | 11 | @Field({ nullable: true }) 12 | description?: string 13 | } 14 | -------------------------------------------------------------------------------- /api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { AppModule } from './app.module' 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule, { 6 | logger: console 7 | }) 8 | app.enableCors() 9 | await app.listen(3000) 10 | } 11 | bootstrap() 12 | -------------------------------------------------------------------------------- /api/src/photon/photon.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common' 2 | import { Photon } from '@generated/photon' 3 | 4 | @Injectable() 5 | export class PhotonService extends Photon implements OnModuleInit, OnModuleDestroy { 6 | constructor () { 7 | super() 8 | } 9 | async onModuleInit () { 10 | await this.connect() 11 | } 12 | 13 | async onModuleDestroy () { 14 | await this.disconnect() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api/src/user/model/user.ts: -------------------------------------------------------------------------------- 1 | import { Field, Int, ObjectType } from 'type-graphql' 2 | 3 | @ObjectType() 4 | export class User { 5 | @Field(type => String) 6 | id: string 7 | 8 | @Field(type => String) 9 | username: string = '' 10 | 11 | @Field(type => String) 12 | emailAddress: string 13 | 14 | @Field(type => String) 15 | password: string 16 | 17 | @Field(type => Int) 18 | role: number 19 | 20 | @Field(type => [String], { nullable: true }) 21 | canActAs?: string[] 22 | 23 | @Field(type => [String], { nullable: true }) 24 | hasActingAs?: string[] 25 | } 26 | -------------------------------------------------------------------------------- /api/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { UserService } from './user.service' 3 | import { PhotonService } from '../photon/photon.service' 4 | import { UserResolver } from './user.resolver' 5 | 6 | @Module({ 7 | providers: [UserService, UserResolver, PhotonService], 8 | exports: [UserService] 9 | }) 10 | export class UserModule {} 11 | -------------------------------------------------------------------------------- /api/src/user/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from '@nestjs/graphql' 2 | import { UseGuards } from '@nestjs/common' 3 | import { GqlAuthGuard } from '../auth/gql.authGuard' 4 | import { CurrentUser } from '../auth/gql.currentUser' 5 | import { User } from '../user/model/user' 6 | import { RoleAuthGuard } from '../auth/role.authGuard' 7 | import { AppRolePermissions } from 'saas-common/dist/app.roles' 8 | import { UserService } from './user.service' 9 | 10 | @Resolver(of => User) 11 | @UseGuards(GqlAuthGuard) 12 | export class UserResolver { 13 | constructor ( 14 | private readonly userService: UserService 15 | ) {} 16 | 17 | @Query(returns => [User]) 18 | @UseGuards(new RoleAuthGuard(AppRolePermissions.CanMimicUsers)) 19 | getUsersToMimic (@CurrentUser() user: User, @Args('needle') needle: string): Promise { 20 | return this.userService.getUsersToMimic(user, needle) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api/src/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService } from './user.service'; 3 | 4 | describe('UserService', () => { 5 | let service: UserService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserService], 10 | }).compile(); 11 | 12 | service = module.get(UserService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | import { PhotonService } from '../photon/photon.service' 4 | import { User } from './model/user' 5 | import { AppRole } from 'saas-common/dist/app.roles' 6 | 7 | @Injectable() 8 | export class UserService { 9 | constructor (private readonly photonService: PhotonService) { } 10 | 11 | private users: User[] = [ 12 | { 13 | id: 'userid0', 14 | username: 'admin', 15 | emailAddress: 'admin@dev.com', 16 | password: '123', 17 | role: AppRole.Admin 18 | }, 19 | { 20 | id: 'userid1', 21 | username: 'webnoob', 22 | emailAddress: 'me@allangaunt.dev', 23 | password: 'changeme', 24 | role: AppRole.Author, 25 | hasActingAs: ['userid2'] 26 | }, 27 | { 28 | id: 'userid2', 29 | username: 'va', 30 | emailAddress: 'va@dev.com', 31 | password: '123', 32 | role: AppRole.VirtualAssistant, 33 | canActAs: ['userid1'] 34 | } 35 | ] 36 | 37 | async getById (id: string): Promise { 38 | return this.users.find(f => f.id === id) 39 | } 40 | 41 | async getByUsername (username: string): Promise { 42 | const user = await this.photonService 43 | .users 44 | .findMany({ 45 | where: { 46 | username 47 | } 48 | }) 49 | 50 | // return user[0] 51 | 52 | // Demo data at the moment 53 | return this.users.find(f => f.username === username) 54 | } 55 | 56 | /** 57 | * Get a list of users an authenticated user is allowed to mimic. 58 | * For a user to be returned, they must: 59 | * a) Be included in the auth'd users canActAs 60 | * b) Auth'd user must be included in the users hasActingAs (this ensures no tampering client side - although that's not possible with JWT) 61 | * c) Needle must match 62 | * OR 63 | * a) User is an admin - they can mimic anyone 64 | * AND 65 | * a) User must not be an admin - no point allowing admins to mimic admins 66 | * @param user 67 | * @param needle 68 | */ 69 | async getUsersToMimic (user: User, needle: string): Promise { 70 | return this.users.filter(f => { 71 | const hasCorrectPerms = 72 | user.role === AppRole.Admin || 73 | (user.canActAs.includes(f.id) && f.hasActingAs.includes(user.id)) 74 | const isNotAdminUser = f.role !== AppRole.Admin 75 | 76 | return hasCorrectPerms && isNotAdminUser && f.username.toLowerCase().indexOf(needle) > -1 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /api/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../src/app.module'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /api/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "prisma", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false, 16 | "semicolon": false, 17 | "space-before-function-paren": true, 18 | "trailing-comma": false, 19 | "no-bitwise": false 20 | }, 21 | "rulesDirectory": [] 22 | } 23 | -------------------------------------------------------------------------------- /app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-syntax-dynamic-import", 4 | "@babel/plugin-proposal-class-properties" 5 | ], 6 | "env": { 7 | "test": { 8 | "plugins": ["dynamic-import-node"], 9 | "presets": [ 10 | [ 11 | "@babel/preset-env", 12 | { 13 | "modules": "commonjs", 14 | "targets": { 15 | "node": "current" 16 | } 17 | } 18 | ] 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /app/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | // Rules order is important, please avoid shuffling them 5 | extends: [ 6 | // Base ESLint recommended rules 7 | 'eslint:recommended', 8 | 9 | // ESLint typescript rules 10 | // See https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage 11 | 'plugin:@typescript-eslint/eslint-recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | 14 | // `plugin:vue/essential` by default, consider switching to `plugin:vue/strongly-recommended` 15 | // or `plugin:vue/recommended` for stricter rules. 16 | // See https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 17 | 'plugin:vue/essential', 18 | 19 | // Usage with Prettier, provided by 'eslint-config-prettier'. 20 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage-with-prettier 21 | 'prettier', 22 | 'prettier/@typescript-eslint', 23 | 'prettier/vue' 24 | ], 25 | 26 | plugins: [ 27 | // Required to apply rules which need type information 28 | '@typescript-eslint', 29 | // Required to lint *.vue files 30 | // See https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file 31 | 'vue' 32 | // Prettier has not been included as plugin to avoid performance impact 33 | // See https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674 34 | // Add it as an extension 35 | ], 36 | 37 | // Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working 38 | // See https://eslint.vuejs.org/user-guide/#how-to-use-custom-parser 39 | // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted 40 | parserOptions: { 41 | parser: '@typescript-eslint/parser', 42 | sourceType: 'module', 43 | project: './tsconfig.json' 44 | }, 45 | 46 | env: { 47 | browser: true 48 | }, 49 | 50 | globals: { 51 | ga: true, // Google Analytics 52 | cordova: true, 53 | __statics: true, 54 | process: true 55 | }, 56 | 57 | // add your custom rules here 58 | rules: { 59 | 'prefer-promise-reject-errors': 'off', 60 | quotes: ['warn', 'single'], 61 | '@typescript-eslint/indent': ['warn', 2], 62 | 63 | // allow console.log during development only 64 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 65 | // allow debugger during development only 66 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 67 | 68 | // Custom 69 | 'vue/component-name-in-template-casing': ['error', 'kebab-case'], 70 | 71 | // Correct typescript linting until at least 2.0.0 major release 72 | // See https://github.com/typescript-eslint/typescript-eslint/issues/501 73 | // See https://github.com/typescript-eslint/typescript-eslint/issues/493 74 | '@typescript-eslint/explicit-function-return-type': 'off', 75 | '@typescript-eslint/interface-name-prefix': 0, 76 | '@typescript-eslint/no-parameter-properties': 0, 77 | "@typescript-eslint/no-empty-interface": 0, 78 | "@typescript-eslint/no-explicit-any": 0 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | .quasar 2 | .DS_Store 3 | .thumbs.db 4 | node_modules 5 | /dist 6 | /src-cordova/node_modules 7 | /src-cordova/platforms 8 | /src-cordova/plugins 9 | /src-cordova/www 10 | /test/jest/coverage 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.map 23 | -------------------------------------------------------------------------------- /app/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } -------------------------------------------------------------------------------- /app/.stylintrc: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": "never", 3 | "brackets": "never", 4 | "colons": "never", 5 | "colors": "always", 6 | "commaSpace": "always", 7 | "commentSpace": "always", 8 | "cssLiteral": "never", 9 | "depthLimit": false, 10 | "duplicates": true, 11 | "efficient": "always", 12 | "extendPref": false, 13 | "globalDupe": true, 14 | "indentPref": 2, 15 | "leadingZero": "never", 16 | "maxErrors": false, 17 | "maxWarnings": false, 18 | "mixed": false, 19 | "namingConvention": false, 20 | "namingConventionStrict": false, 21 | "none": "never", 22 | "noImportant": false, 23 | "parenSpace": "never", 24 | "placeholder": false, 25 | "prefixVarsWithDollar": "always", 26 | "quotePref": "single", 27 | "semicolons": "never", 28 | "sortOrder": false, 29 | "stackedProperties": "never", 30 | "trailingWhitespace": "never", 31 | "universal": "never", 32 | "valid": true, 33 | "zeroUnits": "never", 34 | "zIndexNormalize": false 35 | } 36 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Quasar App (app) 2 | 3 | A Quasar Framework app 4 | 5 | ## Install the dependencies 6 | ```bash 7 | yarn 8 | ``` 9 | 10 | ### Start the app in development mode (hot-code reloading, error reporting, etc.) 11 | ```bash 12 | quasar dev 13 | ``` 14 | 15 | ### Lint the files 16 | ```bash 17 | yarn run lint 18 | ``` 19 | 20 | ### Build the app for production 21 | ```bash 22 | quasar build 23 | ``` 24 | 25 | ### Customize the configuration 26 | See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js). 27 | 28 | ### Development Concepts 29 | 30 | 1. Components access the store 31 | 2. Store accesses services 32 | 3. Services return data and don't modify the state directly. 33 | 3a. They can however dispatch events 34 | -------------------------------------------------------------------------------- /app/babel.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | let extend = undefined 3 | 4 | /** 5 | * The .babelrc file has been created to assist Jest for transpiling. 6 | * You should keep your application's babel rules in this file. 7 | */ 8 | 9 | if (fs.existsSync('./.babelrc')) { 10 | extend = './.babelrc' 11 | } 12 | 13 | module.exports = { 14 | presets: [ 15 | '@quasar/babel-preset-app' 16 | ], 17 | extends: extend 18 | } 19 | -------------------------------------------------------------------------------- /app/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8080/", 3 | "fixturesFolder": "test/cypress/fixtures", 4 | "integrationFolder": "test/cypress/integration", 5 | "pluginsFile": "test/cypress/plugins/index.js", 6 | "screenshotsFolder": "test/cypress/screenshots", 7 | "supportFile": "test/cypress/support/index.js", 8 | "videosFolder": "test/cypress/videos", 9 | "video": true, 10 | "json.schemas": [ 11 | { 12 | "fileMatch": [ 13 | "cypress.json" 14 | ], 15 | "url": "https://on.cypress.io/cypress.schema.json" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /app/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | # This file was generated based on ".graphqlconfig". Do not edit manually. 2 | 3 | schema { 4 | query: Query 5 | mutation: Mutation 6 | } 7 | 8 | type Book { 9 | description: String 10 | id: String! 11 | title: String! 12 | } 13 | 14 | type Mutation { 15 | create(description: String, id: String, title: String = ""): Book! 16 | delete(id: String!): Book! 17 | } 18 | 19 | type Query { 20 | books(skip: Int = 0, take: Int = 25): [Book!]! 21 | getUsersToMimic(needle: String!): [User!]! 22 | } 23 | 24 | type User { 25 | id: String! 26 | username: String! 27 | emailAddress: String! 28 | password: String! 29 | role: Int! 30 | canActAs: [String!] 31 | hasActingAs: [String!] 32 | } 33 | -------------------------------------------------------------------------------- /app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | __DEV__: true, 4 | 'ts-jest': { 5 | // ... 6 | diagnostics: { 7 | ignoreCodes: [151001] 8 | } 9 | } 10 | }, 11 | setupFilesAfterEnv: [ 12 | '/test/jest/jest.setup.js' 13 | ], 14 | // noStackTrace: true, 15 | // bail: true, 16 | cache: false, 17 | // verbose: true, 18 | // watch: true, 19 | collectCoverage: false, 20 | coverageDirectory: '/test/jest/coverage', 21 | collectCoverageFrom: [ 22 | '/src/**/*.vue', 23 | '/src/**/*.js', 24 | '/src/**/*.ts', 25 | '/src/**/*.jsx' 26 | ], 27 | coverageThreshold: { 28 | global: { 29 | // branches: 50, 30 | // functions: 50, 31 | // lines: 50, 32 | // statements: 50 33 | } 34 | }, 35 | testMatch: [ 36 | '/test/jest/__tests__/**/*.spec.js', 37 | '/test/jest/__tests__/**/*.test.js', 38 | '/src/**/__tests__/*_jest.spec.js', 39 | '/src/modules/**/__tests__/*.spec.ts' 40 | ], 41 | moduleFileExtensions: [ 42 | 'vue', 43 | 'js', 44 | 'jsx', 45 | 'json', 46 | 'ts', 47 | 'tsx' 48 | ], 49 | moduleNameMapper: { 50 | '^vue$': '/node_modules/vue/dist/vue.common.js', 51 | '^test-utils$': '/node_modules/@vue/test-utils/dist/vue-test-utils.js', 52 | '^quasar$': '/node_modules/quasar/dist/quasar.common.js', 53 | '^~/(.*)$': '/$1', 54 | '^src/(.*)$': '/src/$1', 55 | '.*css$': '/test/jest/utils/stub.css' 56 | }, 57 | transform: { 58 | "^.+\\.ts?$": "ts-jest", 59 | '.*\\.vue$': 'vue-jest', 60 | '.*\\.js$': 'babel-jest', 61 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 62 | // use these if NPM is being flaky 63 | // '.*\\.vue$': '/node_modules/@quasar/quasar-app-extension-testing-unit-jest/node_modules/vue-jest', 64 | // '.*\\.js$': '/node_modules/@quasar/quasar-app-extension-testing-unit-jest/node_modules/babel-jest' 65 | }, 66 | transformIgnorePatterns: [ 67 | '/node_modules/(?!quasar/lang)' 68 | ], 69 | snapshotSerializers: [ 70 | '/node_modules/jest-serializer-vue' 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.1", 4 | "description": "A Quasar Framework app", 5 | "productName": "Quasar App", 6 | "cordovaId": "org.cordova.quasar.app", 7 | "author": "webnoob ", 8 | "private": true, 9 | "scripts": { 10 | "dev": "yarn concurrently:dev:jest", 11 | "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./", 12 | "test": "echo \"See package.json => scripts for available tests.\" && exit 0", 13 | "test:unit": "jest --updateSnapshot", 14 | "test:unit:coverage": "jest --coverage", 15 | "test:unit:watch": "jest --watch", 16 | "test:unit:watchAll": "jest --watchAll", 17 | "serve:test:coverage": "quasar serve src/test/jest/coverage/lcov-report/ --port 8788", 18 | "concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"", 19 | "test:e2e": "cypress open", 20 | "test:e2e:CI": "cypress run" 21 | }, 22 | "dependencies": { 23 | "@quasar/extras": "^1.0.0", 24 | "apollo-cache-inmemory": "^1.6.3", 25 | "apollo-client": "^2.6.4", 26 | "apollo-link-context": "^1.0.19", 27 | "apollo-link-error": "^1.1.12", 28 | "apollo-link-http": "^1.5.16", 29 | "axios": "^0.18.1", 30 | "booksprout": "link:../shared/common", 31 | "graphql": "^14.5.8", 32 | "graphql-tag": "^2.10.1", 33 | "inversify-props": "^1.4.3", 34 | "node-fetch": "^2.6.0", 35 | "quasar": "^1.0.0", 36 | "reflect-metadata": "^0.1.13", 37 | "vue-class-component": "^7.1.0", 38 | "vue-i18n": "^8.0.0", 39 | "vue-property-decorator": "^8.2.2", 40 | "vuex-class": "^0.3.2" 41 | }, 42 | "devDependencies": { 43 | "@babel/plugin-proposal-class-properties": "^7.5.5", 44 | "@babel/preset-typescript": "^7.6.0", 45 | "@quasar/app": "^1.0.0", 46 | "@quasar/quasar-app-extension-testing": "^1.0.0", 47 | "@quasar/quasar-app-extension-testing-e2e-cypress": "^1.0.0-beta.10", 48 | "@quasar/quasar-app-extension-testing-unit-jest": "^1.0.0", 49 | "@quasar/quasar-app-extension-typescript": "^1.0.0-beta.1", 50 | "@types/jest": "^24.0.19", 51 | "@types/node": "11.9.5", 52 | "@types/node-fetch": "^2.5.2", 53 | "@typescript-eslint/eslint-plugin": "^1.12.0", 54 | "@typescript-eslint/parser": "^1.12.0", 55 | "@vue/eslint-config-standard": "^4.0.0", 56 | "babel-eslint": "^10.0.1", 57 | "eslint": "^5.10.0", 58 | "eslint-config-prettier": "^6.0.0", 59 | "eslint-loader": "^2.1.1", 60 | "eslint-plugin-vue": "^5.0.0", 61 | "ts-jest": "^24.1.0", 62 | "typescript": "^3.3.3" 63 | }, 64 | "engines": { 65 | "node": ">= 8.9.0", 66 | "npm": ">= 5.6.0", 67 | "yarn": ">= 1.6.0" 68 | }, 69 | "browserslist": [ 70 | "last 1 version, not dead, ie >= 11" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /app/quasar.conf.js: -------------------------------------------------------------------------------- 1 | // Configuration for your app 2 | // https://quasar.dev/quasar-cli/quasar-conf-js 3 | 4 | module.exports = function (ctx) { 5 | return { 6 | preFetch: true, 7 | // Quasar looks for *.js files by default 8 | sourceFiles: { 9 | router: 'src/router/index.ts', 10 | store: 'src/store/index.ts' 11 | }, 12 | // app boot file (/src/boot) 13 | // --> boot files are part of "main.js" 14 | // https://quasar.dev/quasar-cli/cli-documentation/boot-files 15 | boot: [ 16 | 'i18n', 17 | 'axios', 18 | 'routeGuard', 19 | 'components', 20 | 'directives' 21 | ], 22 | 23 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 24 | css: [ 25 | 'app.sass' 26 | ], 27 | 28 | // https://github.com/quasarframework/quasar/tree/dev/extras 29 | extras: [ 30 | // 'ionicons-v4', 31 | // 'mdi-v4', 32 | // 'fontawesome-v5', 33 | // 'eva-icons', 34 | // 'themify', 35 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 36 | 37 | 'roboto-font', // optional, you are not bound to it 38 | 'material-icons' // optional, you are not bound to it 39 | ], 40 | 41 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 42 | framework: { 43 | // iconSet: 'ionicons-v4', // Quasar icon set 44 | // lang: 'de', // Quasar language pack 45 | 46 | // Possible values for "all": 47 | // * 'auto' - Auto-import needed Quasar components & directives 48 | // (slightly higher compile time; next to minimum bundle size; most convenient) 49 | // * false - Manually specify what to import 50 | // (fastest compile time; minimum bundle size; most tedious) 51 | // * true - Import everything from Quasar 52 | // (not treeshaking Quasar; biggest bundle size; convenient) 53 | all: true, 54 | 55 | components: [], 56 | directives: [], 57 | 58 | // Quasar plugins 59 | plugins: [] 60 | }, 61 | 62 | // https://quasar.dev/quasar-cli/cli-documentation/supporting-ie 63 | supportIE: false, 64 | 65 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 66 | build: { 67 | scopeHoisting: true, 68 | vueRouterMode: 'history', 69 | // showProgress: false, 70 | // gzip: true, 71 | // analyze: true, 72 | // preloadChunks: false, 73 | // extractCSS: false, 74 | 75 | // https://quasar.dev/quasar-cli/cli-documentation/handling-webpack 76 | extendWebpack (cfg) { 77 | cfg.module.rules.push({ 78 | enforce: 'pre', 79 | test: /\.(js|vue)$/, 80 | loader: 'eslint-loader', 81 | exclude: /node_modules/, 82 | options: { 83 | formatter: require('eslint').CLIEngine.getFormatter('stylish') 84 | } 85 | }) 86 | } 87 | }, 88 | 89 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 90 | devServer: { 91 | // https: true, 92 | // port: 8080, 93 | open: true // opens browser window automatically 94 | }, 95 | 96 | // animations: 'all', // --- includes all animations 97 | // https://quasar.dev/options/animations 98 | animations: [], 99 | 100 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr 101 | ssr: { 102 | pwa: false 103 | }, 104 | 105 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa 106 | pwa: { 107 | // workboxPluginMode: 'InjectManifest', 108 | // workboxOptions: {}, // only for NON InjectManifest 109 | manifest: { 110 | // name: 'Quasar App', 111 | // short_name: 'Quasar App', 112 | // description: 'A Quasar Framework app', 113 | display: 'standalone', 114 | orientation: 'portrait', 115 | background_color: '#ffffff', 116 | theme_color: '#027be3', 117 | icons: [ 118 | { 119 | 'src': 'statics/icons/icon-128x128.png', 120 | 'sizes': '128x128', 121 | 'type': 'image/png' 122 | }, 123 | { 124 | 'src': 'statics/icons/icon-192x192.png', 125 | 'sizes': '192x192', 126 | 'type': 'image/png' 127 | }, 128 | { 129 | 'src': 'statics/icons/icon-256x256.png', 130 | 'sizes': '256x256', 131 | 'type': 'image/png' 132 | }, 133 | { 134 | 'src': 'statics/icons/icon-384x384.png', 135 | 'sizes': '384x384', 136 | 'type': 'image/png' 137 | }, 138 | { 139 | 'src': 'statics/icons/icon-512x512.png', 140 | 'sizes': '512x512', 141 | 'type': 'image/png' 142 | } 143 | ] 144 | } 145 | }, 146 | 147 | // https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 148 | cordova: { 149 | // id: 'org.cordova.quasar.app', 150 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 151 | }, 152 | 153 | // https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 154 | electron: { 155 | // bundler: 'builder', // or 'packager' 156 | 157 | extendWebpack (cfg) { 158 | // do something with Electron main process Webpack cfg 159 | // chainWebpack also available besides this extendWebpack 160 | }, 161 | 162 | packager: { 163 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 164 | 165 | // OS X / Mac App Store 166 | // appBundleId: '', 167 | // appCategoryType: '', 168 | // osxSign: '', 169 | // protocol: 'myapp://path', 170 | 171 | // Windows only 172 | // win32metadata: { ... } 173 | }, 174 | 175 | builder: { 176 | // https://www.electron.build/configuration/configuration 177 | 178 | // appId: 'app' 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /app/quasar.extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "@quasar/testing": { 3 | "harnesses": [ 4 | "unit-jest", 5 | "e2e-cypress" 6 | ] 7 | }, 8 | "@quasar/testing-unit-jest": { 9 | "babel": "babelrc", 10 | "options": [ 11 | "scripts" 12 | ] 13 | }, 14 | "@quasar/testing-e2e-cypress": { 15 | "options": [ 16 | "scripts" 17 | ] 18 | }, 19 | "@quasar/typescript": { 20 | "webpack": "plugin", 21 | "rename": true, 22 | "vscode": true, 23 | "prettier": true 24 | } 25 | } -------------------------------------------------------------------------------- /app/quasar.testing.json: -------------------------------------------------------------------------------- 1 | { 2 | "unit-jest": { 3 | "runnerCommand": "jest" 4 | }, 5 | "e2e-cypress": { 6 | "runnerCommand": "cypress run --config baseUrl=${serverUrl}" 7 | } 8 | } -------------------------------------------------------------------------------- /app/shim.d.ts: -------------------------------------------------------------------------------- 1 | import VueI18n from 'vue-i18n' 2 | import { Cookies } from 'quasar' 3 | 4 | declare module 'vue/types/vue' { 5 | interface Vue { 6 | i18n: VueI18n 7 | showMessage (message: string, ...optionalParams: any[]): void 8 | showError (message: string, ...optionalParams: any[]): void 9 | } 10 | } 11 | 12 | declare module 'vuex/types/index' { 13 | interface Store { 14 | $cookies: Cookies 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src-ssr/extension.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | * 5 | * All content of this folder will be copied as is to the output folder. So only import: 6 | * 1. node_modules (and yarn/npm install dependencies -- NOT to devDependecies though) 7 | * 2. create files in this folder and import only those with the relative path 8 | * 9 | * Note: This file is used for both PRODUCTION & DEVELOPMENT. 10 | * Note: Changes to this file (but not any file it imports!) are picked up by the 11 | * development server, but such updates are costly since the dev-server needs a reboot. 12 | */ 13 | 14 | module.exports.extendApp = function ({ app, ssr }) { 15 | /* 16 | Extend the parts of the express app that you 17 | want to use with development server too. 18 | 19 | Example: app.use(), app.get() etc 20 | */ 21 | } 22 | -------------------------------------------------------------------------------- /app/src-ssr/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | * 5 | * All content of this folder will be copied as is to the output folder. So only import: 6 | * 1. node_modules (and yarn/npm install dependencies -- NOT to devDependecies though) 7 | * 2. create files in this folder and import only those with the relative path 8 | * 9 | * Note: This file is used only for PRODUCTION. It is not picked up while in dev mode. 10 | * If you are looking to add common DEV & PROD logic to the express app, then use 11 | * "src-ssr/extension.js" 12 | */ 13 | 14 | const 15 | express = require('express'), 16 | compression = require('compression') 17 | 18 | const 19 | ssr = require('../ssr'), 20 | extension = require('./extension'), 21 | app = express(), 22 | port = process.env.PORT || 3000 23 | 24 | const serve = (path, cache) => express.static(ssr.resolveWWW(path), { 25 | maxAge: cache ? 1000 * 60 * 60 * 24 * 30 : 0 26 | }) 27 | 28 | // gzip 29 | app.use(compression({ threshold: 0 })) 30 | 31 | // serve this with no cache, if built with PWA: 32 | if (ssr.settings.pwa) { 33 | app.use('/service-worker.js', serve('service-worker.js')) 34 | } 35 | 36 | // serve "www" folder 37 | app.use('/', serve('.', true)) 38 | 39 | // we extend the custom common dev & prod parts here 40 | extension.extendApp({ app, ssr }) 41 | 42 | // this should be last get(), rendering with SSR 43 | app.get('*', (req, res) => { 44 | res.setHeader('Content-Type', 'text/html') 45 | 46 | // SECURITY HEADERS 47 | // read more about headers here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers 48 | // the following headers help protect your site from common XSS attacks in browsers that respect headers 49 | // you will probably want to use .env variables to drop in appropriate URLs below, 50 | // and potentially look here for inspiration: 51 | // https://ponyfoo.com/articles/content-security-policy-in-express-apps 52 | 53 | // https://developer.mozilla.org/en-us/docs/Web/HTTP/Headers/X-Frame-Options 54 | // res.setHeader('X-frame-options', 'SAMEORIGIN') // one of DENY | SAMEORIGIN | ALLOW-FROM https://example.com 55 | 56 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection 57 | // res.setHeader('X-XSS-Protection', 1) 58 | 59 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 60 | // res.setHeader('X-Content-Type-Options', 'nosniff') 61 | 62 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 63 | // res.setHeader('Access-Control-Allow-Origin', '*') // one of '*', '' where origin is one SINGLE origin 64 | 65 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control 66 | // res.setHeader('X-DNS-Prefetch-Control', 'off') // may be slower, but stops some leaks 67 | 68 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 69 | // res.setHeader('Content-Security-Policy', 'default-src https:') 70 | 71 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox 72 | // res.setHeader('Content-Security-Policy', 'sandbox') // this will lockdown your server!!! 73 | // here are a few that you might like to consider adding to your CSP 74 | // object-src, media-src, script-src, frame-src, unsafe-inline 75 | 76 | ssr.renderToString({ req, res }, (err, html) => { 77 | if (err) { 78 | if (err.url) { 79 | res.redirect(err.url) 80 | } 81 | else if (err.code === 404) { 82 | res.status(404).send('404 | Page Not Found') 83 | } 84 | else { 85 | // Render Error Page or Redirect 86 | res.status(500).send('500 | Internal Server Error') 87 | if (ssr.settings.debug) { 88 | console.error(`500 on ${req.url}`) 89 | console.error(err) 90 | console.error(err.stack) 91 | } 92 | } 93 | } 94 | else { 95 | res.send(html) 96 | } 97 | }) 98 | }) 99 | 100 | app.listen(port, () => { 101 | console.log(`Server listening at port ${port}`) 102 | }) 103 | -------------------------------------------------------------------------------- /app/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | -------------------------------------------------------------------------------- /app/src/areas/default/layouts/Default.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | -------------------------------------------------------------------------------- /app/src/areas/default/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /app/src/areas/loggedIn/layouts/Default.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 119 | -------------------------------------------------------------------------------- /app/src/areas/loggedIn/pages/Group2Up.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | -------------------------------------------------------------------------------- /app/src/areas/loggedIn/pages/Group3Up.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | -------------------------------------------------------------------------------- /app/src/areas/loggedIn/pages/Group6Up.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | -------------------------------------------------------------------------------- /app/src/areas/loggedIn/pages/Group7Up.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | -------------------------------------------------------------------------------- /app/src/areas/loggedIn/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /app/src/areas/notLoggedIn/layouts/Default.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /app/src/areas/notLoggedIn/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 78 | -------------------------------------------------------------------------------- /app/src/areas/notLoggedIn/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | -------------------------------------------------------------------------------- /app/src/areas/notLoggedIn/pages/Page2.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 44 | -------------------------------------------------------------------------------- /app/src/assets/quasar-logo-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 66 | 69 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | 113 | 118 | 126 | 133 | 142 | 151 | 160 | 169 | 178 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /app/src/assets/sad.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/boot/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/boot/.gitkeep -------------------------------------------------------------------------------- /app/src/boot/axios.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | 4 | Vue.prototype.$axios = axios 5 | -------------------------------------------------------------------------------- /app/src/boot/components.ts: -------------------------------------------------------------------------------- 1 | import { BootFileParams } from 'quasar' 2 | 3 | export default async ({ Vue }: BootFileParams) => { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/boot/directives.ts: -------------------------------------------------------------------------------- 1 | import { BootFileParams } from 'quasar' 2 | import getDirective from 'src/components/directives/can' 3 | 4 | export default async ({ Vue, store, router, app, ssrContext }: BootFileParams) => { 5 | const directiveParams = { Vue, store, router, app, ssrContext } 6 | Vue.directive('can', getDirective(directiveParams)) 7 | } 8 | -------------------------------------------------------------------------------- /app/src/boot/i18n.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import messages from 'src/i18n' 4 | import VueConstructor from '*.vue' 5 | 6 | Vue.use(VueI18n) 7 | 8 | const i18n = new VueI18n({ 9 | locale: 'en-us', 10 | fallbackLocale: 'en-us', 11 | messages 12 | }) 13 | 14 | export default ({ app }: { app: VueConstructor }) => { 15 | // Set i18n instance on app 16 | app.i18n = i18n 17 | } 18 | 19 | export { i18n } 20 | -------------------------------------------------------------------------------- /app/src/boot/routeGuard.ts: -------------------------------------------------------------------------------- 1 | import { BootFileParams } from 'quasar' 2 | import { AUTH_INVALID, AUTH_LOAD, AUTH_REQUIRED } from '../store/actions/auth' 3 | import { canAccessByRole } from 'saas-common/dist/app.roles' 4 | 5 | export default ({ app, router, store, ssrContext }: BootFileParams) => { 6 | store.dispatch(AUTH_LOAD) 7 | 8 | router.beforeEach((to, from, next) => { 9 | if (to.matched.some(record => record.meta.requiresAuth)) { 10 | // If we require auth for this route (or any in the chain to get here). Redirect. 11 | if (store.getters.isAuthenticated !== true) { 12 | return store.dispatch(AUTH_REQUIRED) 13 | } 14 | 15 | const 16 | authenticatedUser = store.getters.authenticatedUser, 17 | mimicUser = store.getters.mimicUser 18 | 19 | // Go through the route hierarchy. Check if the parent has roles but child doesn't then assume child is also 20 | // restricted unless child has specific roles (this then overrides the parent) but the user must also have perms 21 | // for all the previous routes as well. Children can only extend permissions 22 | let hasPermission = true 23 | for (const currentRoute of to.matched) { 24 | // If the current route doens't have any roles, defer to it's parents hasPermission value. 25 | if (currentRoute.meta.roles === void 0) { 26 | continue 27 | } 28 | 29 | hasPermission = hasPermission && canAccessByRole(authenticatedUser, mimicUser, currentRoute.meta.roles) 30 | } 31 | 32 | if (hasPermission) { 33 | next() // All good, let can pass 34 | } else { 35 | // None shall pass! 36 | // Access Control failed - return them to the previous route. 37 | store.dispatch(AUTH_INVALID) 38 | router.push(from) 39 | } 40 | } else { 41 | next() // Page doesn't require auth, let them in. 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /app/src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/components/.gitkeep -------------------------------------------------------------------------------- /app/src/components/CanBeAccessBy.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /app/src/components/SelectUserToMimic.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 70 | -------------------------------------------------------------------------------- /app/src/components/directives/can.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveFunction, DirectiveOptions } from 'vue' 2 | import { canAccessByRole, canAccessByPermission } from 'booksprout/dist/app.roles' 3 | import { BootFileParams } from 'quasar' 4 | 5 | const getDirective = ({ store }: BootFileParams): DirectiveOptions | DirectiveFunction => { 6 | const directive: DirectiveOptions | DirectiveFunction = { 7 | // Run this directive when the element is inserted into the parent element in the DOM. 8 | // Failure to do this would mean the element would show until next render update. 9 | inserted: (el, binding, vnode) => { 10 | const 11 | authenticatedUser = store.getters.authenticatedUser, 12 | mimicUser = store.getters.mimicUser, 13 | // If we're checking a perm or a role 14 | userHasPermission = binding.arg === 'perm' 15 | ? canAccessByPermission(authenticatedUser, mimicUser, binding.value) 16 | : canAccessByRole(authenticatedUser, mimicUser, binding.value) 17 | 18 | if (!userHasPermission) { 19 | // Doesn't have permission to access this item so remove the element from the dom 20 | if (vnode && vnode.elm && vnode.elm.parentElement) { 21 | vnode.elm.parentElement.removeChild(vnode.elm) 22 | } 23 | } 24 | } 25 | } 26 | 27 | return directive 28 | } 29 | 30 | export default getDirective 31 | -------------------------------------------------------------------------------- /app/src/components/mixins/base.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component } from 'vue-property-decorator' 2 | import { AppRolePermissions, AppRole } from 'booksprout/dist/app.roles' 3 | import { Getter } from 'vuex-class' 4 | import User from '../../modules/_base/user/user.model' 5 | 6 | @Component 7 | export default class BaseMixin extends Vue { 8 | @Getter('authenticatedUser') private _authenticatedUser!: User 9 | @Getter('mimicUser') private _mimicUser!: User 10 | 11 | @Getter('isAuthenticated') protected isAuthenticated!: boolean 12 | 13 | protected get authenticatedUser () { 14 | return this._authenticatedUser 15 | ? this._authenticatedUser 16 | : new User() 17 | } 18 | 19 | protected get mimicUser () { 20 | return this._mimicUser 21 | ? this._mimicUser 22 | : new User() 23 | } 24 | 25 | protected appRolePermissions = AppRolePermissions 26 | protected appRole = AppRole 27 | 28 | public showMessage (message: string, ...optionalParams: any[]): void { 29 | this.$q.notify({ 30 | message: message.concat(optionalParams.join(' ')), 31 | color: 'green-5', 32 | position: 'top' 33 | }) 34 | } 35 | 36 | public showError (message: string, ...optionalParams: any[]) { 37 | this.$q.notify({ 38 | message: message.concat(optionalParams.join(' ')), 39 | color: 'red-9', 40 | position: 'top' 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/css/app.sass: -------------------------------------------------------------------------------- 1 | // app global css in Sass form 2 | -------------------------------------------------------------------------------- /app/src/css/quasar.variables.sass: -------------------------------------------------------------------------------- 1 | // Quasar Sass (& SCSS) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary : #027BE3 16 | $secondary : #26A69A 17 | $accent : #9C27B0 18 | 19 | $positive : #21BA45 20 | $negative : #C10015 21 | $info : #31CCEC 22 | $warning : #F2C037 23 | -------------------------------------------------------------------------------- /app/src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | NODE_ENV: string 4 | VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined 5 | VUE_ROUTER_BASE: string | undefined 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/src/i18n/en-us/index.ts: -------------------------------------------------------------------------------- 1 | // This is just an example, 2 | // so you can safely delete all default props below 3 | 4 | export default { 5 | failed: 'Action failed', 6 | success: 'Action was successful' 7 | } 8 | -------------------------------------------------------------------------------- /app/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import enUS from './en-us' 2 | 3 | export default { 4 | 'en-us': enUS 5 | } 6 | -------------------------------------------------------------------------------- /app/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/index.ts -------------------------------------------------------------------------------- /app/src/modules/_base/apollo/__tests__/fakes/apolloClient.service.mock.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-client' 2 | import { InMemoryCache } from 'apollo-cache-inmemory' 3 | import fetch from 'node-fetch' 4 | import { createHttpLink } from 'apollo-link-http' 5 | import { injectable } from 'inversify-props' 6 | 7 | import ApolloClientServiceInterface from '../../apolloClient.service.interface' 8 | 9 | @injectable() 10 | class ApolloClientService implements ApolloClientServiceInterface { 11 | public client: ApolloClient 12 | 13 | public constructor () { 14 | const httpLink = createHttpLink({ 15 | uri: 'http://localhost:3000/graphql', 16 | fetch: fetch as any 17 | }) 18 | 19 | // Create the apollo client 20 | const apolloClient = new ApolloClient({ 21 | link: httpLink, 22 | cache: new InMemoryCache(), 23 | connectToDevTools: true 24 | }) 25 | 26 | this.client = apolloClient 27 | } 28 | } 29 | 30 | export default ApolloClientService 31 | -------------------------------------------------------------------------------- /app/src/modules/_base/apollo/apolloClient.service.interface.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-client' 2 | 3 | export default interface ApolloClientServiceInterface { 4 | client: ApolloClient 5 | } 6 | -------------------------------------------------------------------------------- /app/src/modules/_base/apollo/apolloClient.service.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | import { ApolloClient } from 'apollo-client' 3 | import { InMemoryCache } from 'apollo-cache-inmemory' 4 | import { createHttpLink } from 'apollo-link-http' 5 | import { setContext } from 'apollo-link-context' 6 | import { ApolloLink } from 'apollo-link' 7 | import { onError } from 'apollo-link-error' 8 | import { injectable } from 'inversify-props' 9 | import { uid } from 'quasar' 10 | 11 | import ApolloClientServiceInterface from './apolloClient.service.interface' 12 | import { injectSingleton } from '../../diContainer' 13 | import { AUTH_INVALID, AUTH_REQUIRED } from '../../../store/actions/auth' 14 | import StoreService from '../store.service' 15 | import { ERROR_GENERIC } from '../../../store/actions/error' 16 | 17 | @injectable() 18 | class ApolloClientService implements ApolloClientServiceInterface { 19 | public client: ApolloClient 20 | 21 | public readonly uid: string = uid() 22 | 23 | public constructor ( 24 | @injectSingleton(StoreService) storeService: StoreService 25 | ) { 26 | // console.log('Creating apollo client service', this.uid) 27 | 28 | const httpLink = createHttpLink({ 29 | uri: 'http://localhost:3000/graphql', 30 | fetch: fetch as any 31 | }) 32 | 33 | // If we have an authenticated user, attach the appropriate headers. 34 | let authLink!: ApolloLink 35 | if (storeService.store.getters.isAuthenticated) { 36 | authLink = setContext((_, { headers }) => { 37 | return { 38 | headers: { 39 | ...headers, 40 | authorization: `Bearer ${storeService.store.getters.authToken}`, 41 | } 42 | } 43 | }) 44 | } 45 | 46 | const logoutLink = onError((error) => { 47 | const errorRes = error.response 48 | if (errorRes) { 49 | const errors = errorRes.errors 50 | if (errors && errors.length) { 51 | const error = errors[0] 52 | // @ts-ignore 53 | switch (error.message.statusCode) { 54 | case 401: storeService.store.dispatch(AUTH_REQUIRED); break 55 | case 403: storeService.store.dispatch(AUTH_INVALID); break 56 | default: storeService.store.dispatch(ERROR_GENERIC, error.message); break 57 | } 58 | } 59 | } 60 | }) 61 | 62 | const apolloClient = new ApolloClient({ 63 | link: ApolloLink.from([ 64 | logoutLink, 65 | authLink, 66 | httpLink 67 | ]), 68 | cache: new InMemoryCache(), 69 | connectToDevTools: true, 70 | defaultOptions: { 71 | query: { 72 | fetchPolicy: 'network-only', 73 | errorPolicy: 'all' 74 | }, 75 | mutate: { 76 | errorPolicy: 'all' 77 | } 78 | } 79 | }) 80 | 81 | this.client = apolloClient 82 | } 83 | } 84 | 85 | export default ApolloClientService 86 | -------------------------------------------------------------------------------- /app/src/modules/_base/apollo/baseApolloCrud.service.ts: -------------------------------------------------------------------------------- 1 | import BaseCrudServiceInterface from '../baseCrud.service.interface' 2 | import ApolloClientService from './apolloClient.service' 3 | import { injectSingleton } from '../../diContainer' 4 | import { injectable } from 'inversify-props' 5 | 6 | @injectable() 7 | export default abstract class BaseApolloCrudService implements BaseCrudServiceInterface { 8 | /** 9 | * Our Apollo Client instance. 10 | * Note: Will (and should) be a singleton. 11 | */ 12 | @injectSingleton(ApolloClientService) 13 | public readonly apolloClientService!: ApolloClientService 14 | 15 | abstract create (dto: DTO): Promise 16 | 17 | abstract delete (id: string): Promise 18 | 19 | abstract get (params?: any): Promise 20 | 21 | abstract getById (id: string): Promise 22 | 23 | abstract update (dto: DTO): Promise 24 | } 25 | -------------------------------------------------------------------------------- /app/src/modules/_base/auth/auth.service.interface.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios' 2 | 3 | import User from '../user/user.model' 4 | 5 | export interface AuthServiceError extends AxiosError { } 6 | 7 | export interface LoginToken { 8 | data: string, 9 | expires: number 10 | } 11 | 12 | export interface LoginResult { 13 | user: User, 14 | token: LoginToken, 15 | mimicUser: User | null 16 | } 17 | 18 | export default interface AuthServiceInterface { 19 | login (username: string, password: string): Promise 20 | logout (): Promise 21 | setMimicUser (user: User): Promise 22 | } 23 | -------------------------------------------------------------------------------- /app/src/modules/_base/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios' 2 | import { injectable } from 'inversify-props' 3 | 4 | import User from '../user/user.model' 5 | import AuthServiceInterface, { LoginResult } from './auth.service.interface' 6 | import AxiosService from '../axios/axios.service' 7 | import { injectSingleton } from '../../diContainer' 8 | 9 | @injectable() 10 | class AuthService implements AuthServiceInterface { 11 | 12 | public constructor ( 13 | @injectSingleton(AxiosService) private readonly axiosService: AxiosService 14 | ) { } 15 | 16 | public async login (username: string, password: string): Promise { 17 | return this.axiosService.axios.post('auth/login', { 18 | username, 19 | password 20 | }).then((r: AxiosResponse) => { 21 | const loginResponse: LoginResult = r.data 22 | const user: User = { 23 | ...new User(), 24 | ...loginResponse.user 25 | } 26 | 27 | return { 28 | user, 29 | token: { 30 | data: loginResponse.token.data, 31 | expires: loginResponse.token.expires 32 | }, 33 | mimicUser: null 34 | } 35 | }) 36 | } 37 | 38 | public async logout (): Promise { 39 | return this.axiosService.axios.post('auth/logout') 40 | } 41 | 42 | public async setMimicUser (mimicUser: User): Promise { 43 | return this.axiosService.axios.post('auth/setMimicUser', { 44 | id: mimicUser ? mimicUser.id : null 45 | }).then((r: AxiosResponse) => { 46 | return r.data 47 | }) 48 | } 49 | } 50 | 51 | export default AuthService 52 | 53 | -------------------------------------------------------------------------------- /app/src/modules/_base/axios/axios.service.interface.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios' 2 | 3 | export default interface AxiosServiceInterface { 4 | readonly axios: AxiosInstance 5 | } 6 | -------------------------------------------------------------------------------- /app/src/modules/_base/axios/axios.service.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify-props' 2 | import axiosInstance, { AxiosInstance } from 'axios' 3 | 4 | import AxiosServiceInterface from './axios.service.interface' 5 | import StoreService from '../store.service' 6 | import { injectSingleton } from '../../diContainer' 7 | import { AUTH_REQUIRED } from '../../../store/actions/auth' 8 | 9 | @injectable() 10 | class AxiosService implements AxiosServiceInterface { 11 | public readonly axios: AxiosInstance 12 | 13 | public constructor ( 14 | @injectSingleton(StoreService) storeService: StoreService 15 | ) { 16 | this.axios = axiosInstance.create({ 17 | baseURL: process.env.DEV ? 'http://localhost:3000/' : 'http://TODO.at.somepoint', 18 | timeout: 10000 19 | }) 20 | 21 | this.axios.interceptors.request.use( 22 | config => { 23 | config.headers["Authorization"] = "bearer " + storeService.store.getters.authToken 24 | return config 25 | }, 26 | error => { 27 | Promise.reject(error) 28 | } 29 | ) 30 | 31 | this.axios.interceptors.response.use(response => { 32 | return response 33 | }, error => { 34 | console.log('Axios Error: ', error) 35 | storeService.store.dispatch(AUTH_REQUIRED) 36 | throw { 37 | ...error.response.data 38 | } 39 | }) 40 | } 41 | } 42 | 43 | export default AxiosService 44 | -------------------------------------------------------------------------------- /app/src/modules/_base/base.model.ts: -------------------------------------------------------------------------------- 1 | export default class BaseModel { 2 | public id!: string 3 | public createdDate!: Date 4 | } 5 | -------------------------------------------------------------------------------- /app/src/modules/_base/baseCrud.service.interface.ts: -------------------------------------------------------------------------------- 1 | export default interface BaseCrudServiceInterface { 2 | get (params?: any): Promise // TODO: Type this 3 | getById (id: string): Promise 4 | create (dto: DTO): Promise 5 | update (dto: DTO): Promise 6 | delete (id: string): Promise 7 | } 8 | -------------------------------------------------------------------------------- /app/src/modules/_base/baseCrud.service.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify-props' 2 | 3 | import BaseApolloCrudService from './apollo/baseApolloCrud.service' 4 | 5 | @injectable() 6 | export default abstract class BaseCrudService extends BaseApolloCrudService { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/modules/_base/store.service.interface.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex' 2 | import { RootState } from '../../store/types' 3 | 4 | export default interface StoreServiceInterface { 5 | configure (store: Store): void 6 | } 7 | -------------------------------------------------------------------------------- /app/src/modules/_base/store.service.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify-props' 2 | import { Store } from 'vuex' 3 | 4 | import StoreServiceInterface from './store.service.interface' 5 | import { RootState } from '../../store/types' 6 | 7 | @injectable() 8 | class StoreService implements StoreServiceInterface { 9 | public store!: Store 10 | 11 | public configure (store: Store): void { 12 | this.store = store 13 | } 14 | } 15 | 16 | export default StoreService 17 | -------------------------------------------------------------------------------- /app/src/modules/_base/user/dto/userCrud.dto.ts: -------------------------------------------------------------------------------- 1 | export default class UserCrudDto { 2 | public id?: string 3 | public username: string = '' 4 | public emailAddress: string = '' 5 | } 6 | -------------------------------------------------------------------------------- /app/src/modules/_base/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import BaseModel from '../base.model' 2 | import { AppRoles } from 'saas-common/dist/app.roles' 3 | 4 | export default class User extends BaseModel { 5 | public username: string = '' 6 | public emailAddress: string = '' 7 | public role!: AppRoles 8 | public canActAs!: string[] 9 | public hasActingAs!: string[] 10 | } 11 | -------------------------------------------------------------------------------- /app/src/modules/_base/user/user.service.interface.ts: -------------------------------------------------------------------------------- 1 | import User from './user.model' 2 | 3 | export default interface UserServiceInterface { 4 | getUsersToMimic (needle: string): Promise 5 | } 6 | -------------------------------------------------------------------------------- /app/src/modules/_base/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify-props' 2 | import UserServiceInterface from './user.service.interface' 3 | import UserCrudDto from './dto/userCrud.dto' 4 | import User from './user.model' 5 | import BaseCrudService from '../baseCrud.service' 6 | import gql from 'graphql-tag' 7 | 8 | @injectable() 9 | class UserService extends BaseCrudService implements UserServiceInterface { 10 | public getUsersToMimic (needle: string): Promise { 11 | return this.apolloClientService.client.query({ 12 | variables: { 13 | needle 14 | }, 15 | query: gql` 16 | query GetUsersToMimic ($needle: String!) { 17 | getUsersToMimic (needle: $needle) { 18 | id, 19 | username, 20 | role 21 | } 22 | } 23 | ` 24 | }).then((r: any) => { 25 | return r.data.getUsersToMimic 26 | }) 27 | } 28 | 29 | public create (dto: UserCrudDto): Promise { 30 | return Promise.resolve(new User()) 31 | } 32 | 33 | public delete (id: string): Promise { 34 | return Promise.resolve() 35 | } 36 | 37 | public get (params?: any): Promise { 38 | return Promise.resolve([new User()]) 39 | } 40 | 41 | public getById (id: string): Promise { 42 | return Promise.resolve(new User()) 43 | } 44 | 45 | public update (dto: UserCrudDto): Promise { 46 | return Promise.resolve(new User()) 47 | } 48 | } 49 | 50 | export default UserService 51 | -------------------------------------------------------------------------------- /app/src/modules/book/__tests__/bookComponent.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * @jest-environment jsdom 4 | */ 5 | 6 | import 'reflect-metadata' 7 | import { container } from 'inversify-props' 8 | container.unbindAll() 9 | 10 | import { mount, createLocalVue } from '@vue/test-utils' 11 | import * as All from 'quasar' 12 | import { VueConstructor } from 'vue/types/vue' 13 | 14 | import BookServiceInterface from '../book.service.interface' 15 | import BookCrudDto from '../dto/bookCrud.dto' 16 | import Book from '../book.model' 17 | import BookService from './fakes/book.service.mock' 18 | import BookComponent from '../components/book.component' 19 | import IApolloClientService from '../../_base/apollo/apolloClient.service.interface' 20 | import ApolloClientService from '../../_base/apollo/__tests__/fakes/apolloClient.service.mock' 21 | 22 | // import langEn from 'quasar/lang/en-us' // change to any language you wish! => this breaks wallaby :( 23 | const { Quasar } = All 24 | 25 | function isComponent(value: any): value is VueConstructor { 26 | return value && value.component && value.component.name != null 27 | } 28 | 29 | const components = Object.keys(All).reduce<{ [index: string]: VueConstructor }>( 30 | (object, key) => { 31 | const val = (All as any)[key] 32 | if (isComponent(val)) { 33 | object[key] = val 34 | } 35 | return object 36 | }, {} 37 | ) 38 | 39 | describe('Test Book Component', () => { 40 | container.addSingleton(ApolloClientService) 41 | container.addTransient>(BookService) 42 | 43 | const localVue = createLocalVue() 44 | localVue.use(Quasar, { components }) // , lang: langEn 45 | 46 | const wrapper = mount(BookComponent, { 47 | localVue 48 | }) 49 | const vm: BookComponent = wrapper.vm 50 | 51 | it('passes the sanity check and creates a wrapper', () => { 52 | expect(wrapper.isVueInstance()).toBe(true) 53 | }) 54 | 55 | it('has our heading on', () => { 56 | expect(wrapper.find('h1').text()).toContain('Adding a book!') 57 | }) 58 | 59 | it('creates a book', async () => { 60 | vm.title = 'Alakazam!' 61 | const book = await vm.saveBook() 62 | expect(book.title).toBe('Alakazam!') 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /app/src/modules/book/__tests__/bookService.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * @jest-environment jsdom 4 | */ 5 | 6 | import 'reflect-metadata' 7 | import { container } from 'inversify-props' 8 | container.unbindAll() 9 | 10 | import BookServiceInterface from '../book.service.interface' 11 | import BookCrudDto from '../dto/bookCrud.dto' 12 | import Book from '../book.model' 13 | import BookService from '../book.service' 14 | import BaseCrudService from '../../_base/baseCrud.service' 15 | import IApolloClientService from '../../_base/apollo/apolloClient.service.interface' 16 | import ApolloClientService from '../../_base/apollo/__tests__/fakes/apolloClient.service.mock' 17 | 18 | describe('Test Book Service', () => { 19 | container.addSingleton(ApolloClientService) 20 | container.addTransient>(BookService) 21 | const bookService = container.resolve(BookService) 22 | 23 | it('is instance of BaseCrudService', () => { 24 | expect(bookService).toBeInstanceOf(BaseCrudService) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /app/src/modules/book/__tests__/fakes/book.service.mock.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify-props' 2 | 3 | import BaseCrudService from '../../../_base/baseCrud.service' 4 | import Book from '../../book.model' 5 | import BookServiceInterface from '../../book.service.interface' 6 | import BookCrudDto from '../../dto/bookCrud.dto' 7 | 8 | @injectable() 9 | class BookService extends BaseCrudService implements BookServiceInterface { 10 | public constructor () { 11 | super() 12 | } 13 | 14 | public get (): Promise { 15 | return Promise.resolve([{ 16 | ...new Book(), 17 | id: 'bob', 18 | title: 'Alakazam!', 19 | description: 'Aladdins book!' 20 | }]) 21 | } 22 | 23 | public getById (id: string): Promise { 24 | return Promise.resolve(new Book()) 25 | } 26 | 27 | public create (dto: BookCrudDto): Promise { 28 | return Promise.resolve({ 29 | ...new Book(), 30 | ...dto 31 | }) 32 | } 33 | 34 | public update (dto: BookCrudDto): Promise { 35 | return Promise.resolve(new Book()) 36 | } 37 | 38 | public delete (id: string): Promise { 39 | return Promise.resolve() 40 | } 41 | } 42 | 43 | export default BookService 44 | -------------------------------------------------------------------------------- /app/src/modules/book/book.model.ts: -------------------------------------------------------------------------------- 1 | import BaseModel from '../_base/base.model' 2 | 3 | export default class Book extends BaseModel { 4 | public title: string = '' 5 | public description!: string 6 | } 7 | -------------------------------------------------------------------------------- /app/src/modules/book/book.service.interface.ts: -------------------------------------------------------------------------------- 1 | import BaseCrudServiceInterface from '../_base/baseCrud.service.interface' 2 | 3 | export default interface BookServiceInterface extends BaseCrudServiceInterface { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/modules/book/book.service.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | import { injectable } from 'inversify-props' 3 | 4 | import BaseCrudService from '../_base/baseCrud.service' 5 | import Book from './book.model' 6 | import BookServiceInterface from './book.service.interface' 7 | import BookCrudDto from './dto/bookCrud.dto' 8 | 9 | @injectable() 10 | class BookService extends BaseCrudService implements BookServiceInterface { 11 | public get (): Promise { 12 | return this.apolloClientService.client.query({ 13 | query: gql` 14 | query GetBooks { 15 | books { 16 | id, 17 | title, 18 | description 19 | } 20 | } 21 | ` 22 | }).then((r: any) => { 23 | return r.data.books 24 | }) 25 | } 26 | 27 | public getById (id: string): Promise { 28 | return Promise.resolve(new Book()) 29 | } 30 | 31 | public create (dto: BookCrudDto): Promise { 32 | return this.apolloClientService.client.mutate({ 33 | variables: { 34 | title: dto.title 35 | }, 36 | mutation: gql` 37 | mutation CreateBook ($title: String) { 38 | create (title: $title) { 39 | id, 40 | title, 41 | description 42 | } 43 | } 44 | ` 45 | }).then((r: any) => { 46 | return r.data ? r.data.create : null 47 | }) 48 | } 49 | 50 | public update (dto: BookCrudDto): Promise { 51 | return Promise.resolve(new Book()) 52 | } 53 | 54 | public delete (id: string): Promise { 55 | return this.apolloClientService.client.mutate({ 56 | variables: { 57 | id 58 | }, 59 | mutation: gql` 60 | mutation DeleteBook ($id: String!) { 61 | delete (id: $id) { 62 | id 63 | } 64 | } 65 | ` 66 | }).then((r: any) => { 67 | return r.data ? r.data.create : null 68 | }) 69 | } 70 | } 71 | 72 | export default BookService 73 | -------------------------------------------------------------------------------- /app/src/modules/book/components/book.component.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component } from 'vue-property-decorator' 2 | import { inject } from 'inversify-props' 3 | 4 | import Book from '../book.model' 5 | import BookCrudDto from '../dto/bookCrud.dto' 6 | import BookServiceInterface from '../book.service.interface' 7 | 8 | @Component 9 | export default class BookComponent extends Vue { 10 | public showingAddBook: boolean = false 11 | public title: string = '' 12 | public books: Book[] = [] 13 | 14 | @inject() 15 | private bookService!: BookServiceInterface 16 | 17 | public showAddBook () { 18 | this.showingAddBook = !this.showingAddBook 19 | } 20 | 21 | public saveBook (): Promise { 22 | const dto = new BookCrudDto() 23 | dto.title = this.title 24 | return this.bookService.create(dto).then((book: Book) => { 25 | return book 26 | }) 27 | } 28 | 29 | public save (): void { 30 | this.saveBook().then(book => { 31 | if (book) { 32 | this.loadBooks() 33 | this.$q.notify({ 34 | message: 'Book created: ' + book.id 35 | }) 36 | } 37 | }) 38 | } 39 | 40 | public cancel () { } 41 | 42 | public loadBooks () { 43 | this.bookService.get().then(books => { 44 | this.books = books.reverse() 45 | }) 46 | } 47 | 48 | public deleteBook (id: string) { 49 | this.bookService.delete(id).then(r => { 50 | this.loadBooks() 51 | }) 52 | } 53 | 54 | public created () { 55 | this.loadBooks() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/modules/book/components/book.component.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 26 | -------------------------------------------------------------------------------- /app/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import routes from './routes' 5 | 6 | Vue.use(VueRouter) 7 | 8 | /* 9 | * If not building with SSR mode, you can 10 | * directly export the Router instantiation 11 | */ 12 | 13 | let router 14 | export default function (/* { store, ssrContext } */) { 15 | router = new VueRouter({ 16 | scrollBehavior: () => ({ x: 0, y: 0 }), 17 | routes, 18 | 19 | // Leave these as is and change from quasar.conf.js instead! 20 | // quasar.conf.js -> build -> vueRouterMode 21 | // quasar.conf.js -> build -> publicPath 22 | mode: process.env.VUE_ROUTER_MODE, 23 | base: process.env.VUE_ROUTER_BASE 24 | }) 25 | 26 | return router 27 | } 28 | 29 | export { router } 30 | -------------------------------------------------------------------------------- /app/src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from 'vue-router' 2 | import { AppRoles } from 'saas-common/dist/app.roles' 3 | 4 | const routes: RouteConfig[] = [ 5 | { 6 | path: '', 7 | meta: { 8 | requiresAuth: false, 9 | authLayer: true 10 | }, 11 | component: () => import('src/areas/notLoggedIn/layouts/Default.vue'), 12 | children: [ 13 | { path: '', component: () => import('src/areas/notLoggedIn/pages/Index.vue') }, 14 | { path: 'login', component: () => import('src/areas/notLoggedIn/pages/Login.vue') }, 15 | { path: 'page2', component: () => import('src/areas/notLoggedIn/pages/Page2.vue') } 16 | ] 17 | }, 18 | { 19 | path: '/dashboard', 20 | meta: { 21 | requiresAuth: true, 22 | roles: AppRoles.Reviewer 23 | }, 24 | component: () => import('src/areas/loggedIn/layouts/Default.vue'), 25 | children: [ 26 | { path: '', component: () => import('src/areas/loggedIn/pages/Index.vue') }, 27 | { 28 | path: 'g2', 29 | component: () => import('src/areas/loggedIn/pages/Group2Up.vue'), 30 | meta: { 31 | roles: AppRoles.Reviewer 32 | } 33 | }, 34 | { 35 | path: 'g3', 36 | component: () => import('src/areas/loggedIn/pages/Group3Up.vue'), 37 | meta: { 38 | roles: AppRoles.Author 39 | } 40 | }, 41 | { 42 | path: 'g6', 43 | component: () => import('src/areas/loggedIn/pages/Group6Up.vue'), 44 | meta: { 45 | roles: AppRoles.VirtualAssistant 46 | } 47 | }, 48 | { 49 | path: 'g7', 50 | component: () => import('src/areas/loggedIn/pages/Group7Up.vue'), 51 | meta: { 52 | roles: AppRoles.Admin 53 | } 54 | } 55 | ] 56 | } 57 | ] 58 | 59 | // Always leave this as last one 60 | if (process.env.MODE !== 'ssr') { 61 | routes.push({ 62 | path: '*', 63 | component: () => import('pages/Error404.vue') 64 | }) 65 | } 66 | 67 | export default routes 68 | -------------------------------------------------------------------------------- /app/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /app/src/statics/app-logo-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/app-logo-128x128.png -------------------------------------------------------------------------------- /app/src/statics/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /app/src/statics/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /app/src/statics/icons/apple-icon-167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/apple-icon-167x167.png -------------------------------------------------------------------------------- /app/src/statics/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /app/src/statics/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/favicon-16x16.png -------------------------------------------------------------------------------- /app/src/statics/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/favicon-32x32.png -------------------------------------------------------------------------------- /app/src/statics/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/favicon-96x96.png -------------------------------------------------------------------------------- /app/src/statics/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/favicon.ico -------------------------------------------------------------------------------- /app/src/statics/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/icon-128x128.png -------------------------------------------------------------------------------- /app/src/statics/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/icon-192x192.png -------------------------------------------------------------------------------- /app/src/statics/icons/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/icon-256x256.png -------------------------------------------------------------------------------- /app/src/statics/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/icon-384x384.png -------------------------------------------------------------------------------- /app/src/statics/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/icon-512x512.png -------------------------------------------------------------------------------- /app/src/statics/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/src/statics/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /app/src/statics/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/store/actions/auth.ts: -------------------------------------------------------------------------------- 1 | export const AUTH_LOAD = 'AUTH_LOAD' 2 | export const AUTH_REQUEST = 'AUTH_REQUEST' 3 | export const AUTH_SUCCESS = 'AUTH_SUCCESS' 4 | export const AUTH_ERROR = 'AUTH_ERROR' 5 | export const AUTH_LOGOUT = 'AUTH_LOGOUT' 6 | export const AUTH_REQUIRED = 'AUTH_REQUIRED' 7 | export const AUTH_LOGOUT_REQUEST = 'AUTH_LOGOUT_REQUEST' 8 | export const AUTH_INVALID = 'AUTH_INVALID' 9 | export const SET_MIMIC_USER = 'SET_MIMIC_USER' 10 | -------------------------------------------------------------------------------- /app/src/store/actions/error.ts: -------------------------------------------------------------------------------- 1 | export const ERROR_GENERIC = 'ERROR_GENERIC' 2 | -------------------------------------------------------------------------------- /app/src/store/actions/ui.ts: -------------------------------------------------------------------------------- 1 | export const NOTIFY_ERROR = 'NOTIFY_ERROR' 2 | export const CLEAR_NOTIFICATIONS = 'CLEAR_NOTIFICATIONS' 3 | -------------------------------------------------------------------------------- /app/src/store/actions/user.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex, { Store } from 'vuex' 3 | 4 | import { book } from './modules/book' 5 | import { auth } from './modules/auth' 6 | import { ui } from './modules/ui' 7 | import { user } from './modules/user' 8 | import { RootState } from './types' 9 | 10 | import { buildDependencyContainer } from '../modules/diContainer' 11 | import { QSsrContext } from 'quasar' 12 | 13 | Vue.use(Vuex) 14 | 15 | /* 16 | * If not building with SSR mode, you can 17 | * directly export the Store instantiation 18 | */ 19 | 20 | let store: Store 21 | 22 | export default function ({ ssrContext }: { ssrContext: QSsrContext }) { 23 | store = new Vuex.Store( { 24 | modules: { 25 | book, 26 | auth, 27 | ui, 28 | user 29 | }, 30 | 31 | // enable strict mode (adds overhead!) 32 | // for dev mode only 33 | strict: !!process.env.DEV 34 | }) 35 | 36 | buildDependencyContainer(ssrContext, store) 37 | 38 | return store 39 | } 40 | 41 | export { store } 42 | -------------------------------------------------------------------------------- /app/src/store/modules/auth/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree } from 'vuex' 2 | 3 | import { container } from 'src/modules/diContainer' 4 | import AuthService from 'src/modules/_base/auth/auth.service' 5 | import { router } from 'src/router/index' 6 | import { LoginResult } from 'src/modules/_base/auth/auth.service.interface' 7 | import { RootState } from '../../types' 8 | import { AuthState } from './types' 9 | import { NOTIFY_ERROR } from '../../actions/ui' 10 | import { 11 | AUTH_INVALID, 12 | AUTH_LOAD, 13 | AUTH_LOGOUT, 14 | AUTH_LOGOUT_REQUEST, 15 | AUTH_REQUEST, 16 | AUTH_REQUIRED, 17 | AUTH_SUCCESS, 18 | SET_MIMIC_USER 19 | } from '../../actions/auth' 20 | import User from '../../../modules/_base/user/user.model' 21 | 22 | export const actions: ActionTree = { 23 | [AUTH_LOAD] ({ commit }): void { 24 | const 25 | userCookie = this.$cookies.get('user'), 26 | user = userCookie ? userCookie as unknown as User : null, 27 | mimicUserCookie = this.$cookies.get('muser'), 28 | mimicUser = mimicUserCookie ? mimicUserCookie as unknown as User : null 29 | 30 | const loginState = { 31 | user, 32 | token: { 33 | data: this.$cookies.get('user-token'), 34 | expires: parseInt(this.$cookies.get('user-token-expires')) 35 | }, 36 | mimicUser 37 | } 38 | commit(AUTH_LOAD, loginState) 39 | }, 40 | [AUTH_REQUEST] ({ commit }, payload): Promise { 41 | const authService: AuthService = container.get(AuthService) 42 | 43 | return authService.login(payload.username, payload.password).then((loginResult: LoginResult) => { 44 | this.$cookies.set('user-token', loginResult.token.data) 45 | this.$cookies.set('user-token-expires', loginResult.token.expires.toString()) 46 | this.$cookies.set('user', JSON.stringify(loginResult.user)) 47 | 48 | if (loginResult.mimicUser !== null) { 49 | // @ts-ignore 50 | this.$cookies.set('muser', JSON.stringify(loginResult.mimicUser)) 51 | } else { 52 | this.$cookies.remove('muser') 53 | } 54 | 55 | commit(AUTH_SUCCESS, loginResult) 56 | return loginResult 57 | }) 58 | }, 59 | [AUTH_REQUIRED] ({ dispatch }) { 60 | dispatch(AUTH_LOGOUT) 61 | router.push('/login') // This could trigger a popup or something to stay on the same page if needs be. 62 | }, 63 | [AUTH_LOGOUT] ({ commit }) { 64 | this.$cookies.remove('user-token') 65 | this.$cookies.remove('user-token-expires') 66 | this.$cookies.remove('user') 67 | this.$cookies.remove('muser') 68 | 69 | commit(AUTH_LOGOUT) 70 | }, 71 | [AUTH_LOGOUT_REQUEST] ({ dispatch }) { 72 | const authService: AuthService = container.get(AuthService) 73 | 74 | authService.logout().then(() => { 75 | dispatch(AUTH_LOGOUT) 76 | }) 77 | }, 78 | [AUTH_INVALID] ({ dispatch }) { 79 | // This would be replaced by a translated version and also dispatch something so the user is prompted 80 | // to upgrade. UPSELL UPSELL UPSELL! 81 | dispatch(NOTIFY_ERROR, 'You do not have permission to do this.') 82 | }, 83 | [SET_MIMIC_USER] ({ commit }, user) { 84 | const authService: AuthService = container.get(AuthService) 85 | 86 | authService.setMimicUser(user).then((result: LoginResult) => { 87 | // This will set the mimicUser and update all our tokens so we have a secure way of the server 88 | // knowing which user we're mimicing as it'll be baked into the auth token which is signed server side. 89 | commit(AUTH_SUCCESS, result) 90 | }) 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /app/src/store/modules/auth/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from 'vuex' 2 | import { AuthState } from './types' 3 | import { RootState } from '../../types' 4 | 5 | export const getters: GetterTree = { 6 | isAuthenticated (state) { 7 | return state.token.data !== '' && state.token.expires > new Date().getTime() / 1000 8 | }, 9 | authToken (state) { 10 | return state.token.data 11 | }, 12 | authenticatedUser (state) { 13 | return state.user 14 | }, 15 | mimicUser (state) { 16 | return state.mimicUser 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/store/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex' 2 | import authState from './state' 3 | import { getters } from './getters' 4 | import { mutations } from './mutations' 5 | import { actions } from './actions' 6 | import { AuthState } from './types' 7 | import { RootState } from '../../types' 8 | 9 | export const auth: Module = { 10 | state: authState, 11 | getters, 12 | mutations, 13 | actions 14 | } 15 | -------------------------------------------------------------------------------- /app/src/store/modules/auth/mutations.ts: -------------------------------------------------------------------------------- 1 | import { MutationTree } from 'vuex' 2 | 3 | import { LoginResult } from 'src/modules/_base/auth/auth.service.interface' 4 | import { AUTH_LOAD, AUTH_LOGOUT, AUTH_SUCCESS } from '../../actions/auth' 5 | import { AuthState } from './types' 6 | import { resetAuthState } from './state' 7 | 8 | export const mutations: MutationTree = { 9 | [AUTH_LOAD] (state: AuthState, data: LoginResult) { 10 | state.user = data.user 11 | state.mimicUser = data.mimicUser 12 | state.token = data.token 13 | }, 14 | [AUTH_SUCCESS] (state: AuthState, loginResult: LoginResult) { 15 | state.token = loginResult.token 16 | state.user = loginResult.user 17 | 18 | if (loginResult.mimicUser !== null) { 19 | state.mimicUser = loginResult.mimicUser 20 | } else { 21 | // We've got an auth with no mimic, make sure we're don't have an existing one set. 22 | state.mimicUser = null 23 | } 24 | }, 25 | [AUTH_LOGOUT] (state: AuthState) { 26 | Object.assign(state, resetAuthState()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/store/modules/auth/state.ts: -------------------------------------------------------------------------------- 1 | import { AuthState } from './types' 2 | 3 | const resetAuthState = (): AuthState => { 4 | return { 5 | user: null, 6 | token: { 7 | data: '', 8 | expires: 0 9 | }, 10 | mimicUser: null 11 | } 12 | } 13 | 14 | export default (): AuthState => ({ 15 | ...resetAuthState() 16 | }) 17 | 18 | export { resetAuthState } 19 | -------------------------------------------------------------------------------- /app/src/store/modules/auth/types.ts: -------------------------------------------------------------------------------- 1 | import User from 'src/modules/_base/user/user.model' 2 | import { LoginToken } from 'src/modules/_base/auth/auth.service.interface' 3 | 4 | export interface AuthState { 5 | token: LoginToken, 6 | user: User | null, 7 | mimicUser: User | null 8 | } 9 | 10 | -------------------------------------------------------------------------------- /app/src/store/modules/book/actions.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../types' 2 | import { ActionTree } from 'vuex' 3 | import { BookState } from './types' 4 | 5 | export const actions: ActionTree = { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/store/modules/book/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from 'vuex' 2 | import { BookState } from './types' 3 | import { RootState } from '../../types' 4 | 5 | export const getters: GetterTree = { 6 | get (state) { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/store/modules/book/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex' 2 | import bookState from './state' 3 | import { getters } from './getters' 4 | import { mutations } from './mutations' 5 | import { actions } from './actions' 6 | import { BookState } from './types' 7 | import { RootState } from '../../types' 8 | 9 | export const state: BookState = { 10 | ...bookState 11 | } 12 | 13 | const namespaced = true 14 | 15 | export const book: Module = { 16 | namespaced, 17 | state, 18 | getters, 19 | mutations, 20 | actions 21 | } 22 | -------------------------------------------------------------------------------- /app/src/store/modules/book/mutations.ts: -------------------------------------------------------------------------------- 1 | import { BookState } from './types' 2 | import { MutationTree } from 'vuex' 3 | 4 | export const mutations: MutationTree = { 5 | } 6 | -------------------------------------------------------------------------------- /app/src/store/modules/book/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | } 3 | -------------------------------------------------------------------------------- /app/src/store/modules/book/types.ts: -------------------------------------------------------------------------------- 1 | export interface BookState { 2 | title?: string 3 | description?: string 4 | } 5 | -------------------------------------------------------------------------------- /app/src/store/modules/ui/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree } from 'vuex' 2 | 3 | import { RootState } from '../../types' 4 | import { UIState } from './types' 5 | import { CLEAR_NOTIFICATIONS, NOTIFY_ERROR } from '../../actions/ui' 6 | 7 | export const actions: ActionTree = { 8 | [NOTIFY_ERROR] ({ commit }, message) { 9 | commit(NOTIFY_ERROR, message) 10 | }, 11 | [CLEAR_NOTIFICATIONS] ({ commit }) { 12 | commit(CLEAR_NOTIFICATIONS) 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app/src/store/modules/ui/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from 'vuex' 2 | import { UIState } from './types' 3 | import { RootState } from '../../types' 4 | 5 | export const getters: GetterTree = { 6 | getNotifications (state) { 7 | return state.notifications 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/store/modules/ui/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex' 2 | import uiState from './state' 3 | import { getters } from './getters' 4 | import { mutations } from './mutations' 5 | import { actions } from './actions' 6 | import { UIState } from './types' 7 | import { RootState } from '../../types' 8 | 9 | export const ui: Module = { 10 | state: uiState, 11 | getters, 12 | mutations, 13 | actions 14 | } 15 | -------------------------------------------------------------------------------- /app/src/store/modules/ui/mutations.ts: -------------------------------------------------------------------------------- 1 | import { MutationTree } from 'vuex' 2 | import { UIState, UINotification, UINotificationType } from './types' 3 | import { CLEAR_NOTIFICATIONS, NOTIFY_ERROR } from '../../actions/ui' 4 | import { resetNotificationState } from './state' 5 | 6 | export const mutations: MutationTree = { 7 | [NOTIFY_ERROR] (state: UIState, message: string) { 8 | const notification: UINotification = { 9 | type: UINotificationType.Error, 10 | message 11 | } 12 | state.notifications.push(notification) 13 | }, 14 | [CLEAR_NOTIFICATIONS] (state) { 15 | state.notifications = resetNotificationState() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/store/modules/ui/state.ts: -------------------------------------------------------------------------------- 1 | import { UIState, UINotification } from './types' 2 | 3 | const resetNotificationState = (): UINotification[] => { 4 | return [] 5 | } 6 | 7 | export default (): UIState => ({ 8 | notifications: resetNotificationState() 9 | }) 10 | 11 | export { resetNotificationState } 12 | -------------------------------------------------------------------------------- /app/src/store/modules/ui/types.ts: -------------------------------------------------------------------------------- 1 | export enum UINotificationType { 2 | Error = 0, 3 | Standard = 1 4 | } 5 | 6 | export interface UINotification { 7 | type: UINotificationType, 8 | message: string 9 | } 10 | 11 | export interface UIState { 12 | notifications: UINotification[] 13 | } 14 | -------------------------------------------------------------------------------- /app/src/store/modules/user/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree } from 'vuex' 2 | 3 | import { RootState } from '../../types' 4 | import { UserState } from './types' 5 | 6 | export const actions: ActionTree = { 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/store/modules/user/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from 'vuex' 2 | import { UserState } from './types' 3 | import { RootState } from '../../types' 4 | 5 | export const getters: GetterTree = { 6 | } 7 | -------------------------------------------------------------------------------- /app/src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'vuex' 2 | import userState from './state' 3 | import { getters } from './getters' 4 | import { mutations } from './mutations' 5 | import { actions } from './actions' 6 | import { UserState } from './types' 7 | import { RootState } from '../../types' 8 | 9 | export const user: Module = { 10 | state: userState, 11 | getters, 12 | mutations, 13 | actions 14 | } 15 | -------------------------------------------------------------------------------- /app/src/store/modules/user/mutations.ts: -------------------------------------------------------------------------------- 1 | import { MutationTree } from 'vuex' 2 | 3 | import { UserState } from './types' 4 | 5 | export const mutations: MutationTree = { 6 | } 7 | -------------------------------------------------------------------------------- /app/src/store/modules/user/state.ts: -------------------------------------------------------------------------------- 1 | import { UserState } from './types' 2 | 3 | const resetUserState = (): UserState => { 4 | return { 5 | } 6 | } 7 | 8 | export default (): UserState => ({ 9 | ...resetUserState() 10 | }) 11 | 12 | export { resetUserState } 13 | -------------------------------------------------------------------------------- /app/src/store/modules/user/types.ts: -------------------------------------------------------------------------------- 1 | export interface UserState { 2 | } 3 | 4 | -------------------------------------------------------------------------------- /app/src/store/types.ts: -------------------------------------------------------------------------------- 1 | import { AuthState } from './modules/auth/types' 2 | import { BookState } from './modules/book/types' 3 | import { UIState } from './modules/ui/types' 4 | import { UserState } from './modules/user/types' 5 | 6 | export interface RootState { 7 | auth: AuthState 8 | book: BookState 9 | ui: UIState, 10 | user: UserState 11 | } 12 | -------------------------------------------------------------------------------- /app/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/test/.gitkeep -------------------------------------------------------------------------------- /app/test/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /app/test/cypress/integration/home/init.spec.js: -------------------------------------------------------------------------------- 1 | import * as ctx from '../../../../quasar.conf.js' 2 | 3 | describe('Landing', () => { 4 | beforeEach(() => { 5 | cy.visit('/') 6 | }) 7 | it('.should() - assert that is correct', () => { 8 | cy.title().should('include', 'Quasar') 9 | }) 10 | }) 11 | 12 | // describe('Home page tests', () => { 13 | // beforeEach(() => { 14 | // cy.visit('/'); 15 | // }); 16 | // it('has pretty background', () => { 17 | // cy.get('.landing-wrapper') 18 | // .should('have.css', 'background').and('match', /(".+(\/img\/background).+\.png)/); 19 | // }); 20 | // it('has pretty logo', () => { 21 | // cy.get('.landing-wrapper img') 22 | // .should('have.class', 'logo-main') 23 | // .and('have.attr', 'src') 24 | // .and('match', /^(data:image\/svg\+xml).+/); 25 | // }); 26 | // it('has very important information', () => { 27 | // cy.get('.instruction-wrapper') 28 | // .should('contain', 'SETUP INSTRUCTIONS') 29 | // .and('contain', 'Configure Authentication') 30 | // .and('contain', 'Database Configuration and CRUD operations') 31 | // .and('contain', 'Continuous Integration & Continuous Deployment CI/CD'); 32 | // }); 33 | // }); 34 | -------------------------------------------------------------------------------- /app/test/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | // cypress/plugins/index.js 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | // console.log(config); // see what all is in here! 20 | 21 | 22 | // Chrome:: Hack for shaking AUT. Cypress Issue: https://github.com/cypress-io/cypress/issues/1620 23 | on('before:browser:launch', (browser = {}, args) => { 24 | if (browser.name === 'chrome') { 25 | args.push('--disable-blink-features=RootLayerScrolling'); 26 | return args; 27 | } 28 | return true; 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /app/test/cypress/screenshots/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/test/cypress/screenshots/.gitkeep -------------------------------------------------------------------------------- /app/test/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | 13 | // these two commands let you persist local storage between tests 14 | const LOCAL_STORAGE_MEMORY = {} 15 | 16 | Cypress.Commands.add('saveLocalStorage', () => { 17 | Object.keys(localStorage).forEach((key) => { 18 | LOCAL_STORAGE_MEMORY[key] = localStorage[key] 19 | }) 20 | }) 21 | 22 | Cypress.Commands.add('restoreLocalStorage', () => { 23 | Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => { 24 | localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]) 25 | }) 26 | }) 27 | 28 | // CHAINABLE QUASAR INPUT FIELD TYPES 29 | // usage: 30 | // 31 | // cy.get('[data-cy=target-element]').quasar('text', ''); 32 | // 33 | // todo: make sure that this is still compliant with 1.0 34 | // 35 | Cypress.Commands.add('testRoute', (route) => { 36 | cy.location().should((loc) => { 37 | expect(loc.hash).to.contain(route) 38 | }) 39 | }) 40 | 41 | Cypress.Commands.add('quasar', { prevSubject: 'element' }, (subject, mode, option) => { 42 | if (mode === 'select') { 43 | cy.wrap(subject) 44 | .invoke('show') 45 | .click({ force: true }) 46 | .then(() => { 47 | cy.get('.q-popover') 48 | .contains(option) 49 | .click() 50 | }) 51 | } else if (mode === 'grid') { 52 | cy.wrap(subject).within(() => { 53 | cy.get('input').click({ force: true, multiple: true }); 54 | }) 55 | } else if (mode === 'tag-list') { 56 | Object.keys(option) 57 | .forEach((x) => { 58 | cy.wrap(subject) 59 | .within(() => { 60 | cy.get('input') 61 | .first() 62 | .type(`${option[x]}{enter}`) 63 | }) 64 | }) 65 | } else { 66 | cy.wrap(subject) 67 | .invoke('show') 68 | .within(($subject) => { // eslint-disable-line 69 | switch (mode) { 70 | case 'date': 71 | case 'text': 72 | case 'email': 73 | cy.get('input:first') 74 | .type(option) 75 | .should('have.value', option) 76 | break; 77 | case 'radio': 78 | case 'checkbox': 79 | cy.contains(option) 80 | .click() 81 | break 82 | default: 83 | break 84 | } 85 | }) 86 | } 87 | }) 88 | 89 | // Cypress.Commands.add('loadStore', () => {}); 90 | // 91 | // 92 | // -- This is a child command -- 93 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 94 | // 95 | // 96 | // -- This is a dual command -- 97 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 98 | // 99 | // 100 | // -- This is will overwrite an existing command -- 101 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 102 | -------------------------------------------------------------------------------- /app/test/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | const resizeObserverLoopErrRe = /^ResizeObserver loop limit exceeded/ 20 | 21 | Cypress.on('uncaught:exception', (err) => { 22 | if (resizeObserverLoopErrRe.test(err.message)) { 23 | // returning false here prevents Cypress from 24 | // failing the test 25 | return false 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /app/test/cypress/videos/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2/6feb1a09249df8f3ad734a468301e8788c057bef/app/test/cypress/videos/.gitkeep -------------------------------------------------------------------------------- /app/test/jest/__tests__/App.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * @jest-environment jsdom 4 | */ 5 | 6 | import { mount, createLocalVue, shallowMount } from '@vue/test-utils' 7 | import QBUTTON from './demo/QBtn-demo.vue' 8 | import * as All from 'quasar' 9 | // import langEn from 'quasar/lang/en-us' // change to any language you wish! => this breaks wallaby :( 10 | const { Quasar, date } = All 11 | 12 | const components = Object.keys(All).reduce((object, key) => { 13 | const val = All[key] 14 | if (val && val.component && val.component.name != null) { 15 | object[key] = val 16 | } 17 | return object 18 | }, {}) 19 | 20 | describe('Mount Quasar', () => { 21 | const localVue = createLocalVue() 22 | localVue.use(Quasar, { components }) // , lang: langEn 23 | 24 | const wrapper = mount(QBUTTON, { 25 | localVue 26 | }) 27 | const vm = wrapper.vm 28 | 29 | it('passes the sanity check and creates a wrapper', () => { 30 | expect(wrapper.isVueInstance()).toBe(true) 31 | }) 32 | 33 | it('has a created hook', () => { 34 | expect(typeof vm.increment).toBe('function') 35 | }) 36 | 37 | it('accesses the shallowMount', () => { 38 | expect(vm.$el.textContent).toContain('rocket muffin') 39 | expect(wrapper.text()).toContain('rocket muffin') // easier 40 | expect(wrapper.find('p').text()).toContain('rocket muffin') 41 | }) 42 | 43 | it('sets the correct default data', () => { 44 | expect(typeof vm.counter).toBe('number') 45 | const defaultData2 = QBUTTON.data() 46 | expect(defaultData2.counter).toBe(0) 47 | }) 48 | 49 | it('correctly updates data when button is pressed', () => { 50 | const button = wrapper.find('button') 51 | button.trigger('click') 52 | expect(vm.counter).toBe(1) 53 | }) 54 | 55 | it('formats a date without throwing exception', () => { 56 | // test will automatically fail if an exception is thrown 57 | // MMMM and MMM require that a language is 'installed' in Quasar 58 | let formattedString = date.formatDate(Date.now(), 'YYYY MMMM MMM DD') 59 | console.log('formattedString', formattedString) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /app/test/jest/__tests__/demo/QBtn-demo.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <p class="textContent">{{ input }}</p> 4 | <span>{{ counter }}</span> 5 | <q-btn id="mybutton" @click="increment()"></q-btn> 6 | </div> 7 | </template> 8 | 9 | <script> 10 | export default { 11 | name: 'QBUTTON', 12 | data () { 13 | return { 14 | counter: 0, 15 | input: 'rocket muffin' 16 | } 17 | }, 18 | methods: { 19 | increment () { 20 | this.counter++ 21 | } 22 | } 23 | } 24 | </script> 25 | -------------------------------------------------------------------------------- /app/test/jest/jest.setup.js: -------------------------------------------------------------------------------- 1 | // No console.log() / setTimeout 2 | // console.log = jest.fn(() => { throw new Error('Do not use console.log() in production') }) 3 | jest.setTimeout(1000) 4 | 5 | // jest speedup when errors are part of the game 6 | // Error.stackTraceLimit = 0 7 | 8 | global.Promise = require('promise') 9 | 10 | /* 11 | import chai from 'chai' 12 | // Make sure chai and jasmine ".not" play nice together 13 | // https://medium.com/@RubenOostinga/combining-chai-and-jest-matchers-d12d1ffd0303 14 | // updated here: https://www.andrewsouthpaw.com/jest-chai/ 15 | const originalNot = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'not').get 16 | Object.defineProperty(chai.Assertion.prototype, 'not', { 17 | get() { 18 | Object.assign(this, this.assignedNot) 19 | return originalNot.apply(this) 20 | }, 21 | set(newNot) { 22 | this.assignedNot = newNot 23 | return newNot 24 | } 25 | }) 26 | 27 | // Combine both jest and chai matchers on expect 28 | const originalExpect = global.expect 29 | 30 | global.expect = (actual) => { 31 | const originalMatchers = originalExpect(actual) 32 | const chaiMatchers = chai.expect(actual) 33 | 34 | // Add middleware to Chai matchers to increment Jest assertions made 35 | const { assertionsMade } = originalExpect.getState() 36 | Object.defineProperty(chaiMatchers, 'to', { 37 | get() { 38 | originalExpect.setState({ assertionsMade: assertionsMade + 1 }) 39 | return chai.expect(actual) 40 | }, 41 | }) 42 | 43 | const combinedMatchers = Object.assign(chaiMatchers, originalMatchers) 44 | return combinedMatchers 45 | } 46 | Object.keys(originalExpect).forEach(key => (global.expect[key] = originalExpect[key])) 47 | */ 48 | 49 | // do this to make sure we don't get multiple hits from both webpacks when running SSR 50 | setTimeout(()=>{ 51 | // do nothing 52 | }, 1) 53 | -------------------------------------------------------------------------------- /app/test/jest/utils/index.js: -------------------------------------------------------------------------------- 1 | // this is mapped in jest.config.js to resolve @vue/test-utils 2 | import { createLocalVue, shallowMount } from 'test-utils' 3 | 4 | import Vuex from 'vuex' 5 | import VueRouter from 'vue-router' 6 | import Quasar, { Cookies } from 'quasar' 7 | 8 | const mockSsrContext = () => { 9 | return { 10 | req: { 11 | headers: {} 12 | }, 13 | res: { 14 | setHeader: () => undefined 15 | } 16 | } 17 | } 18 | 19 | // https://eddyerburgh.me/mock-vuex-in-vue-unit-tests 20 | export const mountQuasar = (component, options = {}) => { 21 | const localVue = createLocalVue() 22 | const app = {} 23 | 24 | localVue.use(Vuex) 25 | localVue.use(VueRouter) 26 | localVue.use(Quasar) 27 | const store = new Vuex.Store({}) 28 | const router = new VueRouter() 29 | 30 | if (options) { 31 | const ssrContext = options.ssr ? mockSsrContext() : null 32 | 33 | if (options.cookies) { 34 | const cookieStorage = ssrContext ? Cookies.parseSSR(ssrContext) : Cookies 35 | const cookies = options.cookies 36 | Object.keys(cookies).forEach(key => { 37 | cookieStorage.set(key, cookies[key]) 38 | }) 39 | } 40 | 41 | if (options.plugins) { 42 | options.plugins.forEach(plugin => { 43 | plugin({ app, store, router, Vue: localVue, ssrContext }) 44 | }) 45 | } 46 | } 47 | 48 | // mock vue-i18n 49 | const $t = () => {} 50 | const $tc = () => {} 51 | const $n = () => {} 52 | const $d = () => {} 53 | 54 | return shallowMount(component, { 55 | localVue: localVue, 56 | store, 57 | router, 58 | mocks: { $t, $tc, $n, $d }, 59 | // Injections for Components with a QPage root Element 60 | provide: { 61 | pageContainer: true, 62 | layout: { 63 | header: {}, 64 | right: {}, 65 | footer: {}, 66 | left: {} 67 | } 68 | } 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /app/test/jest/utils/stub.css: -------------------------------------------------------------------------------- 1 | /* for mocking out css files in jest.config.js */ 2 | /* 3 | 4 | moduleNameMapper: { 5 | ... 6 | '.*css$': '<rootDir>/test/jest/utils/stub.css' 7 | }, 8 | 9 | */ 10 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "sourceMap": true, 5 | "target": "es6", 6 | "strict": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "baseUrl": ".", 12 | "noUnusedLocals": true, 13 | "noImplicitReturns": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strictNullChecks": true 16 | }, 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Please Note 2 | This repo is well out of date with changes I've been making to my main project. I'm thinking of making this a proper framework. It serves as an example (albeit very rough) for now. 3 | 4 | # Quasar SAAS 5 | 6 | The idea behind this is to provide a good starter for a SAAS application. It contains very basic examples. 7 | 8 | **PLEASE NOTE: This readme is in no way complete - READ THE CODE** 9 | You will need to read the code to get a proper understanding of what its doing 10 | 11 | #### Using the following tech 12 | 13 | * Typescript 14 | * Quasar Framework 15 | * Including SSR 16 | * NestJS 17 | * Auth with PassportJS 18 | * ApolloClient 19 | * Prisma2 (using SQLite for now) 20 | * Inversify (Depedency Injection) 21 | * Jest Testing 22 | * Quasar testing AE 23 | 24 | #### Features 25 | 26 | * API 27 | * Frontend 28 | * Shared code module between App / API 29 | * User authentication 30 | * Roles 31 | * Permissions 32 | * Mimic Users 33 | * Guarded routes based on permissions / roles. 34 | * Areas - Logged in Area, Not Logged In Area (allows front end site and back end) 35 | 36 | #### Getting Started 37 | 38 | You will need 4 terminals to get this running :) 39 | 40 | **Globals** 41 | 42 | 1. `npm i -g @quasar/cli` 43 | 2. `npm i -g nestjs` 44 | 3. `npm i -g prisma2` 45 | 46 | **App** (terminal 1) 47 | 1. `cd app` 48 | 2. `yarn` 49 | 3. `quasar dev -m ssr` 50 | 51 | **Shared Module** (terminal 2) 52 | 53 | Note: You only need to do step 3 if you plan on making any changes so they propagate to the APP and API 54 | 1. `cd shared/common` 55 | 2. `yarn` 56 | 3. `yarn dev` 57 | 58 | **API** 59 | 1. `cd api` 60 | 2. `yarn` 61 | 3. Terminal 3: `primsma2 dev` 62 | 4. Terminal 4: `yarn start:dev` 63 | 64 | #### Authentication / Roles / Permissions / Mimic Users 65 | 66 | You can login with any user located in: `api/src/user/user.service.ts`. 67 | 68 | Permissions / Roles can be found in `shared/common/src/auth/app.roles.ts` 69 | 70 | User mimic is allowed via the `AppRolePermissions.CanMimicUsers` permission. The user must also have `user.canActAs` set 71 | and the user being mimic'd needs `user.hasActingAs` set (see the current service for an example): 72 | 73 | If you log in as user: `va` with pass: `123` you will be able to search for `webnoob` and mimic that users permissions. 74 | 75 | #### Issues? 76 | 77 | Whilst I'm not looking into making this a "go to" boilerplate, feel free to post any issues or PR's 78 | and I'll look into addressing them. 79 | -------------------------------------------------------------------------------- /shared/common/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /shared/common/index.js: -------------------------------------------------------------------------------- 1 | export AppRoles from './dist/app.roles' 2 | export getAppRoleNames from './dist/app.roles' 3 | -------------------------------------------------------------------------------- /shared/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-common", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist", 6 | "scripts": { 7 | "build": "tsc", 8 | "dev": "tsc-watch -p tsconfig.json", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/express": "^4.17.1", 15 | "@types/node": "^12.7.5", 16 | "prettier": "^1.18.2", 17 | "ts-loader": "^6.1.1", 18 | "ts-node": "^8.4.1", 19 | "tsc-watch": "^4.0.0", 20 | "tsconfig-paths": "^3.9.0", 21 | "tslint": "^5.20.0", 22 | "typescript": "^3.6.3" 23 | }, 24 | "dependencies": {} 25 | } 26 | -------------------------------------------------------------------------------- /shared/common/src/auth/app.roles.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create some flags which we will use to determine what the user has access to. 3 | * We're using flags here as it allows us to scale permissions well, for instance 4 | * a VirtualAssistant is an Author user as well but we don't want to check for 5 | * "user.role === 'VirtualAssistant' || user.role === 'Author'" 6 | * instead we can do "user.roles & AppRoles.Author" as the VirtualAssistant users will have the flag for both 7 | * Author and VirtualAssistant by means of "user.roles = AppRoles.Author | AppRoles.VirtualAssistant" when the 8 | * user is created 9 | */ 10 | export declare enum AppRoles { 11 | None = 0, 12 | Reviewer = 1, 13 | Author = 2, 14 | VirtualAssistant = 16, 15 | Admin = 31 16 | } 17 | /** 18 | * Get a list of flag names from the AppRoles. 19 | * @param roles: AppRoles - Flags of which roles we want the descriptions for 20 | * @returns string[] - i.e ['Standard','VirtualAssistant'] 21 | */ 22 | export declare const getAppRoleNames: (roles: AppRoles) => string; 23 | -------------------------------------------------------------------------------- /shared/common/src/auth/app.roles.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * Create some flags which we will use to determine what the user has access to. 5 | * We're using flags here as it allows us to scale permissions well, for instance 6 | * a VirtualAssistant is an Author user as well but we don't want to check for 7 | * "user.role === 'VirtualAssistant' || user.role === 'Author'" 8 | * instead we can do "user.roles & AppRoles.Author" as the VirtualAssistant users will have the flag for both 9 | * Author and VirtualAssistant by means of "user.roles = AppRoles.Author | AppRoles.VirtualAssistant" when the 10 | * user is created 11 | */ 12 | var AppRoles; 13 | (function (AppRoles) { 14 | AppRoles[AppRoles["None"] = 0] = "None"; 15 | AppRoles[AppRoles["Reviewer"] = 1] = "Reviewer"; 16 | AppRoles[AppRoles["Author"] = 2] = "Author"; 17 | AppRoles[AppRoles["VirtualAssistant"] = 16] = "VirtualAssistant"; 18 | AppRoles[AppRoles["Admin"] = 31] = "Admin"; // All 19 | })(AppRoles = exports.AppRoles || (exports.AppRoles = {})); 20 | /** 21 | * Get a list of flag names from the AppRoles. 22 | * @param roles: AppRoles - Flags of which roles we want the descriptions for 23 | * @returns string[] - i.e ['Standard','VirtualAssistant'] 24 | */ 25 | exports.getAppRoleNames = function (roles) { 26 | var result = []; 27 | var i = 0; 28 | var perm; 29 | while (AppRoles[perm = 1 << i++]) { 30 | if (roles & perm) { 31 | result.push(AppRoles[perm]); 32 | } 33 | } 34 | return result.join(','); 35 | }; 36 | -------------------------------------------------------------------------------- /shared/common/src/auth/app.roles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * These enums form the basis of our permissions, they are then grouped into Roles 3 | */ 4 | export enum AppRolePermissions { 5 | None = 0, 6 | CreateBooks = 1 << 0, 7 | ReadBooks = 1 << 1, 8 | UpdateBooks = 1 << 2, 9 | DeleteBooks = 1 << 3, 10 | CanMimicUsers = 1 << 4, 11 | All = ~(~0 << 5) 12 | } 13 | 14 | const 15 | arp = AppRolePermissions, 16 | arpReviewer = arp.ReadBooks, 17 | arpAuthor = arpReviewer | arp.CreateBooks | arp.UpdateBooks | arp.DeleteBooks, 18 | arpVirtualAssistant = arpReviewer | arp.CanMimicUsers, 19 | arpAdmin = arp.All 20 | 21 | /** 22 | * Sets of permissions each AppRole will have access to. 23 | */ 24 | export const AppRolePermissionGroups = { 25 | Reviewer: arpReviewer, 26 | Author: arpAuthor, 27 | VirtualAssistant: arpVirtualAssistant, 28 | Admin: arpAdmin 29 | } 30 | 31 | /** 32 | * Create some flags which we will use to determine what the user has access to. 33 | * We're using flags here as it allows us to scale permissions well, for instance 34 | * a VirtualAssistant is an Author user as well but we don't want to check for 35 | * "user.role === 'VirtualAssistant' || user.role === 'Author'" 36 | * instead we can do "user.roles & AppRoles.Author" as the VirtualAssistant users will have the flag for both 37 | * Author and VirtualAssistant by means of "user.roles = AppRoles.Author | AppRoles.VirtualAssistant" when the 38 | * user is created 39 | */ 40 | export enum AppRoles { 41 | // Not logged in 42 | None = 0, 43 | Reviewer = 1 << 0, // Not strictly required but looks consistent 44 | Author = 1 << 1, 45 | VirtualAssistant = 1 << 2, 46 | Admin = ~(~0 << 3) // All 47 | } 48 | 49 | const 50 | Reviewer = AppRoles.Reviewer, 51 | Author = Reviewer | AppRoles.Author, 52 | VirtualAssistant = Reviewer | AppRoles.VirtualAssistant, 53 | Admin = AppRoles.Admin 54 | 55 | /** 56 | * AppRole will simplify checking. Each user will only have ONE role but this role might inherit permissions 57 | * from multiple roles. 58 | * Note: Most of the work will be done with AppRoles but this is a handy shortcut to assign a role to a user. 59 | */ 60 | export const AppRole = { 61 | // Not logged in 62 | None: AppRoles.None, 63 | // Logged in 64 | Reviewer, 65 | Author, 66 | VirtualAssistant, 67 | Admin 68 | } 69 | 70 | /** 71 | * Get an AppRole (2) and convert it into a Name (Reviewer). 72 | * With that name, lookup the Permission Group (has to match based on key) 73 | * @param appRole - i.e 2 (Reviewer) 74 | * @returns AppRolePermissions - i.e 2 (ReadBooks) 75 | */ 76 | export const getAppRolePermissions = (appRole: any): AppRolePermissions => { 77 | // @ts-ignore 78 | const roleName: string = Object.keys(AppRole).find(k => AppRole[k] === appRole) 79 | // @ts-ignore 80 | return AppRolePermissionGroups[roleName] 81 | } 82 | 83 | /** 84 | * Go over an Enum and return the names based on the flags that have been set. 85 | * @param enumData 86 | * @param values 87 | */ 88 | const getEnumNames = (enumData: any, values: any) => { 89 | const result = [] 90 | 91 | let i = 0 92 | let perm: number 93 | 94 | while (enumData[perm = 1 << i++]) { 95 | if (values & perm) { 96 | result.push(enumData[perm]) 97 | } 98 | } 99 | 100 | return result.join(',') 101 | } 102 | 103 | /** 104 | * Get a list of flag names from the AppRoles. 105 | * @param roles: AppRoles - Flags of which roles we want the descriptions for 106 | * @returns string[] - i.e ['Standard','VirtualAssistant'] 107 | */ 108 | export const getAppRoleNames = (roles: AppRoles): string => { 109 | return getEnumNames(AppRoles, roles) 110 | } 111 | 112 | /** 113 | * Get a list of flag names from the AppRolePermissions. 114 | * @param roles: AppRolePermissions - Flags of which roles we want the descriptions for 115 | * @returns string[] - i.e ['CreateBook','ReadBook'] 116 | */ 117 | export const getAppRolePermissionNames = (appRole: any) => { 118 | const rolePermissions = getAppRolePermissions(appRole) 119 | return getEnumNames(AppRolePermissions, rolePermissions) 120 | } 121 | 122 | /** 123 | * Can the user passed in or the user being mimic'd access the required role 124 | * @param user 125 | * @param mimicUser 126 | * @param requiredRole 127 | */ 128 | export const canAccessByRole = (user: any, mimicUser: any, requiredRole: AppRoles) => { 129 | const combinedRoles = (user ? user.role : 0) | (mimicUser ? mimicUser.role : 0) 130 | // No role required OR we have a user with the correct role based on what's been passed in 131 | return requiredRole === AppRoles.None || (combinedRoles & requiredRole) === requiredRole 132 | } 133 | 134 | /** 135 | * * Can the user passed in or the user being mimic'd access the required permission 136 | * @param user 137 | * @param mimicUser 138 | * @param requiredPermissions 139 | */ 140 | export const canAccessByPermission = (user: any, mimicUser: any, requiredPermissions: AppRolePermissions) => { 141 | const combinedPermissions = getAppRolePermissions((user ? user.role : 0)) | getAppRolePermissions(mimicUser ? mimicUser.role : 0) 142 | // No perm required OR we have a user with permissions that match what's been passed in 143 | return requiredPermissions === AppRolePermissions.None || (combinedPermissions & requiredPermissions) === requiredPermissions 144 | } 145 | -------------------------------------------------------------------------------- /shared/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules", "**/__tests__/*"] 11 | } 12 | --------------------------------------------------------------------------------